diff --git a/packages/examples/packages/bip32/README.md b/packages/examples/packages/bip32/README.md index 170604ab9f..c841341d79 100644 --- a/packages/examples/packages/bip32/README.md +++ b/packages/examples/packages/bip32/README.md @@ -56,4 +56,4 @@ JSON-RPC methods: (ECDSA for `secp256k1` and EdDSA for `ed25519`). For more information, you can refer to -[the end-to-end tests](./src/index.test.ts). +[the end-to-end tests](./src/index.test.tsx). diff --git a/packages/examples/packages/bip32/snap.config.ts b/packages/examples/packages/bip32/snap.config.ts index 910d5efc4a..a667f5b7d9 100644 --- a/packages/examples/packages/bip32/snap.config.ts +++ b/packages/examples/packages/bip32/snap.config.ts @@ -2,7 +2,7 @@ import type { SnapConfig } from '@metamask/snaps-cli'; import { resolve } from 'path'; const config: SnapConfig = { - input: resolve(__dirname, 'src/index.ts'), + input: resolve(__dirname, 'src/index.tsx'), server: { port: 8001, }, diff --git a/packages/examples/packages/bip32/snap.manifest.json b/packages/examples/packages/bip32/snap.manifest.json index 62b7cabfee..4276aeb027 100644 --- a/packages/examples/packages/bip32/snap.manifest.json +++ b/packages/examples/packages/bip32/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "dzOD3mPn8PgVXC3LT3OtKqs09WIcvuYo/mIZGfhsTPw=", + "shasum": "Yv3r9ZD/tEehqvSUa/EfcAKa61eYLNvhqtRXxYy++q4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/bip32/src/index.test.ts b/packages/examples/packages/bip32/src/index.test.tsx similarity index 61% rename from packages/examples/packages/bip32/src/index.test.ts rename to packages/examples/packages/bip32/src/index.test.tsx index 7aca481873..11704345c1 100644 --- a/packages/examples/packages/bip32/src/index.test.ts +++ b/packages/examples/packages/bip32/src/index.test.tsx @@ -1,6 +1,6 @@ import { expect } from '@jest/globals'; import { assertIsConfirmationDialog, installSnap } from '@metamask/snaps-jest'; -import { copyable, heading, panel, text } from '@metamask/snaps-sdk'; +import { Box, Copyable, Heading, Text } from '@metamask/snaps-sdk/jsx'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { @@ -38,6 +38,23 @@ describe('onRpcRequest', () => { ); }); + it('returns an secp256k1 public key for a given BIP-32 path with a different source', async () => { + const { request } = await installSnap(); + + const response = await request({ + method: 'getPublicKey', + params: { + path: ['m', "44'", "0'"], + curve: 'secp256k1', + source: 'alternative', + }, + }); + + expect(response).toRespondWith( + '0x04f72f0e3684b0d7295f391616f12a469070bfcd175c55366239047495a2c1c410b4d820fb4147de213a2d25fb19f9451354ad5949fc881a2d219529703416de73', + ); + }); + it('returns a compressed secp256k1 public key for a given BIP-32 path', async () => { const { request } = await installSnap(); @@ -110,15 +127,14 @@ describe('onRpcRequest', () => { const ui = await response.getInterface(); assertIsConfirmationDialog(ui); expect(ui).toRender( - panel([ - heading('Signature request'), - text( - `Do you want to secp256k1 sign "Hello, world!" with the following public key?`, - ), - copyable( - '0x0423a6a6f8800b2d0710595969f40148a28953c9eebc0c0da78a89be3b3935f59c0069dfe1cace1a083e9c962c9f2ef932e9346cd907e647d993d787c4e59d03d1', - ), - ]), + + Signature request + + Do you want to {'secp256k1'} sign "{'Hello, world!'}" with the + following public key? + + + , ); await ui.ok(); @@ -128,6 +144,39 @@ describe('onRpcRequest', () => { ); }); + it('signs a message for the given BIP-32 path using secp256k1 with a different source', async () => { + const { request } = await installSnap(); + + const response = request({ + method: 'signMessage', + params: { + path: ['m', "44'", "0'"], + curve: 'secp256k1', + message: 'Hello, world!', + source: 'alternative', + }, + }); + + const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); + expect(ui).toRender( + + Signature request + + Do you want to {'secp256k1'} sign "{'Hello, world!'}" with the + following public key? + + + , + ); + + await ui.ok(); + + expect(await response).toRespondWith( + '0x3044022049a3e74ed526df8b2a8e16e95a181d909255c90f6f63eb8efc16625af917b07d022014f2b203b0749058cbfc3ad0456c7b2bdf1ab809fd5913c6ee272cfc56f30ef2', + ); + }); + it('signs a message for the given BIP-32 path using ed25519', async () => { const { request } = await installSnap(); @@ -143,15 +192,14 @@ describe('onRpcRequest', () => { const ui = await response.getInterface(); assertIsConfirmationDialog(ui); expect(ui).toRender( - panel([ - heading('Signature request'), - text( - `Do you want to ed25519 sign "Hello, world!" with the following public key?`, - ), - copyable( - '0x000b96ba23cae9597de51e0187d7ef1b2d1a782dc2d5ceac770a327de3844dd533', - ), - ]), + + Signature request + + Do you want to {'ed25519'} sign "{'Hello, world!'}" with the + following public key? + + + , ); await ui.ok(); @@ -176,15 +224,14 @@ describe('onRpcRequest', () => { const ui = await response.getInterface(); assertIsConfirmationDialog(ui); expect(ui).toRender( - panel([ - heading('Signature request'), - text( - `Do you want to ed25519Bip32 sign "Hello, world!" with the following public key?`, - ), - copyable( - '0x2c3ac523b470dead7981df46c93d894ed4381e94c23aa1ec3806a320ff8ceb42', - ), - ]), + + Signature request + + Do you want to {'ed25519Bip32'} sign "{'Hello, world!'}" with the + following public key? + + + , ); await ui.ok(); @@ -217,4 +264,29 @@ describe('onRpcRequest', () => { }); }); }); + + describe('getEntropySources', () => { + it('returns the entropy sources', async () => { + const { request } = await installSnap(); + + const response = request({ + method: 'getEntropySources', + }); + + expect(await response).toRespondWith([ + { + id: 'default', + name: 'Default Secret Recovery Phrase', + type: 'mnemonic', + primary: true, + }, + { + id: 'alternative', + name: 'Alternative Secret Recovery Phrase', + type: 'mnemonic', + primary: false, + }, + ]); + }); + }); }); diff --git a/packages/examples/packages/bip32/src/index.ts b/packages/examples/packages/bip32/src/index.tsx similarity index 82% rename from packages/examples/packages/bip32/src/index.ts rename to packages/examples/packages/bip32/src/index.tsx index f9a196c70b..fb42800f09 100644 --- a/packages/examples/packages/bip32/src/index.ts +++ b/packages/examples/packages/bip32/src/index.tsx @@ -1,14 +1,11 @@ import type { OnRpcRequestHandler } from '@metamask/snaps-sdk'; import { DialogType, - panel, - text, - heading, - copyable, InvalidParamsError, UserRejectedRequestError, MethodNotFoundError, } from '@metamask/snaps-sdk'; +import { Box, Copyable, Heading, Text } from '@metamask/snaps-sdk/jsx'; import { add0x, assert, @@ -30,6 +27,7 @@ import { getPrivateNode, getPublicKey } from './utils'; * key is returned in hex format. * - `signMessage`: Derive a BIP-32 private key for a given BIP-32 path, and use * it to sign a message. The signature is returned in hex format. + * - `getEntropySources`: Get the list of entropy sources available to the Snap. * * @param params - The request parameters. * @param params.request - The JSON-RPC request object. @@ -62,13 +60,16 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { method: 'snap_dialog', params: { type: DialogType.Confirmation, - content: panel([ - heading('Signature request'), - text( - `Do you want to ${curve} sign "${message}" with the following public key?`, - ), - copyable(add0x(node.publicKey)), - ]), + content: ( + + Signature request + + Do you want to {curve} sign "{message}" with the following + public key? + + + + ), }, }); @@ -97,6 +98,12 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { throw new Error(`Unsupported curve: ${String(curve)}.`); } + case 'getEntropySources': { + return await snap.request({ + method: 'snap_listEntropySources', + }); + } + default: throw new MethodNotFoundError({ method: request.method }); } diff --git a/packages/examples/packages/bip32/src/types.ts b/packages/examples/packages/bip32/src/types.ts index 38a3b07df4..0d7c957eda 100644 --- a/packages/examples/packages/bip32/src/types.ts +++ b/packages/examples/packages/bip32/src/types.ts @@ -20,6 +20,12 @@ export type GetBip32PublicKeyParams = { */ compressed?: boolean | undefined; + /** + * The entropy source to use for the signature. If not provided, the primary + * entropy source will be used. + */ + source?: string | undefined; + /** * Miscellaneous parameters, which are passed to `snap_getBip32PublicKey`. */ @@ -47,4 +53,10 @@ export type SignMessageParams = { * The curve used to derive the account. */ curve: 'secp256k1' | 'ed25519' | 'ed25519Bip32'; + + /** + * The entropy source to use for the signature. If not provided, the primary + * entropy source will be used. + */ + source?: string | undefined; }; diff --git a/packages/examples/packages/bip44/README.md b/packages/examples/packages/bip44/README.md index 56c13c1c3e..4de7507a0c 100644 --- a/packages/examples/packages/bip44/README.md +++ b/packages/examples/packages/bip44/README.md @@ -44,4 +44,4 @@ JSON-RPC methods: elliptic curve to sign the message. For more information, you can refer to -[the end-to-end tests](./src/index.test.ts). +[the end-to-end tests](./src/index.test.tsx). diff --git a/packages/examples/packages/bip44/snap.config.ts b/packages/examples/packages/bip44/snap.config.ts index 15b2a229ba..804529ef52 100644 --- a/packages/examples/packages/bip44/snap.config.ts +++ b/packages/examples/packages/bip44/snap.config.ts @@ -2,7 +2,7 @@ import type { SnapConfig } from '@metamask/snaps-cli'; import { resolve } from 'path'; const config: SnapConfig = { - input: resolve(__dirname, 'src/index.ts'), + input: resolve(__dirname, 'src/index.tsx'), server: { port: 8002, }, diff --git a/packages/examples/packages/bip44/snap.manifest.json b/packages/examples/packages/bip44/snap.manifest.json index 7f013f7763..f90ec24e8d 100644 --- a/packages/examples/packages/bip44/snap.manifest.json +++ b/packages/examples/packages/bip44/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "t0+OXVvAOnJOEgxjLB9Gz9CtAhNJr0nj9WC7nEeEZUs=", + "shasum": "s8y7K5HyKWgxVflPhs/Tdt69c/L2QT/vixntD9dnu+o=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/bip44/src/index.test.ts b/packages/examples/packages/bip44/src/index.test.tsx similarity index 57% rename from packages/examples/packages/bip44/src/index.test.ts rename to packages/examples/packages/bip44/src/index.test.tsx index 92f8794857..39008c704d 100644 --- a/packages/examples/packages/bip44/src/index.test.ts +++ b/packages/examples/packages/bip44/src/index.test.tsx @@ -1,6 +1,6 @@ import { expect } from '@jest/globals'; import { assertIsConfirmationDialog, installSnap } from '@metamask/snaps-jest'; -import { copyable, heading, panel, text } from '@metamask/snaps-sdk'; +import { Box, Copyable, Heading, Text } from '@metamask/snaps-sdk/jsx'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { @@ -38,6 +38,23 @@ describe('onRpcRequest', () => { ); }); + it('returns a BIP-44 public key for a given coin type and address index with a different source', async () => { + const { request } = await installSnap(); + + const response = await request({ + method: 'getPublicKey', + params: { + coinType: 3, + addressIndex: 5, + source: 'alternative', + }, + }); + + expect(response).toRespondWith( + '0xadf494f336c373de010491dfb442a9dda4b6f640fbb81f0139deed7edb236817eaf8621722b01553134c4c91fbe89c45', + ); + }); + it('returns a BIP-44 public key for the default coin type and address index if no parameters are provided', async () => { const { request } = await installSnap(); @@ -86,15 +103,14 @@ describe('onRpcRequest', () => { assertIsConfirmationDialog(ui); expect(ui).toRender( - panel([ - heading('Signature request'), - text( - `Do you want to BLS sign "Hello, world!" with the following public key?`, - ), - copyable( - '0x96e2b36a8af526928326683f6d8ddb82fbfcd1ba1cd3f0382a4f092a19fcb46b87e836dd34075514c9b1a3b8f7bdc4f0', - ), - ]), + + Signature request + + Do you want to BLS sign "{'Hello, world!'}" with the following + public key? + + + , ); await ui.ok(); @@ -104,6 +120,40 @@ describe('onRpcRequest', () => { ); }); + it('signs a message for the given coin type and address index with a different source', async () => { + const { request } = await installSnap(); + + const response = request({ + method: 'signMessage', + params: { + coinType: 3, + addressIndex: 5, + message: 'Hello, world!', + source: 'alternative', + }, + }); + + const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); + + expect(ui).toRender( + + Signature request + + Do you want to BLS sign "{'Hello, world!'}" with the following + public key? + + + , + ); + + await ui.ok(); + + expect(await response).toRespondWith( + '0xb01b6dd84f1aae227b25f8679d739508112c304819276843d057806e436cc0ccba16966e7b5b1bef1d55699531f31ed40908ce2a8d349e0bb5a8a5d840fb928b3fad499c6f117afdc740ac205d76c1ece8d4134822f88156243e926ddac99ab0', + ); + }); + it('signs a message using the default coin type and address index', async () => { const { request } = await installSnap(); @@ -118,15 +168,14 @@ describe('onRpcRequest', () => { assertIsConfirmationDialog(ui); expect(ui).toRender( - panel([ - heading('Signature request'), - text( - `Do you want to BLS sign "Hello, world!" with the following public key?`, - ), - copyable( - '0xa9ad546540fca1662bdf3de110a456f2d825271e6d960cc5028224d4dc37c0e7fdd806b22fe94d9325548933e9c1ee68', - ), - ]), + + Signature request + + Do you want to BLS sign "{'Hello, world!'}" with the following + public key? + + + , ); await ui.ok(); @@ -158,4 +207,29 @@ describe('onRpcRequest', () => { }); }); }); + + describe('getEntropySources', () => { + it('returns the entropy sources', async () => { + const { request } = await installSnap(); + + const response = request({ + method: 'getEntropySources', + }); + + expect(await response).toRespondWith([ + { + id: 'default', + name: 'Default Secret Recovery Phrase', + type: 'mnemonic', + primary: true, + }, + { + id: 'alternative', + name: 'Alternative Secret Recovery Phrase', + type: 'mnemonic', + primary: false, + }, + ]); + }); + }); }); diff --git a/packages/examples/packages/bip44/src/index.ts b/packages/examples/packages/bip44/src/index.tsx similarity index 75% rename from packages/examples/packages/bip44/src/index.ts rename to packages/examples/packages/bip44/src/index.tsx index 4025ea5155..f845116c14 100644 --- a/packages/examples/packages/bip44/src/index.ts +++ b/packages/examples/packages/bip44/src/index.tsx @@ -1,13 +1,10 @@ import type { OnRpcRequestHandler } from '@metamask/snaps-sdk'; import { DialogType, - panel, - text, - heading, - copyable, MethodNotFoundError, UserRejectedRequestError, } from '@metamask/snaps-sdk'; +import { Box, Copyable, Heading, Text } from '@metamask/snaps-sdk/jsx'; import { bytesToHex, stringToBytes } from '@metamask/utils'; import { getPublicKey, sign } from '@noble/bls12-381'; @@ -16,13 +13,14 @@ import { getPrivateKey } from './utils'; /** * Handle incoming JSON-RPC requests from the dapp, sent through the - * `wallet_invokeSnap` method. This handler handles two methods: + * `wallet_invokeSnap` method. This handler handles three methods: * * - `getPublicKey`: Get a BIP-44 public key for a given BIP-44 coin type and * address index. The public key is returned in hex format. * - `signMessage`: Derive a BIP-44 private key for a given BIP-44 coin type and * address index, and use it to sign a message. The signature is returned in hex * format. + * - `getEntropySources`: Get the list of entropy sources available to the Snap. * * @param params - The request parameters. * @param params.request - The JSON-RPC request object. @@ -48,13 +46,16 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { method: 'snap_dialog', params: { type: DialogType.Confirmation, - content: panel([ - heading('Signature request'), - text( - `Do you want to BLS sign "${message}" with the following public key?`, - ), - copyable(publicKey), - ]), + content: ( + + Signature request + + Do you want to BLS sign "{message}" with the following public + key? + + + + ), }, }); @@ -66,6 +67,12 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { return bytesToHex(newLocal); } + case 'getEntropySources': { + return await snap.request({ + method: 'snap_listEntropySources', + }); + } + default: throw new MethodNotFoundError({ method: request.method }); } diff --git a/packages/examples/packages/bip44/src/types.ts b/packages/examples/packages/bip44/src/types.ts index 8f76d8044c..342f6ff35d 100644 --- a/packages/examples/packages/bip44/src/types.ts +++ b/packages/examples/packages/bip44/src/types.ts @@ -14,6 +14,12 @@ export type GetAccountParams = { * defaults to the first address (`address_index` = 0). */ addressIndex?: number; + + /** + * The entropy source to use for the signature. If not provided, the primary + * entropy source will be used. + */ + source?: string | undefined; }; /** diff --git a/packages/examples/packages/bip44/src/utils.ts b/packages/examples/packages/bip44/src/utils.ts index 622073abf4..1f5ea3c464 100644 --- a/packages/examples/packages/bip44/src/utils.ts +++ b/packages/examples/packages/bip44/src/utils.ts @@ -13,12 +13,14 @@ import type { GetAccountParams } from './types'; * specified, it defaults to the Bitcoin coin type (1). * @param params.addressIndex - The address index to get the account for. If * this is not specified, it defaults to the first address (`address_index` = 0). + * @param params.source - The entropy source to use for the entropy derivation. * @returns A private key, as a hexadecimal string, without the leading `0x`. * @see https://docs.metamask.io/snaps/reference/rpc-api/#snap_getbip44entropy */ export const getPrivateKey = async ({ coinType = 1, addressIndex = 0, + source, }: GetAccountParams = {}) => { // `snap_getBip44Entropy` returns a `JsonBIP44CoinTypeNode` object, which can // be used with the `deriveBIP44AddressKey` function from `@metamask/key-tree` @@ -27,6 +29,7 @@ export const getPrivateKey = async ({ method: 'snap_getBip44Entropy', params: { coinType, + source, }, }); diff --git a/packages/examples/packages/get-entropy/src/index.tsx b/packages/examples/packages/get-entropy/src/index.tsx index 5598dea4cc..815155d6f3 100644 --- a/packages/examples/packages/get-entropy/src/index.tsx +++ b/packages/examples/packages/get-entropy/src/index.tsx @@ -13,10 +13,11 @@ import { getEntropy, getEntropySourceName } from './utils'; /** * Handle incoming JSON-RPC requests from the dapp, sent through the - * `wallet_invokeSnap` method. This handler handles a single method: + * `wallet_invokeSnap` method. This handler handles two methods: * - * - `signMessage`: Derive a private key using the snap's own entropy, and sign + * - `signMessage`: Derive a private key using the Snap's own entropy, and sign * a message using it. The signature is returned in hex format. + * - `getEntropySources`: Get the list of entropy sources available to the Snap. * * @param params - The request parameters. * @param params.request - The JSON-RPC request object. diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 96d5e326d5..67c7595d39 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 95.17, - functions: 98.61, - lines: 98.81, - statements: 98.49, + branches: 95.19, + functions: 98.62, + lines: 98.82, + statements: 98.5, }, }, }); diff --git a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts index c984075863..2a725e6686 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip32Entropy.ts @@ -16,7 +16,7 @@ import type { NonEmptyArray } from '@metamask/utils'; import { assert } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; -import { getNode } from '../utils'; +import { getSecretRecoveryPhrase, getNode } from '../utils'; const targetName = 'snap_getBip32Entropy'; @@ -130,10 +130,15 @@ export function getBip32EntropyImplementation({ const { params } = args; assert(params); + const secretRecoveryPhrase = await getSecretRecoveryPhrase( + getMnemonic, + params.source, + ); + const node = await getNode({ curve: params.curve, path: params.path, - secretRecoveryPhrase: await getMnemonic(params.source), + secretRecoveryPhrase, cryptographicFunctions: getClientCryptography(), }); diff --git a/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts b/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts index 316e7bd9df..c0ff3d1c4e 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip32PublicKey.ts @@ -22,7 +22,7 @@ import type { NonEmptyArray } from '@metamask/utils'; import { assertStruct } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; -import { getNode } from '../utils'; +import { getSecretRecoveryPhrase, getNode } from '../utils'; const targetName = 'snap_getBip32PublicKey'; @@ -150,10 +150,15 @@ export function getBip32PublicKeyImplementation({ ); const { params } = args; + const secretRecoveryPhrase = await getSecretRecoveryPhrase( + getMnemonic, + params.source, + ); + const node = await getNode({ curve: params.curve, path: params.path, - secretRecoveryPhrase: await getMnemonic(params.source), + secretRecoveryPhrase, cryptographicFunctions: getClientCryptography(), }); diff --git a/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts b/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts index 833349a78b..709e195fff 100644 --- a/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts +++ b/packages/snaps-rpc-methods/src/restricted/getBip44Entropy.ts @@ -16,6 +16,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import type { NonEmptyArray } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; +import { getSecretRecoveryPhrase } from '../utils'; const targetName = 'snap_getBip44Entropy'; @@ -131,13 +132,13 @@ export function getBip44EntropyImplementation({ // `args.params` is validated by the decorator, so it's safe to assert here. const params = args.params as GetBip44EntropyParams; + const secretRecoveryPhrase = await getSecretRecoveryPhrase( + getMnemonic, + params.source, + ); const node = await BIP44CoinTypeNode.fromDerivationPath( - [ - await getMnemonic(params.source), - `bip32:44'`, - `bip32:${params.coinType}'`, - ], + [secretRecoveryPhrase, `bip32:44'`, `bip32:${params.coinType}'`], 'mainnet', getClientCryptography(), ); diff --git a/packages/snaps-rpc-methods/src/restricted/getEntropy.ts b/packages/snaps-rpc-methods/src/restricted/getEntropy.ts index b872a363ff..032fd0625e 100644 --- a/packages/snaps-rpc-methods/src/restricted/getEntropy.ts +++ b/packages/snaps-rpc-methods/src/restricted/getEntropy.ts @@ -14,7 +14,7 @@ import type { NonEmptyArray } from '@metamask/utils'; import { assertStruct } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; -import { deriveEntropy } from '../utils'; +import { getSecretRecoveryPhrase, deriveEntropy } from '../utils'; const targetName = 'snap_getEntropy'; @@ -136,7 +136,10 @@ function getEntropyImplementation({ ); await getUnlockPromise(true); - const mnemonicPhrase = await getMnemonic(params.source); + const mnemonicPhrase = await getSecretRecoveryPhrase( + getMnemonic, + params.source, + ); return deriveEntropy({ input: origin, diff --git a/packages/snaps-rpc-methods/src/utils.test.ts b/packages/snaps-rpc-methods/src/utils.test.ts index 211be807ef..4d064eb5a3 100644 --- a/packages/snaps-rpc-methods/src/utils.test.ts +++ b/packages/snaps-rpc-methods/src/utils.test.ts @@ -1,3 +1,4 @@ +import { rpcErrors } from '@metamask/rpc-errors'; import { SIP_6_MAGIC_VALUE } from '@metamask/snaps-utils'; import { TEST_SECRET_RECOVERY_PHRASE_BYTES } from '@metamask/snaps-utils/test-utils'; import { create, is } from '@metamask/superstruct'; @@ -7,6 +8,7 @@ import { deriveEntropy, getNode, getPathPrefix, + getSecretRecoveryPhrase, isValidStateKey, StateKeyStruct, } from './utils'; @@ -127,3 +129,42 @@ describe('StateKeyStruct', () => { }, ); }); + +describe('getSecretRecoveryPhrase', () => { + it('returns the secret recovery phrase', async () => { + const getMnemonic = jest + .fn() + .mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES); + + const secretRecoveryPhrase = await getSecretRecoveryPhrase( + getMnemonic, + 'foo', + ); + + expect(secretRecoveryPhrase).toBe(TEST_SECRET_RECOVERY_PHRASE_BYTES); + expect(getMnemonic).toHaveBeenCalledWith('foo'); + }); + + it('throws an invalid params error if `getMnemonic` throws with an error', async () => { + const getMnemonic = jest.fn().mockRejectedValue(new Error('foo')); + + await expect(getSecretRecoveryPhrase(getMnemonic)).rejects.toThrow( + rpcErrors.invalidParams({ + message: 'foo', + }), + ); + }); + + it('throws an internal error if `getMnemonic` throws with a non-error', async () => { + const getMnemonic = jest.fn().mockRejectedValue('foo'); + + await expect(getSecretRecoveryPhrase(getMnemonic)).rejects.toThrow( + rpcErrors.internal({ + message: 'An unknown error occurred.', + data: { + error: 'foo', + }, + }), + ); + }); +}); diff --git a/packages/snaps-rpc-methods/src/utils.ts b/packages/snaps-rpc-methods/src/utils.ts index ae76a7f16a..3803262790 100644 --- a/packages/snaps-rpc-methods/src/utils.ts +++ b/packages/snaps-rpc-methods/src/utils.ts @@ -6,6 +6,7 @@ import type { CryptographicFunctions, } from '@metamask/key-tree'; import { SLIP10Node } from '@metamask/key-tree'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { MagicValue } from '@metamask/snaps-utils'; import { refine, string } from '@metamask/superstruct'; import type { Hex } from '@metamask/utils'; @@ -263,3 +264,33 @@ export const StateKeyStruct = refine(string(), 'state key', (value) => { return true; }); + +/** + * Get the secret recovery phrase of the user. This calls the `getMnemonic` hook + * and handles any errors that occur, throwing formatted JSON-RPC errors. + * + * @param getMnemonic - The `getMnemonic` hook. + * @param source - The entropy source to use. + * @returns The secret recovery phrase. + */ +export async function getSecretRecoveryPhrase( + getMnemonic: (source?: string | undefined) => Promise, + source?: string | undefined, +): Promise { + try { + return await getMnemonic(source); + } catch (error) { + if (error instanceof Error) { + throw rpcErrors.invalidParams({ + message: error.message, + }); + } + + throw rpcErrors.internal({ + message: 'An unknown error occurred.', + data: { + error: error.toString(), + }, + }); + } +} diff --git a/packages/snaps-sdk/src/types/methods/list-entropy-sources.ts b/packages/snaps-sdk/src/types/methods/list-entropy-sources.ts index 2328165b0e..854dccf08d 100644 --- a/packages/snaps-sdk/src/types/methods/list-entropy-sources.ts +++ b/packages/snaps-sdk/src/types/methods/list-entropy-sources.ts @@ -26,9 +26,6 @@ export type EntropySource = { /** * The request parameters for the `snap_listEntropySources` method. - * - * @property snapId - The ID of the snap to invoke. - * @property request - The JSON-RPC request to send to the snap. */ export type ListEntropySourcesParams = never; diff --git a/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.ts b/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.ts index 56c7e0f6d9..d8adce3945 100644 --- a/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.ts +++ b/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.ts @@ -2,8 +2,8 @@ * Get the implementation of the `getEntropySources` hook. * * @returns The implementation of the `getEntropySources` hook. Right now, it - * only returns a single hard coded entropy source. In the future, it could - * return a configurable list of entropy sources. + * only returns two hard coded entropy source. In the future, it could return a + * configurable list of entropy sources. */ export function getGetEntropySourcesImplementation() { return () => { diff --git a/packages/snaps-simulation/src/methods/hooks/get-mnemonic.test.ts b/packages/snaps-simulation/src/methods/hooks/get-mnemonic.test.ts index a50c06db34..2f77680db8 100644 --- a/packages/snaps-simulation/src/methods/hooks/get-mnemonic.test.ts +++ b/packages/snaps-simulation/src/methods/hooks/get-mnemonic.test.ts @@ -36,7 +36,7 @@ describe('getGetMnemonicImplementation', () => { it('throws an error for an unknown entropy source', async () => { const getMnemonic = getGetMnemonicImplementation(); await expect(getMnemonic('unknown')).rejects.toThrow( - 'Unknown entropy source: "unknown".', + 'Entropy source with ID "unknown" not found.', ); }); }); diff --git a/packages/snaps-simulation/src/methods/hooks/get-mnemonic.ts b/packages/snaps-simulation/src/methods/hooks/get-mnemonic.ts index 6c0a615df0..491f0b6965 100644 --- a/packages/snaps-simulation/src/methods/hooks/get-mnemonic.ts +++ b/packages/snaps-simulation/src/methods/hooks/get-mnemonic.ts @@ -23,7 +23,7 @@ export function getGetMnemonicImplementation( case 'alternative': return mnemonicPhraseToBytes(DEFAULT_ALTERNATIVE_SRP); default: - throw new Error(`Unknown entropy source: "${source}".`); + throw new Error(`Entropy source with ID "${source}" not found.`); } }; } diff --git a/packages/test-snaps/src/features/snaps/bip32/BIP32.tsx b/packages/test-snaps/src/features/snaps/bip32/BIP32.tsx index 8ee6490ae5..e446e25ef9 100644 --- a/packages/test-snaps/src/features/snaps/bip32/BIP32.tsx +++ b/packages/test-snaps/src/features/snaps/bip32/BIP32.tsx @@ -3,8 +3,14 @@ import type { FunctionComponent } from 'react'; import { PublicKey, SignMessage } from './components'; import { BIP_32_PORT, BIP_32_SNAP_ID, BIP_32_VERSION } from './constants'; import { Snap } from '../../../components'; +import { useEntropySelector } from '../get-entropy/hooks'; export const BIP32: FunctionComponent = () => { + const { selector, source } = useEntropySelector({ + snapId: BIP_32_SNAP_ID, + port: BIP_32_PORT, + }); + return ( { version={BIP_32_VERSION} testId="bip32" > - - - - + {selector} + + + + ); }; diff --git a/packages/test-snaps/src/features/snaps/bip32/components/PublicKey.tsx b/packages/test-snaps/src/features/snaps/bip32/components/PublicKey.tsx index 342955a165..b9c18993a3 100644 --- a/packages/test-snaps/src/features/snaps/bip32/components/PublicKey.tsx +++ b/packages/test-snaps/src/features/snaps/bip32/components/PublicKey.tsx @@ -8,7 +8,11 @@ import { Result } from '../../../../components'; import { getSnapId } from '../../../../utils'; import { BIP_32_PORT, BIP_32_SNAP_ID } from '../constants'; -export const PublicKey: FunctionComponent = () => { +export type PublicKeyProps = { + source?: string | undefined; +}; + +export const PublicKey: FunctionComponent = ({ source }) => { const [invokeSnap, { isLoading, data }] = useInvokeMutation(); const handleClick = (method: string, params: JsonRpcParams) => () => { @@ -28,6 +32,7 @@ export const PublicKey: FunctionComponent = () => { path: ['m', "44'", "0'"], curve: 'secp256k1', compressed: false, + ...(source !== undefined && { source }), })} disabled={isLoading} > @@ -51,6 +56,7 @@ export const PublicKey: FunctionComponent = () => { onClick={handleClick('getPublicKey', { path: ['m', "44'", "1'"], curve: 'secp256k1', + ...(source !== undefined && { source }), })} disabled={isLoading} > diff --git a/packages/test-snaps/src/features/snaps/bip32/components/SignMessage.tsx b/packages/test-snaps/src/features/snaps/bip32/components/SignMessage.tsx index 60a9665c9d..6ee4cd3b82 100644 --- a/packages/test-snaps/src/features/snaps/bip32/components/SignMessage.tsx +++ b/packages/test-snaps/src/features/snaps/bip32/components/SignMessage.tsx @@ -10,9 +10,13 @@ import { BIP_32_PORT, BIP_32_SNAP_ID } from '../constants'; export type SignMessageProps = { curve: 'secp256k1' | 'ed25519' | 'ed25519Bip32'; + source?: string | undefined; }; -export const SignMessage: FunctionComponent = ({ curve }) => { +export const SignMessage: FunctionComponent = ({ + curve, + source, +}) => { const [message, setMessage] = useState(''); const [invokeSnap, { isLoading, data, error }] = useInvokeMutation(); @@ -30,6 +34,7 @@ export const SignMessage: FunctionComponent = ({ curve }) => { message, curve, path: ['m', "44'", "0'"], + ...(source !== undefined && { source }), }, }).catch(logError); }; diff --git a/packages/test-snaps/src/features/snaps/bip44/BIP44.tsx b/packages/test-snaps/src/features/snaps/bip44/BIP44.tsx index 4aa047269a..2c64ab54ab 100644 --- a/packages/test-snaps/src/features/snaps/bip44/BIP44.tsx +++ b/packages/test-snaps/src/features/snaps/bip44/BIP44.tsx @@ -7,9 +7,14 @@ import { BIP_44_PORT, BIP_44_SNAP_ID, BIP_44_VERSION } from './constants'; import { useInvokeMutation } from '../../../api'; import { Result, Snap } from '../../../components'; import { getSnapId } from '../../../utils'; +import { useEntropySelector } from '../get-entropy/hooks'; export const BIP44: FunctionComponent = () => { const [invokeSnap, { isLoading, data, error }] = useInvokeMutation(); + const { selector, source } = useEntropySelector({ + snapId: BIP_44_SNAP_ID, + port: BIP_44_PORT, + }); const handleClick = (method: string, coinType: number) => () => { invokeSnap({ @@ -17,6 +22,7 @@ export const BIP44: FunctionComponent = () => { method, params: { coinType, + ...(source !== undefined && { source }), }, }).catch(logError); }; @@ -29,6 +35,7 @@ export const BIP44: FunctionComponent = () => { version={BIP_44_VERSION} testId="bip44" > + {selector} { - const [source, setSource] = useState(undefined); + const { selector, source } = useEntropySelector({ + raw: true, + snapId: GET_ENTROPY_SNAP_ID, + port: GET_ENTROPY_PORT, + }); return ( { version={GET_ENTROPY_VERSION} testId="GetEntropySnap" > - + {selector} ); diff --git a/packages/test-snaps/src/features/snaps/get-entropy/components/EntropySources.tsx b/packages/test-snaps/src/features/snaps/get-entropy/components/EntropySelector.tsx similarity index 61% rename from packages/test-snaps/src/features/snaps/get-entropy/components/EntropySources.tsx rename to packages/test-snaps/src/features/snaps/get-entropy/components/EntropySelector.tsx index 60ef77b688..bbbceab55e 100644 --- a/packages/test-snaps/src/features/snaps/get-entropy/components/EntropySources.tsx +++ b/packages/test-snaps/src/features/snaps/get-entropy/components/EntropySelector.tsx @@ -2,10 +2,11 @@ import type { EntropySource } from '@metamask/snaps-sdk'; import type { ChangeEvent, FunctionComponent } from 'react'; import { Result } from '../../../../components'; -import { useEntropySources } from '../hooks'; export type EntropySourcesProps = { - onChange: (source: string) => void; + sources: EntropySource[]; + raw: boolean; + onChange: (source: string | undefined) => void; }; /** @@ -24,37 +25,42 @@ function getSourceName(source: EntropySource) { return name; } -export const EntropySources: FunctionComponent = ({ +export const EntropySelector: FunctionComponent = ({ + sources, + raw, onChange, }) => { - const entropySources = useEntropySources(); - const handleChange = (event: ChangeEvent) => { + if (event.target.value === '') { + onChange(undefined); + return; + } + onChange(event.target.value); }; return ( <> - Entropy source + Entropy source - - Select an entropy source - - {entropySources?.map((source) => ( + None + {sources.map((source) => ( {getSourceName(source)} ))} - - - {JSON.stringify(entropySources, null, 2)} - - + {raw && ( + + + {JSON.stringify(sources, null, 2)} + + + )} > ); }; diff --git a/packages/test-snaps/src/features/snaps/get-entropy/components/index.ts b/packages/test-snaps/src/features/snaps/get-entropy/components/index.ts index 66244c0ab3..3315e2cb17 100644 --- a/packages/test-snaps/src/features/snaps/get-entropy/components/index.ts +++ b/packages/test-snaps/src/features/snaps/get-entropy/components/index.ts @@ -1,2 +1,2 @@ -export * from './EntropySources'; +export * from './EntropySelector'; export * from './SignMessage'; diff --git a/packages/test-snaps/src/features/snaps/get-entropy/hooks/index.ts b/packages/test-snaps/src/features/snaps/get-entropy/hooks/index.ts index 63cc9534fc..cb68cafc46 100644 --- a/packages/test-snaps/src/features/snaps/get-entropy/hooks/index.ts +++ b/packages/test-snaps/src/features/snaps/get-entropy/hooks/index.ts @@ -1 +1 @@ -export * from './useEntropySources'; +export * from './useEntropySelector'; diff --git a/packages/test-snaps/src/features/snaps/get-entropy/hooks/useEntropySelector.tsx b/packages/test-snaps/src/features/snaps/get-entropy/hooks/useEntropySelector.tsx new file mode 100644 index 0000000000..604359a184 --- /dev/null +++ b/packages/test-snaps/src/features/snaps/get-entropy/hooks/useEntropySelector.tsx @@ -0,0 +1,59 @@ +import type { EntropySource } from '@metamask/snaps-sdk'; +import { useState } from 'react'; + +import { Tag, useInvokeQuery } from '../../../../api'; +import { getSnapId, useInstalled } from '../../../../utils'; +import { EntropySelector } from '../components'; + +export type UseEntropySelectorOptions = { + /** + * Whether to show the raw list of entropy sources. + */ + raw?: boolean; + + /** + * The snap ID to use for the entropy sources. + */ + snapId: `npm:${string}`; + + /** + * The port to use for the entropy sources. + */ + port: number; +}; + +/** + * Use a selector to select the entropy source to use. + * + * @param options - The options to use. + * @param options.snapId - The snap ID to use for the entropy sources. + * @param options.port - The port to use for the entropy sources. + * @param options.raw - Whether to show the raw list of entropy sources. + * @returns The entropy source and selector. + */ +export const useEntropySelector = ({ + snapId: publicSnapId, + port, + raw = false, +}: UseEntropySelectorOptions) => { + const [source, setSource] = useState(undefined); + const snapId = getSnapId(publicSnapId, port); + const isInstalled = useInstalled(snapId); + + const { data = [] } = useInvokeQuery<{ data: EntropySource[] }>( + { + snapId, + method: 'getEntropySources', + tags: [Tag.EntropySources], + }, + { + skip: !isInstalled, + }, + ); + + return { + source, + sources: data, + selector: , + }; +}; diff --git a/packages/test-snaps/src/features/snaps/get-entropy/hooks/useEntropySources.ts b/packages/test-snaps/src/features/snaps/get-entropy/hooks/useEntropySources.ts deleted file mode 100644 index dbbf871db0..0000000000 --- a/packages/test-snaps/src/features/snaps/get-entropy/hooks/useEntropySources.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { EntropySource } from '@metamask/snaps-sdk'; - -import { Tag, useInvokeQuery } from '../../../../api'; -import { getSnapId, useInstalled } from '../../../../utils'; -import { GET_ENTROPY_PORT, GET_ENTROPY_SNAP_ID } from '../constants'; - -export const useEntropySources = () => { - const snapId = getSnapId(GET_ENTROPY_SNAP_ID, GET_ENTROPY_PORT); - const isInstalled = useInstalled(snapId); - - const { data } = useInvokeQuery<{ data: EntropySource[] }>( - { - snapId, - method: 'getEntropySources', - tags: [Tag.EntropySources], - }, - { - skip: !isInstalled, - }, - ); - - return data; -};
- {JSON.stringify(entropySources, null, 2)} -
+ {JSON.stringify(sources, null, 2)} +