Fix lint transaction_builder.ts
This commit is contained in:
parent
e6ea0389a2
commit
512b03e284
5 changed files with 336 additions and 330 deletions
|
@ -1,16 +1,16 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const networks = require("./networks");
|
|
||||||
const bufferutils_1 = require("./bufferutils");
|
|
||||||
const transaction_1 = require("./transaction");
|
|
||||||
const ECPair = require("./ecpair");
|
|
||||||
const types = require("./types");
|
|
||||||
const baddress = require("./address");
|
const baddress = require("./address");
|
||||||
const bcrypto = require("./crypto");
|
const bufferutils_1 = require("./bufferutils");
|
||||||
const bscript = require("./script");
|
|
||||||
const payments = require("./payments");
|
|
||||||
const classify = require("./classify");
|
const classify = require("./classify");
|
||||||
|
const bcrypto = require("./crypto");
|
||||||
|
const ECPair = require("./ecpair");
|
||||||
|
const networks = require("./networks");
|
||||||
|
const payments = require("./payments");
|
||||||
|
const bscript = require("./script");
|
||||||
const script_1 = require("./script");
|
const script_1 = require("./script");
|
||||||
|
const transaction_1 = require("./transaction");
|
||||||
|
const types = require("./types");
|
||||||
const typeforce = require('typeforce');
|
const typeforce = require('typeforce');
|
||||||
const SCRIPT_TYPES = classify.types;
|
const SCRIPT_TYPES = classify.types;
|
||||||
function txIsString(tx) {
|
function txIsString(tx) {
|
||||||
|
@ -20,15 +20,6 @@ function txIsTransaction(tx) {
|
||||||
return tx instanceof transaction_1.Transaction;
|
return tx instanceof transaction_1.Transaction;
|
||||||
}
|
}
|
||||||
class TransactionBuilder {
|
class TransactionBuilder {
|
||||||
constructor(network, maximumFeeRate) {
|
|
||||||
this.__prevTxSet = {};
|
|
||||||
this.network = network || networks.bitcoin;
|
|
||||||
// WARNING: This is __NOT__ to be relied on, its just another potential safety mechanism (safety in-depth)
|
|
||||||
this.maximumFeeRate = maximumFeeRate || 2500;
|
|
||||||
this.__inputs = [];
|
|
||||||
this.__tx = new transaction_1.Transaction();
|
|
||||||
this.__tx.version = 2;
|
|
||||||
}
|
|
||||||
static fromTransaction(transaction, network) {
|
static fromTransaction(transaction, network) {
|
||||||
const txb = new TransactionBuilder(network);
|
const txb = new TransactionBuilder(network);
|
||||||
// Copy transaction fields
|
// Copy transaction fields
|
||||||
|
@ -47,33 +38,42 @@ class TransactionBuilder {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// fix some things not possible through the public API
|
// fix some things not possible through the public API
|
||||||
txb.__inputs.forEach((input, i) => {
|
txb.__INPUTS.forEach((input, i) => {
|
||||||
fixMultisigOrder(input, transaction, i);
|
fixMultisigOrder(input, transaction, i);
|
||||||
});
|
});
|
||||||
return txb;
|
return txb;
|
||||||
}
|
}
|
||||||
|
constructor(network, maximumFeeRate) {
|
||||||
|
this.__PREV_TX_SET = {};
|
||||||
|
this.network = network || networks.bitcoin;
|
||||||
|
// WARNING: This is __NOT__ to be relied on, its just another potential safety mechanism (safety in-depth)
|
||||||
|
this.maximumFeeRate = maximumFeeRate || 2500;
|
||||||
|
this.__INPUTS = [];
|
||||||
|
this.__TX = new transaction_1.Transaction();
|
||||||
|
this.__TX.version = 2;
|
||||||
|
}
|
||||||
setLockTime(locktime) {
|
setLockTime(locktime) {
|
||||||
typeforce(types.UInt32, locktime);
|
typeforce(types.UInt32, locktime);
|
||||||
// if any signatures exist, throw
|
// if any signatures exist, throw
|
||||||
if (this.__inputs.some(input => {
|
if (this.__INPUTS.some(input => {
|
||||||
if (!input.signatures)
|
if (!input.signatures)
|
||||||
return false;
|
return false;
|
||||||
return input.signatures.some(s => s !== undefined);
|
return input.signatures.some(s => s !== undefined);
|
||||||
})) {
|
})) {
|
||||||
throw new Error('No, this would invalidate signatures');
|
throw new Error('No, this would invalidate signatures');
|
||||||
}
|
}
|
||||||
this.__tx.locktime = locktime;
|
this.__TX.locktime = locktime;
|
||||||
}
|
}
|
||||||
setVersion(version) {
|
setVersion(version) {
|
||||||
typeforce(types.UInt32, version);
|
typeforce(types.UInt32, version);
|
||||||
// XXX: this might eventually become more complex depending on what the versions represent
|
// XXX: this might eventually become more complex depending on what the versions represent
|
||||||
this.__tx.version = version;
|
this.__TX.version = version;
|
||||||
}
|
}
|
||||||
addInput(txHash, vout, sequence, prevOutScript) {
|
addInput(txHash, vout, sequence, prevOutScript) {
|
||||||
if (!this.__canModifyInputs()) {
|
if (!this.__canModifyInputs()) {
|
||||||
throw new Error('No, this would invalidate signatures');
|
throw new Error('No, this would invalidate signatures');
|
||||||
}
|
}
|
||||||
let value = undefined;
|
let value;
|
||||||
// is it a hex string?
|
// is it a hex string?
|
||||||
if (txIsString(txHash)) {
|
if (txIsString(txHash)) {
|
||||||
// transaction hashs's are displayed in reverse order, un-reverse it
|
// transaction hashs's are displayed in reverse order, un-reverse it
|
||||||
|
@ -87,17 +87,90 @@ class TransactionBuilder {
|
||||||
txHash = txHash.getHash(false);
|
txHash = txHash.getHash(false);
|
||||||
}
|
}
|
||||||
return this.__addInputUnsafe(txHash, vout, {
|
return this.__addInputUnsafe(txHash, vout, {
|
||||||
sequence: sequence,
|
sequence,
|
||||||
prevOutScript: prevOutScript,
|
prevOutScript,
|
||||||
value: value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
addOutput(scriptPubKey, value) {
|
||||||
|
if (!this.__canModifyOutputs()) {
|
||||||
|
throw new Error('No, this would invalidate signatures');
|
||||||
|
}
|
||||||
|
// Attempt to get a script if it's a base58 or bech32 address string
|
||||||
|
if (typeof scriptPubKey === 'string') {
|
||||||
|
scriptPubKey = baddress.toOutputScript(scriptPubKey, this.network);
|
||||||
|
}
|
||||||
|
return this.__TX.addOutput(scriptPubKey, value);
|
||||||
|
}
|
||||||
|
build() {
|
||||||
|
return this.__build(false);
|
||||||
|
}
|
||||||
|
buildIncomplete() {
|
||||||
|
return this.__build(true);
|
||||||
|
}
|
||||||
|
sign(vin, keyPair, redeemScript, hashType, witnessValue, witnessScript) {
|
||||||
|
// TODO: remove keyPair.network matching in 4.0.0
|
||||||
|
if (keyPair.network && keyPair.network !== this.network)
|
||||||
|
throw new TypeError('Inconsistent network');
|
||||||
|
if (!this.__INPUTS[vin])
|
||||||
|
throw new Error('No input at index: ' + vin);
|
||||||
|
hashType = hashType || transaction_1.Transaction.SIGHASH_ALL;
|
||||||
|
if (this.__needsOutputs(hashType))
|
||||||
|
throw new Error('Transaction needs outputs');
|
||||||
|
const input = this.__INPUTS[vin];
|
||||||
|
// if redeemScript was previously provided, enforce consistency
|
||||||
|
if (input.redeemScript !== undefined &&
|
||||||
|
redeemScript &&
|
||||||
|
!input.redeemScript.equals(redeemScript)) {
|
||||||
|
throw new Error('Inconsistent redeemScript');
|
||||||
|
}
|
||||||
|
const ourPubKey = keyPair.publicKey || keyPair.getPublicKey();
|
||||||
|
if (!canSign(input)) {
|
||||||
|
if (witnessValue !== undefined) {
|
||||||
|
if (input.value !== undefined && input.value !== witnessValue)
|
||||||
|
throw new Error('Input did not match witnessValue');
|
||||||
|
typeforce(types.Satoshi, witnessValue);
|
||||||
|
input.value = witnessValue;
|
||||||
|
}
|
||||||
|
if (!canSign(input)) {
|
||||||
|
const prepared = prepareInput(input, ourPubKey, redeemScript, witnessScript);
|
||||||
|
// updates inline
|
||||||
|
Object.assign(input, prepared);
|
||||||
|
}
|
||||||
|
if (!canSign(input))
|
||||||
|
throw Error(input.prevOutType + ' not supported');
|
||||||
|
}
|
||||||
|
// ready to sign
|
||||||
|
let signatureHash;
|
||||||
|
if (input.hasWitness) {
|
||||||
|
signatureHash = this.__TX.hashForWitnessV0(vin, input.signScript, input.value, hashType);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
signatureHash = this.__TX.hashForSignature(vin, input.signScript, hashType);
|
||||||
|
}
|
||||||
|
// enforce in order signing of public keys
|
||||||
|
const signed = input.pubkeys.some((pubKey, i) => {
|
||||||
|
if (!ourPubKey.equals(pubKey))
|
||||||
|
return false;
|
||||||
|
if (input.signatures[i])
|
||||||
|
throw new Error('Signature already exists');
|
||||||
|
// TODO: add tests
|
||||||
|
if (ourPubKey.length !== 33 && input.hasWitness) {
|
||||||
|
throw new Error('BIP143 rejects uncompressed public keys in P2WPKH or P2WSH');
|
||||||
|
}
|
||||||
|
const signature = keyPair.sign(signatureHash);
|
||||||
|
input.signatures[i] = bscript.signature.encode(signature, hashType);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (!signed)
|
||||||
|
throw new Error('Key pair cannot sign for this input');
|
||||||
|
}
|
||||||
__addInputUnsafe(txHash, vout, options) {
|
__addInputUnsafe(txHash, vout, options) {
|
||||||
if (transaction_1.Transaction.isCoinbaseHash(txHash)) {
|
if (transaction_1.Transaction.isCoinbaseHash(txHash)) {
|
||||||
throw new Error('coinbase inputs not supported');
|
throw new Error('coinbase inputs not supported');
|
||||||
}
|
}
|
||||||
const prevTxOut = txHash.toString('hex') + ':' + vout;
|
const prevTxOut = txHash.toString('hex') + ':' + vout;
|
||||||
if (this.__prevTxSet[prevTxOut] !== undefined)
|
if (this.__PREV_TX_SET[prevTxOut] !== undefined)
|
||||||
throw new Error('Duplicate TxOut: ' + prevTxOut);
|
throw new Error('Duplicate TxOut: ' + prevTxOut);
|
||||||
let input = {};
|
let input = {};
|
||||||
// derive what we can from the scriptSig
|
// derive what we can from the scriptSig
|
||||||
|
@ -122,37 +195,21 @@ class TransactionBuilder {
|
||||||
input.prevOutScript = options.prevOutScript;
|
input.prevOutScript = options.prevOutScript;
|
||||||
input.prevOutType = prevOutType || classify.output(options.prevOutScript);
|
input.prevOutType = prevOutType || classify.output(options.prevOutScript);
|
||||||
}
|
}
|
||||||
const vin = this.__tx.addInput(txHash, vout, options.sequence, options.scriptSig);
|
const vin = this.__TX.addInput(txHash, vout, options.sequence, options.scriptSig);
|
||||||
this.__inputs[vin] = input;
|
this.__INPUTS[vin] = input;
|
||||||
this.__prevTxSet[prevTxOut] = true;
|
this.__PREV_TX_SET[prevTxOut] = true;
|
||||||
return vin;
|
return vin;
|
||||||
}
|
}
|
||||||
addOutput(scriptPubKey, value) {
|
|
||||||
if (!this.__canModifyOutputs()) {
|
|
||||||
throw new Error('No, this would invalidate signatures');
|
|
||||||
}
|
|
||||||
// Attempt to get a script if it's a base58 or bech32 address string
|
|
||||||
if (typeof scriptPubKey === 'string') {
|
|
||||||
scriptPubKey = baddress.toOutputScript(scriptPubKey, this.network);
|
|
||||||
}
|
|
||||||
return this.__tx.addOutput(scriptPubKey, value);
|
|
||||||
}
|
|
||||||
build() {
|
|
||||||
return this.__build(false);
|
|
||||||
}
|
|
||||||
buildIncomplete() {
|
|
||||||
return this.__build(true);
|
|
||||||
}
|
|
||||||
__build(allowIncomplete) {
|
__build(allowIncomplete) {
|
||||||
if (!allowIncomplete) {
|
if (!allowIncomplete) {
|
||||||
if (!this.__tx.ins.length)
|
if (!this.__TX.ins.length)
|
||||||
throw new Error('Transaction has no inputs');
|
throw new Error('Transaction has no inputs');
|
||||||
if (!this.__tx.outs.length)
|
if (!this.__TX.outs.length)
|
||||||
throw new Error('Transaction has no outputs');
|
throw new Error('Transaction has no outputs');
|
||||||
}
|
}
|
||||||
const tx = this.__tx.clone();
|
const tx = this.__TX.clone();
|
||||||
// create script signatures from inputs
|
// create script signatures from inputs
|
||||||
this.__inputs.forEach((input, i) => {
|
this.__INPUTS.forEach((input, i) => {
|
||||||
if (!input.prevOutType && !allowIncomplete)
|
if (!input.prevOutType && !allowIncomplete)
|
||||||
throw new Error('Transaction is not complete');
|
throw new Error('Transaction is not complete');
|
||||||
const result = build(input.prevOutType, input, allowIncomplete);
|
const result = build(input.prevOutType, input, allowIncomplete);
|
||||||
|
@ -174,65 +231,8 @@ class TransactionBuilder {
|
||||||
}
|
}
|
||||||
return tx;
|
return tx;
|
||||||
}
|
}
|
||||||
sign(vin, keyPair, redeemScript, hashType, witnessValue, witnessScript) {
|
|
||||||
// TODO: remove keyPair.network matching in 4.0.0
|
|
||||||
if (keyPair.network && keyPair.network !== this.network)
|
|
||||||
throw new TypeError('Inconsistent network');
|
|
||||||
if (!this.__inputs[vin])
|
|
||||||
throw new Error('No input at index: ' + vin);
|
|
||||||
hashType = hashType || transaction_1.Transaction.SIGHASH_ALL;
|
|
||||||
if (this.__needsOutputs(hashType))
|
|
||||||
throw new Error('Transaction needs outputs');
|
|
||||||
const input = this.__inputs[vin];
|
|
||||||
// if redeemScript was previously provided, enforce consistency
|
|
||||||
if (input.redeemScript !== undefined &&
|
|
||||||
redeemScript &&
|
|
||||||
!input.redeemScript.equals(redeemScript)) {
|
|
||||||
throw new Error('Inconsistent redeemScript');
|
|
||||||
}
|
|
||||||
const ourPubKey = keyPair.publicKey || keyPair.getPublicKey();
|
|
||||||
if (!canSign(input)) {
|
|
||||||
if (witnessValue !== undefined) {
|
|
||||||
if (input.value !== undefined && input.value !== witnessValue)
|
|
||||||
throw new Error("Input didn't match witnessValue");
|
|
||||||
typeforce(types.Satoshi, witnessValue);
|
|
||||||
input.value = witnessValue;
|
|
||||||
}
|
|
||||||
if (!canSign(input)) {
|
|
||||||
const prepared = prepareInput(input, ourPubKey, redeemScript, witnessScript);
|
|
||||||
// updates inline
|
|
||||||
Object.assign(input, prepared);
|
|
||||||
}
|
|
||||||
if (!canSign(input))
|
|
||||||
throw Error(input.prevOutType + ' not supported');
|
|
||||||
}
|
|
||||||
// ready to sign
|
|
||||||
let signatureHash;
|
|
||||||
if (input.hasWitness) {
|
|
||||||
signatureHash = this.__tx.hashForWitnessV0(vin, input.signScript, input.value, hashType);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
signatureHash = this.__tx.hashForSignature(vin, input.signScript, hashType);
|
|
||||||
}
|
|
||||||
// enforce in order signing of public keys
|
|
||||||
const signed = input.pubkeys.some((pubKey, i) => {
|
|
||||||
if (!ourPubKey.equals(pubKey))
|
|
||||||
return false;
|
|
||||||
if (input.signatures[i])
|
|
||||||
throw new Error('Signature already exists');
|
|
||||||
// TODO: add tests
|
|
||||||
if (ourPubKey.length !== 33 && input.hasWitness) {
|
|
||||||
throw new Error('BIP143 rejects uncompressed public keys in P2WPKH or P2WSH');
|
|
||||||
}
|
|
||||||
const signature = keyPair.sign(signatureHash);
|
|
||||||
input.signatures[i] = bscript.signature.encode(signature, hashType);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
if (!signed)
|
|
||||||
throw new Error('Key pair cannot sign for this input');
|
|
||||||
}
|
|
||||||
__canModifyInputs() {
|
__canModifyInputs() {
|
||||||
return this.__inputs.every(input => {
|
return this.__INPUTS.every(input => {
|
||||||
if (!input.signatures)
|
if (!input.signatures)
|
||||||
return true;
|
return true;
|
||||||
return input.signatures.every(signature => {
|
return input.signatures.every(signature => {
|
||||||
|
@ -247,12 +247,12 @@ class TransactionBuilder {
|
||||||
}
|
}
|
||||||
__needsOutputs(signingHashType) {
|
__needsOutputs(signingHashType) {
|
||||||
if (signingHashType === transaction_1.Transaction.SIGHASH_ALL) {
|
if (signingHashType === transaction_1.Transaction.SIGHASH_ALL) {
|
||||||
return this.__tx.outs.length === 0;
|
return this.__TX.outs.length === 0;
|
||||||
}
|
}
|
||||||
// if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs
|
// if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs
|
||||||
// .build() will fail, but .buildIncomplete() is OK
|
// .build() will fail, but .buildIncomplete() is OK
|
||||||
return (this.__tx.outs.length === 0 &&
|
return (this.__TX.outs.length === 0 &&
|
||||||
this.__inputs.some(input => {
|
this.__INPUTS.some(input => {
|
||||||
if (!input.signatures)
|
if (!input.signatures)
|
||||||
return false;
|
return false;
|
||||||
return input.signatures.some(signature => {
|
return input.signatures.some(signature => {
|
||||||
|
@ -266,9 +266,9 @@ class TransactionBuilder {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
__canModifyOutputs() {
|
__canModifyOutputs() {
|
||||||
const nInputs = this.__tx.ins.length;
|
const nInputs = this.__TX.ins.length;
|
||||||
const nOutputs = this.__tx.outs.length;
|
const nOutputs = this.__TX.outs.length;
|
||||||
return this.__inputs.every(input => {
|
return this.__INPUTS.every(input => {
|
||||||
if (input.signatures === undefined)
|
if (input.signatures === undefined)
|
||||||
return true;
|
return true;
|
||||||
return input.signatures.every(signature => {
|
return input.signatures.every(signature => {
|
||||||
|
@ -290,10 +290,10 @@ class TransactionBuilder {
|
||||||
}
|
}
|
||||||
__overMaximumFees(bytes) {
|
__overMaximumFees(bytes) {
|
||||||
// not all inputs will have .value defined
|
// not all inputs will have .value defined
|
||||||
const incoming = this.__inputs.reduce((a, x) => a + (x.value >>> 0), 0);
|
const incoming = this.__INPUTS.reduce((a, x) => a + (x.value >>> 0), 0);
|
||||||
// but all outputs do, and if we have any input value
|
// but all outputs do, and if we have any input value
|
||||||
// we can immediately determine if the outputs are too small
|
// we can immediately determine if the outputs are too small
|
||||||
const outgoing = this.__tx.outs.reduce((a, x) => a + x.value, 0);
|
const outgoing = this.__TX.outs.reduce((a, x) => a + x.value, 0);
|
||||||
const fee = incoming - outgoing;
|
const fee = incoming - outgoing;
|
||||||
const feeRate = fee / bytes;
|
const feeRate = fee / bytes;
|
||||||
return feeRate > this.maximumFeeRate;
|
return feeRate > this.maximumFeeRate;
|
||||||
|
@ -350,8 +350,8 @@ function expandInput(scriptSig, witnessStack, type, scriptPubKey) {
|
||||||
}, { allowIncomplete: true });
|
}, { allowIncomplete: true });
|
||||||
return {
|
return {
|
||||||
prevOutType: SCRIPT_TYPES.P2MS,
|
prevOutType: SCRIPT_TYPES.P2MS,
|
||||||
pubkeys: pubkeys,
|
pubkeys,
|
||||||
signatures: signatures,
|
signatures,
|
||||||
maxSignatures: m,
|
maxSignatures: m,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -488,7 +488,9 @@ function expandOutput(script, ourPubKey) {
|
||||||
}
|
}
|
||||||
function prepareInput(input, ourPubKey, redeemScript, witnessScript) {
|
function prepareInput(input, ourPubKey, redeemScript, witnessScript) {
|
||||||
if (redeemScript && witnessScript) {
|
if (redeemScript && witnessScript) {
|
||||||
const p2wsh = (payments.p2wsh({ redeem: { output: witnessScript } }));
|
const p2wsh = payments.p2wsh({
|
||||||
|
redeem: { output: witnessScript },
|
||||||
|
});
|
||||||
const p2wshAlt = payments.p2wsh({ output: redeemScript });
|
const p2wshAlt = payments.p2wsh({ output: redeemScript });
|
||||||
const p2sh = payments.p2sh({ redeem: { output: redeemScript } });
|
const p2sh = payments.p2sh({ redeem: { output: redeemScript } });
|
||||||
const p2shAlt = payments.p2sh({ redeem: p2wsh });
|
const p2shAlt = payments.p2sh({ redeem: p2wsh });
|
||||||
|
@ -506,7 +508,7 @@ function prepareInput(input, ourPubKey, redeemScript, witnessScript) {
|
||||||
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
||||||
expanded.signatures = input.signatures;
|
expanded.signatures = input.signatures;
|
||||||
}
|
}
|
||||||
let signScript = witnessScript;
|
const signScript = witnessScript;
|
||||||
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
||||||
throw new Error('P2SH(P2WSH(P2WPKH)) is a consensus failure');
|
throw new Error('P2SH(P2WSH(P2WPKH)) is a consensus failure');
|
||||||
return {
|
return {
|
||||||
|
@ -579,7 +581,7 @@ function prepareInput(input, ourPubKey, redeemScript, witnessScript) {
|
||||||
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
||||||
expanded.signatures = input.signatures;
|
expanded.signatures = input.signatures;
|
||||||
}
|
}
|
||||||
let signScript = witnessScript;
|
const signScript = witnessScript;
|
||||||
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
||||||
throw new Error('P2WSH(P2WPKH) is a consensus failure');
|
throw new Error('P2WSH(P2WPKH) is a consensus failure');
|
||||||
return {
|
return {
|
||||||
|
@ -614,7 +616,8 @@ function prepareInput(input, ourPubKey, redeemScript, witnessScript) {
|
||||||
}
|
}
|
||||||
let signScript = input.prevOutScript;
|
let signScript = input.prevOutScript;
|
||||||
if (expanded.type === SCRIPT_TYPES.P2WPKH) {
|
if (expanded.type === SCRIPT_TYPES.P2WPKH) {
|
||||||
signScript = (payments.p2pkh({ pubkey: expanded.pubkeys[0] }).output);
|
signScript = payments.p2pkh({ pubkey: expanded.pubkeys[0] })
|
||||||
|
.output;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
prevOutType: expanded.type,
|
prevOutType: expanded.type,
|
||||||
|
@ -630,7 +633,7 @@ function prepareInput(input, ourPubKey, redeemScript, witnessScript) {
|
||||||
const prevOutScript = payments.p2pkh({ pubkey: ourPubKey }).output;
|
const prevOutScript = payments.p2pkh({ pubkey: ourPubKey }).output;
|
||||||
return {
|
return {
|
||||||
prevOutType: SCRIPT_TYPES.P2PKH,
|
prevOutType: SCRIPT_TYPES.P2PKH,
|
||||||
prevOutScript: prevOutScript,
|
prevOutScript,
|
||||||
hasWitness: false,
|
hasWitness: false,
|
||||||
signScript: prevOutScript,
|
signScript: prevOutScript,
|
||||||
signType: SCRIPT_TYPES.P2PKH,
|
signType: SCRIPT_TYPES.P2PKH,
|
||||||
|
|
2
test/fixtures/transaction_builder.json
vendored
2
test/fixtures/transaction_builder.json
vendored
|
@ -1976,7 +1976,7 @@
|
||||||
"sign": [
|
"sign": [
|
||||||
{
|
{
|
||||||
"description": "Transaction w/ witness value mismatch",
|
"description": "Transaction w/ witness value mismatch",
|
||||||
"exception": "Input didn\\'t match witnessValue",
|
"exception": "Input did not match witnessValue",
|
||||||
"network": "testnet",
|
"network": "testnet",
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -164,7 +164,7 @@ describe('TransactionBuilder', function () {
|
||||||
const tx = Transaction.fromHex(fixtures.valid.classification.hex)
|
const tx = Transaction.fromHex(fixtures.valid.classification.hex)
|
||||||
const txb = TransactionBuilder.fromTransaction(tx)
|
const txb = TransactionBuilder.fromTransaction(tx)
|
||||||
|
|
||||||
txb.__inputs.forEach(function (i) {
|
txb.__INPUTS.forEach(function (i) {
|
||||||
assert.strictEqual(i.prevOutType, 'scripthash')
|
assert.strictEqual(i.prevOutType, 'scripthash')
|
||||||
assert.strictEqual(i.redeemScriptType, 'multisig')
|
assert.strictEqual(i.redeemScriptType, 'multisig')
|
||||||
})
|
})
|
||||||
|
@ -191,22 +191,22 @@ describe('TransactionBuilder', function () {
|
||||||
const vin = txb.addInput(txHash, 1, 54)
|
const vin = txb.addInput(txHash, 1, 54)
|
||||||
assert.strictEqual(vin, 0)
|
assert.strictEqual(vin, 0)
|
||||||
|
|
||||||
const txIn = txb.__tx.ins[0]
|
const txIn = txb.__TX.ins[0]
|
||||||
assert.strictEqual(txIn.hash, txHash)
|
assert.strictEqual(txIn.hash, txHash)
|
||||||
assert.strictEqual(txIn.index, 1)
|
assert.strictEqual(txIn.index, 1)
|
||||||
assert.strictEqual(txIn.sequence, 54)
|
assert.strictEqual(txIn.sequence, 54)
|
||||||
assert.strictEqual(txb.__inputs[0].prevOutScript, undefined)
|
assert.strictEqual(txb.__INPUTS[0].prevOutScript, undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts a txHash, index [, sequence number and scriptPubKey]', function () {
|
it('accepts a txHash, index [, sequence number and scriptPubKey]', function () {
|
||||||
const vin = txb.addInput(txHash, 1, 54, scripts[1])
|
const vin = txb.addInput(txHash, 1, 54, scripts[1])
|
||||||
assert.strictEqual(vin, 0)
|
assert.strictEqual(vin, 0)
|
||||||
|
|
||||||
const txIn = txb.__tx.ins[0]
|
const txIn = txb.__TX.ins[0]
|
||||||
assert.strictEqual(txIn.hash, txHash)
|
assert.strictEqual(txIn.hash, txHash)
|
||||||
assert.strictEqual(txIn.index, 1)
|
assert.strictEqual(txIn.index, 1)
|
||||||
assert.strictEqual(txIn.sequence, 54)
|
assert.strictEqual(txIn.sequence, 54)
|
||||||
assert.strictEqual(txb.__inputs[0].prevOutScript, scripts[1])
|
assert.strictEqual(txb.__INPUTS[0].prevOutScript, scripts[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts a prevTx, index [and sequence number]', function () {
|
it('accepts a prevTx, index [and sequence number]', function () {
|
||||||
|
@ -217,11 +217,11 @@ describe('TransactionBuilder', function () {
|
||||||
const vin = txb.addInput(prevTx, 1, 54)
|
const vin = txb.addInput(prevTx, 1, 54)
|
||||||
assert.strictEqual(vin, 0)
|
assert.strictEqual(vin, 0)
|
||||||
|
|
||||||
const txIn = txb.__tx.ins[0]
|
const txIn = txb.__TX.ins[0]
|
||||||
assert.deepEqual(txIn.hash, prevTx.getHash())
|
assert.deepEqual(txIn.hash, prevTx.getHash())
|
||||||
assert.strictEqual(txIn.index, 1)
|
assert.strictEqual(txIn.index, 1)
|
||||||
assert.strictEqual(txIn.sequence, 54)
|
assert.strictEqual(txIn.sequence, 54)
|
||||||
assert.strictEqual(txb.__inputs[0].prevOutScript, scripts[1])
|
assert.strictEqual(txb.__INPUTS[0].prevOutScript, scripts[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the input index', function () {
|
it('returns the input index', function () {
|
||||||
|
@ -251,7 +251,7 @@ describe('TransactionBuilder', function () {
|
||||||
const vout = txb.addOutput(address, 1000)
|
const vout = txb.addOutput(address, 1000)
|
||||||
assert.strictEqual(vout, 0)
|
assert.strictEqual(vout, 0)
|
||||||
|
|
||||||
const txout = txb.__tx.outs[0]
|
const txout = txb.__TX.outs[0]
|
||||||
assert.deepEqual(txout.script, scripts[0])
|
assert.deepEqual(txout.script, scripts[0])
|
||||||
assert.strictEqual(txout.value, 1000)
|
assert.strictEqual(txout.value, 1000)
|
||||||
})
|
})
|
||||||
|
@ -260,7 +260,7 @@ describe('TransactionBuilder', function () {
|
||||||
const vout = txb.addOutput(scripts[0], 1000)
|
const vout = txb.addOutput(scripts[0], 1000)
|
||||||
assert.strictEqual(vout, 0)
|
assert.strictEqual(vout, 0)
|
||||||
|
|
||||||
const txout = txb.__tx.outs[0]
|
const txout = txb.__TX.outs[0]
|
||||||
assert.deepEqual(txout.script, scripts[0])
|
assert.deepEqual(txout.script, scripts[0])
|
||||||
assert.strictEqual(txout.value, 1000)
|
assert.strictEqual(txout.value, 1000)
|
||||||
})
|
})
|
||||||
|
@ -533,10 +533,10 @@ describe('TransactionBuilder', function () {
|
||||||
'194a565cd6aa4cc38b8eaffa343402201c5b4b61d73fa38e49c1ee68cc0e6dfd2f5dae453dd86eb142e87a' +
|
'194a565cd6aa4cc38b8eaffa343402201c5b4b61d73fa38e49c1ee68cc0e6dfd2f5dae453dd86eb142e87a' +
|
||||||
'0bafb1bc8401210283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f44800000000'
|
'0bafb1bc8401210283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f44800000000'
|
||||||
const txb = TransactionBuilder.fromTransaction(Transaction.fromHex(rawtx))
|
const txb = TransactionBuilder.fromTransaction(Transaction.fromHex(rawtx))
|
||||||
txb.__inputs[0].value = 241530
|
txb.__INPUTS[0].value = 241530
|
||||||
txb.__inputs[1].value = 241530
|
txb.__INPUTS[1].value = 241530
|
||||||
txb.__inputs[2].value = 248920
|
txb.__INPUTS[2].value = 248920
|
||||||
txb.__inputs[3].value = 248920
|
txb.__INPUTS[3].value = 248920
|
||||||
|
|
||||||
assert.throws(function () {
|
assert.throws(function () {
|
||||||
txb.build()
|
txb.build()
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
import { Network } from './networks';
|
import * as baddress from './address';
|
||||||
import * as networks from './networks';
|
|
||||||
import { reverseBuffer } from './bufferutils';
|
import { reverseBuffer } from './bufferutils';
|
||||||
import { Transaction, Output } from './transaction';
|
import * as classify from './classify';
|
||||||
|
import * as bcrypto from './crypto';
|
||||||
import { ECPairInterface } from './ecpair';
|
import { ECPairInterface } from './ecpair';
|
||||||
import * as ECPair from './ecpair';
|
import * as ECPair from './ecpair';
|
||||||
import * as types from './types';
|
import { Network } from './networks';
|
||||||
import * as baddress from './address';
|
import * as networks from './networks';
|
||||||
import * as bcrypto from './crypto';
|
|
||||||
import * as bscript from './script';
|
|
||||||
import { Payment } from './payments';
|
import { Payment } from './payments';
|
||||||
import * as payments from './payments';
|
import * as payments from './payments';
|
||||||
import * as classify from './classify';
|
import * as bscript from './script';
|
||||||
import { OPS as ops } from './script';
|
import { OPS as ops } from './script';
|
||||||
|
import { Output, Transaction } from './transaction';
|
||||||
|
import * as types from './types';
|
||||||
const typeforce = require('typeforce');
|
const typeforce = require('typeforce');
|
||||||
|
|
||||||
const SCRIPT_TYPES = classify.types;
|
const SCRIPT_TYPES = classify.types;
|
||||||
|
|
||||||
type TxbSignatures = Array<Buffer> | Array<Buffer | undefined>;
|
type MaybeBuffer = Buffer | undefined;
|
||||||
type TxbPubkeys = Array<Buffer | undefined>;
|
type TxbSignatures = Buffer[] | MaybeBuffer[];
|
||||||
type TxbWitness = Array<Buffer>;
|
type TxbPubkeys = MaybeBuffer[];
|
||||||
|
type TxbWitness = Buffer[];
|
||||||
type TxbScriptType = string;
|
type TxbScriptType = string;
|
||||||
type TxbScript = Buffer;
|
type TxbScript = Buffer;
|
||||||
|
|
||||||
|
@ -58,24 +59,6 @@ function txIsTransaction(tx: Buffer | string | Transaction): tx is Transaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TransactionBuilder {
|
export class TransactionBuilder {
|
||||||
network: Network;
|
|
||||||
maximumFeeRate: number;
|
|
||||||
private __prevTxSet: { [index: string]: boolean };
|
|
||||||
private __inputs: Array<TxbInput>;
|
|
||||||
private __tx: Transaction;
|
|
||||||
|
|
||||||
constructor(network?: Network, maximumFeeRate?: number) {
|
|
||||||
this.__prevTxSet = {};
|
|
||||||
this.network = network || networks.bitcoin;
|
|
||||||
|
|
||||||
// WARNING: This is __NOT__ to be relied on, its just another potential safety mechanism (safety in-depth)
|
|
||||||
this.maximumFeeRate = maximumFeeRate || 2500;
|
|
||||||
|
|
||||||
this.__inputs = [];
|
|
||||||
this.__tx = new Transaction();
|
|
||||||
this.__tx.version = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromTransaction(
|
static fromTransaction(
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
network?: Network,
|
network?: Network,
|
||||||
|
@ -88,7 +71,7 @@ export class TransactionBuilder {
|
||||||
|
|
||||||
// Copy outputs (done first to avoid signature invalidation)
|
// Copy outputs (done first to avoid signature invalidation)
|
||||||
transaction.outs.forEach(txOut => {
|
transaction.outs.forEach(txOut => {
|
||||||
txb.addOutput(txOut.script, (<Output>txOut).value);
|
txb.addOutput(txOut.script, (txOut as Output).value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy inputs
|
// Copy inputs
|
||||||
|
@ -101,19 +84,37 @@ export class TransactionBuilder {
|
||||||
});
|
});
|
||||||
|
|
||||||
// fix some things not possible through the public API
|
// fix some things not possible through the public API
|
||||||
txb.__inputs.forEach((input, i) => {
|
txb.__INPUTS.forEach((input, i) => {
|
||||||
fixMultisigOrder(input, transaction, i);
|
fixMultisigOrder(input, transaction, i);
|
||||||
});
|
});
|
||||||
|
|
||||||
return txb;
|
return txb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
network: Network;
|
||||||
|
maximumFeeRate: number;
|
||||||
|
private __PREV_TX_SET: { [index: string]: boolean };
|
||||||
|
private __INPUTS: TxbInput[];
|
||||||
|
private __TX: Transaction;
|
||||||
|
|
||||||
|
constructor(network?: Network, maximumFeeRate?: number) {
|
||||||
|
this.__PREV_TX_SET = {};
|
||||||
|
this.network = network || networks.bitcoin;
|
||||||
|
|
||||||
|
// WARNING: This is __NOT__ to be relied on, its just another potential safety mechanism (safety in-depth)
|
||||||
|
this.maximumFeeRate = maximumFeeRate || 2500;
|
||||||
|
|
||||||
|
this.__INPUTS = [];
|
||||||
|
this.__TX = new Transaction();
|
||||||
|
this.__TX.version = 2;
|
||||||
|
}
|
||||||
|
|
||||||
setLockTime(locktime: number): void {
|
setLockTime(locktime: number): void {
|
||||||
typeforce(types.UInt32, locktime);
|
typeforce(types.UInt32, locktime);
|
||||||
|
|
||||||
// if any signatures exist, throw
|
// if any signatures exist, throw
|
||||||
if (
|
if (
|
||||||
this.__inputs.some(input => {
|
this.__INPUTS.some(input => {
|
||||||
if (!input.signatures) return false;
|
if (!input.signatures) return false;
|
||||||
|
|
||||||
return input.signatures.some(s => s !== undefined);
|
return input.signatures.some(s => s !== undefined);
|
||||||
|
@ -122,14 +123,14 @@ export class TransactionBuilder {
|
||||||
throw new Error('No, this would invalidate signatures');
|
throw new Error('No, this would invalidate signatures');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__tx.locktime = locktime;
|
this.__TX.locktime = locktime;
|
||||||
}
|
}
|
||||||
|
|
||||||
setVersion(version: number): void {
|
setVersion(version: number): void {
|
||||||
typeforce(types.UInt32, version);
|
typeforce(types.UInt32, version);
|
||||||
|
|
||||||
// XXX: this might eventually become more complex depending on what the versions represent
|
// XXX: this might eventually become more complex depending on what the versions represent
|
||||||
this.__tx.version = version;
|
this.__TX.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
addInput(
|
addInput(
|
||||||
|
@ -142,7 +143,7 @@ export class TransactionBuilder {
|
||||||
throw new Error('No, this would invalidate signatures');
|
throw new Error('No, this would invalidate signatures');
|
||||||
}
|
}
|
||||||
|
|
||||||
let value: number | undefined = undefined;
|
let value: number | undefined;
|
||||||
|
|
||||||
// is it a hex string?
|
// is it a hex string?
|
||||||
if (txIsString(txHash)) {
|
if (txIsString(txHash)) {
|
||||||
|
@ -153,18 +154,128 @@ export class TransactionBuilder {
|
||||||
} else if (txIsTransaction(txHash)) {
|
} else if (txIsTransaction(txHash)) {
|
||||||
const txOut = txHash.outs[vout];
|
const txOut = txHash.outs[vout];
|
||||||
prevOutScript = txOut.script;
|
prevOutScript = txOut.script;
|
||||||
value = (<Output>txOut).value;
|
value = (txOut as Output).value;
|
||||||
|
|
||||||
txHash = <Buffer>txHash.getHash(false);
|
txHash = txHash.getHash(false) as Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.__addInputUnsafe(txHash, vout, {
|
return this.__addInputUnsafe(txHash, vout, {
|
||||||
sequence: sequence,
|
sequence,
|
||||||
prevOutScript: prevOutScript,
|
prevOutScript,
|
||||||
value: value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOutput(scriptPubKey: string | Buffer, value: number): number {
|
||||||
|
if (!this.__canModifyOutputs()) {
|
||||||
|
throw new Error('No, this would invalidate signatures');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to get a script if it's a base58 or bech32 address string
|
||||||
|
if (typeof scriptPubKey === 'string') {
|
||||||
|
scriptPubKey = baddress.toOutputScript(scriptPubKey, this.network);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.__TX.addOutput(scriptPubKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): Transaction {
|
||||||
|
return this.__build(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildIncomplete(): Transaction {
|
||||||
|
return this.__build(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
sign(
|
||||||
|
vin: number,
|
||||||
|
keyPair: ECPairInterface,
|
||||||
|
redeemScript: Buffer,
|
||||||
|
hashType: number,
|
||||||
|
witnessValue: number,
|
||||||
|
witnessScript: Buffer,
|
||||||
|
) {
|
||||||
|
// TODO: remove keyPair.network matching in 4.0.0
|
||||||
|
if (keyPair.network && keyPair.network !== this.network)
|
||||||
|
throw new TypeError('Inconsistent network');
|
||||||
|
if (!this.__INPUTS[vin]) throw new Error('No input at index: ' + vin);
|
||||||
|
|
||||||
|
hashType = hashType || Transaction.SIGHASH_ALL;
|
||||||
|
if (this.__needsOutputs(hashType))
|
||||||
|
throw new Error('Transaction needs outputs');
|
||||||
|
|
||||||
|
const input = this.__INPUTS[vin];
|
||||||
|
|
||||||
|
// if redeemScript was previously provided, enforce consistency
|
||||||
|
if (
|
||||||
|
input.redeemScript !== undefined &&
|
||||||
|
redeemScript &&
|
||||||
|
!input.redeemScript.equals(redeemScript)
|
||||||
|
) {
|
||||||
|
throw new Error('Inconsistent redeemScript');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ourPubKey = keyPair.publicKey || keyPair.getPublicKey!();
|
||||||
|
if (!canSign(input)) {
|
||||||
|
if (witnessValue !== undefined) {
|
||||||
|
if (input.value !== undefined && input.value !== witnessValue)
|
||||||
|
throw new Error('Input did not match witnessValue');
|
||||||
|
typeforce(types.Satoshi, witnessValue);
|
||||||
|
input.value = witnessValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canSign(input)) {
|
||||||
|
const prepared = prepareInput(
|
||||||
|
input,
|
||||||
|
ourPubKey,
|
||||||
|
redeemScript,
|
||||||
|
witnessScript,
|
||||||
|
);
|
||||||
|
|
||||||
|
// updates inline
|
||||||
|
Object.assign(input, prepared);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canSign(input)) throw Error(input.prevOutType + ' not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ready to sign
|
||||||
|
let signatureHash: Buffer;
|
||||||
|
if (input.hasWitness) {
|
||||||
|
signatureHash = this.__TX.hashForWitnessV0(
|
||||||
|
vin,
|
||||||
|
input.signScript as Buffer,
|
||||||
|
input.value as number,
|
||||||
|
hashType,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
signatureHash = this.__TX.hashForSignature(
|
||||||
|
vin,
|
||||||
|
input.signScript as Buffer,
|
||||||
|
hashType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforce in order signing of public keys
|
||||||
|
const signed = input.pubkeys!.some((pubKey, i) => {
|
||||||
|
if (!ourPubKey.equals(pubKey!)) return false;
|
||||||
|
if (input.signatures![i]) throw new Error('Signature already exists');
|
||||||
|
|
||||||
|
// TODO: add tests
|
||||||
|
if (ourPubKey.length !== 33 && input.hasWitness) {
|
||||||
|
throw new Error(
|
||||||
|
'BIP143 rejects uncompressed public keys in P2WPKH or P2WSH',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = keyPair.sign(signatureHash);
|
||||||
|
input.signatures![i] = bscript.signature.encode(signature, hashType);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!signed) throw new Error('Key pair cannot sign for this input');
|
||||||
|
}
|
||||||
|
|
||||||
private __addInputUnsafe(
|
private __addInputUnsafe(
|
||||||
txHash: Buffer,
|
txHash: Buffer,
|
||||||
vout: number,
|
vout: number,
|
||||||
|
@ -175,10 +286,10 @@ export class TransactionBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevTxOut = txHash.toString('hex') + ':' + vout;
|
const prevTxOut = txHash.toString('hex') + ':' + vout;
|
||||||
if (this.__prevTxSet[prevTxOut] !== undefined)
|
if (this.__PREV_TX_SET[prevTxOut] !== undefined)
|
||||||
throw new Error('Duplicate TxOut: ' + prevTxOut);
|
throw new Error('Duplicate TxOut: ' + prevTxOut);
|
||||||
|
|
||||||
let input = <TxbInput>{};
|
let input: TxbInput = {};
|
||||||
|
|
||||||
// derive what we can from the scriptSig
|
// derive what we can from the scriptSig
|
||||||
if (options.script !== undefined) {
|
if (options.script !== undefined) {
|
||||||
|
@ -208,48 +319,27 @@ export class TransactionBuilder {
|
||||||
input.prevOutType = prevOutType || classify.output(options.prevOutScript);
|
input.prevOutType = prevOutType || classify.output(options.prevOutScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
const vin = this.__tx.addInput(
|
const vin = this.__TX.addInput(
|
||||||
txHash,
|
txHash,
|
||||||
vout,
|
vout,
|
||||||
options.sequence,
|
options.sequence,
|
||||||
options.scriptSig,
|
options.scriptSig,
|
||||||
);
|
);
|
||||||
this.__inputs[vin] = input;
|
this.__INPUTS[vin] = input;
|
||||||
this.__prevTxSet[prevTxOut] = true;
|
this.__PREV_TX_SET[prevTxOut] = true;
|
||||||
return vin;
|
return vin;
|
||||||
}
|
}
|
||||||
|
|
||||||
addOutput(scriptPubKey: string | Buffer, value: number): number {
|
|
||||||
if (!this.__canModifyOutputs()) {
|
|
||||||
throw new Error('No, this would invalidate signatures');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to get a script if it's a base58 or bech32 address string
|
|
||||||
if (typeof scriptPubKey === 'string') {
|
|
||||||
scriptPubKey = baddress.toOutputScript(scriptPubKey, this.network);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.__tx.addOutput(scriptPubKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): Transaction {
|
|
||||||
return this.__build(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildIncomplete(): Transaction {
|
|
||||||
return this.__build(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private __build(allowIncomplete?: boolean): Transaction {
|
private __build(allowIncomplete?: boolean): Transaction {
|
||||||
if (!allowIncomplete) {
|
if (!allowIncomplete) {
|
||||||
if (!this.__tx.ins.length) throw new Error('Transaction has no inputs');
|
if (!this.__TX.ins.length) throw new Error('Transaction has no inputs');
|
||||||
if (!this.__tx.outs.length) throw new Error('Transaction has no outputs');
|
if (!this.__TX.outs.length) throw new Error('Transaction has no outputs');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = this.__tx.clone();
|
const tx = this.__TX.clone();
|
||||||
|
|
||||||
// create script signatures from inputs
|
// create script signatures from inputs
|
||||||
this.__inputs.forEach((input, i) => {
|
this.__INPUTS.forEach((input, i) => {
|
||||||
if (!input.prevOutType && !allowIncomplete)
|
if (!input.prevOutType && !allowIncomplete)
|
||||||
throw new Error('Transaction is not complete');
|
throw new Error('Transaction is not complete');
|
||||||
|
|
||||||
|
@ -275,97 +365,8 @@ export class TransactionBuilder {
|
||||||
return tx;
|
return tx;
|
||||||
}
|
}
|
||||||
|
|
||||||
sign(
|
|
||||||
vin: number,
|
|
||||||
keyPair: ECPairInterface,
|
|
||||||
redeemScript: Buffer,
|
|
||||||
hashType: number,
|
|
||||||
witnessValue: number,
|
|
||||||
witnessScript: Buffer,
|
|
||||||
) {
|
|
||||||
// TODO: remove keyPair.network matching in 4.0.0
|
|
||||||
if (keyPair.network && keyPair.network !== this.network)
|
|
||||||
throw new TypeError('Inconsistent network');
|
|
||||||
if (!this.__inputs[vin]) throw new Error('No input at index: ' + vin);
|
|
||||||
|
|
||||||
hashType = hashType || Transaction.SIGHASH_ALL;
|
|
||||||
if (this.__needsOutputs(hashType))
|
|
||||||
throw new Error('Transaction needs outputs');
|
|
||||||
|
|
||||||
const input = this.__inputs[vin];
|
|
||||||
|
|
||||||
// if redeemScript was previously provided, enforce consistency
|
|
||||||
if (
|
|
||||||
input.redeemScript !== undefined &&
|
|
||||||
redeemScript &&
|
|
||||||
!input.redeemScript.equals(redeemScript)
|
|
||||||
) {
|
|
||||||
throw new Error('Inconsistent redeemScript');
|
|
||||||
}
|
|
||||||
|
|
||||||
const ourPubKey = keyPair.publicKey || keyPair.getPublicKey!();
|
|
||||||
if (!canSign(input)) {
|
|
||||||
if (witnessValue !== undefined) {
|
|
||||||
if (input.value !== undefined && input.value !== witnessValue)
|
|
||||||
throw new Error("Input didn't match witnessValue");
|
|
||||||
typeforce(types.Satoshi, witnessValue);
|
|
||||||
input.value = witnessValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canSign(input)) {
|
|
||||||
const prepared = prepareInput(
|
|
||||||
input,
|
|
||||||
ourPubKey,
|
|
||||||
redeemScript,
|
|
||||||
witnessScript,
|
|
||||||
);
|
|
||||||
|
|
||||||
// updates inline
|
|
||||||
Object.assign(input, prepared);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canSign(input)) throw Error(input.prevOutType + ' not supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ready to sign
|
|
||||||
let signatureHash: Buffer;
|
|
||||||
if (input.hasWitness) {
|
|
||||||
signatureHash = this.__tx.hashForWitnessV0(
|
|
||||||
vin,
|
|
||||||
<Buffer>input.signScript,
|
|
||||||
<number>input.value,
|
|
||||||
hashType,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
signatureHash = this.__tx.hashForSignature(
|
|
||||||
vin,
|
|
||||||
<Buffer>input.signScript,
|
|
||||||
hashType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforce in order signing of public keys
|
|
||||||
const signed = input.pubkeys!.some((pubKey, i) => {
|
|
||||||
if (!ourPubKey.equals(pubKey!)) return false;
|
|
||||||
if (input.signatures![i]) throw new Error('Signature already exists');
|
|
||||||
|
|
||||||
// TODO: add tests
|
|
||||||
if (ourPubKey.length !== 33 && input.hasWitness) {
|
|
||||||
throw new Error(
|
|
||||||
'BIP143 rejects uncompressed public keys in P2WPKH or P2WSH',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const signature = keyPair.sign(signatureHash);
|
|
||||||
input.signatures![i] = bscript.signature.encode(signature, hashType);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!signed) throw new Error('Key pair cannot sign for this input');
|
|
||||||
}
|
|
||||||
|
|
||||||
private __canModifyInputs(): boolean {
|
private __canModifyInputs(): boolean {
|
||||||
return this.__inputs.every(input => {
|
return this.__INPUTS.every(input => {
|
||||||
if (!input.signatures) return true;
|
if (!input.signatures) return true;
|
||||||
|
|
||||||
return input.signatures.every(signature => {
|
return input.signatures.every(signature => {
|
||||||
|
@ -381,14 +382,14 @@ export class TransactionBuilder {
|
||||||
|
|
||||||
private __needsOutputs(signingHashType: number): boolean {
|
private __needsOutputs(signingHashType: number): boolean {
|
||||||
if (signingHashType === Transaction.SIGHASH_ALL) {
|
if (signingHashType === Transaction.SIGHASH_ALL) {
|
||||||
return this.__tx.outs.length === 0;
|
return this.__TX.outs.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs
|
// if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs
|
||||||
// .build() will fail, but .buildIncomplete() is OK
|
// .build() will fail, but .buildIncomplete() is OK
|
||||||
return (
|
return (
|
||||||
this.__tx.outs.length === 0 &&
|
this.__TX.outs.length === 0 &&
|
||||||
this.__inputs.some(input => {
|
this.__INPUTS.some(input => {
|
||||||
if (!input.signatures) return false;
|
if (!input.signatures) return false;
|
||||||
|
|
||||||
return input.signatures.some(signature => {
|
return input.signatures.some(signature => {
|
||||||
|
@ -402,10 +403,10 @@ export class TransactionBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private __canModifyOutputs(): boolean {
|
private __canModifyOutputs(): boolean {
|
||||||
const nInputs = this.__tx.ins.length;
|
const nInputs = this.__TX.ins.length;
|
||||||
const nOutputs = this.__tx.outs.length;
|
const nOutputs = this.__TX.outs.length;
|
||||||
|
|
||||||
return this.__inputs.every(input => {
|
return this.__INPUTS.every(input => {
|
||||||
if (input.signatures === undefined) return true;
|
if (input.signatures === undefined) return true;
|
||||||
|
|
||||||
return input.signatures.every(signature => {
|
return input.signatures.every(signature => {
|
||||||
|
@ -427,11 +428,14 @@ export class TransactionBuilder {
|
||||||
|
|
||||||
private __overMaximumFees(bytes: number): boolean {
|
private __overMaximumFees(bytes: number): boolean {
|
||||||
// not all inputs will have .value defined
|
// not all inputs will have .value defined
|
||||||
const incoming = this.__inputs.reduce((a, x) => a + (x.value! >>> 0), 0);
|
const incoming = this.__INPUTS.reduce((a, x) => a + (x.value! >>> 0), 0);
|
||||||
|
|
||||||
// but all outputs do, and if we have any input value
|
// but all outputs do, and if we have any input value
|
||||||
// we can immediately determine if the outputs are too small
|
// we can immediately determine if the outputs are too small
|
||||||
const outgoing = this.__tx.outs.reduce((a, x) => a + (<Output>x).value, 0);
|
const outgoing = this.__TX.outs.reduce(
|
||||||
|
(a, x) => a + (x as Output).value,
|
||||||
|
0,
|
||||||
|
);
|
||||||
const fee = incoming - outgoing;
|
const fee = incoming - outgoing;
|
||||||
const feeRate = fee / bytes;
|
const feeRate = fee / bytes;
|
||||||
|
|
||||||
|
@ -441,7 +445,7 @@ export class TransactionBuilder {
|
||||||
|
|
||||||
function expandInput(
|
function expandInput(
|
||||||
scriptSig: Buffer,
|
scriptSig: Buffer,
|
||||||
witnessStack: Array<Buffer>,
|
witnessStack: Buffer[],
|
||||||
type?: string,
|
type?: string,
|
||||||
scriptPubKey?: Buffer,
|
scriptPubKey?: Buffer,
|
||||||
): TxbInput {
|
): TxbInput {
|
||||||
|
@ -502,8 +506,8 @@ function expandInput(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prevOutType: SCRIPT_TYPES.P2MS,
|
prevOutType: SCRIPT_TYPES.P2MS,
|
||||||
pubkeys: pubkeys,
|
pubkeys,
|
||||||
signatures: signatures,
|
signatures,
|
||||||
maxSignatures: m,
|
maxSignatures: m,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -681,12 +685,12 @@ function prepareInput(
|
||||||
witnessScript: Buffer,
|
witnessScript: Buffer,
|
||||||
): TxbInput {
|
): TxbInput {
|
||||||
if (redeemScript && witnessScript) {
|
if (redeemScript && witnessScript) {
|
||||||
const p2wsh = <Payment>(
|
const p2wsh = payments.p2wsh({
|
||||||
payments.p2wsh({ redeem: { output: witnessScript } })
|
redeem: { output: witnessScript },
|
||||||
);
|
}) as Payment;
|
||||||
const p2wshAlt = <Payment>payments.p2wsh({ output: redeemScript });
|
const p2wshAlt = payments.p2wsh({ output: redeemScript }) as Payment;
|
||||||
const p2sh = <Payment>payments.p2sh({ redeem: { output: redeemScript } });
|
const p2sh = payments.p2sh({ redeem: { output: redeemScript } }) as Payment;
|
||||||
const p2shAlt = <Payment>payments.p2sh({ redeem: p2wsh });
|
const p2shAlt = payments.p2sh({ redeem: p2wsh }) as Payment;
|
||||||
|
|
||||||
// enforces P2SH(P2WSH(...))
|
// enforces P2SH(P2WSH(...))
|
||||||
if (!p2wsh.hash!.equals(p2wshAlt.hash!))
|
if (!p2wsh.hash!.equals(p2wshAlt.hash!))
|
||||||
|
@ -706,7 +710,7 @@ function prepareInput(
|
||||||
expanded.signatures = input.signatures;
|
expanded.signatures = input.signatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
let signScript = witnessScript;
|
const signScript = witnessScript;
|
||||||
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
||||||
throw new Error('P2SH(P2WSH(P2WPKH)) is a consensus failure');
|
throw new Error('P2SH(P2WSH(P2WPKH)) is a consensus failure');
|
||||||
|
|
||||||
|
@ -731,12 +735,12 @@ function prepareInput(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redeemScript) {
|
if (redeemScript) {
|
||||||
const p2sh = <Payment>payments.p2sh({ redeem: { output: redeemScript } });
|
const p2sh = payments.p2sh({ redeem: { output: redeemScript } }) as Payment;
|
||||||
|
|
||||||
if (input.prevOutScript) {
|
if (input.prevOutScript) {
|
||||||
let p2shAlt;
|
let p2shAlt;
|
||||||
try {
|
try {
|
||||||
p2shAlt = <Payment>payments.p2sh({ output: input.prevOutScript });
|
p2shAlt = payments.p2sh({ output: input.prevOutScript }) as Payment;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('PrevOutScript must be P2SH');
|
throw new Error('PrevOutScript must be P2SH');
|
||||||
}
|
}
|
||||||
|
@ -799,7 +803,7 @@ function prepareInput(
|
||||||
expanded.signatures = input.signatures;
|
expanded.signatures = input.signatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
let signScript = witnessScript;
|
const signScript = witnessScript;
|
||||||
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
if (expanded.type === SCRIPT_TYPES.P2WPKH)
|
||||||
throw new Error('P2WSH(P2WPKH) is a consensus failure');
|
throw new Error('P2WSH(P2WPKH) is a consensus failure');
|
||||||
|
|
||||||
|
@ -846,9 +850,8 @@ function prepareInput(
|
||||||
|
|
||||||
let signScript = input.prevOutScript;
|
let signScript = input.prevOutScript;
|
||||||
if (expanded.type === SCRIPT_TYPES.P2WPKH) {
|
if (expanded.type === SCRIPT_TYPES.P2WPKH) {
|
||||||
signScript = <Buffer>(
|
signScript = payments.p2pkh({ pubkey: expanded.pubkeys[0] })
|
||||||
payments.p2pkh({ pubkey: expanded.pubkeys[0] }).output
|
.output as Buffer;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -868,7 +871,7 @@ function prepareInput(
|
||||||
const prevOutScript = payments.p2pkh({ pubkey: ourPubKey }).output;
|
const prevOutScript = payments.p2pkh({ pubkey: ourPubKey }).output;
|
||||||
return {
|
return {
|
||||||
prevOutType: SCRIPT_TYPES.P2PKH,
|
prevOutType: SCRIPT_TYPES.P2PKH,
|
||||||
prevOutScript: prevOutScript,
|
prevOutScript,
|
||||||
|
|
||||||
hasWitness: false,
|
hasWitness: false,
|
||||||
signScript: prevOutScript,
|
signScript: prevOutScript,
|
||||||
|
@ -884,8 +887,8 @@ function build(
|
||||||
input: TxbInput,
|
input: TxbInput,
|
||||||
allowIncomplete?: boolean,
|
allowIncomplete?: boolean,
|
||||||
): Payment | undefined {
|
): Payment | undefined {
|
||||||
const pubkeys = <Array<Buffer>>(input.pubkeys || []);
|
const pubkeys = (input.pubkeys || []) as Buffer[];
|
||||||
let signatures = <Array<Buffer>>(input.signatures || []);
|
let signatures = (input.signatures || []) as Buffer[];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SCRIPT_TYPES.P2PKH: {
|
case SCRIPT_TYPES.P2PKH: {
|
||||||
|
|
14
types/transaction_builder.d.ts
vendored
14
types/transaction_builder.d.ts
vendored
|
@ -1,24 +1,24 @@
|
||||||
/// <reference types="node" />
|
/// <reference types="node" />
|
||||||
|
import { ECPairInterface } from './ecpair';
|
||||||
import { Network } from './networks';
|
import { Network } from './networks';
|
||||||
import { Transaction } from './transaction';
|
import { Transaction } from './transaction';
|
||||||
import { ECPairInterface } from './ecpair';
|
|
||||||
export declare class TransactionBuilder {
|
export declare class TransactionBuilder {
|
||||||
|
static fromTransaction(transaction: Transaction, network?: Network): TransactionBuilder;
|
||||||
network: Network;
|
network: Network;
|
||||||
maximumFeeRate: number;
|
maximumFeeRate: number;
|
||||||
private __prevTxSet;
|
private __PREV_TX_SET;
|
||||||
private __inputs;
|
private __INPUTS;
|
||||||
private __tx;
|
private __TX;
|
||||||
constructor(network?: Network, maximumFeeRate?: number);
|
constructor(network?: Network, maximumFeeRate?: number);
|
||||||
static fromTransaction(transaction: Transaction, network?: Network): TransactionBuilder;
|
|
||||||
setLockTime(locktime: number): void;
|
setLockTime(locktime: number): void;
|
||||||
setVersion(version: number): void;
|
setVersion(version: number): void;
|
||||||
addInput(txHash: Buffer | string | Transaction, vout: number, sequence: number, prevOutScript: Buffer): number;
|
addInput(txHash: Buffer | string | Transaction, vout: number, sequence: number, prevOutScript: Buffer): number;
|
||||||
private __addInputUnsafe;
|
|
||||||
addOutput(scriptPubKey: string | Buffer, value: number): number;
|
addOutput(scriptPubKey: string | Buffer, value: number): number;
|
||||||
build(): Transaction;
|
build(): Transaction;
|
||||||
buildIncomplete(): Transaction;
|
buildIncomplete(): Transaction;
|
||||||
private __build;
|
|
||||||
sign(vin: number, keyPair: ECPairInterface, redeemScript: Buffer, hashType: number, witnessValue: number, witnessScript: Buffer): void;
|
sign(vin: number, keyPair: ECPairInterface, redeemScript: Buffer, hashType: number, witnessValue: number, witnessScript: Buffer): void;
|
||||||
|
private __addInputUnsafe;
|
||||||
|
private __build;
|
||||||
private __canModifyInputs;
|
private __canModifyInputs;
|
||||||
private __needsOutputs;
|
private __needsOutputs;
|
||||||
private __canModifyOutputs;
|
private __canModifyOutputs;
|
||||||
|
|
Loading…
Reference in a new issue