From f376913a4c102dcbca1cea5075a0a99482f86f1d Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 12 Sep 2019 13:15:52 +0900 Subject: [PATCH] Remove TransactionBuilder from tests (besides transaction_builder.spec.ts) --- README.md | 11 +- test/integration/cltv.spec.ts | 45 +- test/integration/csv.spec.ts | 49 +- test/integration/payments.spec.ts | 45 +- test/integration/transactions-psbt.spec.ts | 669 ---------------- test/integration/transactions.spec.ts | 850 +++++++++++++-------- 6 files changed, 623 insertions(+), 1046 deletions(-) delete mode 100644 test/integration/transactions-psbt.spec.ts diff --git a/README.md b/README.md index a94d0ae..b034f20 100644 --- a/README.md +++ b/README.md @@ -85,14 +85,6 @@ The below examples are implemented as integration tests, they should be very eas Otherwise, pull requests are appreciated. Some examples interact (via HTTPS) with a 3rd Party Blockchain Provider (3PBP). -### Warning: Currently the tests use TransactionBuilder, which will be removed in the future (v6.x.x or higher) -We will move towards replacing all instances of TransactionBuilder in the tests with the new Psbt. - -Currently we have a few examples on how to use the newer Psbt class at the following link: -- [Psbt examples](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions-psbt.spec.ts) - -The rest of the examples are below (using TransactionBuilder for Transaction creation) - - [Generate a random address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.spec.ts) - [Import an address via WIF](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.spec.ts) - [Generate a 2-of-3 P2SH multisig address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.spec.ts) @@ -104,7 +96,6 @@ The rest of the examples are below (using TransactionBuilder for Transaction cre - [Generate a Testnet address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.spec.ts) - [Generate a Litecoin address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.spec.ts) - [Create a 1-to-1 Transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) -- [Create a 2-to-2 Transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) - [Create (and broadcast via 3PBP) a typical Transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) - [Create (and broadcast via 3PBP) a Transaction with an OP\_RETURN output](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) - [Create (and broadcast via 3PBP) a Transaction with a 2-of-4 P2SH(multisig) input](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) @@ -112,7 +103,7 @@ The rest of the examples are below (using TransactionBuilder for Transaction cre - [Create (and broadcast via 3PBP) a Transaction with a SegWit P2WPKH input](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) - [Create (and broadcast via 3PBP) a Transaction with a SegWit P2PK input](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) - [Create (and broadcast via 3PBP) a Transaction with a SegWit 3-of-4 P2SH(P2WSH(multisig)) input](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) -- [Verify a Transaction signature](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) +- [Create (and broadcast via 3PBP) a Transaction and sign with an HDSigner interface (bip32)](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts) - [Import a BIP32 testnet xpriv and export to WIF](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/bip32.spec.ts) - [Export a BIP32 xpriv, then import it](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/bip32.spec.ts) - [Export a BIP32 xpub](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/bip32.spec.ts) diff --git a/test/integration/cltv.spec.ts b/test/integration/cltv.spec.ts index afdcaa5..3bc0b14 100644 --- a/test/integration/cltv.spec.ts +++ b/test/integration/cltv.spec.ts @@ -5,6 +5,14 @@ import { regtestUtils } from './_regtest'; const regtest = regtestUtils.network; const bip65 = require('bip65'); +function toOutputScript(address: string): Buffer { + return bitcoin.address.toOutputScript(address, regtest); +} + +function idToHash(txid: string): Buffer { + return Buffer.from(txid, 'hex').reverse() as Buffer; +} + const alice = bitcoin.ECPair.fromWIF( 'cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', regtest, @@ -13,7 +21,6 @@ const bob = bitcoin.ECPair.fromWIF( 'cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x', regtest, ); -console.warn = () => {}; // Silence the Deprecation Warning describe('bitcoinjs-lib (transactions w/ CLTV)', () => { // force update MTP @@ -59,14 +66,13 @@ describe('bitcoinjs-lib (transactions w/ CLTV)', () => { // fund the P2SH(CLTV) address const unspent = await regtestUtils.faucet(address!, 1e5); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.setLockTime(lockTime); + const tx = new bitcoin.Transaction(); + tx.locktime = lockTime; // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. - txb.addInput(unspent.txId, unspent.vout, 0xfffffffe); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4); + tx.addInput(idToHash(unspent.txId), unspent.vout, 0xfffffffe); + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4); // {Alice's signature} OP_TRUE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature(0, redeemScript, hashType); const redeemScriptSig = bitcoin.payments.p2sh({ redeem: { @@ -102,14 +108,13 @@ describe('bitcoinjs-lib (transactions w/ CLTV)', () => { // fund the P2SH(CLTV) address const unspent = await regtestUtils.faucet(address!, 1e5); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.setLockTime(lockTime); + const tx = new bitcoin.Transaction(); + tx.locktime = lockTime; // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. - txb.addInput(unspent.txId, unspent.vout, 0xfffffffe); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4); + tx.addInput(idToHash(unspent.txId), unspent.vout, 0xfffffffe); + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4); // {Alice's signature} OP_TRUE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature(0, redeemScript, hashType); const redeemScriptSig = bitcoin.payments.p2sh({ redeem: { @@ -147,14 +152,13 @@ describe('bitcoinjs-lib (transactions w/ CLTV)', () => { // fund the P2SH(CLTV) address const unspent = await regtestUtils.faucet(address!, 2e5); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.setLockTime(lockTime); + const tx = new bitcoin.Transaction(); + tx.locktime = lockTime; // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. - txb.addInput(unspent.txId, unspent.vout, 0xfffffffe); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 8e4); + tx.addInput(idToHash(unspent.txId), unspent.vout, 0xfffffffe); + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 8e4); // {Alice's signature} {Bob's signature} OP_FALSE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature(0, redeemScript, hashType); const redeemScriptSig = bitcoin.payments.p2sh({ redeem: { @@ -189,14 +193,13 @@ describe('bitcoinjs-lib (transactions w/ CLTV)', () => { // fund the P2SH(CLTV) address const unspent = await regtestUtils.faucet(address!, 2e4); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.setLockTime(lockTime); + const tx = new bitcoin.Transaction(); + tx.locktime = lockTime; // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable. - txb.addInput(unspent.txId, unspent.vout, 0xfffffffe); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4); + tx.addInput(idToHash(unspent.txId), unspent.vout, 0xfffffffe); + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 1e4); // {Alice's signature} OP_TRUE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature(0, redeemScript, hashType); const redeemScriptSig = bitcoin.payments.p2sh({ redeem: { diff --git a/test/integration/csv.spec.ts b/test/integration/csv.spec.ts index 0bfb970..7d4f4a4 100644 --- a/test/integration/csv.spec.ts +++ b/test/integration/csv.spec.ts @@ -5,6 +5,14 @@ import { regtestUtils } from './_regtest'; const regtest = regtestUtils.network; const bip68 = require('bip68'); +function toOutputScript(address: string): Buffer { + return bitcoin.address.toOutputScript(address, regtest); +} + +function idToHash(txid: string): Buffer { + return Buffer.from(txid, 'hex').reverse() as Buffer; +} + const alice = bitcoin.ECPair.fromWIF( 'cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', regtest, @@ -21,7 +29,6 @@ const dave = bitcoin.ECPair.fromWIF( 'cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsMwS4pqnx', regtest, ); -console.warn = () => {}; // Silence the Deprecation Warning describe('bitcoinjs-lib (transactions w/ CSV)', () => { // force update MTP @@ -111,12 +118,12 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => { // fund the P2SH(CSV) address const unspent = await regtestUtils.faucet(p2sh.address!, 1e5); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout, sequence); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4); + const tx = new bitcoin.Transaction(); + tx.version = 2; + tx.addInput(idToHash(unspent.txId), unspent.vout, sequence); + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4); // {Alice's signature} OP_TRUE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature( 0, p2sh.redeem!.output!, @@ -164,12 +171,12 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => { // fund the P2SH(CSV) address const unspent = await regtestUtils.faucet(p2sh.address!, 2e4); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout, sequence); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4); + const tx = new bitcoin.Transaction(); + tx.version = 2; + tx.addInput(idToHash(unspent.txId), unspent.vout, sequence); + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 1e4); // {Alice's signature} OP_TRUE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature( 0, p2sh.redeem!.output!, @@ -219,12 +226,12 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => { // fund the P2SH(CCSV) address const unspent = await regtestUtils.faucet(p2sh.address!, 1e5); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4); + const tx = new bitcoin.Transaction(); + tx.version = 2; + tx.addInput(idToHash(unspent.txId), unspent.vout); + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4); // OP_0 {Bob sig} {Charles sig} OP_TRUE OP_TRUE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature( 0, p2sh.redeem!.output!, @@ -282,12 +289,12 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => { // fund the P2SH(CCSV) address const unspent = await regtestUtils.faucet(p2sh.address!, 1e5); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout, sequence1); // Set sequence1 for input - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4); + const tx = new bitcoin.Transaction(); + tx.version = 2; + tx.addInput(idToHash(unspent.txId), unspent.vout, sequence1); // Set sequence1 for input + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4); // OP_0 {Bob sig} {Alice mediator sig} OP_FALSE OP_TRUE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature( 0, p2sh.redeem!.output!, @@ -345,12 +352,12 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => { // fund the P2SH(CCSV) address const unspent = await regtestUtils.faucet(p2sh.address!, 1e5); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout, sequence2); // Set sequence2 for input - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4); + const tx = new bitcoin.Transaction(); + tx.version = 2; + tx.addInput(idToHash(unspent.txId), unspent.vout, sequence2); // Set sequence2 for input + tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4); // {Alice mediator sig} OP_FALSE - const tx = txb.buildIncomplete(); const signatureHash = tx.hashForSignature( 0, p2sh.redeem!.output!, diff --git a/test/integration/payments.spec.ts b/test/integration/payments.spec.ts index 6592c2c..ef40387 100644 --- a/test/integration/payments.spec.ts +++ b/test/integration/payments.spec.ts @@ -6,7 +6,6 @@ const keyPairs = [ bitcoin.ECPair.makeRandom({ network: NETWORK }), bitcoin.ECPair.makeRandom({ network: NETWORK }), ]; -console.warn = () => {}; // Silence the Deprecation Warning async function buildAndSign( depends: any, @@ -15,37 +14,35 @@ async function buildAndSign( witnessScript: any, ) { const unspent = await regtestUtils.faucetComplex(prevOutput, 5e4); + const utx = await regtestUtils.fetch(unspent.txId); - const txb = new bitcoin.TransactionBuilder(NETWORK); - txb.addInput(unspent.txId, unspent.vout, undefined, prevOutput); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4); - - const posType = depends.prevOutScriptType; - const needsValue = !!witnessScript || posType.slice(-6) === 'p2wpkh'; + const psbt = new bitcoin.Psbt({ network: NETWORK }) + .addInput({ + hash: unspent.txId, + index: unspent.vout, + nonWitnessUtxo: Buffer.from(utx.txHex, 'hex'), + ...(redeemScript ? { redeemScript } : {}), + ...(witnessScript ? { witnessScript } : {}), + }) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }); if (depends.signatures) { keyPairs.forEach(keyPair => { - txb.sign({ - prevOutScriptType: posType, - vin: 0, - keyPair, - redeemScript, - witnessValue: needsValue ? unspent.value : undefined, - witnessScript, - }); + psbt.signInput(0, keyPair); }); } else if (depends.signature) { - txb.sign({ - prevOutScriptType: posType, - vin: 0, - keyPair: keyPairs[0], - redeemScript, - witnessValue: needsValue ? unspent.value : undefined, - witnessScript, - }); + psbt.signInput(0, keyPairs[0]); } - return regtestUtils.broadcast(txb.build().toHex()); + return regtestUtils.broadcast( + psbt + .finalizeAllInputs() + .extractTransaction() + .toHex(), + ); } ['p2ms', 'p2pk', 'p2pkh', 'p2wpkh'].forEach(k => { diff --git a/test/integration/transactions-psbt.spec.ts b/test/integration/transactions-psbt.spec.ts deleted file mode 100644 index a98407d..0000000 --- a/test/integration/transactions-psbt.spec.ts +++ /dev/null @@ -1,669 +0,0 @@ -import * as assert from 'assert'; -import { describe, it } from 'mocha'; -import * as bitcoin from '../..'; -import { regtestUtils } from './_regtest'; -import * as bip32 from 'bip32'; -const rng = require('randombytes'); -const regtest = regtestUtils.network; - -// See bottom of file for some helper functions used to make the payment objects needed. - -describe('bitcoinjs-lib (transactions with psbt)', () => { - it('can create a 1-to-1 Transaction', () => { - const alice = bitcoin.ECPair.fromWIF( - 'L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr', - ); - const psbt = new bitcoin.Psbt(); - psbt.setVersion(2); // These are defaults. This line is not needed. - psbt.setLocktime(0); // These are defaults. This line is not needed. - psbt.addInput({ - // if hash is string, txid, if hash is Buffer, is reversed compared to txid - hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e', - index: 0, - sequence: 0xffffffff, // These are defaults. This line is not needed. - - // non-segwit inputs now require passing the whole previous tx as Buffer - nonWitnessUtxo: Buffer.from( - '0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' + - '452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' + - 'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' + - '9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' + - '631e5e1e66009ce3710ceea5b1ad13ffffffff01' + - // value in satoshis (Int64LE) = 0x015f90 = 90000 - '905f010000000000' + - // scriptPubkey length - '19' + - // scriptPubkey - '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac' + - // locktime - '00000000', - 'hex', - ), - - // // If this input was segwit, instead of nonWitnessUtxo, you would add - // // a witnessUtxo as follows. The scriptPubkey and the value only are needed. - // witnessUtxo: { - // script: Buffer.from( - // '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac', - // 'hex', - // ), - // value: 90000, - // }, - - // Not featured here: - // redeemScript. A Buffer of the redeemScript for P2SH - // witnessScript. A Buffer of the witnessScript for P2WSH - }); - psbt.addOutput({ - address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp', - value: 80000, - }); - psbt.signInput(0, alice); - psbt.validateSignaturesOfInput(0); - psbt.finalizeAllInputs(); - assert.strictEqual( - psbt.extractTransaction().toHex(), - '02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' + - 'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' + - 'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' + - '9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' + - 'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' + - '08a22724efa6f6a07b0ec4c79aa88ac00000000', - ); - }); - - it('can create (and broadcast via 3PBP) a typical Transaction', async () => { - // these are { payment: Payment; keys: ECPair[] } - const alice1 = createPayment('p2pkh'); - const alice2 = createPayment('p2pkh'); - - // give Alice 2 unspent outputs - const inputData1 = await getInputData( - 5e4, - alice1.payment, - false, - 'noredeem', - ); - const inputData2 = await getInputData( - 7e4, - alice2.payment, - false, - 'noredeem', - ); - { - const { - hash, // string of txid or Buffer of tx hash. (txid and hash are reverse order) - index, // the output index of the txo you are spending - nonWitnessUtxo, // the full previous transaction as a Buffer - } = inputData1; - assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1); - } - - // network is only needed if you pass an address to addOutput - // using script (Buffer of scriptPubkey) instead will avoid needed network. - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData1) // alice1 unspent - .addInput(inputData2) // alice2 unspent - .addOutput({ - address: 'mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', - value: 8e4, - }) // the actual "spend" - .addOutput({ - address: alice2.payment.address, // OR script, which is a Buffer. - value: 1e4, - }); // Alice's change - // (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee - - // Let's show a new feature with PSBT. - // We can have multiple signers sign in parrallel and combine them. - // (this is not necessary, but a nice feature) - - // encode to send out to the signers - const psbtBaseText = psbt.toBase64(); - - // each signer imports - const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText); - const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText); - - // Alice signs each input with the respective private keys - // signInput and signInputAsync are better - // (They take the input index explicitly as the first arg) - signer1.signAllInputs(alice1.keys[0]); - signer2.signAllInputs(alice2.keys[0]); - - // If your signer object's sign method returns a promise, use the following - // await signer2.signAllInputsAsync(alice2.keys[0]) - - // encode to send back to combiner (signer 1 and 2 are not near each other) - const s1text = signer1.toBase64(); - const s2text = signer2.toBase64(); - - const final1 = bitcoin.Psbt.fromBase64(s1text); - const final2 = bitcoin.Psbt.fromBase64(s2text); - - // final1.combine(final2) would give the exact same result - psbt.combine(final1, final2); - - // Finalizer wants to check all signatures are valid before finalizing. - // If the finalizer wants to check for specific pubkeys, the second arg - // can be passed. See the first multisig example below. - assert.strictEqual(psbt.validateSignaturesOfInput(0), true); - assert.strictEqual(psbt.validateSignaturesOfInput(1), true); - - // This step it new. Since we separate the signing operation and - // the creation of the scriptSig and witness stack, we are able to - psbt.finalizeAllInputs(); - - // build and broadcast our RegTest network - await regtestUtils.broadcast(psbt.extractTransaction().toHex()); - // to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839 - }); - - it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => { - const alice1 = createPayment('p2pkh'); - const inputData1 = await getInputData( - 2e5, - alice1.payment, - false, - 'noredeem', - ); - - const data = Buffer.from('bitcoinjs-lib', 'utf8'); - const embed = bitcoin.payments.embed({ data: [data] }); - - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData1) - .addOutput({ - script: embed.output!, - value: 1000, - }) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 1e5, - }) - .signInput(0, alice1.keys[0]); - - assert.strictEqual(psbt.validateSignaturesOfInput(0), true); - psbt.finalizeAllInputs(); - - // build and broadcast to the RegTest network - await regtestUtils.broadcast(psbt.extractTransaction().toHex()); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', async () => { - const multisig = createPayment('p2sh-p2ms(2 of 4)'); - const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh'); - { - const { - hash, - index, - nonWitnessUtxo, - redeemScript, // NEW: P2SH needs to give redeemScript when adding an input. - } = inputData1; - assert.deepStrictEqual( - { hash, index, nonWitnessUtxo, redeemScript }, - inputData1, - ); - } - - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData1) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 1e4, - }) - .signInput(0, multisig.keys[0]) - .signInput(0, multisig.keys[2]); - - assert.strictEqual(psbt.validateSignaturesOfInput(0), true); - assert.strictEqual( - psbt.validateSignaturesOfInput(0, multisig.keys[0].publicKey), - true, - ); - assert.throws(() => { - psbt.validateSignaturesOfInput(0, multisig.keys[3].publicKey); - }, new RegExp('No signatures for this pubkey')); - psbt.finalizeAllInputs(); - - const tx = psbt.extractTransaction(); - - // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()); - - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 1e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => { - const p2sh = createPayment('p2sh-p2wpkh'); - const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh'); - const inputData2 = await getInputData(5e4, p2sh.payment, true, 'p2sh'); - { - const { - hash, - index, - witnessUtxo, // NEW: this is an object of the output being spent { script: Buffer; value: Satoshis; } - redeemScript, - } = inputData; - assert.deepStrictEqual( - { hash, index, witnessUtxo, redeemScript }, - inputData, - ); - } - const keyPair = p2sh.keys[0]; - const outputData = { - script: p2sh.payment.output, // sending to myself for fun - value: 2e4, - }; - const outputData2 = { - script: p2sh.payment.output, // sending to myself for fun - value: 7e4, - }; - - const tx = new bitcoin.Psbt() - .addInputs([inputData, inputData2]) - .addOutputs([outputData, outputData2]) - .signAllInputs(keyPair) - .finalizeAllInputs() - .extractTransaction(); - - // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()); - - await regtestUtils.verify({ - txId: tx.getId(), - address: p2sh.payment.address, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input with nonWitnessUtxo', async () => { - // For learning purposes, ignore this test. - // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData - const p2sh = createPayment('p2sh-p2wpkh'); - const inputData = await getInputData(5e4, p2sh.payment, false, 'p2sh'); - const inputData2 = await getInputData(5e4, p2sh.payment, false, 'p2sh'); - const keyPair = p2sh.keys[0]; - const outputData = { - script: p2sh.payment.output, - value: 2e4, - }; - const outputData2 = { - script: p2sh.payment.output, - value: 7e4, - }; - const tx = new bitcoin.Psbt() - .addInputs([inputData, inputData2]) - .addOutputs([outputData, outputData2]) - .signAllInputs(keyPair) - .finalizeAllInputs() - .extractTransaction(); - await regtestUtils.broadcast(tx.toHex()); - await regtestUtils.verify({ - txId: tx.getId(), - address: p2sh.payment.address, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => { - // the only thing that changes is you don't give a redeemscript for input data - - const p2wpkh = createPayment('p2wpkh'); - const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem'); - { - const { hash, index, witnessUtxo } = inputData; - assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData); - } - - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4, - }) - .signInput(0, p2wpkh.keys[0]); - - assert.strictEqual(psbt.validateSignaturesOfInput(0), true); - psbt.finalizeAllInputs(); - - const tx = psbt.extractTransaction(); - - // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()); - - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input with nonWitnessUtxo', async () => { - // For learning purposes, ignore this test. - // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData - const p2wpkh = createPayment('p2wpkh'); - const inputData = await getInputData( - 5e4, - p2wpkh.payment, - false, - 'noredeem', - ); - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4, - }) - .signInput(0, p2wpkh.keys[0]); - psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction(); - await regtestUtils.broadcast(tx.toHex()); - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => { - const p2wsh = createPayment('p2wsh-p2pk'); - const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh'); - { - const { - hash, - index, - witnessUtxo, - witnessScript, // NEW: A Buffer of the witnessScript - } = inputData; - assert.deepStrictEqual( - { hash, index, witnessUtxo, witnessScript }, - inputData, - ); - } - - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4, - }) - .signInput(0, p2wsh.keys[0]); - - assert.strictEqual(psbt.validateSignaturesOfInput(0), true); - psbt.finalizeAllInputs(); - - const tx = psbt.extractTransaction(); - - // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()); - - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input with nonWitnessUtxo', async () => { - // For learning purposes, ignore this test. - // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData - const p2wsh = createPayment('p2wsh-p2pk'); - const inputData = await getInputData(5e4, p2wsh.payment, false, 'p2wsh'); - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4, - }) - .signInput(0, p2wsh.keys[0]); - psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction(); - await regtestUtils.broadcast(tx.toHex()); - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => { - const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)'); - const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh'); - { - const { - hash, - index, - witnessUtxo, - redeemScript, - witnessScript, - } = inputData; - assert.deepStrictEqual( - { hash, index, witnessUtxo, redeemScript, witnessScript }, - inputData, - ); - } - - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4, - }) - .signInput(0, p2sh.keys[0]) - .signInput(0, p2sh.keys[2]) - .signInput(0, p2sh.keys[3]); - - assert.strictEqual(psbt.validateSignaturesOfInput(0), true); - assert.strictEqual( - psbt.validateSignaturesOfInput(0, p2sh.keys[3].publicKey), - true, - ); - assert.throws(() => { - psbt.validateSignaturesOfInput(0, p2sh.keys[1].publicKey); - }, new RegExp('No signatures for this pubkey')); - psbt.finalizeAllInputs(); - - const tx = psbt.extractTransaction(); - - // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()); - - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input with nonWitnessUtxo', async () => { - // For learning purposes, ignore this test. - // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData - const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)'); - const inputData = await getInputData( - 5e4, - p2sh.payment, - false, - 'p2sh-p2wsh', - ); - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4, - }) - .signInput(0, p2sh.keys[0]) - .signInput(0, p2sh.keys[2]) - .signInput(0, p2sh.keys[3]); - psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction(); - await regtestUtils.broadcast(tx.toHex()); - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 2e4, - }); - }); - - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input using HD', async () => { - const hdRoot = bip32.fromSeed(rng(64)); - const masterFingerprint = hdRoot.fingerprint; - const path = "m/84'/0'/0'/0/0"; - const childNode = hdRoot.derivePath(path); - const pubkey = childNode.publicKey; - - // This information should be added to your input via updateInput - // You can add multiple bip32Derivation objects for multisig, but - // each must have a unique pubkey. - // - // This is useful because as long as you store the masterFingerprint on - // the PSBT Creator's server, you can have the PSBT Creator do the heavy - // lifting with derivation from your m/84'/0'/0' xpub, (deriving only 0/0 ) - // and your signer just needs to pass in an HDSigner interface (ie. bip32 library) - const updateData = { - bip32Derivation: [ - { - masterFingerprint, - path, - pubkey, - }, - ], - }; - const p2wpkh = createPayment('p2wpkh', [childNode]); - const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem'); - { - const { hash, index, witnessUtxo } = inputData; - assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData); - } - - // You can add extra attributes for updateData into the addInput(s) object(s) - Object.assign(inputData, updateData); - - const psbt = new bitcoin.Psbt({ network: regtest }) - .addInput(inputData) - // .updateInput(0, updateData) // if you didn't merge the bip32Derivation with inputData - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4, - }) - .signInputHD(0, hdRoot); // must sign with root!!! - - assert.strictEqual(psbt.validateSignaturesOfInput(0), true); - assert.strictEqual( - psbt.validateSignaturesOfInput(0, childNode.publicKey), - true, - ); - psbt.finalizeAllInputs(); - - const tx = psbt.extractTransaction(); - - // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()); - - await regtestUtils.verify({ - txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, - vout: 0, - value: 2e4, - }); - }); -}); - -function createPayment(_type: string, myKeys?: any[], network?: any) { - network = network || regtest; - const splitType = _type.split('-').reverse(); - const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; - const keys = myKeys || []; - let m: number | undefined; - if (isMultisig) { - const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/); - m = parseInt(match![1]); - let n = parseInt(match![2]); - if (keys.length > 0 && keys.length !== n) { - throw new Error('Need n keys for multisig'); - } - while (!myKeys && n > 1) { - keys.push(bitcoin.ECPair.makeRandom({ network })); - n--; - } - } - if (!myKeys) keys.push(bitcoin.ECPair.makeRandom({ network })); - - let payment: any; - splitType.forEach(type => { - if (type.slice(0, 4) === 'p2ms') { - payment = bitcoin.payments.p2ms({ - m, - pubkeys: keys.map(key => key.publicKey).sort(), - network, - }); - } else if (['p2sh', 'p2wsh'].indexOf(type) > -1) { - payment = (bitcoin.payments as any)[type]({ - redeem: payment, - network, - }); - } else { - payment = (bitcoin.payments as any)[type]({ - pubkey: keys[0].publicKey, - network, - }); - } - }); - - return { - payment, - keys, - }; -} - -function getWitnessUtxo(out: any) { - delete out.address; - out.script = Buffer.from(out.script, 'hex'); - return out; -} - -async function getInputData( - amount: number, - payment: any, - isSegwit: boolean, - redeemType: string, -) { - const unspent = await regtestUtils.faucetComplex(payment.output, amount); - const utx = await regtestUtils.fetch(unspent.txId); - // for non segwit inputs, you must pass the full transaction buffer - const nonWitnessUtxo = Buffer.from(utx.txHex, 'hex'); - // for segwit inputs, you only need the output script and value as an object. - const witnessUtxo = getWitnessUtxo(utx.outs[unspent.vout]); - const mixin = isSegwit ? { witnessUtxo } : { nonWitnessUtxo }; - const mixin2: any = {}; - switch (redeemType) { - case 'p2sh': - mixin2.redeemScript = payment.redeem.output; - break; - case 'p2wsh': - mixin2.witnessScript = payment.redeem.output; - break; - case 'p2sh-p2wsh': - mixin2.witnessScript = payment.redeem.redeem.output; - mixin2.redeemScript = payment.redeem.output; - break; - } - return { - hash: unspent.txId, - index: unspent.vout, - ...mixin, - ...mixin2, - }; -} diff --git a/test/integration/transactions.spec.ts b/test/integration/transactions.spec.ts index 8a313f2..a98407d 100644 --- a/test/integration/transactions.spec.ts +++ b/test/integration/transactions.spec.ts @@ -2,190 +2,230 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; import * as bitcoin from '../..'; import { regtestUtils } from './_regtest'; +import * as bip32 from 'bip32'; +const rng = require('randombytes'); const regtest = regtestUtils.network; -console.warn = () => {}; // Silence the Deprecation Warning -function rng() { - return Buffer.from('YT8dAtK4d16A3P1z+TpwB2jJ4aFH3g9M1EioIBkLEV4=', 'base64'); -} +// See bottom of file for some helper functions used to make the payment objects needed. -describe('bitcoinjs-lib (transactions)', () => { +describe('bitcoinjs-lib (transactions with psbt)', () => { it('can create a 1-to-1 Transaction', () => { const alice = bitcoin.ECPair.fromWIF( - 'L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy', + 'L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr', ); - const txb = new bitcoin.TransactionBuilder(); + const psbt = new bitcoin.Psbt(); + psbt.setVersion(2); // These are defaults. This line is not needed. + psbt.setLocktime(0); // These are defaults. This line is not needed. + psbt.addInput({ + // if hash is string, txid, if hash is Buffer, is reversed compared to txid + hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e', + index: 0, + sequence: 0xffffffff, // These are defaults. This line is not needed. - txb.setVersion(1); - txb.addInput( - '61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d', - 0, - ); // Alice's previous transaction output, has 15000 satoshis - txb.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000); - // (in)15000 - (out)12000 = (fee)3000, this is the miner fee + // non-segwit inputs now require passing the whole previous tx as Buffer + nonWitnessUtxo: Buffer.from( + '0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' + + '452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' + + 'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' + + '9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' + + '631e5e1e66009ce3710ceea5b1ad13ffffffff01' + + // value in satoshis (Int64LE) = 0x015f90 = 90000 + '905f010000000000' + + // scriptPubkey length + '19' + + // scriptPubkey + '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac' + + // locktime + '00000000', + 'hex', + ), - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: 0, - keyPair: alice, + // // If this input was segwit, instead of nonWitnessUtxo, you would add + // // a witnessUtxo as follows. The scriptPubkey and the value only are needed. + // witnessUtxo: { + // script: Buffer.from( + // '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac', + // 'hex', + // ), + // value: 90000, + // }, + + // Not featured here: + // redeemScript. A Buffer of the redeemScript for P2SH + // witnessScript. A Buffer of the witnessScript for P2WSH }); - - // prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below + psbt.addOutput({ + address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp', + value: 80000, + }); + psbt.signInput(0, alice); + psbt.validateSignaturesOfInput(0); + psbt.finalizeAllInputs(); assert.strictEqual( - txb.build().toHex(), - '01000000019d344070eac3fe6e394a16d06d7704a7d5c0a10eb2a2c16bc98842b7cc20d561000000006b48304502210088828c0bdfcdca68d8ae0caeb6ec62cd3fd5f9b2191848edae33feb533df35d302202e0beadd35e17e7f83a733f5277028a9b453d525553e3f5d2d7a7aa8010a81d60121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff01e02e0000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac00000000', - ); - }); - - it('can create a 2-to-2 Transaction', () => { - const alice = bitcoin.ECPair.fromWIF( - 'L1Knwj9W3qK3qMKdTvmg3VfzUs3ij2LETTFhxza9LfD5dngnoLG1', - ); - const bob = bitcoin.ECPair.fromWIF( - 'KwcN2pT3wnRAurhy7qMczzbkpY5nXMW2ubh696UBc1bcwctTx26z', - ); - - const txb = new bitcoin.TransactionBuilder(); - txb.setVersion(1); - txb.addInput( - 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c', - 6, - ); // Alice's previous transaction output, has 200000 satoshis - txb.addInput( - '7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730', - 0, - ); // Bob's previous transaction output, has 300000 satoshis - txb.addOutput('1CUNEBjYrCn2y1SdiUMohaKUi4wpP326Lb', 180000); - txb.addOutput('1JtK9CQw1syfWj1WtFMWomrYdV3W2tWBF9', 170000); - // (in)(200000 + 300000) - (out)(180000 + 170000) = (fee)150000, this is the miner fee - - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: 1, - keyPair: bob, - }); // Bob signs his input, which was the second input (1th) - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: 0, - keyPair: alice, - }); // Alice signs her input, which was the first input (0th) - - // prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below - assert.strictEqual( - txb.build().toHex(), - '01000000024c94e48a870b85f41228d33cf25213dfcc8dd796e7211ed6b1f9a014809dbbb5060000006a473044022041450c258ce7cac7da97316bf2ea1ce66d88967c4df94f3e91f4c2a30f5d08cb02203674d516e6bb2b0afd084c3551614bd9cec3c2945231245e891b145f2d6951f0012103e05ce435e462ec503143305feb6c00e06a3ad52fbf939e85c65f3a765bb7baacffffffff3077d9de049574c3af9bc9c09a7c9db80f2d94caaf63988c9166249b955e867d000000006b483045022100aeb5f1332c79c446d3f906e4499b2e678500580a3f90329edf1ba502eec9402e022072c8b863f8c8d6c26f4c691ac9a6610aa4200edc697306648ee844cfbc089d7a012103df7940ee7cddd2f97763f67e1fb13488da3fbdd7f9c68ec5ef0864074745a289ffffffff0220bf0200000000001976a9147dd65592d0ab2fe0d0257d571abf032cd9db93dc88ac10980200000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000', + psbt.extractTransaction().toHex(), + '02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' + + 'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' + + 'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' + + '9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' + + 'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' + + '08a22724efa6f6a07b0ec4c79aa88ac00000000', ); }); it('can create (and broadcast via 3PBP) a typical Transaction', async () => { - const alice1 = bitcoin.ECPair.makeRandom({ network: regtest }); - const alice2 = bitcoin.ECPair.makeRandom({ network: regtest }); - const aliceChange = bitcoin.ECPair.makeRandom({ - network: regtest, - rng: rng, - }); - - const alice1pkh = bitcoin.payments.p2pkh({ - pubkey: alice1.publicKey, - network: regtest, - }); - const alice2pkh = bitcoin.payments.p2pkh({ - pubkey: alice2.publicKey, - network: regtest, - }); - const aliceCpkh = bitcoin.payments.p2pkh({ - pubkey: aliceChange.publicKey, - network: regtest, - }); + // these are { payment: Payment; keys: ECPair[] } + const alice1 = createPayment('p2pkh'); + const alice2 = createPayment('p2pkh'); // give Alice 2 unspent outputs - const unspent0 = await regtestUtils.faucet(alice1pkh.address!, 5e4); + const inputData1 = await getInputData( + 5e4, + alice1.payment, + false, + 'noredeem', + ); + const inputData2 = await getInputData( + 7e4, + alice2.payment, + false, + 'noredeem', + ); + { + const { + hash, // string of txid or Buffer of tx hash. (txid and hash are reverse order) + index, // the output index of the txo you are spending + nonWitnessUtxo, // the full previous transaction as a Buffer + } = inputData1; + assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1); + } - const unspent1 = await regtestUtils.faucet(alice2pkh.address!, 7e4); - - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent0.txId, unspent0.vout); // alice1 unspent - txb.addInput(unspent1.txId, unspent1.vout); // alice2 unspent - txb.addOutput('mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', 8e4); // the actual "spend" - txb.addOutput(aliceCpkh.address!, 1e4); // Alice's change + // network is only needed if you pass an address to addOutput + // using script (Buffer of scriptPubkey) instead will avoid needed network. + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData1) // alice1 unspent + .addInput(inputData2) // alice2 unspent + .addOutput({ + address: 'mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', + value: 8e4, + }) // the actual "spend" + .addOutput({ + address: alice2.payment.address, // OR script, which is a Buffer. + value: 1e4, + }); // Alice's change // (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee + // Let's show a new feature with PSBT. + // We can have multiple signers sign in parrallel and combine them. + // (this is not necessary, but a nice feature) + + // encode to send out to the signers + const psbtBaseText = psbt.toBase64(); + + // each signer imports + const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText); + const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText); + // Alice signs each input with the respective private keys - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: 0, - keyPair: alice1, - }); - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: 1, - keyPair: alice2, - }); + // signInput and signInputAsync are better + // (They take the input index explicitly as the first arg) + signer1.signAllInputs(alice1.keys[0]); + signer2.signAllInputs(alice2.keys[0]); + + // If your signer object's sign method returns a promise, use the following + // await signer2.signAllInputsAsync(alice2.keys[0]) + + // encode to send back to combiner (signer 1 and 2 are not near each other) + const s1text = signer1.toBase64(); + const s2text = signer2.toBase64(); + + const final1 = bitcoin.Psbt.fromBase64(s1text); + const final2 = bitcoin.Psbt.fromBase64(s2text); + + // final1.combine(final2) would give the exact same result + psbt.combine(final1, final2); + + // Finalizer wants to check all signatures are valid before finalizing. + // If the finalizer wants to check for specific pubkeys, the second arg + // can be passed. See the first multisig example below. + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + assert.strictEqual(psbt.validateSignaturesOfInput(1), true); + + // This step it new. Since we separate the signing operation and + // the creation of the scriptSig and witness stack, we are able to + psbt.finalizeAllInputs(); // build and broadcast our RegTest network - await regtestUtils.broadcast(txb.build().toHex()); + await regtestUtils.broadcast(psbt.extractTransaction().toHex()); // to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839 }); it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => { - const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }); - const p2pkh = bitcoin.payments.p2pkh({ - pubkey: keyPair.publicKey, - network: regtest, - }); + const alice1 = createPayment('p2pkh'); + const inputData1 = await getInputData( + 2e5, + alice1.payment, + false, + 'noredeem', + ); - const unspent = await regtestUtils.faucet(p2pkh.address!, 2e5); - - const txb = new bitcoin.TransactionBuilder(regtest); const data = Buffer.from('bitcoinjs-lib', 'utf8'); const embed = bitcoin.payments.embed({ data: [data] }); - txb.addInput(unspent.txId, unspent.vout); - txb.addOutput(embed.output!, 1000); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e5); - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: 0, - keyPair, - }); + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData1) + .addOutput({ + script: embed.output!, + value: 1000, + }) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 1e5, + }) + .signInput(0, alice1.keys[0]); + + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + psbt.finalizeAllInputs(); // build and broadcast to the RegTest network - await regtestUtils.broadcast(txb.build().toHex()); + await regtestUtils.broadcast(psbt.extractTransaction().toHex()); }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', async () => { - const keyPairs = [ - bitcoin.ECPair.makeRandom({ network: regtest }), - bitcoin.ECPair.makeRandom({ network: regtest }), - bitcoin.ECPair.makeRandom({ network: regtest }), - bitcoin.ECPair.makeRandom({ network: regtest }), - ]; - const pubkeys = keyPairs.map(x => x.publicKey); - const p2ms = bitcoin.payments.p2ms({ - m: 2, - pubkeys: pubkeys, - network: regtest, - }); - const p2sh = bitcoin.payments.p2sh({ redeem: p2ms, network: regtest }); + const multisig = createPayment('p2sh-p2ms(2 of 4)'); + const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh'); + { + const { + hash, + index, + nonWitnessUtxo, + redeemScript, // NEW: P2SH needs to give redeemScript when adding an input. + } = inputData1; + assert.deepStrictEqual( + { hash, index, nonWitnessUtxo, redeemScript }, + inputData1, + ); + } - const unspent = await regtestUtils.faucet(p2sh.address!, 2e4); + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData1) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 1e4, + }) + .signInput(0, multisig.keys[0]) + .signInput(0, multisig.keys[2]); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + assert.strictEqual( + psbt.validateSignaturesOfInput(0, multisig.keys[0].publicKey), + true, + ); + assert.throws(() => { + psbt.validateSignaturesOfInput(0, multisig.keys[3].publicKey); + }, new RegExp('No signatures for this pubkey')); + psbt.finalizeAllInputs(); - txb.sign({ - prevOutScriptType: 'p2sh-p2ms', - vin: 0, - keyPair: keyPairs[0], - redeemScript: p2sh.redeem!.output, - }); - txb.sign({ - prevOutScriptType: 'p2sh-p2ms', - vin: 0, - keyPair: keyPairs[2], - redeemScript: p2sh.redeem!.output, - }); - const tx = txb.build(); + const tx = psbt.extractTransaction(); // build and broadcast to the Bitcoin RegTest network await regtestUtils.broadcast(tx.toHex()); @@ -199,27 +239,101 @@ describe('bitcoinjs-lib (transactions)', () => { }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => { - const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }); - const p2wpkh = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network: regtest, + const p2sh = createPayment('p2sh-p2wpkh'); + const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh'); + const inputData2 = await getInputData(5e4, p2sh.payment, true, 'p2sh'); + { + const { + hash, + index, + witnessUtxo, // NEW: this is an object of the output being spent { script: Buffer; value: Satoshis; } + redeemScript, + } = inputData; + assert.deepStrictEqual( + { hash, index, witnessUtxo, redeemScript }, + inputData, + ); + } + const keyPair = p2sh.keys[0]; + const outputData = { + script: p2sh.payment.output, // sending to myself for fun + value: 2e4, + }; + const outputData2 = { + script: p2sh.payment.output, // sending to myself for fun + value: 7e4, + }; + + const tx = new bitcoin.Psbt() + .addInputs([inputData, inputData2]) + .addOutputs([outputData, outputData2]) + .signAllInputs(keyPair) + .finalizeAllInputs() + .extractTransaction(); + + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()); + + await regtestUtils.verify({ + txId: tx.getId(), + address: p2sh.payment.address, + vout: 0, + value: 2e4, }); - const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh, network: regtest }); + }); - const unspent = await regtestUtils.faucet(p2sh.address!, 5e4); - - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4); - txb.sign({ - prevOutScriptType: 'p2sh-p2wpkh', - vin: 0, - keyPair: keyPair, - redeemScript: p2sh.redeem!.output, - witnessValue: unspent.value, + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input with nonWitnessUtxo', async () => { + // For learning purposes, ignore this test. + // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData + const p2sh = createPayment('p2sh-p2wpkh'); + const inputData = await getInputData(5e4, p2sh.payment, false, 'p2sh'); + const inputData2 = await getInputData(5e4, p2sh.payment, false, 'p2sh'); + const keyPair = p2sh.keys[0]; + const outputData = { + script: p2sh.payment.output, + value: 2e4, + }; + const outputData2 = { + script: p2sh.payment.output, + value: 7e4, + }; + const tx = new bitcoin.Psbt() + .addInputs([inputData, inputData2]) + .addOutputs([outputData, outputData2]) + .signAllInputs(keyPair) + .finalizeAllInputs() + .extractTransaction(); + await regtestUtils.broadcast(tx.toHex()); + await regtestUtils.verify({ + txId: tx.getId(), + address: p2sh.payment.address, + vout: 0, + value: 2e4, }); + }); - const tx = txb.build(); + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => { + // the only thing that changes is you don't give a redeemscript for input data + + const p2wpkh = createPayment('p2wpkh'); + const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem'); + { + const { hash, index, witnessUtxo } = inputData; + assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData); + } + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInput(0, p2wpkh.keys[0]); + + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + psbt.finalizeAllInputs(); + + const tx = psbt.extractTransaction(); // build and broadcast to the Bitcoin RegTest network await regtestUtils.broadcast(tx.toHex()); @@ -232,30 +346,26 @@ describe('bitcoinjs-lib (transactions)', () => { }); }); - it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => { - const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }); - const p2wpkh = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network: regtest, - }); - - const unspent = await regtestUtils.faucetComplex(p2wpkh.output!, 5e4); - - // XXX: build the Transaction w/ a P2WPKH input - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout, undefined, p2wpkh.output); // NOTE: provide the prevOutScript! - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4); - txb.sign({ - prevOutScriptType: 'p2wpkh', - vin: 0, - keyPair: keyPair, - witnessValue: unspent.value, - }); // NOTE: no redeem script - const tx = txb.build(); - - // build and broadcast (the P2WPKH transaction) to the Bitcoin RegTest network + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input with nonWitnessUtxo', async () => { + // For learning purposes, ignore this test. + // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData + const p2wpkh = createPayment('p2wpkh'); + const inputData = await getInputData( + 5e4, + p2wpkh.payment, + false, + 'noredeem', + ); + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInput(0, p2wpkh.keys[0]); + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); await regtestUtils.broadcast(tx.toHex()); - await regtestUtils.verify({ txId: tx.getId(), address: regtestUtils.RANDOM_ADDRESS, @@ -265,29 +375,35 @@ describe('bitcoinjs-lib (transactions)', () => { }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => { - const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }); - const p2pk = bitcoin.payments.p2pk({ - pubkey: keyPair.publicKey, - network: regtest, - }); - const p2wsh = bitcoin.payments.p2wsh({ redeem: p2pk, network: regtest }); + const p2wsh = createPayment('p2wsh-p2pk'); + const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh'); + { + const { + hash, + index, + witnessUtxo, + witnessScript, // NEW: A Buffer of the witnessScript + } = inputData; + assert.deepStrictEqual( + { hash, index, witnessUtxo, witnessScript }, + inputData, + ); + } - const unspent = await regtestUtils.faucetComplex(p2wsh.output!, 5e4); + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInput(0, p2wsh.keys[0]); - // XXX: build the Transaction w/ a P2WSH input - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout, undefined, p2wsh.output); // NOTE: provide the prevOutScript! - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4); - txb.sign({ - prevOutScriptType: 'p2wsh-p2pk', - vin: 0, - keyPair: keyPair, - witnessValue: 5e4, - witnessScript: p2wsh.redeem!.output, - }); // NOTE: provide a witnessScript! - const tx = txb.build(); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + psbt.finalizeAllInputs(); - // build and broadcast (the P2WSH transaction) to the Bitcoin RegTest network + const tx = psbt.extractTransaction(); + + // build and broadcast to the Bitcoin RegTest network await regtestUtils.broadcast(tx.toHex()); await regtestUtils.verify({ @@ -298,50 +414,67 @@ describe('bitcoinjs-lib (transactions)', () => { }); }); + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input with nonWitnessUtxo', async () => { + // For learning purposes, ignore this test. + // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData + const p2wsh = createPayment('p2wsh-p2pk'); + const inputData = await getInputData(5e4, p2wsh.payment, false, 'p2wsh'); + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInput(0, p2wsh.keys[0]); + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + await regtestUtils.broadcast(tx.toHex()); + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4, + }); + }); + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => { - const keyPairs = [ - bitcoin.ECPair.makeRandom({ network: regtest }), - bitcoin.ECPair.makeRandom({ network: regtest }), - bitcoin.ECPair.makeRandom({ network: regtest }), - bitcoin.ECPair.makeRandom({ network: regtest }), - ]; - const pubkeys = keyPairs.map(x => x.publicKey); + const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)'); + const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh'); + { + const { + hash, + index, + witnessUtxo, + redeemScript, + witnessScript, + } = inputData; + assert.deepStrictEqual( + { hash, index, witnessUtxo, redeemScript, witnessScript }, + inputData, + ); + } - const p2ms = bitcoin.payments.p2ms({ m: 3, pubkeys, network: regtest }); - const p2wsh = bitcoin.payments.p2wsh({ redeem: p2ms, network: regtest }); - const p2sh = bitcoin.payments.p2sh({ redeem: p2wsh, network: regtest }); + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInput(0, p2sh.keys[0]) + .signInput(0, p2sh.keys[2]) + .signInput(0, p2sh.keys[3]); - const unspent = await regtestUtils.faucet(p2sh.address!, 6e4); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + assert.strictEqual( + psbt.validateSignaturesOfInput(0, p2sh.keys[3].publicKey), + true, + ); + assert.throws(() => { + psbt.validateSignaturesOfInput(0, p2sh.keys[1].publicKey); + }, new RegExp('No signatures for this pubkey')); + psbt.finalizeAllInputs(); - const txb = new bitcoin.TransactionBuilder(regtest); - txb.addInput(unspent.txId, unspent.vout, undefined, p2sh.output); - txb.addOutput(regtestUtils.RANDOM_ADDRESS, 3e4); - txb.sign({ - prevOutScriptType: 'p2sh-p2wsh-p2ms', - vin: 0, - keyPair: keyPairs[0], - redeemScript: p2sh.redeem!.output, - witnessValue: unspent.value, - witnessScript: p2wsh.redeem!.output, - }); - txb.sign({ - prevOutScriptType: 'p2sh-p2wsh-p2ms', - vin: 0, - keyPair: keyPairs[2], - redeemScript: p2sh.redeem!.output, - witnessValue: unspent.value, - witnessScript: p2wsh.redeem!.output, - }); - txb.sign({ - prevOutScriptType: 'p2sh-p2wsh-p2ms', - vin: 0, - keyPair: keyPairs[3], - redeemScript: p2sh.redeem!.output, - witnessValue: unspent.value, - witnessScript: p2wsh.redeem!.output, - }); - - const tx = txb.build(); + const tx = psbt.extractTransaction(); // build and broadcast to the Bitcoin RegTest network await regtestUtils.broadcast(tx.toHex()); @@ -350,72 +483,187 @@ describe('bitcoinjs-lib (transactions)', () => { txId: tx.getId(), address: regtestUtils.RANDOM_ADDRESS, vout: 0, - value: 3e4, + value: 2e4, }); }); - it('can verify Transaction (P2PKH) signatures', () => { - const txHex = - '010000000321c5f7e7bc98b3feda84aad36a5c99a02bcb8823a2f3eccbcd5da209698b5c20000000006b48304502210099e021772830207cf7c55b69948d3b16b4dcbf1f55a9cd80ebf8221a169735f9022064d33f11d62cd28240b3862afc0b901adc9f231c7124dd19bdb30367b61964c50121032b4c06c06c3ec0b7fa29519dfa5aae193ee2cc35ca127f29f14ec605d62fb63dffffffff8a75ce85441ddb3f342708ee33cc8ed418b07d9ba9e0e7c4e1cccfe9f52d8a88000000006946304302207916c23dae212c95a920423902fa44e939fb3d542f4478a7b46e9cde53705800021f0d74e9504146e404c1b8f9cba4dff2d4782e3075491c9ed07ce4a7d1c4461a01210216c92abe433106491bdeb4a261226f20f5a4ac86220cc6e37655aac6bf3c1f2affffffffdfef93f69fe32e944fad79fa8f882b3a155d80383252348caba1a77a5abbf7ef000000006b483045022100faa6e9ca289b46c64764a624c59ac30d9abcf1d4a04c4de9089e67cbe0d300a502206930afa683f6807502de5c2431bf9a1fd333c8a2910a76304df0f3d23d83443f0121039e05da8b8ea4f9868ecebb25998c7701542986233f4401799551fbecf316b18fffffffff01ff4b0000000000001976a9146c86476d1d85cd60116cd122a274e6a570a5a35c88acc96d0700'; - const keyPairs = [ - '032b4c06c06c3ec0b7fa29519dfa5aae193ee2cc35ca127f29f14ec605d62fb63d', - '0216c92abe433106491bdeb4a261226f20f5a4ac86220cc6e37655aac6bf3c1f2a', - '039e05da8b8ea4f9868ecebb25998c7701542986233f4401799551fbecf316b18f', - ].map(q => { - return bitcoin.ECPair.fromPublicKey(Buffer.from(q, 'hex')); - }); - - const tx = bitcoin.Transaction.fromHex(txHex); - - tx.ins.forEach((input, i) => { - const keyPair = keyPairs[i]; - const p2pkh = bitcoin.payments.p2pkh({ - pubkey: keyPair.publicKey, - input: input.script, - }); - - const ss = bitcoin.script.signature.decode(p2pkh.signature!); - const hash = tx.hashForSignature(i, p2pkh.output!, ss.hashType); - - assert.strictEqual(keyPair.verify(hash, ss.signature), true); + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input with nonWitnessUtxo', async () => { + // For learning purposes, ignore this test. + // REPEATING ABOVE BUT WITH nonWitnessUtxo by passing false to getInputData + const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)'); + const inputData = await getInputData( + 5e4, + p2sh.payment, + false, + 'p2sh-p2wsh', + ); + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInput(0, p2sh.keys[0]) + .signInput(0, p2sh.keys[2]) + .signInput(0, p2sh.keys[3]); + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + await regtestUtils.broadcast(tx.toHex()); + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4, }); }); - it('can verify Transaction (P2SH(P2WPKH)) signatures', () => { - const utxos = { - 'f72d1d83ac40fcedd01415751556a905844ab5f44bbb7728565ebb91b1590109:0': { - value: 50000, - }, + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input using HD', async () => { + const hdRoot = bip32.fromSeed(rng(64)); + const masterFingerprint = hdRoot.fingerprint; + const path = "m/84'/0'/0'/0/0"; + const childNode = hdRoot.derivePath(path); + const pubkey = childNode.publicKey; + + // This information should be added to your input via updateInput + // You can add multiple bip32Derivation objects for multisig, but + // each must have a unique pubkey. + // + // This is useful because as long as you store the masterFingerprint on + // the PSBT Creator's server, you can have the PSBT Creator do the heavy + // lifting with derivation from your m/84'/0'/0' xpub, (deriving only 0/0 ) + // and your signer just needs to pass in an HDSigner interface (ie. bip32 library) + const updateData = { + bip32Derivation: [ + { + masterFingerprint, + path, + pubkey, + }, + ], }; + const p2wpkh = createPayment('p2wpkh', [childNode]); + const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem'); + { + const { hash, index, witnessUtxo } = inputData; + assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData); + } - const txHex = - '02000000000101090159b191bb5e562877bb4bf4b54a8405a95615751514d0edfc40ac831d2df7000000001716001435a179e5516947a39ae9c8a25e9fe62c0fc598edffffffff01204e0000000000001976a91431d43308d3c886d53e9ae8a45728370571ff456988ac0247304402206ec41f685b997a51f325b07ee852e82a535f6b52ef54485cc133e05168aa052a022070bafa86108acb51c77b2b259ae8fb7fd1efa10fef804fcfe9b13c2db719acf5012103fb03e9d0a9af86cbed94225dbb8bb70f6b82109bce0a61ddcf41dab6cbb4871100000000'; - const tx = bitcoin.Transaction.fromHex(txHex); + // You can add extra attributes for updateData into the addInput(s) object(s) + Object.assign(inputData, updateData); - tx.ins.forEach((input, i) => { - const txId = (Buffer.from(input.hash).reverse() as Buffer).toString( - 'hex', - ); - const utxo = (utxos as any)[`${txId}:${i}`]; - if (!utxo) throw new Error('Missing utxo'); + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + // .updateInput(0, updateData) // if you didn't merge the bip32Derivation with inputData + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInputHD(0, hdRoot); // must sign with root!!! - const p2sh = bitcoin.payments.p2sh({ - input: input.script, - witness: input.witness, - }); - const p2wpkh = bitcoin.payments.p2wpkh(p2sh.redeem!); - const p2pkh = bitcoin.payments.p2pkh({ pubkey: p2wpkh.pubkey }); // because P2WPKH is annoying + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + assert.strictEqual( + psbt.validateSignaturesOfInput(0, childNode.publicKey), + true, + ); + psbt.finalizeAllInputs(); - const ss = bitcoin.script.signature.decode(p2wpkh.signature!); - const hash = tx.hashForWitnessV0( - i, - p2pkh.output!, - utxo.value, - ss.hashType, - ); - const keyPair = bitcoin.ECPair.fromPublicKey(p2wpkh.pubkey!); // aka, cQ3EtF4mApRcogNGSeyPTKbmfxxn3Yfb1wecfKSws9a8bnYuxoAk + const tx = psbt.extractTransaction(); - assert.strictEqual(keyPair.verify(hash, ss.signature), true); + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()); + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4, }); }); }); + +function createPayment(_type: string, myKeys?: any[], network?: any) { + network = network || regtest; + const splitType = _type.split('-').reverse(); + const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; + const keys = myKeys || []; + let m: number | undefined; + if (isMultisig) { + const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/); + m = parseInt(match![1]); + let n = parseInt(match![2]); + if (keys.length > 0 && keys.length !== n) { + throw new Error('Need n keys for multisig'); + } + while (!myKeys && n > 1) { + keys.push(bitcoin.ECPair.makeRandom({ network })); + n--; + } + } + if (!myKeys) keys.push(bitcoin.ECPair.makeRandom({ network })); + + let payment: any; + splitType.forEach(type => { + if (type.slice(0, 4) === 'p2ms') { + payment = bitcoin.payments.p2ms({ + m, + pubkeys: keys.map(key => key.publicKey).sort(), + network, + }); + } else if (['p2sh', 'p2wsh'].indexOf(type) > -1) { + payment = (bitcoin.payments as any)[type]({ + redeem: payment, + network, + }); + } else { + payment = (bitcoin.payments as any)[type]({ + pubkey: keys[0].publicKey, + network, + }); + } + }); + + return { + payment, + keys, + }; +} + +function getWitnessUtxo(out: any) { + delete out.address; + out.script = Buffer.from(out.script, 'hex'); + return out; +} + +async function getInputData( + amount: number, + payment: any, + isSegwit: boolean, + redeemType: string, +) { + const unspent = await regtestUtils.faucetComplex(payment.output, amount); + const utx = await regtestUtils.fetch(unspent.txId); + // for non segwit inputs, you must pass the full transaction buffer + const nonWitnessUtxo = Buffer.from(utx.txHex, 'hex'); + // for segwit inputs, you only need the output script and value as an object. + const witnessUtxo = getWitnessUtxo(utx.outs[unspent.vout]); + const mixin = isSegwit ? { witnessUtxo } : { nonWitnessUtxo }; + const mixin2: any = {}; + switch (redeemType) { + case 'p2sh': + mixin2.redeemScript = payment.redeem.output; + break; + case 'p2wsh': + mixin2.witnessScript = payment.redeem.output; + break; + case 'p2sh-p2wsh': + mixin2.witnessScript = payment.redeem.redeem.output; + mixin2.redeemScript = payment.redeem.output; + break; + } + return { + hash: unspent.txId, + index: unspent.vout, + ...mixin, + ...mixin2, + }; +}