Add clone, addInputs, addOutputs

This commit is contained in:
junderw 2019-07-09 18:03:15 +09:00
parent b8c341dea0
commit 01c7ac39b6
No known key found for this signature in database
GPG key ID: B256185D3A971908
4 changed files with 221 additions and 152 deletions

View file

@ -95,6 +95,12 @@ class Psbt extends bip174_1.Psbt {
get inputCount() { get inputCount() {
return this.inputs.length; return this.inputs.length;
} }
clone() {
// TODO: more efficient cloning
const res = Psbt.fromBuffer(this.toBuffer());
res.opts = JSON.parse(JSON.stringify(this.opts));
return res;
}
setMaximumFeeRate(satoshiPerByte) { setMaximumFeeRate(satoshiPerByte) {
check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw
this.opts.maximumFeeRate = satoshiPerByte; this.opts.maximumFeeRate = satoshiPerByte;
@ -129,6 +135,10 @@ class Psbt extends bip174_1.Psbt {
c.__EXTRACTED_TX = undefined; c.__EXTRACTED_TX = undefined;
return this; return this;
} }
addInputs(inputDatas) {
inputDatas.forEach(inputData => this.addInput(inputData));
return this;
}
addInput(inputData) { addInput(inputData) {
checkInputsForPartialSig(this.inputs, 'addInput'); checkInputsForPartialSig(this.inputs, 'addInput');
const c = this.__CACHE; const c = this.__CACHE;
@ -138,6 +148,10 @@ class Psbt extends bip174_1.Psbt {
c.__EXTRACTED_TX = undefined; c.__EXTRACTED_TX = undefined;
return this; return this;
} }
addOutputs(outputDatas) {
outputDatas.forEach(outputData => this.addOutput(outputData));
return this;
}
addOutput(outputData) { addOutput(outputData) {
checkInputsForPartialSig(this.inputs, 'addOutput'); checkInputsForPartialSig(this.inputs, 'addOutput');
const { address } = outputData; const { address } = outputData;

View file

@ -1,17 +1,19 @@
const { describe, it } = require('mocha') const { describe, it } = require('mocha');
const assert = require('assert') const assert = require('assert');
const bitcoin = require('../../') const bitcoin = require('../../');
const regtestUtils = require('./_regtest') const regtestUtils = require('./_regtest');
const regtest = regtestUtils.network const regtest = regtestUtils.network;
// See bottom of file for some helper functions used to make the payment objects needed. // See bottom of file for some helper functions used to make the payment objects needed.
describe('bitcoinjs-lib (transactions with psbt)', () => { describe('bitcoinjs-lib (transactions with psbt)', () => {
it('can create a 1-to-1 Transaction', () => { it('can create a 1-to-1 Transaction', () => {
const alice = bitcoin.ECPair.fromWIF('L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr') const alice = bitcoin.ECPair.fromWIF(
const psbt = new bitcoin.Psbt() 'L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr',
psbt.setVersion(2) // These are defaults. This line is not needed. );
psbt.setLocktime(0) // These are defaults. This line is not needed. 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({ psbt.addInput({
// if hash is string, txid, if hash is Buffer, is reversed compared to txid // if hash is string, txid, if hash is Buffer, is reversed compared to txid
hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e', hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e',
@ -21,18 +23,18 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
// non-segwit inputs now require passing the whole previous tx as Buffer // non-segwit inputs now require passing the whole previous tx as Buffer
nonWitnessUtxo: Buffer.from( nonWitnessUtxo: Buffer.from(
'0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' + '0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' +
'452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' + '452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' +
'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' + 'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' +
'9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' + '9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' +
'631e5e1e66009ce3710ceea5b1ad13ffffffff01' + '631e5e1e66009ce3710ceea5b1ad13ffffffff01' +
// value in satoshis (Int64LE) = 0x015f90 = 90000 // value in satoshis (Int64LE) = 0x015f90 = 90000
'905f010000000000' + '905f010000000000' +
// scriptPubkey length // scriptPubkey length
'19' + '19' +
// scriptPubkey // scriptPubkey
'76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac' + '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac' +
// locktime // locktime
'00000000', '00000000',
'hex', 'hex',
), ),
@ -47,40 +49,50 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
// }, // },
// Not featured here: redeemScript. A Buffer of the redeemScript // Not featured here: redeemScript. A Buffer of the redeemScript
}) });
psbt.addOutput({ psbt.addOutput({
address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp', address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp',
value: 80000 value: 80000,
}) });
psbt.signInput(0, alice) psbt.signInput(0, alice);
psbt.validateSignatures(0) psbt.validateSignatures(0);
psbt.finalizeAllInputs() psbt.finalizeAllInputs();
assert.strictEqual( assert.strictEqual(
psbt.extractTransaction().toHex(), psbt.extractTransaction().toHex(),
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' + '02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +
'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' + 'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' +
'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' + 'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' +
'9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' + '9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' +
'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' + 'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' +
'08a22724efa6f6a07b0ec4c79aa88ac00000000', '08a22724efa6f6a07b0ec4c79aa88ac00000000',
) );
}) });
it('can create (and broadcast via 3PBP) a typical Transaction', async () => { it('can create (and broadcast via 3PBP) a typical Transaction', async () => {
// these are { payment: Payment; keys: ECPair[] } // these are { payment: Payment; keys: ECPair[] }
const alice1 = createPayment('p2pkh') const alice1 = createPayment('p2pkh');
const alice2 = createPayment('p2pkh') const alice2 = createPayment('p2pkh');
// give Alice 2 unspent outputs // give Alice 2 unspent outputs
const inputData1 = await getInputData(5e4, alice1.payment, false, 'noredeem') const inputData1 = await getInputData(
const inputData2 = await getInputData(7e4, alice2.payment, false, 'noredeem') 5e4,
alice1.payment,
false,
'noredeem',
);
const inputData2 = await getInputData(
7e4,
alice2.payment,
false,
'noredeem',
);
{ {
const { const {
hash, // string of txid or Buffer of tx hash. (txid and hash are reverse order) 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 index, // the output index of the txo you are spending
nonWitnessUtxo, // the full previous transaction as a Buffer nonWitnessUtxo, // the full previous transaction as a Buffer
} = inputData1 } = inputData1;
assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1) assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1);
} }
// network is only needed if you pass an address to addOutput // network is only needed if you pass an address to addOutput
@ -90,12 +102,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
.addInput(inputData2) // alice2 unspent .addInput(inputData2) // alice2 unspent
.addOutput({ .addOutput({
address: 'mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', address: 'mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf',
value: 8e4 value: 8e4,
}) // the actual "spend" }) // the actual "spend"
.addOutput({ .addOutput({
address: alice2.payment.address, // OR script, which is a Buffer. address: alice2.payment.address, // OR script, which is a Buffer.
value: 1e4 value: 1e4,
}) // Alice's change }); // Alice's change
// (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee // (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee
// Let's show a new feature with PSBT. // Let's show a new feature with PSBT.
@ -103,231 +115,248 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
// (this is not necessary, but a nice feature) // (this is not necessary, but a nice feature)
// encode to send out to the signers // encode to send out to the signers
const psbtBaseText = psbt.toBase64() const psbtBaseText = psbt.toBase64();
// each signer imports // each signer imports
const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText) const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText);
const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText) const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText);
// Alice signs each input with the respective private keys // Alice signs each input with the respective private keys
// signInput and signInputAsync are better // signInput and signInputAsync are better
// (They take the input index explicitly as the first arg) // (They take the input index explicitly as the first arg)
signer1.sign(alice1.keys[0]) signer1.sign(alice1.keys[0]);
signer2.sign(alice2.keys[0]) signer2.sign(alice2.keys[0]);
// If your signer object's sign method returns a promise, use the following // If your signer object's sign method returns a promise, use the following
// await signer2.signAsync(alice2.keys[0]) // await signer2.signAsync(alice2.keys[0])
// encode to send back to combiner (signer 1 and 2 are not near each other) // encode to send back to combiner (signer 1 and 2 are not near each other)
const s1text = signer1.toBase64() const s1text = signer1.toBase64();
const s2text = signer2.toBase64() const s2text = signer2.toBase64();
const final1 = bitcoin.Psbt.fromBase64(s1text) const final1 = bitcoin.Psbt.fromBase64(s1text);
const final2 = bitcoin.Psbt.fromBase64(s2text) const final2 = bitcoin.Psbt.fromBase64(s2text);
// final1.combine(final2) would give the exact same result // final1.combine(final2) would give the exact same result
psbt.combine(final1, final2) psbt.combine(final1, final2);
// Finalizer wants to check all signatures are valid before finalizing. // Finalizer wants to check all signatures are valid before finalizing.
// If the finalizer wants to check for specific pubkeys, the second arg // If the finalizer wants to check for specific pubkeys, the second arg
// can be passed. See the first multisig example below. // can be passed. See the first multisig example below.
assert.strictEqual(psbt.validateSignatures(0), true) assert.strictEqual(psbt.validateSignatures(0), true);
assert.strictEqual(psbt.validateSignatures(1), true) assert.strictEqual(psbt.validateSignatures(1), true);
// This step it new. Since we separate the signing operation and // This step it new. Since we separate the signing operation and
// the creation of the scriptSig and witness stack, we are able to // the creation of the scriptSig and witness stack, we are able to
psbt.finalizeAllInputs() psbt.finalizeAllInputs();
// it returns an array of the success of each input, also a result attribute // it returns an array of the success of each input, also a result attribute
// which is true if all array items are true. // which is true if all array items are true.
// build and broadcast our RegTest network // build and broadcast our RegTest network
await regtestUtils.broadcast(psbt.extractTransaction().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 // 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 () => { it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => {
const alice1 = createPayment('p2pkh') const alice1 = createPayment('p2pkh');
const inputData1 = await getInputData(2e5, alice1.payment, false, 'noredeem') const inputData1 = await getInputData(
2e5,
alice1.payment,
false,
'noredeem',
);
const data = Buffer.from('bitcoinjs-lib', 'utf8') const data = Buffer.from('bitcoinjs-lib', 'utf8');
const embed = bitcoin.payments.embed({ data: [data] }) const embed = bitcoin.payments.embed({ data: [data] });
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData1) .addInput(inputData1)
.addOutput({ .addOutput({
script: embed.output, script: embed.output,
value: 1000 value: 1000,
}) })
.addOutput({ .addOutput({
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
value: 1e5 value: 1e5,
}) })
.signInput(0, alice1.keys[0]) .signInput(0, alice1.keys[0]);
assert.strictEqual(psbt.validateSignatures(0), true) assert.strictEqual(psbt.validateSignatures(0), true);
psbt.finalizeAllInputs() psbt.finalizeAllInputs();
// build and broadcast to the RegTest network // build and broadcast to the RegTest network
await regtestUtils.broadcast(psbt.extractTransaction().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 () => { 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 multisig = createPayment('p2sh-p2ms(2 of 4)');
const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh') const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh');
{ {
const { const {
hash, hash,
index, index,
nonWitnessUtxo, nonWitnessUtxo,
redeemScript, // NEW: P2SH needs to give redeemScript when adding an input. redeemScript, // NEW: P2SH needs to give redeemScript when adding an input.
} = inputData1 } = inputData1;
assert.deepStrictEqual({ hash, index, nonWitnessUtxo, redeemScript }, inputData1) assert.deepStrictEqual(
{ hash, index, nonWitnessUtxo, redeemScript },
inputData1,
);
} }
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData1) .addInput(inputData1)
.addOutput({ .addOutput({
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
value: 1e4 value: 1e4,
}) })
.signInput(0, multisig.keys[0]) .signInput(0, multisig.keys[0])
.signInput(0, multisig.keys[2]) .signInput(0, multisig.keys[2]);
assert.strictEqual(psbt.validateSignatures(0), true) assert.strictEqual(psbt.validateSignatures(0), true);
assert.strictEqual(psbt.validateSignatures(0, multisig.keys[0].publicKey), true) assert.strictEqual(
psbt.validateSignatures(0, multisig.keys[0].publicKey),
true,
);
assert.throws(() => { assert.throws(() => {
psbt.validateSignatures(0, multisig.keys[3].publicKey) psbt.validateSignatures(0, multisig.keys[3].publicKey);
}, new RegExp('No signatures for this pubkey')) }, new RegExp('No signatures for this pubkey'));
psbt.finalizeAllInputs() psbt.finalizeAllInputs();
const tx = psbt.extractTransaction() const tx = psbt.extractTransaction();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 1e4 value: 1e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => { it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => {
const p2sh = createPayment('p2sh-p2wpkh') const p2sh = createPayment('p2sh-p2wpkh');
const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh') const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh');
const inputData2 = await getInputData(5e4, p2sh.payment, true, 'p2sh');
{ {
const { const {
hash, hash,
index, index,
witnessUtxo, // NEW: this is an object of the output being spent { script: Buffer; value: Satoshis; } witnessUtxo, // NEW: this is an object of the output being spent { script: Buffer; value: Satoshis; }
redeemScript, redeemScript,
} = inputData } = inputData;
assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript }, inputData) assert.deepStrictEqual(
{ hash, index, witnessUtxo, redeemScript },
inputData,
);
} }
const keyPair = p2sh.keys[0] const keyPair = p2sh.keys[0];
const outputData = { const outputData = {
script: p2sh.payment.output, // sending to myself for fun script: p2sh.payment.output, // sending to myself for fun
value: 2e4 value: 2e4,
} };
const outputData2 = {
script: p2sh.payment.output, // sending to myself for fun
value: 7e4,
};
const tx = new bitcoin.Psbt() const tx = new bitcoin.Psbt()
.addInput(inputData) .addInputs([inputData, inputData2])
.addOutput(outputData) .addOutputs([outputData, outputData2])
.sign(keyPair) .sign(keyPair)
.finalizeAllInputs() .finalizeAllInputs()
.extractTransaction() .extractTransaction();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: p2sh.payment.address, address: p2sh.payment.address,
vout: 0, vout: 0,
value: 2e4 value: 2e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => { 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 // the only thing that changes is you don't give a redeemscript for input data
const p2wpkh = createPayment('p2wpkh') const p2wpkh = createPayment('p2wpkh');
const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem') const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem');
{ {
const { const { hash, index, witnessUtxo } = inputData;
hash, assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData);
index,
witnessUtxo,
} = inputData
assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData)
} }
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData) .addInput(inputData)
.addOutput({ .addOutput({
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
value: 2e4 value: 2e4,
}) })
.signInput(0, p2wpkh.keys[0]) .signInput(0, p2wpkh.keys[0]);
assert.strictEqual(psbt.validateSignatures(0), true) assert.strictEqual(psbt.validateSignatures(0), true);
psbt.finalizeAllInputs() psbt.finalizeAllInputs();
const tx = psbt.extractTransaction() const tx = psbt.extractTransaction();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 2e4 value: 2e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => { it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => {
const p2wsh = createPayment('p2wsh-p2pk') const p2wsh = createPayment('p2wsh-p2pk');
const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh') const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh');
{ {
const { const {
hash, hash,
index, index,
witnessUtxo, witnessUtxo,
witnessScript, // NEW: A Buffer of the witnessScript witnessScript, // NEW: A Buffer of the witnessScript
} = inputData } = inputData;
assert.deepStrictEqual({ hash, index, witnessUtxo, witnessScript }, inputData) assert.deepStrictEqual(
{ hash, index, witnessUtxo, witnessScript },
inputData,
);
} }
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData) .addInput(inputData)
.addOutput({ .addOutput({
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
value: 2e4 value: 2e4,
}) })
.signInput(0, p2wsh.keys[0]) .signInput(0, p2wsh.keys[0]);
assert.strictEqual(psbt.validateSignatures(0), true) assert.strictEqual(psbt.validateSignatures(0), true);
psbt.finalizeAllInputs() psbt.finalizeAllInputs();
const tx = psbt.extractTransaction() const tx = psbt.extractTransaction();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 2e4 value: 2e4,
}) });
}) });
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => { 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 p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)');
const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh') const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh');
{ {
const { const {
hash, hash,
@ -335,54 +364,60 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
witnessUtxo, witnessUtxo,
redeemScript, redeemScript,
witnessScript, witnessScript,
} = inputData } = inputData;
assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript, witnessScript }, inputData) assert.deepStrictEqual(
{ hash, index, witnessUtxo, redeemScript, witnessScript },
inputData,
);
} }
const psbt = new bitcoin.Psbt({ network: regtest }) const psbt = new bitcoin.Psbt({ network: regtest })
.addInput(inputData) .addInput(inputData)
.addOutput({ .addOutput({
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
value: 2e4 value: 2e4,
}) })
.signInput(0, p2sh.keys[0]) .signInput(0, p2sh.keys[0])
.signInput(0, p2sh.keys[2]) .signInput(0, p2sh.keys[2])
.signInput(0, p2sh.keys[3]) .signInput(0, p2sh.keys[3]);
assert.strictEqual(psbt.validateSignatures(0), true) assert.strictEqual(psbt.validateSignatures(0), true);
assert.strictEqual(psbt.validateSignatures(0, p2sh.keys[3].publicKey), true) assert.strictEqual(
psbt.validateSignatures(0, p2sh.keys[3].publicKey),
true,
);
assert.throws(() => { assert.throws(() => {
psbt.validateSignatures(0, p2sh.keys[1].publicKey) psbt.validateSignatures(0, p2sh.keys[1].publicKey);
}, new RegExp('No signatures for this pubkey')) }, new RegExp('No signatures for this pubkey'));
psbt.finalizeAllInputs() psbt.finalizeAllInputs();
const tx = psbt.extractTransaction() const tx = psbt.extractTransaction();
// build and broadcast to the Bitcoin RegTest network // build and broadcast to the Bitcoin RegTest network
await regtestUtils.broadcast(tx.toHex()) await regtestUtils.broadcast(tx.toHex());
await regtestUtils.verify({ await regtestUtils.verify({
txId: tx.getId(), txId: tx.getId(),
address: regtestUtils.RANDOM_ADDRESS, address: regtestUtils.RANDOM_ADDRESS,
vout: 0, vout: 0,
value: 2e4 value: 2e4,
}) });
}) });
}) });
function createPayment(_type, network) { function createPayment(_type, network) {
network = network || regtest network = network || regtest;
const splitType = _type.split('-').reverse(); const splitType = _type.split('-').reverse();
const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; const isMultisig = splitType[0].slice(0, 4) === 'p2ms';
const keys = []; const keys = [];
let m; let m;
if (isMultisig) { if (isMultisig) {
const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/) const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/);
m = parseInt(match[1]) m = parseInt(match[1]);
let n = parseInt(match[2]) let n = parseInt(match[2]);
while (n > 1) { while (n > 1) {
keys.push(bitcoin.ECPair.makeRandom({ network })); keys.push(bitcoin.ECPair.makeRandom({ network }));
n-- n--;
} }
} }
keys.push(bitcoin.ECPair.makeRandom({ network })); keys.push(bitcoin.ECPair.makeRandom({ network }));

