From 48a5687014a2fead4ccd7f4e2a98b496ef99edd2 Mon Sep 17 00:00:00 2001 From: Marek Polak Date: Mon, 29 Nov 2021 11:41:49 +0100 Subject: [PATCH] feat(suite): #4512 AOPP integration to Sign&Verify --- .../src/actions/wallet/signVerifyActions.ts | 61 +++++++++++++++++-- .../suite/hocNotification/index.tsx | 17 ++++++ .../wallet/sign-verify/useSignVerifyForm.ts | 45 +++++++++++++- .../src/reducers/suite/notificationReducer.ts | 4 +- packages/suite/src/support/messages.ts | 16 +++++ .../src/views/wallet/sign-verify/index.tsx | 52 +++++++++++++--- 6 files changed, 181 insertions(+), 14 deletions(-) diff --git a/packages/suite/src/actions/wallet/signVerifyActions.ts b/packages/suite/src/actions/wallet/signVerifyActions.ts index 79c90a02cc0..7326a232eea 100644 --- a/packages/suite/src/actions/wallet/signVerifyActions.ts +++ b/packages/suite/src/actions/wallet/signVerifyActions.ts @@ -1,7 +1,11 @@ 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-reducers/protocolReducer'; +import { openModal, openDeferredModal } from '@suite-actions/modalActions'; +import { unfilteredFetch } from '@suite-utils/env'; +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'; @@ -74,7 +78,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, @@ -83,6 +87,7 @@ const signByNetwork = message, useEmptyPassphrase, hex, + no_script_type: aopp, }; switch (account.networkType) { case 'bitcoin': @@ -167,10 +172,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')); @@ -183,3 +188,51 @@ export const verify = .then(throwWhenFailed) .then(onVerifySuccess(dispatch)) .catch(onError(dispatch, 'verify-message-error')); + +export const importAopp = (symbol?: Account['symbol']) => async (dispatch: Dispatch) => { + const result = await dispatch(openDeferredModal({ type: 'qr-reader', allowPaste: true })); + if (!result?.uri) return; + const info = getProtocolInfo(result.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 unfilteredFetch(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 })); + }; diff --git a/packages/suite/src/components/suite/hocNotification/index.tsx b/packages/suite/src/components/suite/hocNotification/index.tsx index 4c153716788..8d46177dd43 100644 --- a/packages/suite/src/components/suite/hocNotification/index.tsx +++ b/packages/suite/src/components/suite/hocNotification/index.tsx @@ -24,6 +24,23 @@ const hocNotification = (notification: NotificationEntry, View: React.ComponentT return withCoinProtocol(View, notification); case 'aopp-protocol': return withAoppProtocol(View, notification); + case 'aopp-success': + return simple(View, { + notification, + variant: 'success', + message: 'TOAST_AOPP_SUCCESS', + }); + case 'aopp-error': + return simple(View, { + notification, + variant: 'error', + message: { + id: 'TOAST_AOPP_ERROR', + values: { + error: notification.error, + }, + }, + }); case 'acquire-error': return simple(View, { notification, diff --git a/packages/suite/src/hooks/wallet/sign-verify/useSignVerifyForm.ts b/packages/suite/src/hooks/wallet/sign-verify/useSignVerifyForm.ts index a6d88008afb..4f5eb085975 100644 --- a/packages/suite/src/hooks/wallet/sign-verify/useSignVerifyForm.ts +++ b/packages/suite/src/hooks/wallet/sign-verify/useSignVerifyForm.ts @@ -1,10 +1,12 @@ -import { useEffect } from 'react'; +import { useEffect, useCallback } from 'react'; import { useForm, useController } from 'react-hook-form'; -import { useTranslation } from '@suite-hooks'; +import { useTranslation, useSelector, useActions } from '@suite-hooks'; +import * as protocolActions from '@suite-actions/protocolActions'; import { isHex } from '@wallet-utils/ethUtils'; import { isASCII } from '@suite-utils/validators'; import { isAddressValid } from '@wallet-utils/validation'; import type { Account } from '@wallet-types'; +import type { AoppState } from '@suite-reducers/protocolReducer'; export const MAX_LENGTH_MESSAGE = 1024; export const MAX_LENGTH_SIGNATURE = 255; @@ -15,6 +17,8 @@ export type SignVerifyFields = { path: string; signature: string; hex: boolean; + aopp: boolean; + callback: string; }; const DEFAULT_VALUES: SignVerifyFields = { @@ -23,6 +27,31 @@ const DEFAULT_VALUES: SignVerifyFields = { path: '', signature: '', hex: false, + aopp: false, + callback: '', +}; + +const useAoppListener = (account: Account | undefined, setAopp: (aopp: AoppState) => void) => { + const { aoppState } = useSelector(state => ({ + aoppState: state.protocol.aopp, + })); + + const { fillAopp } = useActions({ + fillAopp: protocolActions.fillAopp, + }); + + const shouldFill = useCallback( + (aopp: typeof aoppState): aopp is AoppState => + !!aopp.shouldFill && !!aopp.message && aopp.asset === account?.symbol, + [account], + ); + + useEffect(() => { + if (shouldFill(aoppState)) { + setAopp(aoppState); + fillAopp(false); + } + }, [aoppState, shouldFill, fillAopp, setAopp]); }; export const useSignVerifyForm = (page: 'sign' | 'verify', account?: Account) => { @@ -71,6 +100,9 @@ export const useSignVerifyForm = (page: 'sign' | 'verify', account?: Account) => name: 'hex', }); + register('aopp'); + register('callback'); + const messageRef = register({ maxLength: { value: MAX_LENGTH_MESSAGE, @@ -117,6 +149,14 @@ export const useSignVerifyForm = (page: 'sign' | 'verify', account?: Account) => }); }, [reset, account, page]); + const formSetAopp = (aopp: AoppState) => { + setValue('aopp', !!aopp); + setValue('callback', aopp?.callback ?? ''); + setValue('message', aopp?.message ?? ''); + }; + + useAoppListener(account, formSetAopp); + return { formDirty: isDirty, formReset: () => reset(), @@ -129,6 +169,7 @@ export const useSignVerifyForm = (page: 'sign' | 'verify', account?: Account) => signature: errors.signature?.message, }, formSetSignature: (value: string) => setValue('signature', value), + formSetAopp, messageRef, signatureRef, hexField: { diff --git a/packages/suite/src/reducers/suite/notificationReducer.ts b/packages/suite/src/reducers/suite/notificationReducer.ts index e4fc8fa5e5f..4571e76f6a1 100644 --- a/packages/suite/src/reducers/suite/notificationReducer.ts +++ b/packages/suite/src/reducers/suite/notificationReducer.ts @@ -30,7 +30,8 @@ export type ToastPayload = ( | 'backup-success' | 'backup-failed' | 'sign-message-success' - | 'verify-message-success'; + | 'verify-message-success' + | 'aopp-success'; } | { type: 'tx-sent'; @@ -65,6 +66,7 @@ export type ToastPayload = ( | 'verify-address-error' | 'sign-message-error' | 'verify-message-error' + | 'aopp-error' | 'sign-tx-error' | 'metadata-auth-error' | 'metadata-not-found-error' diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 62409002e75..6ad2e2b46e9 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -6882,6 +6882,14 @@ export default defineMessages({ id: 'TR_DO_YOU_REALLY_WANT_TO_SKIP', defaultMessage: 'Do you really want to skip this step?', }, + TR_AOPP_IMPORT: { + id: 'TR_AOPP_IMPORT', + defaultMessage: 'Import AOPP', + }, + TR_AOPP_SEND: { + id: 'TR_AOPP_SEND', + defaultMessage: 'Send ownership proof', + }, TOAST_AOPP_FILL_HEADER: { id: 'TOAST_AOPP_FILL_HEADER', defaultMessage: 'Go to Sign & Verify form', @@ -6890,4 +6898,12 @@ export default defineMessages({ id: 'TOAST_AOPP_FILL_ACTION', defaultMessage: 'Fill form', }, + TOAST_AOPP_SUCCESS: { + id: 'TOAST_AOPP_SUCCESS', + defaultMessage: 'Address ownership proof sent', + }, + TOAST_AOPP_ERROR: { + id: 'TOAST_AOPP_ERROR', + defaultMessage: 'Address ownership proof failed: {error}', + }, }); diff --git a/packages/suite/src/views/wallet/sign-verify/index.tsx b/packages/suite/src/views/wallet/sign-verify/index.tsx index 2824d5a383b..7b3fe87deae 100644 --- a/packages/suite/src/views/wallet/sign-verify/index.tsx +++ b/packages/suite/src/views/wallet/sign-verify/index.tsx @@ -4,7 +4,7 @@ import { Input, Button, Textarea, Card, Switch, variables } from '@trezor/compon import { WalletLayout, WalletLayoutHeader } from '@wallet-components'; import { CharacterCount, Translation } from '@suite-components'; import { useActions, useDevice, useSelector, useTranslation } from '@suite-hooks'; -import { sign as signAction, verify as verifyAction } from '@wallet-actions/signVerifyActions'; +import * as signVerifyActions from '@wallet-actions/signVerifyActions'; import Navigation, { NavPages } from './components/Navigation'; import SignAddressInput from './components/SignAddressInput'; import { useCopySignedMessage } from '@wallet-hooks/sign-verify/useCopySignedMessage'; @@ -21,6 +21,9 @@ const Row = styled.div` & + & { padding-top: 12px; } + & > * + * { + margin-left: 10px; + } `; const StyledButton = styled(Button)` @@ -49,7 +52,7 @@ const SignVerify = () => { revealedAddresses: state.wallet.receive, })); - const { isLocked } = useDevice(); + const { isLocked, device } = useDevice(); const { translationString } = useTranslation(); const { @@ -59,6 +62,7 @@ const SignVerify = () => { formValues, formErrors, formSetSignature, + formSetAopp, messageRef, signatureRef, hexField, @@ -66,15 +70,17 @@ const SignVerify = () => { pathField, } = useSignVerifyForm(page, selectedAccount.account); - const { sign, verify } = useActions({ - sign: signAction, - verify: verifyAction, + const { sign, verify, importAopp, sendAopp } = useActions({ + sign: signVerifyActions.sign, + verify: signVerifyActions.verify, + importAopp: signVerifyActions.importAopp, + sendAopp: signVerifyActions.sendAopp, }); const onSubmit = async (data: SignVerifyFields) => { - const { address, path, message, signature, hex } = data; + const { address, path, message, signature, hex, aopp } = data; if (page === 'sign') { - const result = await sign(path, message, hex); + const result = await sign(path, message, hex, aopp); if (result?.signSignature) formSetSignature(result.signSignature); } else { await verify(address, message, signature, hex); @@ -88,6 +94,21 @@ const SignVerify = () => { return ( + {!device?.unavailableCapabilities?.aopp && ( + /* TODO: This button available only for networks with aopp feature */ + + )} {page === 'sign' && canCopy && (