1 changed files with 366 additions and 0 deletions
@ -0,0 +1,366 @@ |
|||||
|
const { describe, it } = require('mocha') |
||||
|
const assert = require('assert') |
||||
|
const bitcoin = require('../../') |
||||
|
const regtestUtils = require('./_regtest') |
||||
|
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 (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
|
||||
|
signer1.signInput(0, alice1.keys[0]) |
||||
|
signer2.signInput(1, 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) |
||||
|
|
||||
|
// 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() |
||||
|
// it returns an array of the success of each input, also a result attribute
|
||||
|
// which is true if all array items are true.
|
||||
|
|
||||
|
// 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]) |
||||
|
|
||||
|
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]) |
||||
|
|
||||
|
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 { |
||||
|
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 psbt = new bitcoin.Psbt({ network: regtest }) |
||||
|
.addInput(inputData) |
||||
|
.addOutput({ |
||||
|
address: regtestUtils.RANDOM_ADDRESS, |
||||
|
value: 2e4 |
||||
|
}) |
||||
|
.signInput(0, p2sh.keys[0]) |
||||
|
|
||||
|
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', 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]) |
||||
|
|
||||
|
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', 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]) |
||||
|
|
||||
|
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', 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]) |
||||
|
|
||||
|
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) { |
||||
|
const splitType = _type.split('-').reverse(); |
||||
|
const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; |
||||
|
const keys = []; |
||||
|
let m; |
||||
|
if (isMultisig) { |
||||
|
const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/) |
||||
|
m = parseInt(match[1]) |
||||
|
let n = parseInt(match[2]) |
||||
|
while (n > 1) { |
||||
|
keys.push(bitcoin.ECPair.makeRandom({ network: regtest })); |
||||
|
n-- |
||||
|
} |
||||
|
} |
||||
|
keys.push(bitcoin.ECPair.makeRandom({ network: regtest })); |
||||
|
|
||||
|
let payment; |
||||
|
splitType.forEach(type => { |
||||
|
if (type.slice(0, 4) === 'p2ms') { |
||||
|
payment = bitcoin.payments.p2ms({ |
||||
|
m, |
||||
|
pubkeys: keys.map(key => key.publicKey).sort(), |
||||
|
network: regtest, |
||||
|
}); |
||||
|
} else if (['p2sh', 'p2wsh'].indexOf(type) > -1) { |
||||
|
payment = bitcoin.payments[type]({ |
||||
|
redeem: payment, |
||||
|
network: regtest, |
||||
|
}); |
||||
|
} else { |
||||
|
payment = bitcoin.payments[type]({ |
||||
|
pubkey: keys[0].publicKey, |
||||
|
network: regtest, |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
payment, |
||||
|
keys, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function getWitnessUtxo(out) { |
||||
|
delete out.address; |
||||
|
out.script = Buffer.from(out.script, 'hex'); |
||||
|
return out; |
||||
|
} |
||||
|
|
||||
|
async function getInputData(amount, payment, isSegwit, redeemType) { |
||||
|
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 = {}; |
||||
|
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, |
||||
|
}; |
||||
|
} |
Loading…
Reference in new issue