View file

@ -130,6 +130,13 @@ export class Psbt extends PsbtBase {
return this.inputs.length; return this.inputs.length;
} }
clone(): Psbt {
// TODO: more efficient cloning
const res = Psbt.fromBuffer(this.toBuffer());
res.opts = JSON.parse(JSON.stringify(this.opts));
return res;
}
setMaximumFeeRate(satoshiPerByte: number): void { setMaximumFeeRate(satoshiPerByte: number): void {
check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw
this.opts.maximumFeeRate = satoshiPerByte; this.opts.maximumFeeRate = satoshiPerByte;
@ -168,6 +175,11 @@ export class Psbt extends PsbtBase {
return this; return this;
} }
addInputs(inputDatas: TransactionInput[]): this {
inputDatas.forEach(inputData => this.addInput(inputData));
return this;
}
addInput(inputData: TransactionInput): this { addInput(inputData: TransactionInput): this {
checkInputsForPartialSig(this.inputs, 'addInput'); checkInputsForPartialSig(this.inputs, 'addInput');
const c = this.__CACHE; const c = this.__CACHE;
@ -178,6 +190,11 @@ export class Psbt extends PsbtBase {
return this; return this;
} }
addOutputs(outputDatas: TransactionOutput[]): this {
outputDatas.forEach(outputData => this.addOutput(outputData));
return this;
}
addOutput(outputData: TransactionOutput): this { addOutput(outputData: TransactionOutput): this {
checkInputsForPartialSig(this.inputs, 'addOutput'); checkInputsForPartialSig(this.inputs, 'addOutput');
const { address } = outputData as any; const { address } = outputData as any;

3
types/psbt.d.ts vendored
View file

@ -11,11 +11,14 @@ export declare class Psbt extends PsbtBase {
private opts; private opts;
constructor(opts?: PsbtOptsOptional); constructor(opts?: PsbtOptsOptional);
readonly inputCount: number; readonly inputCount: number;
clone(): Psbt;
setMaximumFeeRate(satoshiPerByte: number): void; setMaximumFeeRate(satoshiPerByte: number): void;
setVersion(version: number): this; setVersion(version: number): this;
setLocktime(locktime: number): this; setLocktime(locktime: number): this;
setSequence(inputIndex: number, sequence: number): this; setSequence(inputIndex: number, sequence: number): this;
addInputs(inputDatas: TransactionInput[]): this;
addInput(inputData: TransactionInput): this; addInput(inputData: TransactionInput): this;
addOutputs(outputDatas: TransactionOutput[]): this;
addOutput(outputData: TransactionOutput): this; addOutput(outputData: TransactionOutput): this;
addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this; addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this;
extractTransaction(disableFeeCheck?: boolean): Transaction; extractTransaction(disableFeeCheck?: boolean): Transaction;