Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pr/aopp #4572

Merged
merged 9 commits into from
Jan 11, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const Options = styled.div`
`;

const Option = styled.div<{ isSelected: boolean }>`
white-space: nowrap;
padding: 0 14px;
margin: 2px 0;
padding-top: 1px;
Expand Down
3 changes: 2 additions & 1 deletion packages/suite-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"protocols": {
"name": "Trezor Suite",
"schemes": [
"bitcoin"
"bitcoin",
"aopp"
]
},
"publish": {
Expand Down
2 changes: 1 addition & 1 deletion packages/suite-desktop/src-electron/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ declare interface ILogger {

declare type BeforeRequestListener = (
details: Electron.OnBeforeRequestListenerDetails,
) => Electron.Response | undefined;
) => Promise<Electron.Response | undefined>;

declare interface RequestInterceptor {
onBeforeRequest(listener: BeforeRequestListener): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ export const createInterceptor = (): RequestInterceptor => {
let beforeRequestListeners: BeforeRequestListener[] = [];

const filter = { urls: ['*://*/*'] };
session.defaultSession.webRequest.onBeforeRequest(filter, (details, callback) => {
session.defaultSession.webRequest.onBeforeRequest(filter, async (details, callback) => {
for (let i = 0; i < beforeRequestListeners.length; ++i) {
const res = beforeRequestListeners[i](details);
/* eslint-disable no-await-in-loop */
const res = await beforeRequestListeners[i](details);
if (res) {
callback(res);
return;
Expand Down
31 changes: 24 additions & 7 deletions packages/suite-desktop/src-electron/modules/request-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,27 @@
* Request Filter feature (blocks non-allowed requests and displays a warning)
*/
import { dialog } from 'electron';

import * as config from '../config';

const domainFilter = () => {
const allowedDomains = [...config.allowedDomains];

const isAllowed = (hostname: string) =>
allowedDomains.find(d => hostname.endsWith(d)) !== undefined;

const addAllowed = (hostname: string) => allowedDomains.push(hostname);

return { isAllowed, addAllowed };
};

const init = ({ mainWindow, interceptor }: Dependencies) => {
const { logger } = global;

const { isAllowed, addAllowed } = domainFilter();

const resourceTypeFilter = ['xhr']; // What resource types we want to filter
const caughtDomainExceptions: string[] = []; // Domains that have already shown an exception
interceptor.onBeforeRequest(details => {
interceptor.onBeforeRequest(async details => {
if (!resourceTypeFilter.includes(details.resourceType)) {
logger.debug(
'request-filter',
Expand All @@ -21,7 +33,7 @@ const init = ({ mainWindow, interceptor }: Dependencies) => {

const { hostname } = new URL(details.url);

if (config.allowedDomains.find(d => hostname.endsWith(d)) !== undefined) {
if (isAllowed(hostname)) {
logger.info(
'request-filter',
`${details.url} was allowed because ${hostname} is in the exception list`,
Expand All @@ -30,12 +42,17 @@ const init = ({ mainWindow, interceptor }: Dependencies) => {
}

if (caughtDomainExceptions.find(d => d === hostname) === undefined) {
caughtDomainExceptions.push(hostname);
dialog.showMessageBox(mainWindow, {
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'warning',
message: `Suite blocked a request to ${hostname}.\n\nIf you believe this is an error, please contact our support.`,
buttons: ['OK'],
message: `Suite is trying to communicate with unknown host: ${hostname}.\n\nWould you like to allow the communication?`,
buttons: ['Allow', 'Deny'],
});
if (response === 0) {
addAllowed(hostname);
logger.info('request-filter', `${details.url} was manually allowed by the user`);
return;
}
caughtDomainExceptions.push(hostname);
}

logger.warn(
Expand Down
5 changes: 3 additions & 2 deletions packages/suite-desktop/src-electron/modules/tor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ const init = async ({ mainWindow, store, interceptor }: Dependencies) => {
protocol === 'https:'
) {
logger.info('tor', `Rewriting ${details.url} to .onion URL`);
return {
return Promise.resolve({
redirectURL: details.url.replace(
/https:\/\/(([a-z0-9]+\.)*)trezor\.io(.*)/,
`http://$1${onionDomain}$3`,
),
};
});
}
return Promise.resolve(undefined);
});

