Add clone, addInputs, addOutputs
This commit is contained in:
parent
b8c341dea0
commit
01c7ac39b6
4 changed files with 221 additions and 152 deletions
14
src/psbt.js
14
src/psbt.js
|
@ -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;
|
||||||
|
|
|
@ -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 }));
|
||||||
|
|
|
@ -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
3
types/psbt.d.ts
vendored
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue