Skip to content
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
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": "HEAbfXBUqw5fNP+sJVyvUpXucZy6CwiCFXJi36CEfUw=",
"shasum": "0SVjl4Z8ECRoFFjmcqh9C7PT29LKUT8+Y8DYD7/lzQ8=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/browserify/snap.manifest.json
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": "mCoDlMSdhDJAXd9zT74ST7jHysifHdQ8r0++b8uPbOs=",
"shasum": "XNNYTsORJu7+K/g/oQlRLpN5RYbOzSY6WRHKWwi4mVg=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
1 change: 1 addition & 0 deletions packages/examples/packages/ledger/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
Expand Down
1 change: 1 addition & 0 deletions packages/examples/packages/ledger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"dependencies": {
"@ledgerhq/devices": "^8.4.4",
"@ledgerhq/errors": "^6.19.1",
"@ledgerhq/hw-app-eth": "^6.41.0",
"@ledgerhq/hw-transport": "^6.31.4",
"@metamask/snaps-sdk": "workspace:^",
"@metamask/utils": "^10.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/ledger/snap.manifest.json
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": "PaohqUxJniFTXeGB1eD3l3vIJHRBBPKAkmmlpk/K7H0=",
"shasum": "OvH5LaGRY+j5/bDleyl7BHu7ces0k59Wmssm+fU2rfs=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
import { Box, Button, Heading } from '@metamask/snaps-sdk/jsx';

export const ConnectHID: SnapComponent = () => (
<Box>
<Heading>Connect with HID</Heading>
<Button name="connect-hid">Connect</Button>
</Box>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
import { Box, Heading, Text } from '@metamask/snaps-sdk/jsx';

export const Unsupported: SnapComponent = () => (
<Box>
<Heading>Unsupported</Heading>
<Text>Ledger hardware wallets are not supported in this browser.</Text>
</Box>
);
2 changes: 2 additions & 0 deletions packages/examples/packages/ledger/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ConnectHID';
export * from './Unsupported';
22 changes: 22 additions & 0 deletions packages/examples/packages/ledger/src/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect } from '@jest/globals';
import { installSnap } from '@metamask/snaps-jest';

describe('onRpcRequest', () => {
it('throws an error if the requested method does not exist', async () => {
const { request } = await installSnap();

const response = await request({
method: 'foo',
});

expect(response).toRespondWithError({
code: -32601,
message: 'The method does not exist / is not available.',
stack: expect.any(String),
data: {
method: 'foo',
cause: null,
},
});
});
});
121 changes: 71 additions & 50 deletions packages/examples/packages/ledger/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
import Eth from '@ledgerhq/hw-app-eth';
import type {
OnRpcRequestHandler,
OnUserInputHandler,
} from '@metamask/snaps-sdk';
import { Box, Button, Text, Copyable } from '@metamask/snaps-sdk/jsx';
import { MethodNotFoundError } from '@metamask/snaps-sdk';
import Eth from '@ledgerhq/hw-app-eth';
import { Box, Button, Text, Copyable } from '@metamask/snaps-sdk/jsx';
import { bytesToHex, stringToBytes } from '@metamask/utils';

import { ConnectHID, Unsupported } from './components';
import TransportSnapsHID from './transport';
import { signatureToHex } from './utils';

/**
* Handle incoming JSON-RPC requests from the dapp, sent through the
* `wallet_invokeSnap` method. This handler handles one method:
*
* - `request`: Display a dialog with a button to request a Ledger device. This
* demonstrates how to request a device using Snaps, and how to handle user
* input events, in order to sign a message with the device.
*
* Note that this only works in browsers that support the WebHID API, and
* the Ledger device must be connected and unlocked.
*
* @param params - The request parameters.
* @param params.request - The JSON-RPC request object.
* @returns The JSON-RPC response.
* @see https://docs.metamask.io/snaps/reference/exports/#onrpcrequest
*/
export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
switch (request.method) {
case 'request': {
const Component = (await TransportSnapsHID.isSupported())
? ConnectHID
: Unsupported;

return snap.request({
method: 'snap_dialog',
params: {
content: (
<Box>
<Button>Request Devices</Button>
</Box>
),
content: <Component />,
},
});
}
Expand All @@ -30,49 +49,51 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
}
};

function hexlifySignature(signature: { r: string; s: string; v: number }) {
const adjustedV = signature.v - 27;
let hexlifiedV = adjustedV.toString(16);
if (hexlifiedV.length < 2) {
hexlifiedV = '0' + hexlifiedV;
}
return `0x${signature.r}${signature.s}${hexlifiedV}`;
}

