From 298d02d07c92d40012b2ce28cd63879b3e7680d9 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Tue, 19 Mar 2024 09:29:41 +0000 Subject: [PATCH 1/2] feat: sign message Signed-off-by: Gregory Hill --- packages/snap/src/index.ts | 3 ++ packages/snap/src/interface.ts | 11 +++++- .../src/rpc/__tests__/fixtures/bitcoinNode.ts | 17 ++++++--- .../src/rpc/__tests__/signMessage.test.ts | 26 ++++++++++++++ packages/snap/src/rpc/index.ts | 1 + packages/snap/src/rpc/signMessage.ts | 35 +++++++++++++++++++ 6 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 packages/snap/src/rpc/__tests__/signMessage.test.ts create mode 100644 packages/snap/src/rpc/signMessage.ts diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index cb367fd0..a8fa14f3 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -11,6 +11,7 @@ import { saveLNDataToSnap, getLNDataFromSnap, signLNInvoice, + signMessage, } from './rpc'; import { SnapError, RequestErrors } from './errors'; import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; @@ -92,6 +93,8 @@ export const onRpcRequest = async ({ origin, request }: RpcRequest) => { ); case 'btc_signLNInvoice': return signLNInvoice(origin, snap, request.params.invoice); + case 'btc_signMessage': + return signMessage(origin, snap, request.params.message, request.params.hdPath); default: throw SnapError.of(RequestErrors.MethodNotSupport); } diff --git a/packages/snap/src/interface.ts b/packages/snap/src/interface.ts index a67fbc0e..9ded88e1 100644 --- a/packages/snap/src/interface.ts +++ b/packages/snap/src/interface.ts @@ -78,6 +78,14 @@ export interface SignLNInvoice { }; } +export interface SignMessage { + method: 'btc_signMessage'; + params: { + message: string; + hdPath: string; + }; +} + export type MetamaskBTCRpcRequest = | GetAllXpubsRequest | GetPublicExtendedKeyRequest @@ -87,7 +95,8 @@ export type MetamaskBTCRpcRequest = | ManageNetwork | SaveLNDataToSnap | GetLNDataFromSnap - | SignLNInvoice; + | SignLNInvoice + | SignMessage; export type BTCMethodCallback = ( originString: string, diff --git a/packages/snap/src/rpc/__tests__/fixtures/bitcoinNode.ts b/packages/snap/src/rpc/__tests__/fixtures/bitcoinNode.ts index 2d1b0ace..382d5bf0 100644 --- a/packages/snap/src/rpc/__tests__/fixtures/bitcoinNode.ts +++ b/packages/snap/src/rpc/__tests__/fixtures/bitcoinNode.ts @@ -28,13 +28,20 @@ export const LNDataFromSnap = { } export const LNDataToSnap = { - domain : 'www.justsnap.io', - walletId: "id00000001", - credential: "testAdmin:123456", - password: "testPassword", - invoice: "lnbc100u1p34k6pppp5332v7z238qt7jrhjz5mkhckdx2uuc50d8xzpfyanj8p3plav9z5sdq8w3jhxaqcqzpgxqyz5vqsp5stj40j57779ahamqp9p3rpq0eudt75f9kxw7yyhuwwaxfmuqsqzq9qyyssqqudc8qc5np9rj5ypn6p9jlafn5sc02nwp60at38cwem4ycz9p9pqdlknk5k3yfayh3pzhndjt2gev8g4rqtnr6art5cagr2c0f3xkxqqfx27k5" + domain: 'www.justsnap.io', + walletId: "id00000001", + credential: "testAdmin:123456", + password: "testPassword", + invoice: "lnbc100u1p34k6pppp5332v7z238qt7jrhjz5mkhckdx2uuc50d8xzpfyanj8p3plav9z5sdq8w3jhxaqcqzpgxqyz5vqsp5stj40j57779ahamqp9p3rpq0eudt75f9kxw7yyhuwwaxfmuqsqzq9qyyssqqudc8qc5np9rj5ypn6p9jlafn5sc02nwp60at38cwem4ycz9p9pqdlknk5k3yfayh3pzhndjt2gev8g4rqtnr6art5cagr2c0f3xkxqqfx27k5" } export const LNSignature = { signature: "1f9b311f576424fe87c769ab9146f6b3613399b0b54f13b781639b2bc3f40e22706012192cdcdaa9830b5a21494e54740cfcec3b1e7f75d35e1c9afeb143d5c1ed" } + +export const MessageAndSig = { + domain: 'www.justsnap.io', + message: "Hello World!", + hdPath: `m/84'/0'/0'/0/0`, + signature: "20d222a9945150cf3b3bb46eb3a3d333a5c0a91cd015bfcbceec0f3d612997fe81413b595ee7b3d3e50d9fb3acd8f852c428be06fc3979cbda77e8b73040b3da17" +} diff --git a/packages/snap/src/rpc/__tests__/signMessage.test.ts b/packages/snap/src/rpc/__tests__/signMessage.test.ts new file mode 100644 index 00000000..7e3f2871 --- /dev/null +++ b/packages/snap/src/rpc/__tests__/signMessage.test.ts @@ -0,0 +1,26 @@ +import { signMessage } from '../signMessage'; +import { SnapMock } from '../__mocks__/snap'; +import { bip44, MessageAndSig } from "./fixtures/bitcoinNode"; + +describe('signLNInvoice', () => { + const snapStub = new SnapMock(); + + afterEach(() => { + snapStub.reset() + }) + + it('should return signature for message', async () => { + snapStub.rpcStubs.snap_dialog.mockResolvedValue(true); + snapStub.rpcStubs.snap_getBip32Entropy.mockResolvedValue(bip44.slip10Node); + const signature = await signMessage(MessageAndSig.domain, snapStub, MessageAndSig.message, MessageAndSig.hdPath); + expect(signature).toBe(MessageAndSig.signature); + }) + + it('should reject the sign request and throw error if user reject the sign the lightning invoice', async () => { + snapStub.rpcStubs.snap_dialog.mockResolvedValue(false); + + await expect(signMessage(MessageAndSig.domain, snapStub, MessageAndSig.message, MessageAndSig.hdPath)) + .rejects + .toThrowError('User reject the sign request'); + }) +}); diff --git a/packages/snap/src/rpc/index.ts b/packages/snap/src/rpc/index.ts index 829ac870..65a19659 100644 --- a/packages/snap/src/rpc/index.ts +++ b/packages/snap/src/rpc/index.ts @@ -8,3 +8,4 @@ export { saveLNDataToSnap } from './saveLNDataToSnap'; export { getLNDataFromSnap } from './getLNDataFromSnap'; export { signLNInvoice } from './signLNInvoice'; export { signInput } from './signInput'; +export { signMessage } from './signMessage'; diff --git a/packages/snap/src/rpc/signMessage.ts b/packages/snap/src/rpc/signMessage.ts new file mode 100644 index 00000000..d512a52a --- /dev/null +++ b/packages/snap/src/rpc/signMessage.ts @@ -0,0 +1,35 @@ +import { Snap } from '../interface'; +import { getHDNode } from '../utils/getHDNode'; +import bitcoinMessage from 'bitcoinjs-message'; +import { RequestErrors, SnapError } from '../errors'; +import { divider, heading, panel, text } from "@metamask/snaps-ui"; + +export async function signMessage( + domain: string, + snap: Snap, + message: string, + hdPath: string, +): Promise { + const result = await snap.request({ + method: 'snap_dialog', + params: { + type: 'confirmation', + content: panel([ + heading('Sign Message'), + text(`Please sign this message from ${domain}`), + divider(), + text(`${message}`), + ]), + }, + }); + + if (result) { + const privateKey = (await getHDNode(snap, hdPath)).privateKey; + const signature = bitcoinMessage + .sign(message, privateKey, true) + .toString('hex'); + return signature; + } else { + throw SnapError.of(RequestErrors.RejectSign); + } +} From fc309bf72b528e676370d3b2c2c28108dff30444 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Mon, 25 Mar 2024 11:07:15 +0000 Subject: [PATCH 2/2] add test to increase coverage Signed-off-by: Gregory Hill --- packages/snap/snap.manifest.json | 2 +- packages/snap/src/__tests__/index.test.ts | 65 +++++++++---------- packages/snap/src/rpc/getExtendedPublicKey.ts | 2 +- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 8f5ce555..587e05ff 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/bob-collective/bob-snap" }, "source": { - "shasum": "3gkQ5n9BJXvJnuTZ0KobpTgDQIP4Pq+sp0ZyEipYn2o=", + "shasum": "Ea+2ea/YTw965c+UA8zhzQIQ8CWIV3yEt5llK6YUjkE=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/__tests__/index.test.ts b/packages/snap/src/__tests__/index.test.ts index ba1aca8b..60b470f7 100644 --- a/packages/snap/src/__tests__/index.test.ts +++ b/packages/snap/src/__tests__/index.test.ts @@ -1,5 +1,5 @@ import { onRpcRequest } from '../index'; -import { getExtendedPublicKey, signPsbt, getMasterFingerprint, manageNetwork, validateRequest, saveLNDataToSnap, signLNInvoice, getLNDataFromSnap } from '../rpc'; +import { getExtendedPublicKey, signPsbt, getMasterFingerprint, manageNetwork, getAllXpubs, saveLNDataToSnap, signLNInvoice, getLNDataFromSnap } from '../rpc'; import { BitcoinNetwork, KeyOptions, ScriptType } from '../interface'; import { SnapMock } from '../rpc/__mocks__/snap'; import { LNDataToSnap } from '../rpc/__tests__/fixtures/bitcoinNode'; @@ -18,7 +18,8 @@ jest.mock('../rpc', () => { validateRequest, saveLNDataToSnap: jest.fn(), signLNInvoice: jest.fn(), - getLNDataFromSnap: jest.fn() + getLNDataFromSnap: jest.fn(), + getAllXpubs: jest.fn(), }; }); @@ -37,33 +38,19 @@ describe('index', () => { describe('validateRequest', () => { it('should throw error when given network not match for getExtendedPublicKey', async () => { await expect(onRpcRequest({ - origin: 'origin', - request: { - method: 'btc_getPublicExtendedKey', - params: { - network: BitcoinNetwork.Main, - scriptType: ScriptType.P2PKH, - }, - }, - }), - ).rejects.toThrowError('Network not match'); - }); - - it('should throw error when given network not match for signPsbt', async () => { - await expect(onRpcRequest({ - origin: 'origin', - request: { - method: 'btc_getPublicExtendedKey', - params: { - network: BitcoinNetwork.Main, - scriptType: ScriptType.P2PKH, - }, + origin: 'origin', + request: { + method: 'btc_getPublicExtendedKey', + params: { + network: BitcoinNetwork.Main, + scriptType: ScriptType.P2PKH, }, - }), + }, + }), ).rejects.toThrowError('Network not match'); }); - it('should throw error if domain not allowed', async() => { + it('should throw error if domain not allowed', async () => { await expect(onRpcRequest({ origin: 'origin', request: { @@ -74,11 +61,11 @@ describe('index', () => { }, }, }), - ).rejects.toThrowError('Domain not allowed'); + ).rejects.toThrowError('Domain not allowed'); }) }); - describe('rpc methods', function() { + describe('rpc methods', function () { it('should call getExtendedPublicKey when method btc_getPublicExtendedKey get called', async () => { await onRpcRequest({ origin: domain, @@ -94,6 +81,18 @@ describe('index', () => { await expect(getExtendedPublicKey).toBeCalled(); }); + it('should getAllXpubs', async () => { + await onRpcRequest({ + origin: domain, + request: { + method: 'btc_getAllXpubs', + params: {}, + }, + }); + + await expect(getAllXpubs).toBeCalled(); + }); + it('should sign PSBT when method btc_signPSBT get called', async () => { await onRpcRequest({ origin: domain, @@ -182,12 +181,12 @@ describe('index', () => { it('should throw error when given method not exist', async () => { await expect(onRpcRequest({ - origin: domain, - request: { - method: 'btc_method_not_exist' as any, - params: {} as any, - }, - }), + origin: domain, + request: { + method: 'btc_method_not_exist' as any, + params: {} as any, + }, + }), ).rejects.toThrowError('Method not found.'); }); }); diff --git a/packages/snap/src/rpc/getExtendedPublicKey.ts b/packages/snap/src/rpc/getExtendedPublicKey.ts index 75942329..096a8d3b 100644 --- a/packages/snap/src/rpc/getExtendedPublicKey.ts +++ b/packages/snap/src/rpc/getExtendedPublicKey.ts @@ -31,7 +31,7 @@ export async function extractAccountPrivateKey(snap: Snap, network: Network, scr path, curve: CRYPTO_CURVE }, - }) as SLIP10Node + }) as SLIP10Node; const privateKeyBuffer = Buffer.from(trimHexPrefix(slip10Node.privateKey), "hex") const chainCodeBuffer = Buffer.from(trimHexPrefix(slip10Node.chainCode), "hex")