Skip to content

Commit e4beb01

Browse files
Add example
1 parent e42aac5 commit e4beb01

File tree

9 files changed

+218
-5
lines changed

9 files changed

+218
-5
lines changed

packages/examples/packages/ethereum-provider/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ JSON-RPC methods:
3333
- `getVersion`: Get the Ethereum network version from an Ethereum provider.
3434
- `getAccounts`: Get the Ethereum accounts made available to the snap from an
3535
Ethereum provider.
36+
- `personalSign`: Sign a message using an Ethereum account made available to the Snap.
37+
- `signTypedData`: Sign a struct using an Ethereum account made available to the Snap.
3638

3739
For more information, you can refer to
3840
[the end-to-end tests](./src/index.test.ts).

packages/examples/packages/ethereum-provider/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snaps.git"
88
},
99
"source": {
10-
"shasum": "wGmGlT7y9asBxBtugpaU+ZkK1bzawwMx3upjmMDhygs=",
10+
"shasum": "I23+R0H/oTfb5PUA99hAW8ILCCn0kFAk2apS+Q0blPA=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/examples/packages/ethereum-provider/src/index.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,48 @@ describe('onRpcRequest', () => {
7676
]);
7777
});
7878
});
79+
80+
describe('personalSign', () => {
81+
const MOCK_SIGNATURE =
82+
'0x16f672a12220dc4d9e27671ef580cfc1397a9a4d5ee19eadea46c0f350b2f72a4922be7c1f16ed9b03ef1d3351eac469e33accf5a36194b1d88923701c2b163f1b';
83+
84+
it('returns a signature', async () => {
85+
const { request, mockJsonRpc } = await installSnap();
86+
87+
// We can mock the signature request with the response we want.
88+
mockJsonRpc({
89+
method: 'personal_sign',
90+
result: MOCK_SIGNATURE,
91+
});
92+
93+
const response = await request({
94+
method: 'personalSign',
95+
params: { message: 'foo' },
96+
});
97+
98+
expect(response).toRespondWith(MOCK_SIGNATURE);
99+
});
100+
});
101+
102+
describe('signTypedData', () => {
103+
const MOCK_SIGNATURE =
104+
'0x01b37713300d99fecf0274bcb0dfb586a23d56c4bf2ed700c5ecf4ada7a2a14825e7b1212b1cc49c9440c375337561f2b7a6e639ba25be6a6f5a16f60e6931d31c';
105+
106+
it('returns a signature', async () => {
107+
const { request, mockJsonRpc } = await installSnap();
108+
109+
// We can mock the signature request with the response we want.
110+
mockJsonRpc({
111+
method: 'eth_signTypedData_v4',
112+
result: MOCK_SIGNATURE,
113+
});
114+
115+
const response = await request({
116+
method: 'signTypedData',
117+
params: { message: 'foo' },
118+
});
119+
120+
expect(response).toRespondWith(MOCK_SIGNATURE);
121+
});
122+
});
79123
});

packages/examples/packages/ethereum-provider/src/index.ts

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
import type { Hex } from '@metamask/utils';
66
import { assert, stringToBytes, bytesToHex } from '@metamask/utils';
77

8-
import type { PersonalSignParams } from './types';
8+
import type { PersonalSignParams, SignTypedDataParams } from './types';
99

