Skip to content

Commit

Permalink
feat: Unblock eth_signTypedData (#2969)
Browse files Browse the repository at this point in the history
Unblock all variants of `eth_signTypedData` as they are deemed lower
risk following UI changes on the clients.

Also adds logic to use it in the Ethereum provider example Snap.
  • Loading branch information
FrederikBolding authored Jan 6, 2025
1 parent 3e615f5 commit 296244d
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 9 deletions.
2 changes: 2 additions & 0 deletions packages/examples/packages/ethereum-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 44 additions & 0 deletions packages/examples/packages/ethereum-provider/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
103 changes: 101 additions & 2 deletions packages/examples/packages/ethereum-provider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Hex>({
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.
Expand All @@ -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 });
Expand Down
4 changes: 4 additions & 0 deletions packages/examples/packages/ethereum-provider/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export type PersonalSignParams = {
message: string;
};

export type SignTypedDataParams = {
message: string;
};
4 changes: 0 additions & 4 deletions packages/snaps-execution-environments/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -60,6 +60,7 @@ export const EthereumProvider: FunctionComponent = () => {
</span>
</Result>
<SignMessage />
<SignTypedData />
</Snap>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const SignMessage: FunctionComponent = () => {

return (
<>
<h3 className="h5">Personal Sign</h3>
<Form onSubmit={handleSubmit} className="mb-3">
<Form.Label>Message</Form.Label>
<Form.Control
Expand All @@ -48,7 +49,7 @@ export const SignMessage: FunctionComponent = () => {
Sign Message
</Button>
</Form>
<Result>
<Result className="mb-3">
<span id="personalSignResult">
{JSON.stringify(data, null, 2)}
{JSON.stringify(error, null, 2)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
setMessage(event.target.value);
};

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

invokeSnap({
snapId: getSnapId(ETHEREUM_PROVIDER_SNAP_ID, ETHEREUM_PROVIDER_SNAP_PORT),
method: 'signTypedData',
params: {
message,
},
}).catch(logError);
};

return (
<>
<h3 className="h5">Sign Typed Data</h3>
<Form onSubmit={handleSubmit} className="mb-3">
<Form.Label>Message</Form.Label>
<Form.Control
type="text"
placeholder="Message"
value={message}
onChange={handleChange}
id="signTypedData"
className="mb-3"
/>

<Button type="submit" id="signTypedDataButton" disabled={isLoading}>
Sign Typed Data
</Button>
</Form>
<Result>
<span id="signTypedDataResult">
{JSON.stringify(data, null, 2)}
{JSON.stringify(error, null, 2)}
</span>
</Result>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './SignMessage';
export * from './SignTypedData';

0 comments on commit 296244d

Please sign in to comment.