Skip to content

Commit d6b2d0e

Browse files
MrtenzFrederikBolding
andauthoredNov 15, 2024··
Abstract device and device manager (#2880)
This adds an abstraction layer for devices using two abstract classes, `Device` (i.e., one device), and `DeviceManager` (which manages one or more `Device` classes). --------- Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>
1 parent a891ca3 commit d6b2d0e

37 files changed

+1233
-262
lines changed
 

‎packages/examples/packages/browserify-plugin/snap.manifest.json

+1-1
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": "HEAbfXBUqw5fNP+sJVyvUpXucZy6CwiCFXJi36CEfUw=",
10+
"shasum": "0SVjl4Z8ECRoFFjmcqh9C7PT29LKUT8+Y8DYD7/lzQ8=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

‎packages/examples/packages/browserify/snap.manifest.json

+1-1
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": "mCoDlMSdhDJAXd9zT74ST7jHysifHdQ8r0++b8uPbOs=",
10+
"shasum": "XNNYTsORJu7+K/g/oQlRLpN5RYbOzSY6WRHKWwi4mVg=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

‎packages/examples/packages/ledger/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Changelog
2+
23
All notable changes to this project will be documented in this file.
34

45
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

‎packages/examples/packages/ledger/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"dependencies": {
4646
"@ledgerhq/devices": "^8.4.4",
4747
"@ledgerhq/errors": "^6.19.1",
48+
"@ledgerhq/hw-app-eth": "^6.41.0",
4849
"@ledgerhq/hw-transport": "^6.31.4",
4950
"@metamask/snaps-sdk": "workspace:^",
5051
"@metamask/utils": "^10.0.0"

‎packages/examples/packages/ledger/snap.manifest.json

+1-1
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": "PaohqUxJniFTXeGB1eD3l3vIJHRBBPKAkmmlpk/K7H0=",
10+
"shasum": "OvH5LaGRY+j5/bDleyl7BHu7ces0k59Wmssm+fU2rfs=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
2+
import { Box, Button, Heading } from '@metamask/snaps-sdk/jsx';
3+
4+
export const ConnectHID: SnapComponent = () => (
5+
<Box>
6+
<Heading>Connect with HID</Heading>
7+
<Button name="connect-hid">Connect</Button>
8+
</Box>
9+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
2+
import { Box, Heading, Text } from '@metamask/snaps-sdk/jsx';
3+
4+
export const Unsupported: SnapComponent = () => (
5+
<Box>
6+
<Heading>Unsupported</Heading>
7+
<Text>Ledger hardware wallets are not supported in this browser.</Text>
8+
</Box>
9+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './ConnectHID';
2+
export * from './Unsupported';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect } from '@jest/globals';
2+
import { installSnap } from '@metamask/snaps-jest';
3+
4+
describe('onRpcRequest', () => {
5+
it('throws an error if the requested method does not exist', async () => {
6+
const { request } = await installSnap();
7+
8+
const response = await request({
9+
method: 'foo',
10+
});
11+
12+
expect(response).toRespondWithError({
13+
code: -32601,
14+
message: 'The method does not exist / is not available.',
15+
stack: expect.any(String),
16+
data: {
17+
method: 'foo',
18+
cause: null,
19+
},
20+
});
21+
});
22+
});
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
1+
import Eth from '@ledgerhq/hw-app-eth';
12
import type {
23
OnRpcRequestHandler,
34
OnUserInputHandler,
45
} from '@metamask/snaps-sdk';
5-
import { Box, Button, Text, Copyable } from '@metamask/snaps-sdk/jsx';
66
import { MethodNotFoundError } from '@metamask/snaps-sdk';
7-
import Eth from '@ledgerhq/hw-app-eth';
7+
import { Box, Button, Text, Copyable } from '@metamask/snaps-sdk/jsx';
8+
import { bytesToHex, stringToBytes } from '@metamask/utils';
89

10+
import { ConnectHID, Unsupported } from './components';
911
import TransportSnapsHID from './transport';
12+
import { signatureToHex } from './utils';
1013

14+
/**
15+
* Handle incoming JSON-RPC requests from the dapp, sent through the
16+
* `wallet_invokeSnap` method. This handler handles one method:
17+
*
18+
* - `request`: Display a dialog with a button to request a Ledger device. This
19+
* demonstrates how to request a device using Snaps, and how to handle user
20+
* input events, in order to sign a message with the device.
21+
*
22+
* Note that this only works in browsers that support the WebHID API, and
23+
* the Ledger device must be connected and unlocked.
24+
*
25+
* @param params - The request parameters.
26+
* @param params.request - The JSON-RPC request object.
27+
* @returns The JSON-RPC response.
28+
* @see https://docs.metamask.io/snaps/reference/exports/#onrpcrequest
29+
*/
1130
export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
1231
switch (request.method) {
1332
case 'request': {
33+
const Component = (await TransportSnapsHID.isSupported())
34+
? ConnectHID
35+
: Unsupported;
36+
1437
return snap.request({
1538
method: 'snap_dialog',
1639
params: {
17-
content: (
18-
<Box>
19-
<Button>Request Devices</Button>
20-
</Box>
21-
),
40+
content: <Component />,
2241
},
2342
});
2443
}
@@ -30,49 +49,51 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
3049
}
3150
};
3251

33-
function hexlifySignature(signature: { r: string; s: string; v: number }) {
34-
const adjustedV = signature.v - 27;
35-
let hexlifiedV = adjustedV.toString(16);
36-
if (hexlifiedV.length < 2) {
37-
hexlifiedV = '0' + hexlifiedV;
38-
}
39-
return `0x${signature.r}${signature.s}${hexlifiedV}`;
40-
}
41-
52+
/**
53+
* Handle incoming user events coming from the Snap interface. This handler
54+
* handles one event:
55+
*
56+
* - `connect-hid`: Request a Ledger device, sign a message, and display the
57+
* signature in the Snap interface.
58+
*
59+
* @param params - The event parameters.
60+
* @param params.id - The Snap interface ID where the event was fired.
61+
* @see https://docs.metamask.io/snaps/reference/exports/#onuserinput
62+
*/
4263
export const onUserInput: OnUserInputHandler = async ({ id }) => {
43-
try {
44-
const transport = await TransportSnapsHID.request();
45-
const eth = new Eth(transport);
46-
const msg = 'test';
47-
const { address } = await eth.getAddress("44'/60'/0'/0/0");
48-
const signature = await eth.signPersonalMessage(
49-
"44'/60'/0'/0/0",
50-
Buffer.from(msg).toString('hex'),
51-
);
64+
// TODO: Handle errors (i.e., Ledger locked, disconnected, etc.)
65+
const transport = await TransportSnapsHID.request();
66+
const eth = new Eth(transport);
5267

53-
const signatureHex = hexlifySignature(signature);
54-
const message = {
55-
address,
56-
msg,
57-
sig: signatureHex,
58-
version: 2,
59-
};
60-
await snap.request({
61-
method: 'snap_updateInterface',
62-
params: {
63-
id,
64-
ui: (
65-
<Box>
66-
<Button>Request Devices</Button>
67-
<Text>Signature:</Text>
68-
<Copyable value={signatureHex} />
69-
<Text>JSON:</Text>
70-
<Copyable value={JSON.stringify(message, null, 2)} />
71-
</Box>
72-
),
73-
},
74-
});
75-
} catch (error) {
76-
console.error(error);
77-
}
68+
// TODO: Make this message configurable.
69+
const message = 'test';
70+
const { address } = await eth.getAddress("44'/60'/0'/0/0");
71+
72+
const signature = await eth.signPersonalMessage(
73+
"44'/60'/0'/0/0",
74+
bytesToHex(stringToBytes(message)),
75+
);
76+
77+
const signatureHex = signatureToHex(signature);
78+
const signatureObject = {
79+
address,
80+
message,
81+
signature: signatureHex,
82+
};
83+
84+
await snap.request({
85+
method: 'snap_updateInterface',
86+
params: {
87+
id,
88+
ui: (
89+
<Box>
90+
<Button>Request Devices</Button>
91+
<Text>Signature:</Text>
92+
<Copyable value={signatureHex} />
93+
<Text>JSON:</Text>
94+
<Copyable value={JSON.stringify(signatureObject, null, 2)} />
95+
</Box>
96+
),
97+
},
98+
});
7899
};

‎packages/examples/packages/ledger/src/transport.ts

+29-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type {
88
Subscription,
99
} from '@ledgerhq/hw-transport';
1010
import Transport from '@ledgerhq/hw-transport';
11-
import type { HidDevice } from '@metamask/snaps-sdk';
11+
import type { HidDeviceMetadata } from '@metamask/snaps-sdk';
12+
import { DeviceType } from '@metamask/snaps-sdk';
1213
import { bytesToHex } from '@metamask/utils';
1314

1415
/**
@@ -21,19 +22,31 @@ async function requestDevice() {
2122
return (await snap.request({
2223
method: 'snap_requestDevice',
2324
params: { type: 'hid', filters: [{ vendorId: ledgerUSBVendorId }] },
24-
})) as HidDevice;
25+
})) as HidDeviceMetadata;
2526
}
2627

2728
export default class TransportSnapsHID extends Transport {
28-
readonly device: HidDevice;
29+
/**
30+
* The device metadata.
31+
*/
32+
readonly device: HidDeviceMetadata;
2933

34+
/**
35+
* The device model, if known.
36+
*/
3037
readonly deviceModel: DeviceModel | null | undefined;
3138

39+
/**
40+
* A random channel to use for communication with the device.
41+
*/
3242
#channel = Math.floor(Math.random() * 0xffff);
3343

44+
/**
45+
* The packet size to use for communication with the device.
46+
*/
3447
#packetSize = 64;
3548

36-
constructor(device: HidDevice) {
49+
constructor(device: HidDeviceMetadata) {
3750
super();
3851

3952
this.device = device;
@@ -51,7 +64,7 @@ export default class TransportSnapsHID extends Transport {
5164
method: 'snap_getSupportedDevices',
5265
});
5366

54-
return types.includes('hid');
67+
return types.includes(DeviceType.HID);
5568
}
5669

5770
/**
@@ -63,7 +76,7 @@ export default class TransportSnapsHID extends Transport {
6376
const devices = (await snap.request({
6477
method: 'snap_listDevices',
6578
params: { type: 'hid' },
66-
})) as HidDevice[];
79+
})) as HidDeviceMetadata[];
6780

6881
return devices.filter(
6982
(device) => device.vendorId === ledgerUSBVendorId && device.available,
@@ -77,7 +90,9 @@ export default class TransportSnapsHID extends Transport {
7790
* @param observer - The observer to notify when a device is found.
7891
* @returns A subscription that can be used to unsubscribe from the observer.
7992
*/
80-
static listen(observer: Observer<DescriptorEvent<HidDevice>>): Subscription {
93+
static listen(
94+
observer: Observer<DescriptorEvent<HidDeviceMetadata>>,
95+
): Subscription {
8196
let unsubscribed = false;
8297

8398
/**
@@ -92,7 +107,7 @@ export default class TransportSnapsHID extends Transport {
92107
*
93108
* @param device - The device to emit.
94109
*/
95-
function emit(device: HidDevice) {
110+
function emit(device: HidDeviceMetadata) {
96111
observer.next({
97112
type: 'add',
98113
descriptor: device,
@@ -181,7 +196,7 @@ export default class TransportSnapsHID extends Transport {
181196
* @param device - The device to connect to.
182197
* @returns A transport.
183198
*/
184-
static async open(device: HidDevice) {
199+
static async open(device: HidDeviceMetadata) {
185200
return new TransportSnapsHID(device);
186201
}
187202

@@ -234,6 +249,11 @@ export default class TransportSnapsHID extends Transport {
234249
});
235250
};
236251

252+
/**
253+
* Set the scramble key for the transport.
254+
*
255+
* This is not supported by the Snaps transport.
256+
*/
237257
setScrambleKey() {
238258
// This transport does not support setting a scramble key.
239259
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Create a hexadecimal encoded signature from a signature object.
3+
*
4+
* @param signature - The signature object.
5+
* @param signature.r - The `r` value of the signature.
6+
* @param signature.s - The `s` value of the signature.
7+
* @param signature.v - The `v` value of the signature.
8+
* @returns The hexadecimal encoded signature.
9+
*/
10+
export function signatureToHex(signature: { r: string; s: string; v: number }) {
11+
const adjustedV = signature.v - 27;
12+
const hexV = adjustedV.toString(16).padStart(2, '0');
13+
14+
return `0x${signature.r}${signature.s}${hexV}`;
15+
}

‎packages/snaps-controllers/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@
9393
"@metamask/snaps-sdk": "workspace:^",
9494
"@metamask/snaps-utils": "workspace:^",
9595
"@metamask/utils": "^9.2.1",
96+
"@types/w3c-web-hid": "^1.0.6",
9697
"@xstate/fsm": "^2.0.0",
98+
"async-mutex": "^0.4.0",
9799
"browserify-zlib": "^0.2.0",
98100
"concat-stream": "^2.0.0",
99101
"fast-deep-equal": "^3.1.3",

0 commit comments

Comments
 (0)
Please sign in to comment.