app.on('before-quit', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import thunk from 'redux-thunk';
import protocolReducer, { State as ProtocolState } from '@suite-reducers/protocolReducer';
import * as protocolActions from '../protocolActions';
import * as protocolConstants from '../constants/protocolConstants';
import { PROTOCOL_SCHEME } from '@suite/support/suite/Protocol';
import { PROTOCOL_SCHEME } from '@suite-constants/protocol';

export const getInitialState = (state?: ProtocolState) => ({
protocol: {
Expand Down Expand Up @@ -37,7 +37,7 @@ describe('Protocol actions', () => {
scheme: PROTOCOL_SCHEME.BITCOIN,
address: '12345abcde',
amount: 1.02,
shouldFillSendForm: false,
shouldFill: false,
},
},
});
Expand All @@ -60,7 +60,7 @@ describe('Protocol actions', () => {
scheme: PROTOCOL_SCHEME.BITCOIN,
address: '12345abcde',
amount: undefined,
shouldFillSendForm: false,
shouldFill: false,
},
},
});
Expand Down Expand Up @@ -119,7 +119,7 @@ describe('Protocol actions', () => {
scheme: PROTOCOL_SCHEME.BITCOIN,
address: '12345abcde',
amount: 1.02,
shouldFillSendForm: false,
shouldFill: false,
},
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const FILL_SEND_FORM = '@protocol/fill-send-form';
export const SAVE_COIN_PROTOCOL = '@protocol/save-coin-protocol';
export const FILL_AOPP = '@protocol/fill-aopp';
export const SAVE_AOPP_PROTOCOL = '@protocol/save-aopp-protocol';
export const RESET = '@protocol/reset';
13 changes: 11 additions & 2 deletions packages/suite/src/actions/suite/modalActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { createDeferred, Deferred, DeferredResponse } from '@suite-utils/deferre
export type UserContextPayload =
| {
type: 'qr-reader';
decision: Deferred<{ address: string; amount?: string }>;
decision: Deferred<string>;
allowPaste?: boolean;
}
| {
type: 'unverified-address';
Expand Down Expand Up @@ -113,6 +114,13 @@ export type UserContextPayload =
}
| {
type: 'safety-checks';
}
| {
type: 'send-aopp-message';
address: string;
signature: string;
callback: string;
decision: Deferred<boolean>;
};

export type ModalAction =
Expand Down Expand Up @@ -199,7 +207,8 @@ type DeferredModals = Extract<
| 'coinmarket-buy-terms'
| 'coinmarket-sell-terms'
| 'coinmarket-exchange-dex-terms'
| 'coinmarket-exchange-terms';
| 'coinmarket-exchange-terms'
| 'send-aopp-message';
}
>;
// extract single modal by `type` util
Expand Down
27 changes: 23 additions & 4 deletions packages/suite/src/actions/suite/protocolActions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { PROTOCOL_SCHEME } from '@suite-support/Protocol';
import { PROTOCOL } from './constants';
import type { PROTOCOL_SCHEME } from '@suite-constants/protocol';
import type { SendFormState, AoppState } from '@suite-reducers/protocolReducer';

export type ProtocolAction =
| {
type: typeof PROTOCOL.FILL_SEND_FORM;
payload: boolean;
}
| {
type: typeof PROTOCOL.FILL_AOPP;
payload: boolean;
}
| {
type: typeof PROTOCOL.SAVE_COIN_PROTOCOL;
payload: { scheme: PROTOCOL_SCHEME; address: string; amount?: number };
payload: SendFormState;
}
| {
type: typeof PROTOCOL.SAVE_AOPP_PROTOCOL;
payload: AoppState;
}
| { type: typeof PROTOCOL.RESET };