1010
/**
1111
* Get the current gas price using the `ethereum` global. This is essentially
@@ -91,13 +91,106 @@ async function personalSign(message: string, from: string) {
9191
return signature;
9292
}
9393

94+
/**
95+
* Sign a struct using the `eth_signTypedData_v4` JSON-RPC method.
96+
*
97+
* This uses the Ether Mail struct for example purposes.
98+
*
99+
* Note that using the `ethereum` global requires the
100+
* `endowment:ethereum-provider` permission.
101+
*
102+
* @param message - The message include in Ether Mail a string.
103+
* @param from - The account to sign the message with as a string.
104+
* @returns A signature for the struct and account.
105+
* @throws If the user rejects the prompt.
106+
* @see https://docs.metamask.io/snaps/reference/permissions/#endowmentethereum-provider
107+
* @see https://docs.metamask.io/wallet/concepts/signing-methods/#eth_signtypeddata_v4
108+
*/
109+
async function signTypedData(message: string, from: string) {
110+
const signature = await ethereum.request<Hex>({
111+
method: 'eth_signTypedData_v4',
112+
params: [
113+
from,
114+
{
115+
types: {
116+
EIP712Domain: [
117+
{
118+
name: 'name',
119+
type: 'string',
120+
},
121+
{
122+
name: 'version',
123+
type: 'string',
124+
},
125+
{
126+
name: 'chainId',
127+
type: 'uint256',
128+
},
129+
{
130+
name: 'verifyingContract',
131+
type: 'address',
132+
},
133+
],
134+
Person: [
135+
{
136+
name: 'name',
137+
type: 'string',
138+
},
139+
{
140+
name: 'wallet',
141+
type: 'address',
142+
},
143+
],
144+
Mail: [
145+
{
146+
name: 'from',
147+
type: 'Person',
148+
},
149+
{
150+
name: 'to',
151+
type: 'Person',
152+
},
153+
{
154+
name: 'contents',
155+
type: 'string',
156+
},
157+
],
158+
},
159+
primaryType: 'Mail',
160+
domain: {
161+
name: 'Ether Mail',
162+
version: '1',
163+
chainId: 1,
164+
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
165+
},
166+
message: {
167+
from: {
168+
name: 'Snap',
169+
wallet: from,
170+
},
171+
to: {
172+
name: 'Bob',
173+
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
174+
},
175+
contents: message,
176+
},
177+
},
178+
],
179+
});
180+
assert(signature, 'Ethereum provider did not return a signature.');
181+
182+
return signature;
183+
}
184+
94185
/**
95186
* Handle incoming JSON-RPC requests from the dapp, sent through the
96-
* `wallet_invokeSnap` method. This handler handles three methods:
187+
* `wallet_invokeSnap` method. This handler handles five methods:
97188
*
98189
* - `getGasPrice`: Get the current Ethereum gas price as a hexadecimal string.
99190
* - `getVersion`: Get the current Ethereum network version as a string.
100191
* - `getAccounts`: Get the Ethereum accounts that the snap has access to.
192+
* - `personalSign`: Sign a message using an Ethereum account.
193+
* - `signTypedData` Sign a struct using an Ethereum account.
101194
*
102195
* @param params - The request parameters.
103196
* @param params.request - The JSON-RPC request object.
@@ -122,6 +215,12 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
122215
return await personalSign(params.message, accounts[0]);
123216
}
124217

218+
case 'signTypedData': {
219+
const params = request.params as SignTypedDataParams;
220+
const accounts = await getAccounts();
221+
return await signTypedData(params.message, accounts[0]);
222+
}
223+
125224
default:
126225
// eslint-disable-next-line @typescript-eslint/no-throw-literal
127226
throw new MethodNotFoundError({ method: request.method });
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export type PersonalSignParams = {
22
message: string;
33
};
4+
5+
export type SignTypedDataParams = {
6+
message: string;
7+
};

packages/test-snaps/src/features/snaps/ethereum-provider/EthereumProvider.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Button, ButtonGroup } from 'react-bootstrap';
55
import { useInvokeMutation } from '../../../api';
66
import { Result, Snap } from '../../../components';
77
import { getSnapId } from '../../../utils';
8-
import { SignMessage } from './components/SignMessage';
8+
import { SignMessage, SignTypedData } from './components';
99
import {
1010
ETHEREUM_PROVIDER_SNAP_ID,
1111
ETHEREUM_PROVIDER_SNAP_PORT,
@@ -60,6 +60,7 @@ export const EthereumProvider: FunctionComponent = () => {
6060
</span>
6161
</Result>
6262
<SignMessage />
63+
<SignTypedData />
6364
</Snap>
6465
);
6566
};

packages/test-snaps/src/features/snaps/ethereum-provider/components/SignMessage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const SignMessage: FunctionComponent = () => {
3333

3434
return (
3535
<>
36+
<h3 className="h5">Personal Sign</h3>
3637
<Form onSubmit={handleSubmit} className="mb-3">
3738
<Form.Label>Message</Form.Label>
3839
<Form.Control
@@ -48,7 +49,7 @@ export const SignMessage: FunctionComponent = () => {
4849
Sign Message
4950
</Button>
5051
</Form>
51-
<Result>
52+
<Result className="mb-3">
5253
<span id="personalSignResult">
5354
{JSON.stringify(data, null, 2)}
5455
{JSON.stringify(error, null, 2)}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { logError } from '@metamask/snaps-utils';
2+
import type { ChangeEvent, FormEvent, FunctionComponent } from 'react';
3+
import { useState } from 'react';
4+
import { Button, Form } from 'react-bootstrap';
5+
6+
import { useInvokeMutation } from '../../../../api';
7+
import { Result } from '../../../../components';
8+
import { getSnapId } from '../../../../utils';
9+
import {
10+
ETHEREUM_PROVIDER_SNAP_ID,
11+
ETHEREUM_PROVIDER_SNAP_PORT,
12+
} from '../constants';
13+
14+
export const SignTypedData: FunctionComponent = () => {
15+
const [message, setMessage] = useState('');
16+
const [invokeSnap, { isLoading, data, error }] = useInvokeMutation();
17+
18+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
19+
setMessage(event.target.value);
20+
};
21+
22+
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
23+
event.preventDefault();
24+
25+
invokeSnap({
26+
snapId: getSnapId(ETHEREUM_PROVIDER_SNAP_ID, ETHEREUM_PROVIDER_SNAP_PORT),
27+
method: 'signTypedData',
28+
params: {
29+
message,
30+
},
31+
}).catch(logError);
32+
};
33+
34+
return (
35+
<>
36+
<h3 className="h5">Sign Typed Data</h3>
37+
<Form onSubmit={handleSubmit} className="mb-3">
38+
<Form.Label>Message</Form.Label>
39+
<Form.Control
40+
type="text"
41+
placeholder="Message"
42+
value={message}
43+
onChange={handleChange}
44+
id="signTypedData"
45+
className="mb-3"
46+
/>
47+
48+
<Button type="submit" id="signTypedDataButton" disabled={isLoading}>
49+
Sign Typed Data
50+
</Button>
51+
</Form>
52+
<Result>
53+
<span id="signTypedDataResult">
54+
{JSON.stringify(data, null, 2)}
55+
{JSON.stringify(error, null, 2)}
56+
</span>
57+
</Result>
58+
</>
59+
);
60+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './SignMessage';
2+
export * from './SignTypedData';

0 commit comments

Comments
 (0)