/**
* Handle incoming user events coming from the Snap interface. This handler
* handles one event:
*
* - `connect-hid`: Request a Ledger device, sign a message, and display the
* signature in the Snap interface.
*
* @param params - The event parameters.
* @param params.id - The Snap interface ID where the event was fired.
* @see https://docs.metamask.io/snaps/reference/exports/#onuserinput
*/
export const onUserInput: OnUserInputHandler = async ({ id }) => {
try {
const transport = await TransportSnapsHID.request();
const eth = new Eth(transport);
const msg = 'test';
const { address } = await eth.getAddress("44'/60'/0'/0/0");
const signature = await eth.signPersonalMessage(
"44'/60'/0'/0/0",
Buffer.from(msg).toString('hex'),
);
// TODO: Handle errors (i.e., Ledger locked, disconnected, etc.)
const transport = await TransportSnapsHID.request();
const eth = new Eth(transport);

const signatureHex = hexlifySignature(signature);
const message = {
address,
msg,
sig: signatureHex,
version: 2,
};
await snap.request({
method: 'snap_updateInterface',
params: {
id,
ui: (
<Box>
<Button>Request Devices</Button>
<Text>Signature:</Text>
<Copyable value={signatureHex} />
<Text>JSON:</Text>
<Copyable value={JSON.stringify(message, null, 2)} />
</Box>
),
},
});
} catch (error) {
console.error(error);
}
// TODO: Make this message configurable.
const message = 'test';
const { address } = await eth.getAddress("44'/60'/0'/0/0");

const signature = await eth.signPersonalMessage(
"44'/60'/0'/0/0",
bytesToHex(stringToBytes(message)),
);

const signatureHex = signatureToHex(signature);
const signatureObject = {
address,
message,
signature: signatureHex,
};

await snap.request({
method: 'snap_updateInterface',
params: {
id,
ui: (
<Box>
<Button>Request Devices</Button>
<Text>Signature:</Text>
<Copyable value={signatureHex} />
<Text>JSON:</Text>
<Copyable value={JSON.stringify(signatureObject, null, 2)} />
</Box>
),
},
});
};
38 changes: 29 additions & 9 deletions packages/examples/packages/ledger/src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type {
Subscription,
} from '@ledgerhq/hw-transport';
import Transport from '@ledgerhq/hw-transport';
import type { HidDevice } from '@metamask/snaps-sdk';
import type { HidDeviceMetadata } from '@metamask/snaps-sdk';
import { DeviceType } from '@metamask/snaps-sdk';
import { bytesToHex } from '@metamask/utils';

/**
Expand All @@ -21,19 +22,31 @@ async function requestDevice() {
return (await snap.request({
method: 'snap_requestDevice',
params: { type: 'hid', filters: [{ vendorId: ledgerUSBVendorId }] },
})) as HidDevice;
})) as HidDeviceMetadata;
}

export default class TransportSnapsHID extends Transport {
readonly device: HidDevice;
/**
* The device metadata.
*/
readonly device: HidDeviceMetadata;

/**
* The device model, if known.
*/
readonly deviceModel: DeviceModel | null | undefined;

/**
* A random channel to use for communication with the device.
*/
#channel = Math.floor(Math.random() * 0xffff);

/**
* The packet size to use for communication with the device.
*/
#packetSize = 64;

constructor(device: HidDevice) {
constructor(device: HidDeviceMetadata) {
super();

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

return types.includes('hid');
return types.includes(DeviceType.HID);
}

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

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

/**
Expand All @@ -92,7 +107,7 @@ export default class TransportSnapsHID extends Transport {
*
* @param device - The device to emit.
*/
function emit(device: HidDevice) {
function emit(device: HidDeviceMetadata) {
observer.next({
type: 'add',
descriptor: device,
Expand Down Expand Up @@ -181,7 +196,7 @@ export default class TransportSnapsHID extends Transport {
* @param device - The device to connect to.
* @returns A transport.
*/
static async open(device: HidDevice) {
static async open(device: HidDeviceMetadata) {
return new TransportSnapsHID(device);
}

Expand Down Expand Up @@ -234,6 +249,11 @@ export default class TransportSnapsHID extends Transport {
});
};

/**
* Set the scramble key for the transport.
*
* This is not supported by the Snaps transport.
*/
setScrambleKey() {
// This transport does not support setting a scramble key.
}
Expand Down
15 changes: 15 additions & 0 deletions packages/examples/packages/ledger/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Create a hexadecimal encoded signature from a signature object.
*
* @param signature - The signature object.
* @param signature.r - The `r` value of the signature.
* @param signature.s - The `s` value of the signature.
* @param signature.v - The `v` value of the signature.
* @returns The hexadecimal encoded signature.
*/
export function signatureToHex(signature: { r: string; s: string; v: number }) {
const adjustedV = signature.v - 27;
const hexV = adjustedV.toString(16).padStart(2, '0');

return `0x${signature.r}${signature.s}${hexV}`;
}
2 changes: 2 additions & 0 deletions packages/snaps-controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@
"@metamask/snaps-sdk": "workspace:^",
"@metamask/snaps-utils": "workspace:^",
"@metamask/utils": "^9.2.1",
"@types/w3c-web-hid": "^1.0.6",
"@xstate/fsm": "^2.0.0",
"async-mutex": "^0.4.0",
"browserify-zlib": "^0.2.0",
"concat-stream": "^2.0.0",
"fast-deep-equal": "^3.1.3",
Expand Down
Loading
Loading