From 20d560574ea20c75cb024632985405abded3551c Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 19 Dec 2024 14:22:06 +0100 Subject: [PATCH 1/2] feat: Unblock eth_signTypedData --- packages/snaps-execution-environments/src/common/utils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/snaps-execution-environments/src/common/utils.ts b/packages/snaps-execution-environments/src/common/utils.ts index d3c756e405..d7e143e086 100644 --- a/packages/snaps-execution-environments/src/common/utils.ts +++ b/packages/snaps-execution-environments/src/common/utils.ts @@ -52,10 +52,6 @@ export const BLOCKED_RPC_METHODS = Object.freeze([ 'wallet_revokePermissions', // We disallow all of these confirmations for now, since the screens are not ready for Snaps. 'eth_sendTransaction', - 'eth_signTypedData', - 'eth_signTypedData_v1', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', 'eth_decrypt', 'eth_getEncryptionPublicKey', 'wallet_addEthereumChain', From c665c0760487540d515e6be4a2c7dd91de1be4a0 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 19 Dec 2024 16:12:58 +0100 Subject: [PATCH 2/2] Add example --- .../packages/ethereum-provider/README.md | 2 + .../ethereum-provider/snap.manifest.json | 2 +- .../ethereum-provider/src/index.test.ts | 44 ++++++++ .../packages/ethereum-provider/src/index.ts | 103 +++++++++++++++++- .../packages/ethereum-provider/src/types.ts | 4 + .../ethereum-provider/EthereumProvider.tsx | 3 +- .../components/SignMessage.tsx | 3 +- .../components/SignTypedData.tsx | 60 ++++++++++ .../ethereum-provider/components/index.ts | 2 + 9 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 packages/test-snaps/src/features/snaps/ethereum-provider/components/SignTypedData.tsx create mode 100644 packages/test-snaps/src/features/snaps/ethereum-provider/components/index.ts diff --git a/packages/examples/packages/ethereum-provider/README.md b/packages/examples/packages/ethereum-provider/README.md index 60eb40fd89..1e8220acf2 100644 --- a/packages/examples/packages/ethereum-provider/README.md +++ b/packages/examples/packages/ethereum-provider/README.md @@ -33,6 +33,8 @@ JSON-RPC methods: - `getVersion`: Get the Ethereum network version from an Ethereum provider. - `getAccounts`: Get the Ethereum accounts made available to the snap from an Ethereum provider. +- `personalSign`: Sign a message using an Ethereum account made available to the Snap. +- `signTypedData`: Sign a struct using an Ethereum account made available to the Snap. For more information, you can refer to [the end-to-end tests](./src/index.test.ts). diff --git a/packages/examples/packages/ethereum-provider/snap.manifest.json b/packages/examples/packages/ethereum-provider/snap.manifest.json index 60de26018f..b21aa40cf6 100644 --- a/packages/examples/packages/ethereum-provider/snap.manifest.json +++ b/packages/examples/packages/ethereum-provider/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "wGmGlT7y9asBxBtugpaU+ZkK1bzawwMx3upjmMDhygs=", + "shasum": "I23+R0H/oTfb5PUA99hAW8ILCCn0kFAk2apS+Q0blPA=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethereum-provider/src/index.test.ts b/packages/examples/packages/ethereum-provider/src/index.test.ts index e3fb91f4d7..44a706c97c 100644 --- a/packages/examples/packages/ethereum-provider/src/index.test.ts +++ b/packages/examples/packages/ethereum-provider/src/index.test.ts @@ -76,4 +76,48 @@ describe('onRpcRequest', () => { ]); }); }); + + describe('personalSign', () => { + const MOCK_SIGNATURE = + '0x16f672a12220dc4d9e27671ef580cfc1397a9a4d5ee19eadea46c0f350b2f72a4922be7c1f16ed9b03ef1d3351eac469e33accf5a36194b1d88923701c2b163f1b'; + + it('returns a signature', async () => { + const { request, mockJsonRpc } = await installSnap(); + + // We can mock the signature request with the response we want. + mockJsonRpc({ + method: 'personal_sign', + result: MOCK_SIGNATURE, + }); + + const response = await request({ + method: 'personalSign', + params: { message: 'foo' }, + }); + + expect(response).toRespondWith(MOCK_SIGNATURE); + }); + }); + + describe('signTypedData', () => { + const MOCK_SIGNATURE = + '0x01b37713300d99fecf0274bcb0dfb586a23d56c4bf2ed700c5ecf4ada7a2a14825e7b1212b1cc49c9440c375337561f2b7a6e639ba25be6a6f5a16f60e6931d31c'; + + it('returns a signature', async () => { + const { request, mockJsonRpc } = await installSnap(); + + // We can mock the signature request with the response we want. + mockJsonRpc({ + method: 'eth_signTypedData_v4', + result: MOCK_SIGNATURE, + }); + + const response = await request({ + method: 'signTypedData', + params: { message: 'foo' }, + }); + + expect(response).toRespondWith(MOCK_SIGNATURE); + }); + }); }); diff --git a/packages/examples/packages/ethereum-provider/src/index.ts b/packages/examples/packages/ethereum-provider/src/index.ts index 9488a5119f..e9548a6773 100644 --- a/packages/examples/packages/ethereum-provider/src/index.ts +++ b/packages/examples/packages/ethereum-provider/src/index.ts @@ -5,7 +5,7 @@ import { import type { Hex } from '@metamask/utils'; import { assert, stringToBytes, bytesToHex } from '@metamask/utils'; -import type { PersonalSignParams } from './types'; +import type { PersonalSignParams, SignTypedDataParams } from './types'; /** * Get the current gas price using the `ethereum` global. This is essentially @@ -91,13 +91,106 @@ async function personalSign(message: string, from: string) { return signature; } +/** + * Sign a struct using the `eth_signTypedData_v4` JSON-RPC method. + * + * This uses the Ether Mail struct for example purposes. + * + * Note that using the `ethereum` global requires the + * `endowment:ethereum-provider` permission. + * + * @param message - The message include in Ether Mail a string. + * @param from - The account to sign the message with as a string. + * @returns A signature for the struct and account. + * @throws If the user rejects the prompt. + * @see https://docs.metamask.io/snaps/reference/permissions/#endowmentethereum-provider + * @see https://docs.metamask.io/wallet/concepts/signing-methods/#eth_signtypeddata_v4 + */ +async function signTypedData(message: string, from: string) { + const signature = await ethereum.request({ + method: 'eth_signTypedData_v4', + params: [ + from, + { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + Person: [ + { + name: 'name', + type: 'string', + }, + { + name: 'wallet', + type: 'address', + }, + ], + Mail: [ + { + name: 'from', + type: 'Person', + }, + { + name: 'to', + type: 'Person', + }, + { + name: 'contents', + type: 'string', + }, + ], + }, + primaryType: 'Mail', + domain: { + name: 'Ether Mail', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + from: { + name: 'Snap', + wallet: from, + }, + to: { + name: 'Bob', + wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + contents: message, + }, + }, + ], + }); + assert(signature, 'Ethereum provider did not return a signature.'); + + return signature; +} + /** * Handle incoming JSON-RPC requests from the dapp, sent through the - * `wallet_invokeSnap` method. This handler handles three methods: + * `wallet_invokeSnap` method. This handler handles five methods: * * - `getGasPrice`: Get the current Ethereum gas price as a hexadecimal string. * - `getVersion`: Get the current Ethereum network version as a string. * - `getAccounts`: Get the Ethereum accounts that the snap has access to. + * - `personalSign`: Sign a message using an Ethereum account. + * - `signTypedData` Sign a struct using an Ethereum account. * * @param params - The request parameters. * @param params.request - The JSON-RPC request object. @@ -122,6 +215,12 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { return await personalSign(params.message, accounts[0]); } + case 'signTypedData': { + const params = request.params as SignTypedDataParams; + const accounts = await getAccounts(); + return await signTypedData(params.message, accounts[0]); + } + default: // eslint-disable-next-line @typescript-eslint/no-throw-literal throw new MethodNotFoundError({ method: request.method }); diff --git a/packages/examples/packages/ethereum-provider/src/types.ts b/packages/examples/packages/ethereum-provider/src/types.ts index 9175295002..807c7ef54c 100644 --- a/packages/examples/packages/ethereum-provider/src/types.ts +++ b/packages/examples/packages/ethereum-provider/src/types.ts @@ -1,3 +1,7 @@ export type PersonalSignParams = { message: string; }; + +export type SignTypedDataParams = { + message: string; +}; diff --git a/packages/test-snaps/src/features/snaps/ethereum-provider/EthereumProvider.tsx b/packages/test-snaps/src/features/snaps/ethereum-provider/EthereumProvider.tsx index 4a00afb898..523028ee67 100644 --- a/packages/test-snaps/src/features/snaps/ethereum-provider/EthereumProvider.tsx +++ b/packages/test-snaps/src/features/snaps/ethereum-provider/EthereumProvider.tsx @@ -5,7 +5,7 @@ import { Button, ButtonGroup } from 'react-bootstrap'; import { useInvokeMutation } from '../../../api'; import { Result, Snap } from '../../../components'; import { getSnapId } from '../../../utils'; -import { SignMessage } from './components/SignMessage'; +import { SignMessage, SignTypedData } from './components'; import { ETHEREUM_PROVIDER_SNAP_ID, ETHEREUM_PROVIDER_SNAP_PORT, @@ -60,6 +60,7 @@ export const EthereumProvider: FunctionComponent = () => { + ); }; diff --git a/packages/test-snaps/src/features/snaps/ethereum-provider/components/SignMessage.tsx b/packages/test-snaps/src/features/snaps/ethereum-provider/components/SignMessage.tsx index 00b7321237..ca7d8ddc27 100644 --- a/packages/test-snaps/src/features/snaps/ethereum-provider/components/SignMessage.tsx +++ b/packages/test-snaps/src/features/snaps/ethereum-provider/components/SignMessage.tsx @@ -33,6 +33,7 @@ export const SignMessage: FunctionComponent = () => { return ( <> +

Personal Sign

Message { Sign Message - + {JSON.stringify(data, null, 2)} {JSON.stringify(error, null, 2)} diff --git a/packages/test-snaps/src/features/snaps/ethereum-provider/components/SignTypedData.tsx b/packages/test-snaps/src/features/snaps/ethereum-provider/components/SignTypedData.tsx new file mode 100644 index 0000000000..a2c019fcbf --- /dev/null +++ b/packages/test-snaps/src/features/snaps/ethereum-provider/components/SignTypedData.tsx @@ -0,0 +1,60 @@ +import { logError } from '@metamask/snaps-utils'; +import type { ChangeEvent, FormEvent, FunctionComponent } from 'react'; +import { useState } from 'react'; +import { Button, Form } from 'react-bootstrap'; + +import { useInvokeMutation } from '../../../../api'; +import { Result } from '../../../../components'; +import { getSnapId } from '../../../../utils'; +import { + ETHEREUM_PROVIDER_SNAP_ID, + ETHEREUM_PROVIDER_SNAP_PORT, +} from '../constants'; + +export const SignTypedData: FunctionComponent = () => { + const [message, setMessage] = useState(''); + const [invokeSnap, { isLoading, data, error }] = useInvokeMutation(); + + const handleChange = (event: ChangeEvent) => { + setMessage(event.target.value); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + invokeSnap({ + snapId: getSnapId(ETHEREUM_PROVIDER_SNAP_ID, ETHEREUM_PROVIDER_SNAP_PORT), + method: 'signTypedData', + params: { + message, + }, + }).catch(logError); + }; + + return ( + <> +

Sign Typed Data

+
+ Message + + + + + + + {JSON.stringify(data, null, 2)} + {JSON.stringify(error, null, 2)} + + + + ); +}; diff --git a/packages/test-snaps/src/features/snaps/ethereum-provider/components/index.ts b/packages/test-snaps/src/features/snaps/ethereum-provider/components/index.ts new file mode 100644 index 0000000000..ff33710d8a --- /dev/null +++ b/packages/test-snaps/src/features/snaps/ethereum-provider/components/index.ts @@ -0,0 +1,2 @@ +export * from './SignMessage'; +export * from './SignTypedData';