From 6d5c679ca799ea7bdcfd41ee883d6a4c2b535593 Mon Sep 17 00:00:00 2001 From: Ilja Date: Tue, 15 Feb 2022 14:41:46 +0200 Subject: [PATCH] Parse and sign typed data --- .../react-wallet-v2/src/components/Modal.tsx | 2 + .../react-wallet-v2/src/data/EIP155Data.ts | 5 +- .../hooks/useWalletConnectEventsManager.ts | 12 ++ wallets/react-wallet-v2/src/pages/index.tsx | 4 +- .../react-wallet-v2/src/store/ModalStore.ts | 2 +- .../src/store/SettingsStore.ts | 7 +- .../react-wallet-v2/src/utils/HelperUtil.ts | 26 ++++ .../src/utils/RequestHandlerUtil.ts | 27 +++- .../react-wallet-v2/src/utils/WalletUtil.ts | 6 +- .../src/views/SessionSignModal.tsx | 9 +- .../src/views/SessionSignTypedDataModal.tsx | 121 ++++++++++++++++++ 11 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx diff --git a/wallets/react-wallet-v2/src/components/Modal.tsx b/wallets/react-wallet-v2/src/components/Modal.tsx index 29f6204..4f66a34 100644 --- a/wallets/react-wallet-v2/src/components/Modal.tsx +++ b/wallets/react-wallet-v2/src/components/Modal.tsx @@ -1,6 +1,7 @@ import ModalStore from '@/store/ModalStore' import SessionProposalModal from '@/views/SessionProposalModal' import SessionRequestModal from '@/views/SessionSignModal' +import SessionSignTypedDataModal from '@/views/SessionSignTypedDataModal' import { Modal as NextModal } from '@nextui-org/react' import { useSnapshot } from 'valtio' @@ -11,6 +12,7 @@ export default function Modal() { {view === 'SessionProposalModal' && } {view === 'SessionSignModal' && } + {view === 'SessionSignTypedDataModal' && } ) } diff --git a/wallets/react-wallet-v2/src/data/EIP155Data.ts b/wallets/react-wallet-v2/src/data/EIP155Data.ts index c9ecf2d..7e7a730 100644 --- a/wallets/react-wallet-v2/src/data/EIP155Data.ts +++ b/wallets/react-wallet-v2/src/data/EIP155Data.ts @@ -16,7 +16,7 @@ export type TEIP155Chain = keyof typeof EIP155_CHAINS /** * Chains */ -export const EIP155_CHAINS = { +export const EIP155_MAINNET_CHAINS = { 'eip155:1': { chainId: 1, name: 'Ethereum', @@ -70,6 +70,8 @@ export const EIP155_TEST_CHAINS = { } } +export const EIP155_CHAINS = { ...EIP155_MAINNET_CHAINS, ...EIP155_TEST_CHAINS } + /** * Methods */ @@ -78,6 +80,7 @@ export const EIP155_SIGNING_METHODS = { ETH_SIGN: 'eth_sign', ETH_SIGN_TRANSACTION: 'eth_signTransaction', ETH_SIGN_TYPED_DATA: 'eth_signTypedData', + ETH_SIGN_TYPED_DATA_V3: 'eth_signTypedData_v3', ETH_SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4', ETH_SIGN_RAW_TRANSACTION: 'eth_sendRawTransaction', ETH_SEND_TRANSACTION: 'eth_sendTransaction' diff --git a/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts b/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts index d5497da..e1cd614 100644 --- a/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts +++ b/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts @@ -20,9 +20,21 @@ export default function useWalletConnectEventsManager(initialized: boolean) { const { method } = request const requestSession = await walletConnectClient.session.get(topic) + // Hanle message signing requests of various formats if ([EIP155_SIGNING_METHODS.ETH_SIGN, EIP155_SIGNING_METHODS.PERSONAL_SIGN].includes(method)) { ModalStore.open('SessionSignModal', { requestEvent, requestSession }) } + + // Hanle data signing requests of various formats + if ( + [ + EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA, + EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3, + EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4 + ].includes(method) + ) { + ModalStore.open('SessionSignTypedDataModal', { requestEvent, requestSession }) + } }, []) useEffect(() => { diff --git a/wallets/react-wallet-v2/src/pages/index.tsx b/wallets/react-wallet-v2/src/pages/index.tsx index 36d6a0e..5946476 100644 --- a/wallets/react-wallet-v2/src/pages/index.tsx +++ b/wallets/react-wallet-v2/src/pages/index.tsx @@ -1,6 +1,6 @@ import AccountCard from '@/components/AccountCard' import PageHeader from '@/components/PageHeader' -import { EIP155_CHAINS, EIP155_TEST_CHAINS } from '@/data/EIP155Data' +import { EIP155_MAINNET_CHAINS, EIP155_TEST_CHAINS } from '@/data/EIP155Data' import SettingsStore from '@/store/SettingsStore' import { wallet } from '@/utils/WalletUtil' import { Text } from '@nextui-org/react' @@ -16,7 +16,7 @@ export default function HomePage() { Mainnets - {Object.values(EIP155_CHAINS).map(({ name, logo, rgb }) => ( + {Object.values(EIP155_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( ))} diff --git a/wallets/react-wallet-v2/src/store/ModalStore.ts b/wallets/react-wallet-v2/src/store/ModalStore.ts index b46b454..2fe913d 100644 --- a/wallets/react-wallet-v2/src/store/ModalStore.ts +++ b/wallets/react-wallet-v2/src/store/ModalStore.ts @@ -13,7 +13,7 @@ interface ModalData { interface State { open: boolean - view?: 'SessionProposalModal' | 'SessionSignModal' + view?: 'SessionProposalModal' | 'SessionSignModal' | 'SessionSignTypedDataModal' data?: ModalData } diff --git a/wallets/react-wallet-v2/src/store/SettingsStore.ts b/wallets/react-wallet-v2/src/store/SettingsStore.ts index 2771f4e..05c664f 100644 --- a/wallets/react-wallet-v2/src/store/SettingsStore.ts +++ b/wallets/react-wallet-v2/src/store/SettingsStore.ts @@ -11,7 +11,7 @@ interface State { * State */ const state = proxy({ - testNets: false + testNets: Boolean(localStorage.getItem('TEST_NETS')) ?? false }) /** @@ -22,6 +22,11 @@ const SettingsStore = { toggleTestNets() { state.testNets = !state.testNets + if (state.testNets) { + localStorage.setItem('TEST_NETS', 'YES') + } else { + localStorage.removeItem('TEST_NETS') + } } } diff --git a/wallets/react-wallet-v2/src/utils/HelperUtil.ts b/wallets/react-wallet-v2/src/utils/HelperUtil.ts index 62c3567..0703c53 100644 --- a/wallets/react-wallet-v2/src/utils/HelperUtil.ts +++ b/wallets/react-wallet-v2/src/utils/HelperUtil.ts @@ -26,3 +26,29 @@ export function convertHexToUtf8(value: string) { return value } + +/** + * Gets message from various signing request methods by filtering out + * a value that is not an address (thus is a message). + * If it is a hex string, it gets converted to utf8 string + */ +export function getSignParamsMessage(params: string[]) { + const message = params.filter(p => !utils.isAddress(p))[0] + + return convertHexToUtf8(message) +} + +/** + * Gets data from various signTypedData request methods by filtering out + * a value that is not an address (thus is data). + * If data is a string convert it to object + */ +export function getSignTypedDataParamsData(params: string[]) { + const data = params.filter(p => !utils.isAddress(p))[0] + + if (typeof data === 'string') { + return JSON.parse(data) + } + + return data +} diff --git a/wallets/react-wallet-v2/src/utils/RequestHandlerUtil.ts b/wallets/react-wallet-v2/src/utils/RequestHandlerUtil.ts index 498e8b5..1db5ef5 100644 --- a/wallets/react-wallet-v2/src/utils/RequestHandlerUtil.ts +++ b/wallets/react-wallet-v2/src/utils/RequestHandlerUtil.ts @@ -1,5 +1,5 @@ import { EIP155_SIGNING_METHODS } from '@/data/EIP155Data' -import { convertHexToUtf8 } from '@/utils/HelperUtil' +import { getSignParamsMessage, getSignTypedDataParamsData } from '@/utils/HelperUtil' import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils' import { RequestEvent } from '@walletconnect/types' import { ERROR } from '@walletconnect/utils' @@ -9,13 +9,28 @@ export async function approveEIP155Request(request: RequestEvent['request'], wal const { method, params, id } = request switch (method) { + /** + * Handle message signing requests + */ case EIP155_SIGNING_METHODS.PERSONAL_SIGN: - const personalSignResult = await wallet.signMessage(convertHexToUtf8(params[0])) - return formatJsonRpcResult(id, personalSignResult) - case EIP155_SIGNING_METHODS.ETH_SIGN: - const ethSignResult = await wallet.signMessage(convertHexToUtf8(params[1])) - return formatJsonRpcResult(id, ethSignResult) + const message = getSignParamsMessage(params) + const signedMessage = await wallet.signMessage(message) + return formatJsonRpcResult(id, signedMessage) + + /** + * Handle data signing requests + */ + case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA: + case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3: + case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4: + const { domain, types, message: data } = getSignTypedDataParamsData(params) + + // https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471 + delete types.EIP712Domain + + const signedData = await wallet._signTypedData(domain, types, data) + return formatJsonRpcResult(id, signedData) default: throw new Error(ERROR.UNKNOWN_JSONRPC_METHOD.format().message) diff --git a/wallets/react-wallet-v2/src/utils/WalletUtil.ts b/wallets/react-wallet-v2/src/utils/WalletUtil.ts index 527a66c..be73ad6 100644 --- a/wallets/react-wallet-v2/src/utils/WalletUtil.ts +++ b/wallets/react-wallet-v2/src/utils/WalletUtil.ts @@ -1,17 +1,15 @@ import { Wallet } from 'ethers' -const STORAGE_KEY = 'WALLET_MNEMONIC' - export let wallet: Wallet export function createOrRestoreWallet() { - const mnemonic = localStorage.getItem(STORAGE_KEY) + const mnemonic = localStorage.getItem('WALLET_MNEMONIC') if (mnemonic) { wallet = Wallet.fromMnemonic(mnemonic) } else { wallet = Wallet.createRandom() // Don't store mnemonic in local storage in a production project! - localStorage.setItem(STORAGE_KEY, wallet.mnemonic.phrase) + localStorage.setItem('WALLET_MNEMONIC', wallet.mnemonic.phrase) } } diff --git a/wallets/react-wallet-v2/src/views/SessionSignModal.tsx b/wallets/react-wallet-v2/src/views/SessionSignModal.tsx index 57a66c5..5c8a6d2 100644 --- a/wallets/react-wallet-v2/src/views/SessionSignModal.tsx +++ b/wallets/react-wallet-v2/src/views/SessionSignModal.tsx @@ -1,6 +1,6 @@ -import { EIP155_CHAINS, EIP155_SIGNING_METHODS, TEIP155Chain } from '@/data/EIP155Data' +import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data' import ModalStore from '@/store/ModalStore' -import { convertHexToUtf8 } from '@/utils/HelperUtil' +import { getSignParamsMessage } from '@/utils/HelperUtil' import { approveEIP155Request, rejectEIP155Request } from '@/utils/RequestHandlerUtil' import { walletConnectClient } from '@/utils/WalletConnectUtil' import { wallet } from '@/utils/WalletUtil' @@ -24,8 +24,7 @@ export default function SessionSignModal() { const { name, icons, url } = requestSession.peer.metadata // Get message, convert it to UTF8 string if it is valid hex - let message = method === EIP155_SIGNING_METHODS.PERSONAL_SIGN ? params[0] : params[1] - message = convertHexToUtf8(message) + const message = getSignParamsMessage(params) // Handle approve action (logic varies based on request method) async function onApprove() { @@ -54,7 +53,7 @@ export default function SessionSignModal() { return ( - Request + Sign Message diff --git a/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx b/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx new file mode 100644 index 0000000..025ce1e --- /dev/null +++ b/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx @@ -0,0 +1,121 @@ +import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data' +import ModalStore from '@/store/ModalStore' +import { getSignTypedDataParamsData } from '@/utils/HelperUtil' +import { approveEIP155Request, rejectEIP155Request } from '@/utils/RequestHandlerUtil' +import { walletConnectClient } from '@/utils/WalletConnectUtil' +import { wallet } from '@/utils/WalletUtil' +import { Avatar, Button, Col, Container, Divider, Link, Modal, Row, Text } from '@nextui-org/react' +import { Fragment } from 'react' + +export default function SessionSignTypedDataModal() { + // Get request and wallet data from store + const requestEvent = ModalStore.state.data?.requestEvent + const requestSession = ModalStore.state.data?.requestSession + + // Ensure request and wallet are defined + if (!requestEvent || !requestSession) { + return Missing request data + } + + // Get required request data + const { chainId } = requestEvent + const { method, params } = requestEvent.request + const { protocol } = requestSession.relay + const { name, icons, url } = requestSession.peer.metadata + + // Get data + const data = getSignTypedDataParamsData(params) + + // Handle approve action (logic varies based on request method) + async function onApprove() { + if (requestEvent) { + const response = await approveEIP155Request(requestEvent.request, wallet) + await walletConnectClient.respond({ + topic: requestEvent.topic, + response + }) + ModalStore.close() + } + } + + // Handle reject action + async function onReject() { + if (requestEvent) { + const response = rejectEIP155Request(requestEvent.request) + await walletConnectClient.respond({ + topic: requestEvent.topic, + response + }) + ModalStore.close() + } + } + + return ( + + + Sign Typed Data + + + + + + + + + + {name} + {url} + + + + + + + + Blockchain + + {EIP155_CHAINS[chainId as TEIP155Chain]?.name ?? chainId} + + + + + + + + + Message + {JSON.stringify(data)} + + + + + + + + Method + {method} + + + + + + + + Relay Protocol + {protocol} + + + + + + + + + + + ) +}