From 8b6f6a12ac271fdc0142d1b9c82300db48a8fa8f Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Mon, 18 Dec 2023 11:51:39 +0000 Subject: [PATCH] enable bip86 signing, cleanup and add tests Signed-off-by: Gregory Hill --- packages/snap/package.json | 3 +- packages/snap/snap.manifest.json | 18 +- packages/snap/src/bitcoin/PsbtValidator.ts | 60 ++-- .../bitcoin/__tests__/PsbtValidator.test.ts | 82 ++++- .../src/bitcoin/__tests__/fixtures/psbt.ts | 2 +- .../src/bitcoin/__tests__/getNetwork.test.ts | 2 +- .../snap/src/bitcoin/__tests__/index.test.ts | 89 +++++- .../src/bitcoin/__tests__/tapSigner.test.ts | 279 ++++++++++++++++++ packages/snap/src/bitcoin/cryptoPath.ts | 2 +- packages/snap/src/bitcoin/index.ts | 56 ++-- packages/snap/src/bitcoin/tapSigner.ts | 155 ++++++++++ packages/snap/src/bitcoin/xpubConverter.ts | 4 + packages/snap/src/index.ts | 16 +- packages/snap/src/interface.ts | 13 +- .../src/rpc/__tests__/getAllXpubs.test.ts | 4 +- .../__tests__/getExtendedPublicKey.test.ts | 2 +- .../rpc/__tests__/getLNDataFromSnap.test.ts | 8 +- .../__tests__/getMasterFingerprint.test.ts | 2 +- .../src/rpc/__tests__/manageNetwork.test.ts | 8 +- .../snap/src/rpc/__tests__/signInput.test.ts | 110 +++++++ .../snap/src/rpc/__tests__/signPSBT.test.ts | 31 +- packages/snap/src/rpc/getAllXpubs.ts | 2 +- packages/snap/src/rpc/getExtendedPublicKey.ts | 15 +- packages/snap/src/rpc/getLNDataFromSnap.ts | 8 +- packages/snap/src/rpc/saveLNDataToSnap.ts | 8 +- packages/snap/src/rpc/signInput.ts | 18 +- packages/snap/src/rpc/signLNInvoice.ts | 6 +- packages/snap/src/rpc/signPSBT.ts | 25 +- packages/snap/src/rpc/validateRequest.ts | 6 +- packages/snap/src/utils/getHDNode.ts | 10 +- packages/snap/src/utils/manageState.ts | 2 +- packages/snap/src/utils/transferLNData.ts | 4 +- packages/snap/src/utils/unitHelper.ts | 6 +- 33 files changed, 897 insertions(+), 159 deletions(-) create mode 100644 packages/snap/src/bitcoin/__tests__/tapSigner.test.ts create mode 100644 packages/snap/src/bitcoin/tapSigner.ts create mode 100644 packages/snap/src/rpc/__tests__/signInput.test.ts diff --git a/packages/snap/package.json b/packages/snap/package.json index 63ae7271..c415ae6e 100644 --- a/packages/snap/package.json +++ b/packages/snap/package.json @@ -60,6 +60,7 @@ "@metamask/snaps-cli": "^0.32.2", "@metamask/snaps-ui": "^0.32.2", "bip32": "^4.0.0", + "bip174": "^2.1.1", "bitcoinjs-lib": "^6.1.5", "bitcoinjs-message": "^2.2.0", "bn.js": "^5.2.1", @@ -80,4 +81,4 @@ "prettier": "^2.7.1", "through2": "^4.0.2" } -} +} \ No newline at end of file diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 3a7e6c7a..9c11b801 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -72,8 +72,24 @@ "1'" ], "curve": "secp256k1" + }, + { + "path": [ + "m", + "86'", + "0'" + ], + "curve": "secp256k1" + }, + { + "path": [ + "m", + "86'", + "1'" + ], + "curve": "secp256k1" } ] }, "manifestVersion": "0.1" -} +} \ No newline at end of file diff --git a/packages/snap/src/bitcoin/PsbtValidator.ts b/packages/snap/src/bitcoin/PsbtValidator.ts index 7eeb8b27..3ec331e1 100644 --- a/packages/snap/src/bitcoin/PsbtValidator.ts +++ b/packages/snap/src/bitcoin/PsbtValidator.ts @@ -4,6 +4,8 @@ import { BitcoinNetwork } from '../interface'; import { PsbtHelper } from '../bitcoin/PsbtHelper'; import { fromHdPathToObj } from './cryptoPath'; import { PsbtValidateErrors, SnapError } from "../errors"; +import { isTaprootInput } from 'bitcoinjs-lib/src/psbt/bip371'; +import { tapInputHasHDKey, tapOutputHasHDKey } from './tapSigner'; const BITCOIN_MAINNET_COIN_TYPE = 0; const BITCOIN_TESTNET_COIN_TYPE = 1; @@ -29,8 +31,8 @@ export class PsbtValidator { this.psbtHelper = new PsbtHelper(this.tx, network); } - get coinType(){ - return this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAINNET_COIN_TYPE: BITCOIN_TESTNET_COIN_TYPE; + get coinType() { + return this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAINNET_COIN_TYPE : BITCOIN_TESTNET_COIN_TYPE; } allInputsHaveRawTxHex() { @@ -43,12 +45,14 @@ export class PsbtValidator { everyInputMatchesNetwork() { const result = this.tx.data.inputs.every(input => { - if (!input.bip32Derivation || input.bip32Derivation.length === 0) { - // ignore if we don't have the derivation path - return true; + if (isTaprootInput(input)) { + return input.tapBip32Derivation.every(derivation => { + const { coinType } = fromHdPathToObj(derivation.path); + return Number(coinType) === this.coinType; + }); } else { return input.bip32Derivation.every(derivation => { - const {coinType} = fromHdPathToObj(derivation.path); + const { coinType } = fromHdPathToObj(derivation.path); return Number(coinType) === this.coinType; }); } @@ -62,15 +66,20 @@ export class PsbtValidator { everyOutputMatchesNetwork() { const addressPattern = this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAIN_NET_ADDRESS_PATTERN : BITCOIN_TEST_NET_ADDRESS_PATTERN; const result = this.tx.data.outputs.every((output, index) => { - if(output.bip32Derivation){ - return output.bip32Derivation.every(derivation => { - const {coinType} = fromHdPathToObj(derivation.path) - return Number(coinType) === this.coinType - }) - } else { - const address = this.tx.txOutputs[index].address; - return addressPattern.test(address); - } + if (output.tapBip32Derivation) { + return output.tapBip32Derivation.every(derivation => { + const { coinType } = fromHdPathToObj(derivation.path) + return Number(coinType) === this.coinType + }) + } else if (output.bip32Derivation) { + return output.bip32Derivation.every(derivation => { + const { coinType } = fromHdPathToObj(derivation.path) + return Number(coinType) === this.coinType + }) + } else { + const address = this.tx.txOutputs[index].address; + return addressPattern.test(address); + } }) if (!result) { @@ -82,9 +91,8 @@ export class PsbtValidator { allInputsBelongToCurrentAccount(accountSigner: AccountSigner) { const result = this.tx.txInputs.every((_, index) => { const input = checkForInput(this.tx.data.inputs, index); - if (!input.bip32Derivation || input.bip32Derivation.length === 0) { - // ignore if we don't have the derivation path - return true; + if (isTaprootInput(input)) { + return tapInputHasHDKey(input, accountSigner); } else { return this.tx.inputHasHDKey(index, accountSigner); } @@ -97,7 +105,9 @@ export class PsbtValidator { changeAddressBelongsToCurrentAccount(accountSigner: AccountSigner) { const result = this.tx.data.outputs.every((output, index) => { - if (output.bip32Derivation) { + if (output.tapBip32Derivation) { + return tapOutputHasHDKey(output, accountSigner); + } else if (output.bip32Derivation) { return this.tx.outputHasHDKey(index, accountSigner); } return true; @@ -137,12 +147,12 @@ export class PsbtValidator { this.error = null; this.allInputsHaveRawTxHex() && - this.everyInputMatchesNetwork() && - this.everyOutputMatchesNetwork() && - this.allInputsBelongToCurrentAccount(accountSigner) && - this.changeAddressBelongsToCurrentAccount(accountSigner) && - this.feeUnderThreshold() && - this.witnessUtxoValueMatchesNoneWitnessOnes(); + this.everyInputMatchesNetwork() && + this.everyOutputMatchesNetwork() && + this.allInputsBelongToCurrentAccount(accountSigner) && + this.changeAddressBelongsToCurrentAccount(accountSigner) && + this.feeUnderThreshold() && + this.witnessUtxoValueMatchesNoneWitnessOnes(); if (this.error) { throw this.error diff --git a/packages/snap/src/bitcoin/__tests__/PsbtValidator.test.ts b/packages/snap/src/bitcoin/__tests__/PsbtValidator.test.ts index 7923eb56..a1940501 100644 --- a/packages/snap/src/bitcoin/__tests__/PsbtValidator.test.ts +++ b/packages/snap/src/bitcoin/__tests__/PsbtValidator.test.ts @@ -1,13 +1,14 @@ -import * as bip32 from 'bip32'; +import BIP32Factory from 'bip32'; import { networks, Psbt } from 'bitcoinjs-lib'; import { PsbtValidator } from '../PsbtValidator'; import { AccountSigner } from '../index'; import { BitcoinNetwork } from '../../interface'; import { psbtFixture } from './fixtures/psbt'; +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; const getAccountSigner = () => { const testPrivateAccountKey = "tprv8gwYx7tEWpLxdJhEa7R8ofchqzRgme6iiuyJpegZ71XNhnAqeMjT6GV4wm3jqsUjXgXj99GB4kDminso5kxnLa6VXt3WVRzfmhbDSrfbCDv"; - const accountNode = bip32.fromBase58(testPrivateAccountKey, networks.testnet); + const accountNode = BIP32Factory(ecc).fromBase58(testPrivateAccountKey, networks.testnet); return new AccountSigner(accountNode, Buffer.from("f812d139", 'hex')); } @@ -27,21 +28,36 @@ describe('psbtValidator', () => { it('should throw error when not all inputs have nonWitnessUtxo', () => { const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); - expect(() => {psbtValidator.validate(signer)}).toThrowError('Not all inputs have prev Tx raw hex'); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Not all inputs have prev Tx raw hex'); }); it('should throw error when not all inputs matches network', () => { - psbt.updateInput(0,{ + psbt.updateInput(0, { nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo, bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation, }) const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Main); - expect(() => {psbtValidator.validate(signer)}).toThrowError('Not every input matches network'); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every input matches network'); + }); + + it('should throw error when not all inputs matches network (taproot)', () => { + psbt.updateInput(0, { + nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo, + tapBip32Derivation: [{ + masterFingerprint: Buffer.alloc(4, 0), + pubkey: Buffer.alloc(32, 0), + path: "m/86'/1'/0'/1/10", + leafHashes: [], + }], + }) + + const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Main); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every input matches network'); }); it('should throw error when not all outputs matches network', () => { - psbt.updateInput(0,{ + psbt.updateInput(0, { nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo, bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation, }) @@ -56,11 +72,31 @@ describe('psbtValidator', () => { }) const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); - expect(() => {psbtValidator.validate(signer)}).toThrowError('Not every output matches network'); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every output matches network'); + }); + + it('should throw error when not all outputs matches network (taproot)', () => { + psbt.updateInput(0, { + nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo, + bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation, + }) + psbt.addOutput({ + script: Buffer.from('0014198d799580d87fb6c0341b1e9619a20ef47cd5f8', 'hex'), + value: 10000, + tapBip32Derivation: [{ + masterFingerprint: Buffer.alloc(4, 0), + pubkey: Buffer.alloc(32, 0), + path: `m/86'/0'/0'/1/1`, + leafHashes: [], + }], + }) + + const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every output matches network'); }); it('should throw error when not all inputs belong to current account', () => { - psbt.updateInput(0,{ + psbt.updateInput(0, { nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo, bip32Derivation: [{ masterFingerprint: Buffer.from('d1f83912', 'hex'), @@ -70,11 +106,27 @@ describe('psbtValidator', () => { }) const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); - expect(() => {psbtValidator.validate(signer)}).toThrowError('Not all inputs belongs to current account'); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Not all inputs belongs to current account'); + }); + + it('should throw error when not all inputs belong to current account (taproot)', () => { + psbt.updateInput(0, { + nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo, + tapInternalKey: Buffer.alloc(32, 0), + tapBip32Derivation: [{ + masterFingerprint: Buffer.alloc(4, 0), + pubkey: Buffer.alloc(32, 0), + path: `m/86'/1'/0'/1/0`, + leafHashes: [], + }], + }) + + const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Not all inputs belongs to current account'); }); it('should throw error when not all change addresses belong to current account', () => { - psbt.updateInput(0,{ + psbt.updateInput(0, { nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo, bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation, }) @@ -87,7 +139,7 @@ describe('psbtValidator', () => { }) const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); - expect(() => {psbtValidator.validate(signer)}).toThrowError(`Change address doesn't belongs to current account`); + expect(() => { psbtValidator.validate(signer) }).toThrowError(`Change address doesn't belongs to current account`); }); it('should throw error when fee is too high', () => { @@ -105,7 +157,7 @@ describe('psbtValidator', () => { psbt.addOutputs(psbtFixture.tx.outputs); const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); - expect(() => {psbtValidator.validate(signer)}).toThrowError('Too much fee'); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Too much fee'); }); it('should throw error given witnessUtxo value not equals to nonWitnessUtxo value', () => { @@ -123,11 +175,11 @@ describe('psbtValidator', () => { psbt.addOutputs(psbtFixture.tx.outputs); const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); - expect(() => {psbtValidator.validate(signer)}).toThrowError('Transaction input amount not match'); + expect(() => { psbtValidator.validate(signer) }).toThrowError('Transaction input amount not match'); }); - it('should return true given a valid psbt', function() { - const psbt = Psbt.fromBase64(psbtFixture.base64, { network: networks.testnet}) + it('should return true given a valid psbt', function () { + const psbt = Psbt.fromBase64(psbtFixture.base64, { network: networks.testnet }) const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test); expect(psbtValidator.validate(signer)).toBe(true); diff --git a/packages/snap/src/bitcoin/__tests__/fixtures/psbt.ts b/packages/snap/src/bitcoin/__tests__/fixtures/psbt.ts index 9de8f2e8..d83f88be 100644 --- a/packages/snap/src/bitcoin/__tests__/fixtures/psbt.ts +++ b/packages/snap/src/bitcoin/__tests__/fixtures/psbt.ts @@ -56,5 +56,5 @@ export const psbtFixture = { ], }, base64: 'cHNidP8BAHECAAAAASpPo+Jtjb8ce88iDgUe9MdBl/N0RXzOyTt6bYRF4KxgAAAAAAD/////AkANAwAAAAAAFgAUBbP+LIMGzIE0s5pdBRLRb/T3kYaazQsAAAAAABYAFDUcL8Uq83TUCsXnr18EDVw08zQ9AAAAAAABAP1UAQIAAAAAAQIbzNzgXMu2XcHbu/VK6Tv2aYkp3WBu0PijNRnjyvh1eQEAAAAA/////wL6QmQPXjZt03YXBX2QbexW+etvDRpeUJE+cz7D5ardAAAAAAD/////Afz3DgAAAAAAFgAUvApnUSw4MVXYWN2ZqWf3hCAWGsoCSDBFAiEA+axvhH4bFn2mSxo6xzybYtrAjdpG0YzlqBam0UNaE3kCIA0lg97qGHi0rKC7hWQXnMSnWbaCII6nGpHErzpoSHNMASEDSB6PkHcBABG+ayUezMfaQN0i6+DO4DwxtF+nbuWWp+ICRzBEAiAYgnrWAOCiD55kZsBSiX/UNMnDsmhcFzKno/6T1nP92gIgPtzzZuB0FjdMrkpzWkDZurYJR7MBcRnszVgqyjX5xEsBIQMR9PpNCfA5TzCasyKhJsp1vevr907O2Kru24aJS/h08QAAAAABAR/89w4AAAAAABYAFLwKZ1EsODFV2Fjdmaln94QgFhrKIgYDSB6PkHcBABG+ayUezMfaQN0i6+DO4DwxtF+nbuWWp+IY+BLROVQAAIABAACAAAAAgAEAAAAKAAAAAAAiAgJgiLaAsqyAi3BUwv3EgxEuXiYfGOFeDgCax2fzYa/sZBj4EtE5VAAAgAEAAIAAAACAAQAAAAsAAAAA', - + }; diff --git a/packages/snap/src/bitcoin/__tests__/getNetwork.test.ts b/packages/snap/src/bitcoin/__tests__/getNetwork.test.ts index 18058497..389efbb4 100644 --- a/packages/snap/src/bitcoin/__tests__/getNetwork.test.ts +++ b/packages/snap/src/bitcoin/__tests__/getNetwork.test.ts @@ -12,6 +12,6 @@ describe('getNetwork', () => { }); it('should throw error given invalid network', () => { - expect(() => {getNetwork('litcoin' as any)}).toThrowError('Network net exist') + expect(() => { getNetwork('litcoin' as any) }).toThrowError('Network net exist') }); }); diff --git a/packages/snap/src/bitcoin/__tests__/index.test.ts b/packages/snap/src/bitcoin/__tests__/index.test.ts index c8c10def..119f2f63 100644 --- a/packages/snap/src/bitcoin/__tests__/index.test.ts +++ b/packages/snap/src/bitcoin/__tests__/index.test.ts @@ -1,15 +1,18 @@ import { Psbt, networks } from 'bitcoinjs-lib'; -import { BtcTx, AccountSigner } from '../index'; -import * as bip32 from 'bip32'; +import * as bitcoin from 'bitcoinjs-lib'; +import { BtcPsbt, AccountSigner } from '../index'; +import BIP32Factory from 'bip32'; import { BitcoinNetwork } from '../../interface'; import { psbtFixture } from './fixtures/psbt'; +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; +import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371'; // only for testing // hybrid betray symbol aim promote vehicle extend west slice silver man belt const testPrivateAccountKey = 'tprv8gwYx7tEWpLxdJhEa7R8ofchqzRgme6iiuyJpegZ71XNhnAqeMjT6GV4wm3jqsUjXgXj99GB4kDminso5kxnLa6VXt3WVRzfmhbDSrfbCDv'; function getAccountSigner() { - const accountNode = bip32.fromBase58(testPrivateAccountKey, networks.testnet); + const accountNode = BIP32Factory(ecc).fromBase58(testPrivateAccountKey, networks.testnet); return new AccountSigner(accountNode, Buffer.from("f812d139", 'hex')); } @@ -28,7 +31,7 @@ describe('bitcoin', () => { describe('AccountSigner', () => { it('should generate account signer', () => { - const node = bip32.fromBase58(testPrivateAccountKey, networks.testnet); + const node = BIP32Factory(ecc).fromBase58(testPrivateAccountKey, networks.testnet); const signer = new AccountSigner( node, Buffer.from('f812d139', 'hex'), @@ -38,13 +41,13 @@ describe('bitcoin', () => { }); it('should raise error if the path is not right', () => { - const node = bip32.fromBase58(testPrivateAccountKey, networks.testnet); + const node = BIP32Factory(ecc).fromBase58(testPrivateAccountKey, networks.testnet); const signer = new AccountSigner(node); expect(() => signer.derivePath('m/0\'/-1')).toThrow('invalid path'); }); it('should able to sign', () => { - const node = bip32.fromBase58(testPrivateAccountKey, networks.testnet); + const node = BIP32Factory(ecc).fromBase58(testPrivateAccountKey, networks.testnet); const signer = new AccountSigner(node); const testBuffer = Buffer.from( '936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af', @@ -55,11 +58,23 @@ describe('bitcoin', () => { '33b5f9376c8c084c01627825f3ab2f52857085df7692fc0a1f10e7d464475b7e62e328634bb52498670b85c147276401a6b896fa88d44c984332be4c951caa8c', ); }); + + it('should tweak node', () => { + const node = BIP32Factory(ecc).fromBase58(testPrivateAccountKey, networks.testnet); + const signer = new AccountSigner(node); + + const childNode = signer.derivePath("m/86'/0'/0'/0/0"); + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + + expect(signer.tweak(childNodeXOnlyPubkey).publicKey.toString('hex')).toBe( + '0340b35b2ddebe0afb6af04291383e9793acd111787a8dfef44fb4d0776629cf19', + ); + }); }); - describe('BtcTx', () => { + describe('BtcPsbt', () => { it('should be able to construct the tx and extract the psbt json', () => { - const tx = new BtcTx(psbtFixture.base64, BitcoinNetwork.Test); + const tx = new BtcPsbt(psbtFixture.base64, BitcoinNetwork.Test); const testJson = tx.extractPsbtJson(); @@ -71,7 +86,7 @@ describe('bitcoin', () => { }); it('should be able to extract PSBT info as JSON string', () => { - const tx = new BtcTx(psbtFixture.base64, BitcoinNetwork.Test); + const tx = new BtcPsbt(psbtFixture.base64, BitcoinNetwork.Test); expect(tx.extractPsbtJsonString()).toBe(` from: tb1qhs9xw5fv8qc4tkzcmkv6jelhssspvxk2wmtd0v to: tb1qqkelutyrqmxgzd9nnfws2yk3dl600yvxagfqu7 @@ -85,13 +100,13 @@ changeAddress: tb1qx5wzl3f27d6dgzk9u7h47pqdts60xdpax4w5rf it('should be able to validate the psbt', () => { const signer = getAccountSigner(); - const tx = new BtcTx(psbtFixture.base64, BitcoinNetwork.Test); - expect(tx.validateTx(signer)).toBe(true); + const tx = new BtcPsbt(psbtFixture.base64, BitcoinNetwork.Test); + expect(tx.validatePsbt(signer)).toBe(true); }); it('should be able to sign transaction and extract txId and txHex', () => { - const tx = new BtcTx(psbtFixture.base64, BitcoinNetwork.Test); - const { txId, txHex } = tx.signTx(signer); + const tx = new BtcPsbt(psbtFixture.base64, BitcoinNetwork.Test); + const { txId, txHex } = tx.signPsbt(signer); expect(txId).toBe( '4db856b1b7049cce26dc298458796f66d0dee39a5d0651b4c51aa0c66326adec', ); @@ -99,5 +114,53 @@ changeAddress: tb1qx5wzl3f27d6dgzk9u7h47pqdts60xdpax4w5rf '020000000001012a4fa3e26d8dbf1c7bcf220e051ef4c74197f374457ccec93b7a6d8445e0ac600000000000ffffffff02400d03000000000016001405b3fe2c8306cc8134b39a5d0512d16ff4f791869acd0b0000000000160014351c2fc52af374d40ac5e7af5f040d5c34f3343d02483045022100cb9909386da8eeda5c505b904a41f3a539d03e9c3af2d2030a6c6bcd3125884802203b922ab5815342501c58dec34317c8a7f02dddccabebbd3deb814dbccb8b4809012103481e8f9077010011be6b251eccc7da40dd22ebe0cee03c31b45fa76ee596a7e200000000', ); }); + + it('should be able to sign taproot transaction and extract txId and txHex', () => { + bitcoin.initEccLib(ecc); + + const path = "m/86'/0'/0'/0/0"; + const childNode = signer.derivePath(path); + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + + const { output } = bitcoin.payments.p2tr({ + internalPubkey: childNodeXOnlyPubkey, + network: bitcoin.networks.bitcoin, + }); + + const nonWitnessUtxo = new bitcoin.Transaction(); + nonWitnessUtxo.addInput(Buffer.alloc(32, 0), 0); + nonWitnessUtxo.addOutput(output!, 1000); + + const psbtBase64 = new bitcoin.Psbt({ network: networks.testnet }) + .addInput({ + hash: Buffer.alloc(32, 0), + index: 0, + witnessUtxo: { value: 1000, script: output }, + nonWitnessUtxo: nonWitnessUtxo.toBuffer(), + tapInternalKey: childNodeXOnlyPubkey, + tapBip32Derivation: [ + { + masterFingerprint: signer.fingerprint, + pubkey: childNodeXOnlyPubkey, + path, + leafHashes: [], + } + ], + }) + .addOutput({ + value: 100, + address: "tb1qx6xg96cgg3nelhthv7du7xfn2t2hsgqrvdg7mj", + }) + .toBase64(); + + const tx = new BtcPsbt(psbtBase64, BitcoinNetwork.Test); + const { txId, txHex } = tx.signPsbt(signer); + expect(txId).toBe( + 'e0a70feca465add48d89fade7c66b68589fc95f21c8e69e3121a4bd6cee3302d', + ); + expect(txHex).toBe( + '0200000000010100000000000000000000000000000000000000000000000000000000000000000000000000ffffffff016400000000000000160014368c82eb0844679fdd77679bcf193352d57820030140dec9eb88adab2689a69d3e8e32f409c6ed7bb6d7916d1bb780603fd4483849e2dacc27ea1a89c221e66944a84cad617d2489bbb5d69726bfdf3b3390a2a4db4900000000', + ); + }); }); }); diff --git a/packages/snap/src/bitcoin/__tests__/tapSigner.test.ts b/packages/snap/src/bitcoin/__tests__/tapSigner.test.ts new file mode 100644 index 00000000..0662e484 --- /dev/null +++ b/packages/snap/src/bitcoin/__tests__/tapSigner.test.ts @@ -0,0 +1,279 @@ +import * as bitcoin from 'bitcoinjs-lib'; +import BIP32Factory, { BIP32Interface } from 'bip32'; +import { BitcoinNetwork } from '../../interface'; +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; +import { toXOnly } from "bitcoinjs-lib/src/psbt/bip371"; +import { BtcPsbt, AccountSigner } from '../index'; +import { PsbtInputExtended } from 'bip174/src/lib/interfaces'; +import { TransactionInput } from 'bitcoinjs-lib/src/psbt'; +import { signAllInputsHD, tapInputHasHDKey, tapOutputHasHDKey } from '../tapSigner'; + +const encoder = new TextEncoder(); + +export function createInscriptionScript(xOnlyPublicKey: Buffer, contentType: Buffer, content: Buffer) { + const protocolId = Buffer.from(encoder.encode("ord")); + return [ + xOnlyPublicKey, + bitcoin.opcodes.OP_CHECKSIG, + bitcoin.opcodes.OP_0, + bitcoin.opcodes.OP_IF, + protocolId, + 1, + 1, + contentType, + bitcoin.opcodes.OP_0, + content, + bitcoin.opcodes.OP_ENDIF, + ]; +} + +function createTaprootPsbt( + rootKey: BIP32Interface, + path: string, + modifyInput: (txInput: PsbtInputExtended & TransactionInput) => PsbtInputExtended & TransactionInput = txInput => txInput +) { + const childNode = rootKey.derivePath(path); + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + + const { output } = bitcoin.payments.p2tr({ + internalPubkey: childNodeXOnlyPubkey, + network: bitcoin.networks.bitcoin, + }); + + const nonWitnessUtxo = new bitcoin.Transaction(); + nonWitnessUtxo.addInput(Buffer.alloc(32, 0), 0); + nonWitnessUtxo.addOutput(output!, 1000); + + return new bitcoin.Psbt({ network: bitcoin.networks.bitcoin }) + .addInput(modifyInput({ + hash: Buffer.alloc(32, 0), + index: 0, + witnessUtxo: { value: 1000, script: output }, + nonWitnessUtxo: nonWitnessUtxo.toBuffer(), + tapInternalKey: childNodeXOnlyPubkey, + tapBip32Derivation: [ + { + masterFingerprint: rootKey.fingerprint, + pubkey: childNodeXOnlyPubkey, + path, + leafHashes: [], + } + ], + })) + .addOutput({ + value: 100, + address: "1Fh7ajXabJBpZPZw8bjD3QU4CuQ3pRty9u", + }); +} + +describe('tapSigner', () => { + const testRootPrivateKey = "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu"; + const rootKey = BIP32Factory(ecc).fromBase58(testRootPrivateKey); + + beforeEach(() => { + bitcoin.initEccLib(ecc); + }) + + it('should sign taproot input', async () => { + const path = "m/86'/0'/0'/0/0"; + const accountSigner = new AccountSigner( + rootKey.deriveHardened(86).deriveHardened(0).deriveHardened(0), + Buffer.from("73c5da0a", "hex"), + ); + + const psbt = createTaprootPsbt(rootKey, path); + const psbtBase64 = psbt.toBase64(); + + const btcPsbt = new BtcPsbt(psbtBase64, BitcoinNetwork.Test); + const signedPsbtBase64 = btcPsbt.signInput(0, accountSigner); + + const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64).finalizeAllInputs(); + expect(signedPsbt.extractTransaction().ins[0].witness.length).toBeGreaterThan(0); + }); + + it('should sign ordinal reveal', async () => { + const path = "m/84'/0'/0'/0/0"; + const accountSigner = new AccountSigner( + rootKey.deriveHardened(84).deriveHardened(0).deriveHardened(0), + Buffer.from("73c5da0a", "hex"), + ); + + const childNode = rootKey.derivePath(path); + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + + const script = createInscriptionScript( + childNodeXOnlyPubkey, + Buffer.from(encoder.encode("text/plain;charset=utf-8")), + Buffer.from(encoder.encode("Hello World")) + ); + const outputScript = bitcoin.script.compile(script); + + const scriptTree = { + output: outputScript, + redeemVersion: 192, + }; + + const scriptTaproot = bitcoin.payments.p2tr({ + internalPubkey: childNodeXOnlyPubkey, + scriptTree, + redeem: scriptTree, + network: bitcoin.networks.bitcoin, + }); + + const cblock = scriptTaproot.witness?.[scriptTaproot.witness.length - 1]; + + const tapLeafScript = { + leafVersion: scriptTaproot.redeemVersion!, + script: outputScript, + controlBlock: cblock, + }; + + const commitTx = new bitcoin.Transaction(); + + // this is the reveal transaction + const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin }) + .addInput({ + hash: commitTx.getId(), + index: 0, + witnessUtxo: { + value: 10000, + script: scriptTaproot.output!, + }, + nonWitnessUtxo: commitTx.toBuffer(), + tapLeafScript: [tapLeafScript], + tapBip32Derivation: [ + { + masterFingerprint: rootKey.fingerprint, + pubkey: childNodeXOnlyPubkey, + path, + leafHashes: [], + } + ], + }) + .addOutput({ + value: 10000, + address: "1Fh7ajXabJBpZPZw8bjD3QU4CuQ3pRty9u", + }); + const psbtBase64 = psbt.toBase64(); + + const btcPsbt = new BtcPsbt(psbtBase64, BitcoinNetwork.Test); + const signedPsbtBase64 = btcPsbt.signInput(0, accountSigner, { disableTweakSigner: true }); + + const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64).finalizeAllInputs(); + expect(signedPsbt.extractTransaction().ins[0].witness.length).toBeGreaterThan(0); + }); + + it('should throw error without tapBip32Derivation', () => { + const path = "m/86'/0'/0'/0/0"; + const accountSigner = new AccountSigner( + rootKey.deriveHardened(86).deriveHardened(0).deriveHardened(0), + Buffer.from("73c5da0a", "hex"), + ); + + const psbt = createTaprootPsbt(rootKey, path, input => { + input.tapBip32Derivation = []; + return input; + }); + const psbtBase64 = psbt.toBase64(); + + const btcPsbt = new BtcPsbt(psbtBase64, BitcoinNetwork.Test); + expect(() => { btcPsbt.signInput(0, accountSigner) }) + .toThrowError('Need tapBip32Derivation to sign with HD'); + }); + + it('should throw error with invalid tapBip32Derivation', () => { + const path = "m/86'/0'/0'/0/0"; + const accountSigner = new AccountSigner( + rootKey.deriveHardened(86).deriveHardened(0).deriveHardened(0), + Buffer.from("73c5da0a", "hex"), + ); + + const psbt = createTaprootPsbt(rootKey, path, input => { + input.tapBip32Derivation[0].masterFingerprint = Buffer.alloc(4, 0); + return input; + }); + const psbtBase64 = psbt.toBase64(); + + const btcPsbt = new BtcPsbt(psbtBase64, BitcoinNetwork.Test); + expect(() => { btcPsbt.signInput(0, accountSigner) }) + .toThrowError('Need one tapBip32Derivation masterFingerprint to match the HDSigner fingerprint'); + }); + + it('should throw error with invalid pubkey', () => { + const path = "m/86'/0'/0'/0/0"; + const accountSigner = new AccountSigner( + rootKey.deriveHardened(86).deriveHardened(0).deriveHardened(0), + Buffer.from("73c5da0a", "hex"), + ); + + const psbt = createTaprootPsbt(rootKey, path, input => { + input.tapBip32Derivation[0].pubkey = Buffer.alloc(32, 0); + return input; + }); + const psbtBase64 = psbt.toBase64(); + + const btcPsbt = new BtcPsbt(psbtBase64, BitcoinNetwork.Test); + expect(() => { btcPsbt.signInput(0, accountSigner) }) + .toThrowError('pubkey did not match tapBip32Derivation'); + }); + + it('should check input has taproot derivation', () => { + const path = "m/86'/0'/0'/0/0"; + const childNode = rootKey.derivePath(path); + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + + expect(tapInputHasHDKey({}, rootKey)).toBe(false); + expect(tapInputHasHDKey({ + tapBip32Derivation: [{ + masterFingerprint: rootKey.fingerprint, + pubkey: childNodeXOnlyPubkey, + path, + leafHashes: [], + }] + }, rootKey)).toBe(true); + }); + + it('should check output has taproot derivation', () => { + const path = "m/86'/0'/0'/0/0"; + const childNode = rootKey.derivePath(path); + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + + expect(tapOutputHasHDKey({}, rootKey)).toBe(false); + expect(tapOutputHasHDKey({ + tapBip32Derivation: [{ + masterFingerprint: rootKey.fingerprint, + pubkey: childNodeXOnlyPubkey, + path, + leafHashes: [], + }] + }, rootKey)).toBe(true); + }); + + it('should throw if options not defined for each input', () => { + const accountSigner = new AccountSigner( + rootKey.deriveHardened(86).deriveHardened(0).deriveHardened(0), + Buffer.from("73c5da0a", "hex"), + ); + + const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin }); + psbt.addInput({ + hash: Buffer.alloc(32, 0), + index: 0, + }); + + expect(() => { signAllInputsHD(psbt, accountSigner, []) }) + .toThrowError('Need options for each input'); + }); + + it('should fail to sign with invalid sighashTypes', () => { + const path = "m/86'/0'/0'/0/0"; + const accountSigner = new AccountSigner( + rootKey.deriveHardened(86).deriveHardened(0).deriveHardened(0), + Buffer.from("73c5da0a", "hex"), + ); + + const psbt = createTaprootPsbt(rootKey, path); + expect(() => { signAllInputsHD(psbt, accountSigner, [{ sighashTypes: [bitcoin.Transaction.SIGHASH_ALL] }]) }) + .toThrowError('No inputs were signed'); + }); +}); diff --git a/packages/snap/src/bitcoin/cryptoPath.ts b/packages/snap/src/bitcoin/cryptoPath.ts index db5e1e3b..4ec6a5fc 100644 --- a/packages/snap/src/bitcoin/cryptoPath.ts +++ b/packages/snap/src/bitcoin/cryptoPath.ts @@ -44,7 +44,7 @@ export const fromHdPathToObj = (hdPath: string): HdPath => { export const parseLightningPath = (hdPath: string): LightningPath => { const regex = /(\d'?)+/g; const numbers = hdPath.match(regex); - const isHardened = (str:string) => { + const isHardened = (str: string) => { return str.indexOf("'") !== -1 } diff --git a/packages/snap/src/bitcoin/index.ts b/packages/snap/src/bitcoin/index.ts index 42279590..d42b1273 100644 --- a/packages/snap/src/bitcoin/index.ts +++ b/packages/snap/src/bitcoin/index.ts @@ -1,10 +1,13 @@ import secp256k1 from 'secp256k1'; import { BIP32Interface } from 'bip32'; import { HDSigner, Psbt, Signer } from 'bitcoinjs-lib'; -import { BitcoinNetwork } from '../interface'; +import { isTaprootInput } from "bitcoinjs-lib/src/psbt/bip371"; +import { BitcoinNetwork, SignInputOptions, SignPsbtOptions } from '../interface'; import { PsbtValidator } from '../bitcoin/PsbtValidator'; import { PsbtHelper } from '../bitcoin/PsbtHelper'; import { getNetwork } from './getNetwork'; +import { signAllInputsHD, signTapInputHD, validateSignaturesOfAllInputs } from './tapSigner'; +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; export class AccountSigner implements HDSigner, Signer { publicKey: Buffer; @@ -17,7 +20,8 @@ export class AccountSigner implements HDSigner, Signer { this.fingerprint = mfp || this.node.fingerprint } - derivePath(path: string): HDSigner { + // m / purpose' / coinType' / account' / change / addressIndex + derivePath(path: string): AccountSigner { try { let splitPath = path.split('/'); if (splitPath[0] == 'm') { @@ -49,28 +53,36 @@ export class AccountSigner implements HDSigner, Signer { signSchnorr(hash: Buffer): Buffer { return this.node.signSchnorr(hash) } + + tweak(t: Buffer): Signer { + return this.node.tweak(t); + } } const validator = (pubkey: Buffer, msghash: Buffer, signature: Buffer) => { return secp256k1.ecdsaVerify(new Uint8Array(signature), new Uint8Array(msghash), new Uint8Array(pubkey)) } -export class BtcTx { - private tx: Psbt; +const schnorrValidator = (pubkey: Buffer, msghash: Buffer, signature: Buffer) => { + return ecc.verifySchnorr(msghash, pubkey, signature); +} + +export class BtcPsbt { + private psbt: Psbt; private network: BitcoinNetwork; constructor(base64Psbt: string, network: BitcoinNetwork) { - this.tx = Psbt.fromBase64(base64Psbt, { network: getNetwork(network) }) + this.psbt = Psbt.fromBase64(base64Psbt, { network: getNetwork(network) }) this.network = network; } - validateTx(accountSigner: AccountSigner) { - const validator = new PsbtValidator(this.tx, this.network); + validatePsbt(accountSigner: AccountSigner) { + const validator = new PsbtValidator(this.psbt, this.network); return validator.validate(accountSigner); } extractPsbtJson() { - const psbtHelper = new PsbtHelper(this.tx, this.network); + const psbtHelper = new PsbtHelper(this.psbt, this.network); const changeAddress = psbtHelper.changeAddresses const unit = this.network === BitcoinNetwork.Main ? 'sats' : 'tsats'; @@ -82,8 +94,8 @@ export class BtcTx { network: `${this.network}net`, } - if(changeAddress.length > 0){ - return {...transaction, changeAddress: changeAddress.join(",")} + if (changeAddress.length > 0) { + return { ...transaction, changeAddress: changeAddress.join(",") } } return transaction } @@ -92,23 +104,27 @@ export class BtcTx { return Object.entries(this.extractPsbtJson()).map(([key, value]) => `${key}: ${value}\n`).join('') } - signInput(inputIndex: number, accountSigner: AccountSigner, path: string) { + signInput(inputIndex: number, accountSigner: AccountSigner, opts: SignInputOptions = {}) { try { - this.tx.signInput(inputIndex, accountSigner.derivePath(path)); - return this.tx.toBase64(); + if (isTaprootInput(this.psbt.data.inputs[inputIndex])) { + signTapInputHD(this.psbt, inputIndex, accountSigner, opts); + } else { + this.psbt.signInputHD(inputIndex, accountSigner); + } + return this.psbt.toBase64(); } catch (e) { - console.log(e); throw new Error(e); } } - signTx(accountSigner: AccountSigner) { + signPsbt(accountSigner: AccountSigner, opts: SignPsbtOptions = {}) { try { - this.tx.signAllInputsHD(accountSigner) - if (this.tx.validateSignaturesOfAllInputs(validator)) { - this.tx.finalizeAllInputs(); - const txId = this.tx.extractTransaction().getId(); - const txHex = this.tx.extractTransaction().toHex(); + const signInputOpts = opts.signInputOpts !== undefined ? opts.signInputOpts : new Array(this.psbt.data.inputs.length).fill({}); + signAllInputsHD(this.psbt, accountSigner, signInputOpts); + if (validateSignaturesOfAllInputs(this.psbt, validator, schnorrValidator)) { + this.psbt.finalizeAllInputs(); + const txId = this.psbt.extractTransaction().getId(); + const txHex = this.psbt.extractTransaction().toHex(); return { txId, txHex diff --git a/packages/snap/src/bitcoin/tapSigner.ts b/packages/snap/src/bitcoin/tapSigner.ts new file mode 100644 index 00000000..5cca451b --- /dev/null +++ b/packages/snap/src/bitcoin/tapSigner.ts @@ -0,0 +1,155 @@ +import { PsbtInput, PsbtOutput, TapBip32Derivation } from "bip174/src/lib/interfaces"; +import { HDSigner, Psbt, Signer, crypto, Transaction } from 'bitcoinjs-lib'; +import { ValidateSigFunction } from "bitcoinjs-lib/src/psbt"; +import { isTaprootInput, toXOnly } from "bitcoinjs-lib/src/psbt/bip371"; +import { SignInputOptions } from "interface"; + +interface TweakHDSigner extends HDSigner { + derivePath(path: string): TweakHDSigner; + tweak(t: Buffer): Signer +} + +function checkForInput(inputs: PsbtInput[], inputIndex: number): PsbtInput { + const input = inputs[inputIndex]; + if (input === undefined) throw new Error(`No input #${inputIndex}`); + return input; +} + +function getTapSignersFromHD( + inputIndex: number, + inputs: PsbtInput[], + hdKeyPair: TweakHDSigner, +): Array { + const input = checkForInput(inputs, inputIndex); + if (!input.tapBip32Derivation || input.tapBip32Derivation.length === 0) { + throw new Error('Need tapBip32Derivation to sign with HD'); + } + const myDerivations = input.tapBip32Derivation + .map(bipDv => { + if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) { + return bipDv; + } else { + return; + } + }) + .filter(v => !!v); + if (myDerivations.length === 0) { + throw new Error( + 'Need one tapBip32Derivation masterFingerprint to match the HDSigner fingerprint', + ); + } + const signers: Array = myDerivations.map(bipDv => { + const node = hdKeyPair.derivePath(bipDv!.path); + const xOnlyPubKey = toXOnly(node.publicKey); + if (!bipDv!.pubkey.equals(xOnlyPubKey)) { + throw new Error('pubkey did not match tapBip32Derivation'); + } + return node; + }); + return signers; +} + +function tweakSigner(childNode: TweakHDSigner) { + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + const tweakedChildNode = childNode.tweak( + crypto.taggedHash('TapTweak', childNodeXOnlyPubkey), + ); + return tweakedChildNode; +} + +export function signTapInputHD( + psbt: Psbt, + inputIndex: number, + hdKeyPair: TweakHDSigner, + signInputOpts?: SignInputOptions, +): Psbt { + const signers = getTapSignersFromHD( + inputIndex, + psbt.data.inputs, + hdKeyPair, + ) as TweakHDSigner[]; + const { + disableTweakSigner = false, + sighashTypes + } = signInputOpts; + signers.forEach(signer => psbt.signTaprootInput( + inputIndex, + disableTweakSigner ? signer : tweakSigner(signer), + undefined, + sighashTypes, + )); + return psbt; +} + +function tapBip32DerivationIsMine( + root: HDSigner, +): (d: TapBip32Derivation) => boolean { + return (d: TapBip32Derivation): boolean => { + if (!d.masterFingerprint.equals(root.fingerprint)) return false; + if (!toXOnly(root.derivePath(d.path).publicKey).equals(d.pubkey)) return false; + return true; + }; +} + +export function tapInputHasHDKey(input: PsbtInput, root: HDSigner): boolean { + const derivationIsMine = tapBip32DerivationIsMine(root); + return ( + !!input.tapBip32Derivation && input.tapBip32Derivation.some(derivationIsMine) + ); +} + +export function tapOutputHasHDKey(output: PsbtOutput, root: HDSigner): boolean { + const derivationIsMine = tapBip32DerivationIsMine(root); + return ( + !!output.tapBip32Derivation && output.tapBip32Derivation.some(derivationIsMine) + ); +} + +function range(n: number): number[] { + return [...Array(n).keys()]; +} + +export function signAllInputsHD( + psbt: Psbt, + hdKeyPair: TweakHDSigner, + signInputOpts: SignInputOptions[], +): Psbt { + if (psbt.data.inputs.length != signInputOpts.length) { + throw new Error('Need options for each input'); + } + + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + throw new Error('Need HDSigner to sign input'); + } + + const results: boolean[] = []; + for (const i of range(psbt.data.inputs.length)) { + try { + if (isTaprootInput(psbt.data.inputs[i])) { + signTapInputHD(psbt, i, hdKeyPair, signInputOpts[i]); + } else { + psbt.signInputHD(i, hdKeyPair, signInputOpts[i].sighashTypes); + } + results.push(true); + } catch (err) { + results.push(false); + } + } + if (results.every(v => v === false)) { + throw new Error('No inputs were signed'); + } + return psbt; +} + +export function validateSignaturesOfAllInputs(psbt: Psbt, validator: ValidateSigFunction, schnorrValidator: ValidateSigFunction): boolean { + checkForInput(psbt.data.inputs, 0); // making sure we have at least one + const results = range(psbt.data.inputs.length).map(idx => { + const input = psbt.data.inputs[idx]; + if (isTaprootInput(input)) { + return psbt.validateSignaturesOfInput(idx, schnorrValidator); + } else { + return psbt.validateSignaturesOfInput(idx, validator); + } + }); + return results.reduce((final, res) => res === true && final, true); +} \ No newline at end of file diff --git a/packages/snap/src/bitcoin/xpubConverter.ts b/packages/snap/src/bitcoin/xpubConverter.ts index a293de11..1b4a3e05 100644 --- a/packages/snap/src/bitcoin/xpubConverter.ts +++ b/packages/snap/src/bitcoin/xpubConverter.ts @@ -27,6 +27,10 @@ const scriptTypeToXpubPrefix: Record { diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 60b40f2d..cb367fd0 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -1,5 +1,5 @@ -import {getNetwork} from './bitcoin/getNetwork'; -import {Snap, MetamaskBTCRpcRequest} from './interface'; +import { getNetwork } from './bitcoin/getNetwork'; +import { Snap, MetamaskBTCRpcRequest } from './interface'; import { getExtendedPublicKey, getAllXpubs, @@ -26,7 +26,7 @@ export type RpcRequest = { request: MetamaskBTCRpcRequest; }; -export const onRpcRequest = async ({origin, request}: RpcRequest) => { +export const onRpcRequest = async ({ origin, request }: RpcRequest) => { await validateRequest(snap, origin, request); initEccLib(ecc); @@ -43,6 +43,7 @@ export const onRpcRequest = async ({origin, request}: RpcRequest) => { origin, snap, ); + // keep this for backwards compatibility case 'btc_signPsbt': return signPsbt( origin, @@ -50,6 +51,7 @@ export const onRpcRequest = async ({origin, request}: RpcRequest) => { request.params.psbt, request.params.network, request.params.scriptType, + request.params.opts, ); case 'btc_signInput': return signInput( @@ -59,8 +61,8 @@ export const onRpcRequest = async ({origin, request}: RpcRequest) => { request.params.network, request.params.scriptType, request.params.inputIndex, - request.params.path, - ); + request.params.opts, + ); case 'btc_getMasterFingerprint': return getMasterFingerprint(snap); case 'btc_network': @@ -84,8 +86,8 @@ export const onRpcRequest = async ({origin, request}: RpcRequest) => { snap, { key: request.params.key, - ...(request.params.walletId && {walletId: request.params.walletId}), - ...(request.params.type && {type: request.params.type}), + ...(request.params.walletId && { walletId: request.params.walletId }), + ...(request.params.type && { type: request.params.type }), } ); case 'btc_signLNInvoice': diff --git a/packages/snap/src/interface.ts b/packages/snap/src/interface.ts index 7079c746..a67fbc0e 100644 --- a/packages/snap/src/interface.ts +++ b/packages/snap/src/interface.ts @@ -11,15 +11,25 @@ export interface GetAllXpubsRequest { params: Record } +export interface SignPsbtOptions { + signInputOpts?: SignInputOptions[]; +} + export interface SignPsbt { method: 'btc_signPsbt'; params: { psbt: string; network: BitcoinNetwork; scriptType: ScriptType; + opts?: SignPsbtOptions; }; } +export interface SignInputOptions { + sighashTypes?: number[]; + disableTweakSigner?: boolean; +} + export interface SignInput { method: 'btc_signInput'; params: { @@ -27,7 +37,7 @@ export interface SignInput { network: BitcoinNetwork; scriptType: ScriptType; inputIndex: number, - path: string, + opts?: SignInputOptions, }; } @@ -96,6 +106,7 @@ export enum ScriptType { P2PKH = 'P2PKH', P2SH_P2WPKH = 'P2SH-P2WPKH', P2WPKH = 'P2WPKH', + P2TR = 'P2TR', } export enum BitcoinNetwork { diff --git a/packages/snap/src/rpc/__tests__/getAllXpubs.test.ts b/packages/snap/src/rpc/__tests__/getAllXpubs.test.ts index adb5f2dd..554629b0 100644 --- a/packages/snap/src/rpc/__tests__/getAllXpubs.test.ts +++ b/packages/snap/src/rpc/__tests__/getAllXpubs.test.ts @@ -19,9 +19,9 @@ describe('getAllXpubs', () => { it('should get all 6 extended public keys from wallet if user approve', async () => { snapStub.rpcStubs.snap_dialog.mockResolvedValue(true); snapStub.rpcStubs.snap_getBip32Entropy.mockResolvedValue(bip44.slip10Node); - const {xpubs} = await getAllXpubs(domain, snapStub); + const { xpubs } = await getAllXpubs(domain, snapStub); - expect(snapStub.rpcStubs.snap_getBip32Entropy).toBeCalledTimes(6); + expect(snapStub.rpcStubs.snap_getBip32Entropy).toBeCalledTimes(8); expect(xpubs).toEqual(expect.arrayContaining([ expect.stringMatching(/^xpub/), expect.stringMatching(/^ypub/), diff --git a/packages/snap/src/rpc/__tests__/getExtendedPublicKey.test.ts b/packages/snap/src/rpc/__tests__/getExtendedPublicKey.test.ts index 2e176a04..09d4b306 100644 --- a/packages/snap/src/rpc/__tests__/getExtendedPublicKey.test.ts +++ b/packages/snap/src/rpc/__tests__/getExtendedPublicKey.test.ts @@ -20,7 +20,7 @@ describe('getExtendedPublicKey', () => { snapStub.rpcStubs.snap_dialog.mockResolvedValue(true); snapStub.rpcStubs.snap_getBip32Entropy.mockResolvedValue(bip44.slip10Node); - const {xpub} = await getExtendedPublicKey(domain, snapStub, ScriptType.P2PKH, networks.bitcoin) + const { xpub } = await getExtendedPublicKey(domain, snapStub, ScriptType.P2PKH, networks.bitcoin) expect(snapStub.rpcStubs.snap_getBip32Entropy).toBeCalledTimes(1); expect(xpub).toBe(bip44.xpub) diff --git a/packages/snap/src/rpc/__tests__/getLNDataFromSnap.test.ts b/packages/snap/src/rpc/__tests__/getLNDataFromSnap.test.ts index 8f2cc23e..27dd12ec 100644 --- a/packages/snap/src/rpc/__tests__/getLNDataFromSnap.test.ts +++ b/packages/snap/src/rpc/__tests__/getLNDataFromSnap.test.ts @@ -1,7 +1,7 @@ -import {SnapMock} from '../__mocks__/snap'; -import {getLNDataFromSnap} from '../getLNDataFromSnap'; -import {KeyOptions} from '../../interface'; -import {bip44, LNDataFromSnap, LNDataToSnap} from './fixtures/bitcoinNode'; +import { SnapMock } from '../__mocks__/snap'; +import { getLNDataFromSnap } from '../getLNDataFromSnap'; +import { KeyOptions } from '../../interface'; +import { bip44, LNDataFromSnap, LNDataToSnap } from './fixtures/bitcoinNode'; describe('getLNDataFromSnap', () => { const snapStub = new SnapMock(); diff --git a/packages/snap/src/rpc/__tests__/getMasterFingerprint.test.ts b/packages/snap/src/rpc/__tests__/getMasterFingerprint.test.ts index b8fcee96..c8e43b86 100644 --- a/packages/snap/src/rpc/__tests__/getMasterFingerprint.test.ts +++ b/packages/snap/src/rpc/__tests__/getMasterFingerprint.test.ts @@ -31,7 +31,7 @@ describe('getMasterFingerprint', () => { }) it("should return undefined string if mfp doesn't exist", async () => { - const {masterFingerprint, ...slip10NodeWithoutMFP} = bip44.slip10Node + const { masterFingerprint, ...slip10NodeWithoutMFP } = bip44.slip10Node snapStub.rpcStubs.snap_getBip32Entropy.mockResolvedValue(slip10NodeWithoutMFP); const xfp = await getMasterFingerprint(snapStub) diff --git a/packages/snap/src/rpc/__tests__/manageNetwork.test.ts b/packages/snap/src/rpc/__tests__/manageNetwork.test.ts index c1b335de..cd58b34c 100644 --- a/packages/snap/src/rpc/__tests__/manageNetwork.test.ts +++ b/packages/snap/src/rpc/__tests__/manageNetwork.test.ts @@ -19,14 +19,14 @@ describe('masterFingerprint', () => { describe("manageNetwork", () => { it('should return network if it exists', async () => { - snapStub.rpcStubs.snap_manageState.mockResolvedValue({mfp, network}); + snapStub.rpcStubs.snap_manageState.mockResolvedValue({ mfp, network }); const storedNetwork = await manageNetwork(origin, snapStub, "get"); expect(storedNetwork).toBe('test') }) it("should return empty string if network doesn't exist", async () => { - snapStub.rpcStubs.snap_manageState.mockResolvedValue({mfp}); + snapStub.rpcStubs.snap_manageState.mockResolvedValue({ mfp }); const storedNetwork = await manageNetwork(origin, snapStub, "get") expect(storedNetwork).toBe("") @@ -41,7 +41,7 @@ describe('masterFingerprint', () => { }) it("should set network to target when user approves given target network", async () => { - snapStub.rpcStubs.snap_manageState.mockResolvedValue({mfp, network}); + snapStub.rpcStubs.snap_manageState.mockResolvedValue({ mfp, network }); snapStub.rpcStubs.snap_dialog.mockResolvedValue(true); jest.spyOn(manageState, 'updatePersistedData'); await manageNetwork(origin, snapStub, "set", BitcoinNetwork.Main); @@ -50,7 +50,7 @@ describe('masterFingerprint', () => { }) it("should not set network to target when user not approve given target network", async () => { - snapStub.rpcStubs.snap_manageState.mockResolvedValue({mfp, network}); + snapStub.rpcStubs.snap_manageState.mockResolvedValue({ mfp, network }); snapStub.rpcStubs.snap_dialog.mockResolvedValue(false); jest.spyOn(manageState, 'updatePersistedData'); await manageNetwork(origin, snapStub, "set", BitcoinNetwork.Main); diff --git a/packages/snap/src/rpc/__tests__/signInput.test.ts b/packages/snap/src/rpc/__tests__/signInput.test.ts new file mode 100644 index 00000000..db592d56 --- /dev/null +++ b/packages/snap/src/rpc/__tests__/signInput.test.ts @@ -0,0 +1,110 @@ +import * as bitcoin from 'bitcoinjs-lib'; +import BIP32Factory from 'bip32'; +import { BitcoinNetwork, ScriptType } from '../../interface'; +import { signInput } from '..'; +import { SnapMock } from '../__mocks__/snap'; +import { extractAccountPrivateKey } from '../getExtendedPublicKey'; +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; +import { toXOnly } from "bitcoinjs-lib/src/psbt/bip371"; +import { RequestErrors } from '../../errors'; + +jest.mock('../../rpc/getExtendedPublicKey'); + +const mockSignInput = jest.fn(); +jest.mock("../../bitcoin", () => { + return { + BtcPsbt: jest.fn().mockImplementation(() => { + return { + validatePsbt: () => true, + extractPsbtJson: jest.fn().mockReturnValue({ + from: "mx5m68zHiGnFEoMjTdkWinmBAYsWyp9DJk", + to: "mx5m68zHiGnFEoMjTdkWinmBAYsWyp9DJk", + value: 9500, + fee: 500, + network: "test" + }), + signInput: mockSignInput + }; + }), + AccountSigner: jest.fn() + }; +}) + +describe('signInput', () => { + const snapStub = new SnapMock(); + const domain = "www.justsnap.io" + const testRootPrivateKey = "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu"; + let testPsbtBase64: string; + + beforeAll(() => { + (extractAccountPrivateKey as jest.Mock).mockResolvedValue({ + node: BIP32Factory(ecc).fromBase58(testRootPrivateKey).deriveHardened(86).deriveHardened(0).deriveHardened(0), + mfp: "73c5da0a" + }); + + bitcoin.initEccLib(ecc); + + const rootKey = BIP32Factory(ecc).fromBase58(testRootPrivateKey); + const path = "m/86'/0'/0'/0/0"; + const childNode = rootKey.derivePath(path); + const childNodeXOnlyPubkey = toXOnly(childNode.publicKey); + const { output } = bitcoin.payments.p2tr({ + internalPubkey: childNodeXOnlyPubkey, + network: bitcoin.networks.bitcoin, + }); + + const nonWitnessUtxo = new bitcoin.Transaction(); + nonWitnessUtxo.addInput(Buffer.alloc(32, 0), 0); + nonWitnessUtxo.addOutput(output!, 1000); + + const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin }) + .addInput({ + hash: Buffer.alloc(32, 0), + index: 0, + witnessUtxo: { value: 1000, script: output }, + nonWitnessUtxo: nonWitnessUtxo.toBuffer(), + tapInternalKey: childNodeXOnlyPubkey, + tapBip32Derivation: [ + { + masterFingerprint: rootKey.fingerprint, + pubkey: Buffer.alloc(32, 0), + path, + leafHashes: [], + } + ], + }) + .addOutput({ + value: 100, + address: "1Fh7ajXabJBpZPZw8bjD3QU4CuQ3pRty9u", + }); + testPsbtBase64 = psbt.toBase64(); + }); + + afterEach(() => { + snapStub.reset() + }); + + it('should sign taproot input if user approved', async () => { + snapStub.rpcStubs.snap_dialog.mockResolvedValue(true); + snapStub.rpcStubs.snap_manageState.mockResolvedValue({ network: BitcoinNetwork.Main }); + + await signInput(domain, snapStub, testPsbtBase64, BitcoinNetwork.Main, ScriptType.P2TR, 0); + await expect(mockSignInput).toHaveBeenCalled(); + }); + + it('should reject if user does not sign the input', async () => { + snapStub.rpcStubs.snap_dialog.mockResolvedValue(false); + + await expect(signInput(domain, snapStub, testPsbtBase64, BitcoinNetwork.Main, ScriptType.P2TR, 0)) + .rejects + .toThrowError('User reject the sign request'); + expect(snapStub.rpcStubs.snap_getBip32Entropy).not.toHaveBeenCalled(); + }); + + it('should reject if network is wrong', async () => { + await expect(signInput(domain, snapStub, testPsbtBase64, BitcoinNetwork.Test, ScriptType.P2TR, 0)) + .rejects + .toThrowError(RequestErrors.NetworkNotMatch.message); + expect(snapStub.rpcStubs.snap_getBip32Entropy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/snap/src/rpc/__tests__/signPSBT.test.ts b/packages/snap/src/rpc/__tests__/signPSBT.test.ts index 3277fdb8..be952c0f 100644 --- a/packages/snap/src/rpc/__tests__/signPSBT.test.ts +++ b/packages/snap/src/rpc/__tests__/signPSBT.test.ts @@ -1,18 +1,20 @@ import { networks } from 'bitcoinjs-lib'; -import * as bip32 from 'bip32'; +import BIP32Factory from 'bip32'; import { BitcoinNetwork, ScriptType } from '../../interface'; import { signPsbt } from '../../rpc'; import { SnapMock } from '../__mocks__/snap'; import { extractAccountPrivateKey } from '../../rpc/getExtendedPublicKey'; +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; +import { RequestErrors } from '../../errors'; jest.mock('../../rpc/getExtendedPublicKey'); const mockSignPsbt = jest.fn(); jest.mock("../../bitcoin", () => { return { - BtcTx: jest.fn().mockImplementation(() => { + BtcPsbt: jest.fn().mockImplementation(() => { return { - validateTx: () => true, + validatePsbt: () => true, extractPsbtJson: jest.fn().mockReturnValue({ from: "mx5m68zHiGnFEoMjTdkWinmBAYsWyp9DJk", to: "mx5m68zHiGnFEoMjTdkWinmBAYsWyp9DJk", @@ -20,7 +22,7 @@ jest.mock("../../bitcoin", () => { fee: 500, network: "test" }), - signTx: mockSignPsbt + signPsbt: mockSignPsbt }; }), AccountSigner: jest.fn() @@ -35,22 +37,22 @@ describe('signPsbt', () => { beforeAll(() => { (extractAccountPrivateKey as jest.Mock).mockResolvedValue({ - node: bip32.fromBase58(testAccountPrivateKey, networks.regtest), + node: BIP32Factory(ecc).fromBase58(testAccountPrivateKey, networks.regtest), mfp: 'fd3e418b' }) - }) + }); afterEach(() => { snapStub.reset() - }) + }); - it('should call BtcTx to sign psbt if user approved', async () => { + it('should call BtcPsbt to sign psbt if user approved', async () => { snapStub.rpcStubs.snap_dialog.mockResolvedValue(true); - snapStub.rpcStubs.snap_manageState.mockResolvedValue({network: BitcoinNetwork.Test}); + snapStub.rpcStubs.snap_manageState.mockResolvedValue({ network: BitcoinNetwork.Test }); await signPsbt(domain, snapStub, testPsbtBase64, BitcoinNetwork.Test, ScriptType.P2PKH) await expect(mockSignPsbt).toHaveBeenCalled(); - }) + }); it('should reject the sign request and throw error if user reject the sign the pbst', async () => { snapStub.rpcStubs.snap_dialog.mockResolvedValue(false); @@ -59,5 +61,12 @@ describe('signPsbt', () => { .rejects .toThrowError('User reject the sign request'); expect(snapStub.rpcStubs.snap_getBip32Entropy).not.toHaveBeenCalled(); - }) + }); + + it('should reject if network is wrong', async () => { + await expect(signPsbt(domain, snapStub, testPsbtBase64, BitcoinNetwork.Main, ScriptType.P2PKH)) + .rejects + .toThrowError(RequestErrors.NetworkNotMatch.message); + expect(snapStub.rpcStubs.snap_getBip32Entropy).not.toHaveBeenCalled(); + }); }); diff --git a/packages/snap/src/rpc/getAllXpubs.ts b/packages/snap/src/rpc/getAllXpubs.ts index 40680664..9f6c19d5 100644 --- a/packages/snap/src/rpc/getAllXpubs.ts +++ b/packages/snap/src/rpc/getAllXpubs.ts @@ -6,7 +6,7 @@ import { extractAccountPrivateKey } from './getExtendedPublicKey'; import { RequestErrors, SnapError } from '../errors'; -export async function getAllXpubs(origin: string, snap: Snap): Promise<{xpubs: string[], mfp: string}> { +export async function getAllXpubs(origin: string, snap: Snap): Promise<{ xpubs: string[], mfp: string }> { const result = await snap.request({ method: 'snap_dialog', params: { diff --git a/packages/snap/src/rpc/getExtendedPublicKey.ts b/packages/snap/src/rpc/getExtendedPublicKey.ts index 68e0f845..75942329 100644 --- a/packages/snap/src/rpc/getExtendedPublicKey.ts +++ b/packages/snap/src/rpc/getExtendedPublicKey.ts @@ -8,15 +8,18 @@ import { RequestErrors, SnapError } from "../errors"; import { heading, panel, text } from "@metamask/snaps-ui"; import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; +// m / purpose' / coinType' export const pathMap: Record = { [ScriptType.P2PKH]: ['m', "44'", "0'"], [ScriptType.P2SH_P2WPKH]: ['m', "49'", "0'"], - [ScriptType.P2WPKH]: ['m', "84'", "0'"] + [ScriptType.P2WPKH]: ['m', "84'", "0'"], + [ScriptType.P2TR]: ['m', "86'", "0'"] } export const CRYPTO_CURVE = "secp256k1"; -export async function extractAccountPrivateKey(snap: Snap, network: Network, scriptType: ScriptType): Promise<{node:BIP32Interface, mfp: string}> { +// m / purpose' / coinType' / 0' +export async function extractAccountPrivateKey(snap: Snap, network: Network, scriptType: ScriptType): Promise<{ node: BIP32Interface, mfp: string }> { const path = [...pathMap[scriptType]] if (network != networks.bitcoin) { path[path.length - 1] = "1'"; @@ -47,13 +50,13 @@ export async function extractAccountPrivateKey(snap: Snap, network: Network, scr }; } - -export async function getExtendedPublicKey(origin: string, snap: Snap, scriptType: ScriptType, network: Network): Promise<{xpub: string, mfp: string}> { +export async function getExtendedPublicKey(origin: string, snap: Snap, scriptType: ScriptType, network: Network): Promise<{ xpub: string, mfp: string }> { const networkName = network == networks.bitcoin ? "mainnet" : "testnet"; switch (scriptType) { case ScriptType.P2PKH: case ScriptType.P2WPKH: case ScriptType.P2SH_P2WPKH: + case ScriptType.P2TR: const result = await snap.request({ method: 'snap_dialog', params: { @@ -65,13 +68,13 @@ export async function getExtendedPublicKey(origin: string, snap: Snap, scriptTyp }, }); - if(result) { + if (result) { const { node: accountNode, mfp } = await extractAccountPrivateKey(snap, network, scriptType) const accountPublicKey = accountNode.neutered(); const xpub = convertXpub(accountPublicKey.toBase58(), scriptType, network); const snapNetwork = await getPersistedData(snap, "network", ""); - if(!snapNetwork) { + if (!snapNetwork) { await updatePersistedData(snap, "network", network == networks.bitcoin ? BitcoinNetwork.Main : BitcoinNetwork.Test); } diff --git a/packages/snap/src/rpc/getLNDataFromSnap.ts b/packages/snap/src/rpc/getLNDataFromSnap.ts index 038a91c6..fb6bb522 100644 --- a/packages/snap/src/rpc/getLNDataFromSnap.ts +++ b/packages/snap/src/rpc/getLNDataFromSnap.ts @@ -1,6 +1,6 @@ -import {getHDNode} from '../utils/getHDNode'; -import {Snap, PersistedData, KeyOptions, LNHdPath} from '../interface'; -import {getPersistedData} from '../utils/manageState'; +import { getHDNode } from '../utils/getHDNode'; +import { Snap, PersistedData, KeyOptions, LNHdPath } from '../interface'; +import { getPersistedData } from '../utils/manageState'; import CryptoJs from 'crypto-js'; import { RequestErrors, SnapError } from "../errors"; import { heading, panel, text } from "@metamask/snaps-ui"; @@ -68,7 +68,7 @@ export async function getLNDataFromSnap( keySize: 512 / 32, iterations: 1000, }); - const credential = CryptoJs.AES.decrypt(encrypted, key, {iv: iv}); + const credential = CryptoJs.AES.decrypt(encrypted, key, { iv: iv }); return credential.toString(CryptoJs.enc.Utf8); } else { diff --git a/packages/snap/src/rpc/saveLNDataToSnap.ts b/packages/snap/src/rpc/saveLNDataToSnap.ts index b8894ff9..a67277ce 100644 --- a/packages/snap/src/rpc/saveLNDataToSnap.ts +++ b/packages/snap/src/rpc/saveLNDataToSnap.ts @@ -1,6 +1,6 @@ -import {Snap, LNHdPath} from '../interface'; -import {getHDNode} from '../utils/getHDNode'; -import {getPersistedData, updatePersistedData} from '../utils/manageState'; +import { Snap, LNHdPath } from '../interface'; +import { getHDNode } from '../utils/getHDNode'; +import { getPersistedData, updatePersistedData } from '../utils/manageState'; import CryptoJs from 'crypto-js'; export async function saveLNDataToSnap( @@ -20,7 +20,7 @@ export async function saveLNDataToSnap( }); const iv = CryptoJs.lib.WordArray.random(16); - const encrypted = CryptoJs.AES.encrypt(credential, key, {iv: iv}); + const encrypted = CryptoJs.AES.encrypt(credential, key, { iv: iv }); const encryptText = salt.toString() + iv.toString() + encrypted.toString(); const result = await getPersistedData(snap, 'lightning', {}); const newLightning = { diff --git a/packages/snap/src/rpc/signInput.ts b/packages/snap/src/rpc/signInput.ts index 4a12f39e..2afc0977 100644 --- a/packages/snap/src/rpc/signInput.ts +++ b/packages/snap/src/rpc/signInput.ts @@ -1,6 +1,6 @@ -import { BitcoinNetwork, ScriptType, Snap } from '../interface'; +import { BitcoinNetwork, ScriptType, SignInputOptions, Snap } from '../interface'; import { extractAccountPrivateKey } from './getExtendedPublicKey'; -import { AccountSigner, BtcTx } from '../bitcoin'; +import { AccountSigner, BtcPsbt } from '../bitcoin'; import { getPersistedData } from '../utils/manageState'; import { getNetwork } from '../bitcoin/getNetwork'; import { SnapError, RequestErrors } from "../errors"; @@ -13,15 +13,15 @@ export async function signInput( network: BitcoinNetwork, scriptType: ScriptType, inputIndex: number, - path: string + opts?: SignInputOptions, ): Promise { const snapNetwork = await getPersistedData(snap, "network", '' as BitcoinNetwork); - if (snapNetwork != network){ + if (snapNetwork != network) { throw SnapError.of(RequestErrors.NetworkNotMatch); } - const btcTx = new BtcTx(psbt, snapNetwork); - const txDetails = btcTx.extractPsbtJson() + const btcPsbt = new BtcPsbt(psbt, snapNetwork); + const txDetails = btcPsbt.extractPsbtJson() const result = await snap.request({ method: 'snap_dialog', @@ -37,10 +37,10 @@ export async function signInput( }); if (result) { - const {node: accountPrivateKey, mfp} = await extractAccountPrivateKey(snap, getNetwork(snapNetwork), scriptType) + const { node: accountPrivateKey, mfp } = await extractAccountPrivateKey(snap, getNetwork(snapNetwork), scriptType); const signer = new AccountSigner(accountPrivateKey, Buffer.from(mfp, 'hex')); - btcTx.validateTx(signer); - return btcTx.signInput(inputIndex, signer, path); + btcPsbt.validatePsbt(signer); + return btcPsbt.signInput(inputIndex, signer, opts); } else { throw SnapError.of(RequestErrors.RejectSign); } diff --git a/packages/snap/src/rpc/signLNInvoice.ts b/packages/snap/src/rpc/signLNInvoice.ts index d228e386..309d8b12 100644 --- a/packages/snap/src/rpc/signLNInvoice.ts +++ b/packages/snap/src/rpc/signLNInvoice.ts @@ -1,6 +1,6 @@ -import {Snap, LNHdPath} from '../interface'; -import {getHDNode} from '../utils/getHDNode'; -import {transferInvoiceContent} from '../utils/transferLNData'; +import { Snap, LNHdPath } from '../interface'; +import { getHDNode } from '../utils/getHDNode'; +import { transferInvoiceContent } from '../utils/transferLNData'; import bitcoinMessage from 'bitcoinjs-message'; import { RequestErrors, SnapError } from '../errors'; import { divider, heading, panel, text } from "@metamask/snaps-ui"; diff --git a/packages/snap/src/rpc/signPSBT.ts b/packages/snap/src/rpc/signPSBT.ts index a74e85f6..3a918ffd 100644 --- a/packages/snap/src/rpc/signPSBT.ts +++ b/packages/snap/src/rpc/signPSBT.ts @@ -1,19 +1,26 @@ -import { BitcoinNetwork, ScriptType, Snap } from '../interface'; +import { BitcoinNetwork, ScriptType, SignPsbtOptions, Snap } from '../interface'; import { extractAccountPrivateKey } from './getExtendedPublicKey'; -import { AccountSigner, BtcTx } from '../bitcoin'; +import { AccountSigner, BtcPsbt } from '../bitcoin'; import { getPersistedData } from '../utils/manageState'; import { getNetwork } from '../bitcoin/getNetwork'; import { SnapError, RequestErrors } from "../errors"; import { heading, panel, text, divider } from "@metamask/snaps-ui"; -export async function signPsbt(domain: string, snap: Snap, psbt: string, network: BitcoinNetwork, scriptType: ScriptType): Promise<{ txId: string, txHex: string }> { +export async function signPsbt( + domain: string, + snap: Snap, + psbt: string, + network: BitcoinNetwork, + scriptType: ScriptType, + opts?: SignPsbtOptions +): Promise<{ txId: string, txHex: string }> { const snapNetwork = await getPersistedData(snap, "network", '' as BitcoinNetwork); - if (snapNetwork != network){ + if (snapNetwork != network) { throw SnapError.of(RequestErrors.NetworkNotMatch); } - const btcTx = new BtcTx(psbt, snapNetwork); - const txDetails = btcTx.extractPsbtJson() + const btcPsbt = new BtcPsbt(psbt, snapNetwork); + const txDetails = btcPsbt.extractPsbtJson() const result = await snap.request({ method: 'snap_dialog', @@ -29,10 +36,10 @@ export async function signPsbt(domain: string, snap: Snap, psbt: string, network }); if (result) { - const {node: accountPrivateKey, mfp} = await extractAccountPrivateKey(snap, getNetwork(snapNetwork), scriptType) + const { node: accountPrivateKey, mfp } = await extractAccountPrivateKey(snap, getNetwork(snapNetwork), scriptType) const signer = new AccountSigner(accountPrivateKey, Buffer.from(mfp, 'hex')); - btcTx.validateTx(signer); - return btcTx.signTx(signer); + btcPsbt.validatePsbt(signer); + return btcPsbt.signPsbt(signer, opts); } else { throw SnapError.of(RequestErrors.RejectSign); } diff --git a/packages/snap/src/rpc/validateRequest.ts b/packages/snap/src/rpc/validateRequest.ts index 57e03b2e..4aae9a3d 100644 --- a/packages/snap/src/rpc/validateRequest.ts +++ b/packages/snap/src/rpc/validateRequest.ts @@ -1,6 +1,6 @@ -import {BitcoinNetwork, Snap} from '../interface'; -import {getPersistedData} from '../utils/manageState'; -import {RpcRequest} from '../index'; +import { BitcoinNetwork, Snap } from '../interface'; +import { getPersistedData } from '../utils/manageState'; +import { RpcRequest } from '../index'; import { RequestErrors, SnapError } from '../errors'; const DOMAIN_WHITELIST = [/\.justsnap\.io$/]; diff --git a/packages/snap/src/utils/getHDNode.ts b/packages/snap/src/utils/getHDNode.ts index 590b424a..e2e95b29 100644 --- a/packages/snap/src/utils/getHDNode.ts +++ b/packages/snap/src/utils/getHDNode.ts @@ -1,15 +1,15 @@ import BIP32Factory from 'bip32'; -import {BIP32Interface} from 'bip32'; -import {BitcoinNetwork, SLIP10Node, Snap} from '../interface'; -import {getNetwork} from '../bitcoin/getNetwork'; -import {parseLightningPath} from '../bitcoin/cryptoPath'; +import { BIP32Interface } from 'bip32'; +import { BitcoinNetwork, SLIP10Node, Snap } from '../interface'; +import { getNetwork } from '../bitcoin/getNetwork'; +import { parseLightningPath } from '../bitcoin/cryptoPath'; import { trimHexPrefix } from '../utils/hexHelper'; import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; const CRYPTO_CURVE = 'secp256k1'; export const getHDNode = async (snap: Snap, hdPath: string) => { - const {purpose, coinType, account, change, index} = + const { purpose, coinType, account, change, index } = parseLightningPath(hdPath); const network = coinType.value === '0' diff --git a/packages/snap/src/utils/manageState.ts b/packages/snap/src/utils/manageState.ts index f31e6ded..08b26ef0 100644 --- a/packages/snap/src/utils/manageState.ts +++ b/packages/snap/src/utils/manageState.ts @@ -1,4 +1,4 @@ -import {PersistedData, Snap} from '../interface'; +import { PersistedData, Snap } from '../interface'; export const getPersistedData = async ( snap: Snap, diff --git a/packages/snap/src/utils/transferLNData.ts b/packages/snap/src/utils/transferLNData.ts index 060b4532..46bf50a8 100644 --- a/packages/snap/src/utils/transferLNData.ts +++ b/packages/snap/src/utils/transferLNData.ts @@ -9,9 +9,9 @@ export const formatTime = (sec: number) => { return `${hours}H ${minutes}M`; }; -const getBoltField = (invoice: Record, key: string) => invoice.find((item:any) => item.name === key); +const getBoltField = (invoice: Record, key: string) => invoice.find((item: any) => item.name === key); -const formatInvoice = (invoice:string) => { +const formatInvoice = (invoice: string) => { const decodedInvoice = require('light-bolt11-decoder').decode(invoice).sections; const expireDatetime = getBoltField(decodedInvoice, 'timestamp').value + getBoltField(decodedInvoice, 'expiry').value; return { diff --git a/packages/snap/src/utils/unitHelper.ts b/packages/snap/src/utils/unitHelper.ts index 6157c2f9..7412da5e 100644 --- a/packages/snap/src/utils/unitHelper.ts +++ b/packages/snap/src/utils/unitHelper.ts @@ -3,7 +3,7 @@ import { SnapError, InvoiceErrors } from '../errors'; const SATS_PER_BTC = new BN(1e8, 10); -type BitcoinDivisor = 'm' | 'u' | 'n' | 'p'; +type BitcoinDivisor = 'm' | 'u' | 'n' | 'p'; const DIVISORS: Record = { m: new BN(1e3, 10), @@ -12,7 +12,7 @@ const DIVISORS: Record = { p: new BN(1e12, 10) }; -export const hrpToSatoshi = (hrp: string):string => { +export const hrpToSatoshi = (hrp: string): string => { let divisor, value; if (hrp.slice(-1).match(/^[munp]$/)) { divisor = hrp.slice(-1); @@ -23,7 +23,7 @@ export const hrpToSatoshi = (hrp: string):string => { value = hrp; } - if (!value.match(/^\d+$/)){ + if (!value.match(/^\d+$/)) { throw SnapError.of(InvoiceErrors.AmountNotValid); }