Fix lint transaction_builder.ts

This commit is contained in:
junderw 2019-03-07 14:03:04 +09:00
parent e6ea0389a2
commit 512b03e284
No known key found for this signature in database
GPG key ID: B256185D3A971908
5 changed files with 336 additions and 330 deletions

View file

@ -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,

View file

@ -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": [
{ {

View file

@ -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()

View file

@ -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: {

View file

@ -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;