diff --git a/src/address/index.ts b/src/address/index.ts index 4ede0e5..3547224 100644 --- a/src/address/index.ts +++ b/src/address/index.ts @@ -1,6 +1,7 @@ import { bitcoin } from "../bitcoin-core"; import { NetworkType, toPsbtNetwork } from "../network"; import { AddressType } from "../types"; +import {toXOnly} from "../utils"; /** * Convert public key to bitcoin payment object. @@ -38,6 +39,11 @@ export function publicKeyToPayment( network, redeem: data, }); + } else if (type === AddressType.RAW_P2TR) { + return bitcoin.payments.p2tr({ + pubkey: toXOnly(pubkey), + network, + }); } } diff --git a/src/keyring/simple-keyring.ts b/src/keyring/simple-keyring.ts index f32c2d4..c3571db 100644 --- a/src/keyring/simple-keyring.ts +++ b/src/keyring/simple-keyring.ts @@ -1,4 +1,5 @@ -import { isTaprootInput } from "bitcoinjs-lib/src/psbt/bip371"; +import { isTaprootInput, serializeTaprootSignature } from "bitcoinjs-lib/src/psbt/bip371"; +import { bech32 } from "bech32"; import { decode } from "bs58check"; import { EventEmitter } from "events"; import { ECPair, ECPairInterface, bitcoin } from "../bitcoin-core"; @@ -6,7 +7,7 @@ import { signMessageOfDeterministicECDSA, verifyMessageOfECDSA, } from "../message"; -import { tweakSigner } from "../utils"; +import {toXOnly, tweakSigner} from "../utils"; const type = "Simple Key Pair"; @@ -31,7 +32,10 @@ export class SimpleKeyring extends EventEmitter { this.wallets = privateKeys.map((key) => { let buf: Buffer; - if (key.length === 64) { + if (key.startsWith('nsec')) { + const { words } = bech32.decode(key) + buf = Buffer.from(new Uint8Array(bech32.fromWords(words))) + } else if (key.length === 64) { // privateKey buf = Buffer.from(key, "hex"); } else { @@ -66,17 +70,43 @@ export class SimpleKeyring extends EventEmitter { publicKey: string; sighashTypes?: number[]; disableTweakSigner?: boolean; + rawTaprootPubkey?: boolean; }[], opts?: any ) { inputs.forEach((input) => { const keyPair = this._getPrivateKeyFor(input.publicKey); + const isTapInput = isTaprootInput(psbt.data.inputs[input.index]); if ( - isTaprootInput(psbt.data.inputs[input.index]) && - !input.disableTweakSigner + isTapInput && + !input.disableTweakSigner && + !input.rawTaprootPubkey ) { const signer = tweakSigner(keyPair, opts); psbt.signInput(input.index, signer, input.sighashTypes); + } else if (isTapInput && input.rawTaprootPubkey) { + const inputAddressInfo = bitcoin.payments.p2tr({ + pubkey: toXOnly(Buffer.from(input.publicKey, 'hex')), + network: this.network, + }); + let hashType + if (!input.sighashTypes) { + hashType = bitcoin.Transaction.SIGHASH_DEFAULT + } else if (input.sighashTypes.length > 1) { + throw new Error('Only one sighash type is supported for raw taproot inputs') + } else { + hashType = input.sighashTypes[0] + } + const hashForSigning = (psbt as any).__CACHE.__TX.hashForWitnessV1( + input.index, + [inputAddressInfo.output], + [psbt.data.inputs[input.index].witnessUtxo.value], + hashType + ); + const signature = keyPair.signSchnorr(hashForSigning); + psbt.updateInput(input.index, { + tapKeySig: serializeTaprootSignature(signature) + }) } else { const signer = keyPair; psbt.signInput(input.index, signer, input.sighashTypes); diff --git a/src/types.ts b/src/types.ts index cbab7d0..5927373 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,4 +61,5 @@ export enum AddressType { P2WSH, P2SH, UNKNOWN, + RAW_P2TR, // for recoveries only } diff --git a/test/address/address.test.ts b/test/address/address.test.ts index c685922..bd60763 100644 --- a/test/address/address.test.ts +++ b/test/address/address.test.ts @@ -35,6 +35,11 @@ const p2pkh_data = { testnet_address: 'mxwqjnnPeuU5yY1yqJsDCQ9nYmicmGTBns', } +const raw_p2tr_data = { + pubkey: '02a276a2f72b2581bbb325c9d51714bd65686a9af95d7df4d625b711d7203fd7ac', + mainnet_address: 'bc1p5fm29aetykqmhve9e823w99av45x4xhet47lf439kugawgpl67kqtgzm0z', +} + const invalid_data = { pubkey: '', mainnet_address: '', @@ -106,6 +111,14 @@ describe('address', function () { NetworkType.TESTNET ) ).eq(p2pkh_data.testnet_address, 'pubkey->p2pkh testnet') + + expect( + publicKeyToAddress( + raw_p2tr_data.pubkey, + AddressType.RAW_P2TR, + NetworkType.MAINNET + ) + ).eq(raw_p2tr_data.mainnet_address, 'pubkey->raw_p2tr mainnet') }) it('test function isValidAddress', async function () { expect( diff --git a/test/keyring/simple-keyring.test.ts b/test/keyring/simple-keyring.test.ts index ab65989..e147cfc 100644 --- a/test/keyring/simple-keyring.test.ts +++ b/test/keyring/simple-keyring.test.ts @@ -11,6 +11,12 @@ const testAccount = { address: "02b57a152325231723ee9faabba930108b19c11a391751572f380d71b447317ae7", }; +const testNsecAccount = { + nsec: "nsec17ynu3aayp7acykfe6fnrfwew30xlk5z2xa4kqvh6y3v5lkfc9gaqe8g5s6", + privateKey: "f127c8f7a40fbb825939d26634bb2e8bcdfb504a376b6032fa24594fd9382a3a", + publicKey: "02a276a2f72b2581bbb325c9d51714bd65686a9af95d7df4d625b711d7203fd7ac" +} + describe("bitcoin-simple-keyring", () => { let keyring: SimpleKeyring; beforeEach(() => { @@ -40,6 +46,19 @@ describe("bitcoin-simple-keyring", () => { }); }); + describe("#handles an nsec formatted private key", function () { + it("properly imports nsec", async function () { + await keyring.deserialize([testNsecAccount.nsec]); + const publicKeys = await keyring.getAccounts(); + console.log(publicKeys) + expect(publicKeys).length(1); + expect(publicKeys[0]).eq(testNsecAccount.publicKey); + const privateKeys = await keyring.serialize(); + expect(privateKeys).length(1); + expect(privateKeys[0]).eq(testNsecAccount.privateKey); + }); + }); + describe("#constructor with a private key", function () { it("has the correct addresses", async function () { const newKeyring = new SimpleKeyring([testAccount.key]); @@ -340,5 +359,54 @@ describe("bitcoin-simple-keyring", () => { psbt.extractTransaction(); expect(psbt.getFee() == 500).to.be.true; }); + + it("sign Raw P2TR input", async function () { + const network = bitcoin.networks.bitcoin; + + const newKeyring = new SimpleKeyring(); + await newKeyring.addAccounts(1); + const accounts = await newKeyring.getAccounts(); + const pubkey = accounts[0]; + const payment = bitcoin.payments.p2tr({ + pubkey: toXOnly(Buffer.from(pubkey, "hex")), + network, + }); + + const prevoutHash = Buffer.from( + "0000000000000000000000000000000000000000000000000000000000000000", + "hex" + ); + const value = 10000; + const prevoutIndex = 0xffffffff; + const sequence = 0; + const txToSpend = new bitcoin.Transaction(); + txToSpend.version = 0; + txToSpend.addInput(prevoutHash, prevoutIndex, sequence); + txToSpend.addOutput(payment.output!, value); + + const psbt = new bitcoin.Psbt({ network }); + psbt.addInput({ + hash: txToSpend.getHash(), + index: 0, + sequence: 0, + witnessUtxo: { + script: payment.output!, + value, + }, + }); + psbt.addOutput({ + address: payment.address!, + value: value - 500, + }); + await newKeyring.signTransaction( + psbt, + [{ index: 0, publicKey: pubkey, rawTaprootPubkey: true }], + { network: bitcoin.networks.bitcoin } + ); + + psbt.finalizeAllInputs(); + psbt.extractTransaction(); + expect(psbt.getFee() == 500).to.be.true; + }); }); -}); +}); \ No newline at end of file