export const fillSendForm = (shouldFillSendForm: boolean): ProtocolAction => ({
export const fillSendForm = (shouldFill: boolean): ProtocolAction => ({
type: PROTOCOL.FILL_SEND_FORM,
payload: shouldFillSendForm,
payload: shouldFill,
});

export const saveCoinProtocol = (
Expand All @@ -26,6 +35,16 @@ export const saveCoinProtocol = (
payload: { scheme, address, amount },
});

export const fillAopp = (shouldFill: boolean): ProtocolAction => ({
type: PROTOCOL.FILL_AOPP,
payload: shouldFill,
});

export const saveAoppProtocol = (payload: AoppState): ProtocolAction => ({
type: PROTOCOL.SAVE_AOPP_PROTOCOL,
payload,
});

export const resetProtocol = (): ProtocolAction => ({
type: PROTOCOL.RESET,
});
71 changes: 64 additions & 7 deletions packages/suite/src/actions/wallet/signVerifyActions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import TrezorConnect, { ButtonRequestMessage, UI, Unsuccessful, Success } from 'trezor-connect';
import { SIGN_VERIFY } from './constants';
import { addToast } from '@suite-actions/notificationActions';
import { openModal } from '@suite-actions/modalActions';
import { PROTOCOL_SCHEME } from '@suite-constants/protocol';
import { openModal, openDeferredModal } from '@suite-actions/modalActions';
import { getProtocolInfo } from '@suite-utils/parseUri';
import { isHex } from '@wallet-utils/ethUtils';
import type { Dispatch, GetState, TrezorDevice } from '@suite-types';
import type { Account } from '@wallet-types';

Expand Down Expand Up @@ -74,7 +77,7 @@ const showAddressByNetwork =
};

const signByNetwork =
(path: string | number[], message: string, hex: boolean) =>
(path: string | number[], message: string, hex: boolean, aopp: boolean) =>
({ account, device, coin, useEmptyPassphrase }: StateParams) => {
const params = {
device,
Expand All @@ -83,6 +86,7 @@ const signByNetwork =
message,
useEmptyPassphrase,
hex,
no_script_type: aopp,
};
switch (account.networkType) {
case 'bitcoin':
Expand Down Expand Up @@ -124,10 +128,11 @@ const onSignSuccess =
type: 'sign-message-success',
}),
);
return dispatch({
dispatch({
type: SIGN_VERIFY.SIGN_SUCCESS,
signSignature: signature,
});
return signature;
};

const onVerifySuccess = (dispatch: Dispatch) => () => {
Expand All @@ -136,9 +141,10 @@ const onVerifySuccess = (dispatch: Dispatch) => () => {
type: 'verify-message-success',
}),
);
return dispatch({
dispatch({
type: SIGN_VERIFY.VERIFY_SUCCESS,
});
return true;
};

const throwWhenFailed = <T>(response: Unsuccessful | Success<T>) =>
Expand All @@ -151,13 +157,15 @@ const onError =
dispatch: Dispatch,
type: 'sign-message-error' | 'verify-message-error' | 'verify-address-error',
) =>
(error: Error) =>
(error: Error) => {
dispatch(
addToast({
type,
error: error.message,
}),
);
return false as const;
};

export const showAddress =
(address: string, path: string) => (dispatch: Dispatch, getState: GetState) =>
Expand All @@ -167,10 +175,10 @@ export const showAddress =
.catch(onError(dispatch, 'verify-address-error'));

export const sign =
(path: string | number[], message: string, hex = false) =>
(path: string | number[], message: string, hex = false, aopp = false) =>
(dispatch: Dispatch, getState: GetState) =>
getStateParams(getState)
.then(signByNetwork(path, message, hex))
.then(signByNetwork(path, message, hex, aopp))
.then(throwWhenFailed)
.then(onSignSuccess(dispatch))
.catch(onError(dispatch, 'sign-message-error'));
Expand All @@ -183,3 +191,52 @@ export const verify =
.then(throwWhenFailed)
.then(onVerifySuccess(dispatch))
.catch(onError(dispatch, 'verify-message-error'));

export const importAopp = (symbol?: Account['symbol']) => async (dispatch: Dispatch) => {
const uri = await dispatch(openDeferredModal({ type: 'qr-reader', allowPaste: true }));
if (!uri) return;
const info = getProtocolInfo(uri);
if (info?.scheme !== PROTOCOL_SCHEME.AOPP) return;
if (info.asset && symbol && info.asset !== symbol) return;
return {
asset: info.asset,
message: info.msg,
callback: info.callback,
format: info.format,
};
};

export const sendAopp =
(address: string, sig: string, callback: string) =>
async (dispatch: Dispatch, getState: GetState) => {
const network = getState().wallet.selectedAccount.account?.networkType;
const signature =
network === 'ethereum' && isHex(sig) ? Buffer.from(sig, 'hex').toString('base64') : sig;

const confirm = await dispatch(
openDeferredModal({ type: 'send-aopp-message', address, signature, callback }),
);
if (!confirm) return;

const { success, error } = await fetch(callback, {
method: 'POST',
headers: { 'content-type': 'application/json; utf-8' },
body: JSON.stringify({
version: 0,
address,
signature,
}),
})
.then(res => ({
success: res.status === 204,
error: undefined,
}))
.catch(err => ({
success: false,
error: err.message,
}));

if (success) dispatch(addToast({ type: 'aopp-success' }));
else dispatch(addToast({ type: 'aopp-error', error }));
return success;
};