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

feat: Unblock eth_signTypedData #2969

Merged
merged 2 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Loading