Add TransactionBuilder
This commit is contained in:
parent
58a6b0e545
commit
fce08352f5
4 changed files with 395 additions and 348 deletions
|
@ -11,13 +11,24 @@ const isOptions = typeforce.maybe(typeforce.compile({
|
|||
network: types.maybe(types.Network)
|
||||
}))
|
||||
|
||||
export interface ECPairOptions {
|
||||
interface ECPairOptions {
|
||||
compressed?: boolean
|
||||
network?: Network
|
||||
rng?(Buffer): Buffer
|
||||
}
|
||||
|
||||
class ECPair {
|
||||
export interface ECPairInterface {
|
||||
compressed: boolean
|
||||
network: Network
|
||||
privateKey: Buffer
|
||||
publicKey: Buffer
|
||||
toWIF(): string
|
||||
sign(hash: Buffer): Buffer
|
||||
verify(hash: Buffer, signature: Buffer): Buffer
|
||||
getPublicKey?(): Buffer
|
||||
}
|
||||
|
||||
class ECPair implements ECPairInterface {
|
||||
compressed: boolean
|
||||
network: Network
|
||||
private __d: Buffer
|
||||
|
|
|
@ -3,7 +3,7 @@ const opcodes = require('bitcoin-ops')
|
|||
import { Block } from './block'
|
||||
import * as ECPair from './ecpair'
|
||||
import { Transaction } from './transaction'
|
||||
import * as TransactionBuilder from './transaction_builder'
|
||||
import { TransactionBuilder } from './transaction_builder'
|
||||
import * as address from './address'
|
||||
import * as bip32 from 'bip32'
|
||||
import * as crypto from './crypto'
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Network } from './networks'
|
||||
import * as networks from './networks'
|
||||
import { Transaction, Output } from './transaction'
|
||||
|
||||
import { ECPairInterface } from './ecpair'
|
||||
const Buffer = require('safe-buffer').Buffer
|
||||
const baddress = require('./address')
|
||||
const bcrypto = require('./crypto')
|
||||
const bscript = require('./script')
|
||||
const networks = require('./networks')
|
||||
const ops = require('bitcoin-ops')
|
||||
const payments = require('./payments')
|
||||
const typeforce = require('typeforce')
|
||||
|
@ -14,7 +15,370 @@ const SCRIPT_TYPES = classify.types
|
|||
|
||||
const ECPair = require('./ecpair')
|
||||
|
||||
function expandInput (scriptSig, witnessStack, type, scriptPubKey) {
|
||||
interface TxbInput {
|
||||
value?: number
|
||||
hasWitness?: boolean
|
||||
signScript?: Buffer
|
||||
signType?: string
|
||||
prevOutScript?: Buffer
|
||||
redeemScript?: Buffer
|
||||
redeemScriptType?: string
|
||||
prevOutType?: string
|
||||
pubkeys?: Array<Buffer>
|
||||
signatures?: Array<Buffer>
|
||||
witness?: Array<Buffer>
|
||||
witnessScript?: Buffer
|
||||
witnessScriptType?: string
|
||||
script?: Buffer
|
||||
sequence?: number
|
||||
scriptSig?: Buffer
|
||||
maxSignatures?: number
|
||||
}
|
||||
|
||||
interface TxbOutput {
|
||||
type: string
|
||||
pubkeys?: Array<Buffer>
|
||||
signatures?: Array<Buffer>
|
||||
maxSignatures?: number
|
||||
}
|
||||
|
||||
function txIsString(tx: Buffer | string | Transaction): tx is string {
|
||||
return typeof (<string>tx) === 'string' || tx instanceof String
|
||||
}
|
||||
|
||||
function txIsTransaction(tx: Buffer | string | Transaction): tx is Transaction {
|
||||
return (<Transaction>tx) instanceof Transaction
|
||||
}
|
||||
|
||||
export class TransactionBuilder {
|
||||
network: Network
|
||||
maximumFeeRate: number
|
||||
private __prevTxSet: Object
|
||||
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 (transaction: Transaction, network?: Network): TransactionBuilder {
|
||||
const txb = new TransactionBuilder(network)
|
||||
|
||||
// Copy transaction fields
|
||||
txb.setVersion(transaction.version)
|
||||
txb.setLockTime(transaction.locktime)
|
||||
|
||||
// Copy outputs (done first to avoid signature invalidation)
|
||||
transaction.outs.forEach(txOut => {
|
||||
txb.addOutput(txOut.script, (<Output>txOut).value)
|
||||
})
|
||||
|
||||
// Copy inputs
|
||||
transaction.ins.forEach(txIn => {
|
||||
txb.__addInputUnsafe(txIn.hash, txIn.index, {
|
||||
sequence: txIn.sequence,
|
||||
script: txIn.script,
|
||||
witness: txIn.witness
|
||||
})
|
||||
})
|
||||
|
||||
// fix some things not possible through the public API
|
||||
txb.__inputs.forEach((input, i) => {
|
||||
fixMultisigOrder(input, transaction, i)
|
||||
})
|
||||
|
||||
return txb
|
||||
}
|
||||
|
||||
setLockTime (locktime: number): void {
|
||||
typeforce(types.UInt32, locktime)
|
||||
|
||||
// if any signatures exist, throw
|
||||
if (this.__inputs.some(input => {
|
||||
if (!input.signatures) return false
|
||||
|
||||
return input.signatures.some(s => s !== undefined)
|
||||
})) {
|
||||
throw new Error('No, this would invalidate signatures')
|
||||
}
|
||||
|
||||
this.__tx.locktime = locktime
|
||||
}
|
||||
|
||||
setVersion (version: number): void {
|
||||
typeforce(types.UInt32, version)
|
||||
|
||||
// XXX: this might eventually become more complex depending on what the versions represent
|
||||
this.__tx.version = version
|
||||
}
|
||||
|
||||
addInput (txHash: Buffer | string | Transaction, vout: number, sequence: number, prevOutScript: Buffer): number {
|
||||
if (!this.__canModifyInputs()) {
|
||||
throw new Error('No, this would invalidate signatures')
|
||||
}
|
||||
|
||||
let value: number
|
||||
|
||||
// is it a hex string?
|
||||
if (txIsString(txHash)) {
|
||||
// transaction hashs's are displayed in reverse order, un-reverse it
|
||||
txHash = <Buffer> Buffer.from(txHash, 'hex').reverse()
|
||||
|
||||
// is it a Transaction object?
|
||||
} else if (txIsTransaction(txHash)) {
|
||||
const txOut = txHash.outs[vout]
|
||||
prevOutScript = txOut.script
|
||||
value = (<Output>txOut).value
|
||||
|
||||
txHash = <Buffer> txHash.getHash(false)
|
||||
}
|
||||
|
||||
return this.__addInputUnsafe(txHash, vout, {
|
||||
sequence: sequence,
|
||||
prevOutScript: prevOutScript,
|
||||
value: value
|
||||
})
|
||||
}
|
||||
|
||||
private __addInputUnsafe (txHash: Buffer, vout: number, options: TxbInput): number {
|
||||
if (Transaction.isCoinbaseHash(txHash)) {
|
||||
throw new Error('coinbase inputs not supported')
|
||||
}
|
||||
|
||||
const prevTxOut = txHash.toString('hex') + ':' + vout
|
||||
if (this.__prevTxSet[prevTxOut] !== undefined) throw new Error('Duplicate TxOut: ' + prevTxOut)
|
||||
|
||||
let input = <TxbInput>{}
|
||||
|
||||
// derive what we can from the scriptSig
|
||||
if (options.script !== undefined) {
|
||||
input = expandInput(options.script, options.witness || [])
|
||||
}
|
||||
|
||||
// if an input value was given, retain it
|
||||
if (options.value !== undefined) {
|
||||
input.value = options.value
|
||||
}
|
||||
|
||||
// derive what we can from the previous transactions output script
|
||||
if (!input.prevOutScript && options.prevOutScript) {
|
||||
let prevOutType
|
||||
|
||||
if (!input.pubkeys && !input.signatures) {
|
||||
const expanded = expandOutput(options.prevOutScript)
|
||||
if (expanded.pubkeys) {
|
||||
input.pubkeys = expanded.pubkeys
|
||||
input.signatures = expanded.signatures
|
||||
}
|
||||
|
||||
prevOutType = expanded.type
|
||||
}
|
||||
|
||||
input.prevOutScript = options.prevOutScript
|
||||
input.prevOutType = prevOutType || classify.output(options.prevOutScript)
|
||||
}
|
||||
|
||||
const vin = this.__tx.addInput(txHash, vout, options.sequence, options.scriptSig)
|
||||
this.__inputs[vin] = input
|
||||
this.__prevTxSet[prevTxOut] = true
|
||||
return vin
|
||||
}
|
||||
|
||||
addOutput (scriptPubKey: string | Buffer, value): 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(<Buffer>scriptPubKey, value)
|
||||
}
|
||||
|
||||
build (): Transaction {
|
||||
return this.__build(false)
|
||||
}
|
||||
|
||||
buildIncomplete (): Transaction {
|
||||
return this.__build(true)
|
||||
}
|
||||
|
||||
private __build (allowIncomplete?: boolean): Transaction {
|
||||
if (!allowIncomplete) {
|
||||
if (!this.__tx.ins.length) throw new Error('Transaction has no inputs')
|
||||
if (!this.__tx.outs.length) throw new Error('Transaction has no outputs')
|
||||
}
|
||||
|
||||
const tx = this.__tx.clone()
|
||||
|
||||
// create script signatures from inputs
|
||||
this.__inputs.forEach((input, i) => {
|
||||
if (!input.prevOutType && !allowIncomplete) throw new Error('Transaction is not complete')
|
||||
|
||||
const result = build(input.prevOutType, input, allowIncomplete)
|
||||
if (!result) {
|
||||
if (!allowIncomplete && input.prevOutType === SCRIPT_TYPES.NONSTANDARD) throw new Error('Unknown input type')
|
||||
if (!allowIncomplete) throw new Error('Not enough information')
|
||||
return
|
||||
}
|
||||
|
||||
tx.setInputScript(i, result.input)
|
||||
tx.setWitness(i, result.witness)
|
||||
})
|
||||
|
||||
if (!allowIncomplete) {
|
||||
// do not rely on this, its merely a last resort
|
||||
if (this.__overMaximumFees(tx.virtualSize())) {
|
||||
throw new Error('Transaction has absurd fees')
|
||||
}
|
||||
}
|
||||
|
||||
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, witnessValue, 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')
|
||||
}
|
||||
|
||||
private __canModifyInputs (): boolean {
|
||||
return this.__inputs.every(input => {
|
||||
if (!input.signatures) return true
|
||||
|
||||
return input.signatures.every(signature => {
|
||||
if (!signature) return true
|
||||
const hashType = signatureHashType(signature)
|
||||
|
||||
// if SIGHASH_ANYONECANPAY is set, signatures would not
|
||||
// be invalidated by more inputs
|
||||
return (hashType & Transaction.SIGHASH_ANYONECANPAY) !== 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private __needsOutputs (signingHashType: number): boolean {
|
||||
if (signingHashType === Transaction.SIGHASH_ALL) {
|
||||
return this.__tx.outs.length === 0
|
||||
}
|
||||
|
||||
// if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs
|
||||
// .build() will fail, but .buildIncomplete() is OK
|
||||
return (this.__tx.outs.length === 0) && this.__inputs.some((input) => {
|
||||
if (!input.signatures) return false
|
||||
|
||||
return input.signatures.some((signature) => {
|
||||
if (!signature) return false // no signature, no issue
|
||||
const hashType = signatureHashType(signature)
|
||||
if (hashType & Transaction.SIGHASH_NONE) return false // SIGHASH_NONE doesn't care about outputs
|
||||
return true // SIGHASH_* does care
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private __canModifyOutputs (): boolean {
|
||||
const nInputs = this.__tx.ins.length
|
||||
const nOutputs = this.__tx.outs.length
|
||||
|
||||
return this.__inputs.every(input => {
|
||||
if (input.signatures === undefined) return true
|
||||
|
||||
return input.signatures.every(signature => {
|
||||
if (!signature) return true
|
||||
const hashType = signatureHashType(signature)
|
||||
|
||||
const hashTypeMod = hashType & 0x1f
|
||||
if (hashTypeMod === Transaction.SIGHASH_NONE) return true
|
||||
if (hashTypeMod === Transaction.SIGHASH_SINGLE) {
|
||||
// if SIGHASH_SINGLE is set, and nInputs > nOutputs
|
||||
// some signatures would be invalidated by the addition
|
||||
// of more outputs
|
||||
return nInputs <= nOutputs
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private __overMaximumFees (bytes: number): boolean {
|
||||
// not all inputs will have .value defined
|
||||
const incoming = this.__inputs.reduce((a, x) => a + (x.value >>> 0), 0)
|
||||
|
||||
// but all outputs do, and if we have any input value
|
||||
// we can immediately determine if the outputs are too small
|
||||
const outgoing = this.__tx.outs.reduce((a, x) => a + (<Output>x).value, 0)
|
||||
const fee = incoming - outgoing
|
||||
const feeRate = fee / bytes
|
||||
|
||||
return feeRate > this.maximumFeeRate
|
||||
}
|
||||
}
|
||||
|
||||
function expandInput (scriptSig: Buffer, witnessStack: Array<Buffer>, type?: string, scriptPubKey?: Buffer): TxbInput {
|
||||
if (scriptSig.length === 0 && witnessStack.length === 0) return {}
|
||||
if (!type) {
|
||||
let ssType = classify.input(scriptSig, true)
|
||||
|
@ -103,7 +467,7 @@ function expandInput (scriptSig, witnessStack, type, scriptPubKey) {
|
|||
const outputType = classify.output(redeem.output)
|
||||
let expanded
|
||||
if (outputType === SCRIPT_TYPES.P2WPKH) {
|
||||
expanded = expandInput(redeem.input, redeem.witness, outputType, undefined)
|
||||
expanded = expandInput(redeem.input, redeem.witness, outputType)
|
||||
} else {
|
||||
expanded = expandInput(bscript.compile(redeem.witness), [], outputType, redeem.output)
|
||||
}
|
||||
|
@ -127,18 +491,18 @@ function expandInput (scriptSig, witnessStack, type, scriptPubKey) {
|
|||
}
|
||||
|
||||
// could be done in expandInput, but requires the original Transaction for hashForSignature
|
||||
function fixMultisigOrder (input, transaction, vin) {
|
||||
function fixMultisigOrder (input: TxbInput, transaction: Transaction, vin: number): void {
|
||||
if (input.redeemScriptType !== SCRIPT_TYPES.P2MS || !input.redeemScript) return
|
||||
if (input.pubkeys.length === input.signatures.length) return
|
||||
|
||||
const unmatched = input.signatures.concat()
|
||||
|
||||
input.signatures = input.pubkeys.map(function (pubKey) {
|
||||
input.signatures = input.pubkeys.map(pubKey => {
|
||||
const keyPair = ECPair.fromPublicKey(pubKey)
|
||||
let match
|
||||
|
||||
// check for a signature
|
||||
unmatched.some(function (signature, i) {
|
||||
unmatched.some((signature, i) => {
|
||||
// skip if undefined || OP_0
|
||||
if (!signature) return false
|
||||
|
||||
|
@ -160,7 +524,7 @@ function fixMultisigOrder (input, transaction, vin) {
|
|||
})
|
||||
}
|
||||
|
||||
function expandOutput (script, ourPubKey) {
|
||||
function expandOutput (script: Buffer, ourPubKey?: Buffer): TxbOutput {
|
||||
typeforce(types.Buffer, script)
|
||||
const type = classify.output(script)
|
||||
|
||||
|
@ -218,7 +582,7 @@ function expandOutput (script, ourPubKey) {
|
|||
return { type }
|
||||
}
|
||||
|
||||
function prepareInput (input, ourPubKey, redeemScript, witnessValue, witnessScript) {
|
||||
function prepareInput (input: TxbInput, ourPubKey: Buffer, redeemScript: Buffer, witnessValue: number, witnessScript: Buffer): TxbInput {
|
||||
if (redeemScript && witnessScript) {
|
||||
const p2wsh = payments.p2wsh({ redeem: { output: witnessScript } })
|
||||
const p2wshAlt = payments.p2wsh({ output: redeemScript })
|
||||
|
@ -231,7 +595,7 @@ function prepareInput (input, ourPubKey, redeemScript, witnessValue, witnessScri
|
|||
|
||||
const expanded = expandOutput(p2wsh.redeem.output, ourPubKey)
|
||||
if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported as witnessScript (' + bscript.toASM(witnessScript) + ')')
|
||||
if (input.signatures && input.signatures.some(x => x)) {
|
||||
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
||||
expanded.signatures = input.signatures
|
||||
}
|
||||
|
||||
|
@ -271,7 +635,7 @@ function prepareInput (input, ourPubKey, redeemScript, witnessValue, witnessScri
|
|||
|
||||
const expanded = expandOutput(p2sh.redeem.output, ourPubKey)
|
||||
if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported as redeemScript (' + bscript.toASM(redeemScript) + ')')
|
||||
if (input.signatures && input.signatures.some(x => x)) {
|
||||
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
||||
expanded.signatures = input.signatures
|
||||
}
|
||||
|
||||
|
@ -307,7 +671,7 @@ function prepareInput (input, ourPubKey, redeemScript, witnessValue, witnessScri
|
|||
|
||||
const expanded = expandOutput(p2wsh.redeem.output, ourPubKey)
|
||||
if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported as witnessScript (' + bscript.toASM(witnessScript) + ')')
|
||||
if (input.signatures && input.signatures.some(x => x)) {
|
||||
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
||||
expanded.signatures = input.signatures
|
||||
}
|
||||
|
||||
|
@ -339,7 +703,7 @@ function prepareInput (input, ourPubKey, redeemScript, witnessValue, witnessScri
|
|||
|
||||
const expanded = expandOutput(input.prevOutScript, ourPubKey)
|
||||
if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported (' + bscript.toASM(input.prevOutScript) + ')')
|
||||
if (input.signatures && input.signatures.some(x => x)) {
|
||||
if (input.signatures && input.signatures.some(x => x !== undefined)) {
|
||||
expanded.signatures = input.signatures
|
||||
}
|
||||
|
||||
|
@ -376,7 +740,7 @@ function prepareInput (input, ourPubKey, redeemScript, witnessValue, witnessScri
|
|||
}
|
||||
}
|
||||
|
||||
function build (type, input, allowIncomplete) {
|
||||
function build (type: string, input: TxbInput, allowIncomplete: boolean): any { //TODO payment type
|
||||
const pubkeys = input.pubkeys || []
|
||||
let signatures = input.signatures || []
|
||||
|
||||
|
@ -439,201 +803,7 @@ function build (type, input, allowIncomplete) {
|
|||
}
|
||||
}
|
||||
|
||||
function TransactionBuilder (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()
|
||||
this.__tx.version = 2
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.setLockTime = function (locktime) {
|
||||
typeforce(types.UInt32, locktime)
|
||||
|
||||
// if any signatures exist, throw
|
||||
if (this.__inputs.some(function (input) {
|
||||
if (!input.signatures) return false
|
||||
|
||||
return input.signatures.some(function (s) { return s })
|
||||
})) {
|
||||
throw new Error('No, this would invalidate signatures')
|
||||
}
|
||||
|
||||
this.__tx.locktime = locktime
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.setVersion = function (version) {
|
||||
typeforce(types.UInt32, version)
|
||||
|
||||
// XXX: this might eventually become more complex depending on what the versions represent
|
||||
this.__tx.version = version
|
||||
}
|
||||
|
||||
TransactionBuilder.fromTransaction = function (transaction, network) {
|
||||
const txb = new TransactionBuilder(network, undefined)
|
||||
|
||||
// Copy transaction fields
|
||||
txb.setVersion(transaction.version)
|
||||
txb.setLockTime(transaction.locktime)
|
||||
|
||||
// Copy outputs (done first to avoid signature invalidation)
|
||||
transaction.outs.forEach(function (txOut) {
|
||||
txb.addOutput(txOut.script, txOut.value)
|
||||
})
|
||||
|
||||
// Copy inputs
|
||||
transaction.ins.forEach(function (txIn) {
|
||||
txb.__addInputUnsafe(txIn.hash, txIn.index, {
|
||||
sequence: txIn.sequence,
|
||||
script: txIn.script,
|
||||
witness: txIn.witness
|
||||
})
|
||||
})
|
||||
|
||||
// fix some things not possible through the public API
|
||||
txb.__inputs.forEach(function (input, i) {
|
||||
fixMultisigOrder(input, transaction, i)
|
||||
})
|
||||
|
||||
return txb
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.addInput = function (txHash, vout, sequence, prevOutScript) {
|
||||
if (!this.__canModifyInputs()) {
|
||||
throw new Error('No, this would invalidate signatures')
|
||||
}
|
||||
|
||||
let value
|
||||
|
||||
// is it a hex string?
|
||||
if (typeof txHash === 'string') {
|
||||
// transaction hashs's are displayed in reverse order, un-reverse it
|
||||
txHash = Buffer.from(txHash, 'hex').reverse()
|
||||
|
||||
// is it a Transaction object?
|
||||
} else if (txHash instanceof Transaction) {
|
||||
const txOut = txHash.outs[vout]
|
||||
prevOutScript = txOut.script
|
||||
value = (<Output>txOut).value
|
||||
|
||||
txHash = txHash.getHash(false)
|
||||
}
|
||||
|
||||
return this.__addInputUnsafe(txHash, vout, {
|
||||
sequence: sequence,
|
||||
prevOutScript: prevOutScript,
|
||||
value: value
|
||||
})
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, options) {
|
||||
if (Transaction.isCoinbaseHash(txHash)) {
|
||||
throw new Error('coinbase inputs not supported')
|
||||
}
|
||||
|
||||
const prevTxOut = txHash.toString('hex') + ':' + vout
|
||||
if (this.__prevTxSet[prevTxOut] !== undefined) throw new Error('Duplicate TxOut: ' + prevTxOut)
|
||||
|
||||
let input = {
|
||||
value: undefined,
|
||||
prevOutScript: undefined,
|
||||
pubkeys: undefined,
|
||||
signatures: undefined,
|
||||
prevOutType: undefined,
|
||||
}
|
||||
|
||||
// derive what we can from the scriptSig
|
||||
if (options.script !== undefined) {
|
||||
input = expandInput(options.script, options.witness || [], undefined, undefined)
|
||||
}
|
||||
|
||||
// if an input value was given, retain it
|
||||
if (options.value !== undefined) {
|
||||
input.value = options.value
|
||||
}
|
||||
|
||||
// derive what we can from the previous transactions output script
|
||||
if (!input.prevOutScript && options.prevOutScript) {
|
||||
let prevOutType
|
||||
|
||||
if (!input.pubkeys && !input.signatures) {
|
||||
const expanded = expandOutput(options.prevOutScript, undefined)
|
||||
if (expanded.pubkeys) {
|
||||
input.pubkeys = expanded.pubkeys
|
||||
input.signatures = expanded.signatures
|
||||
}
|
||||
|
||||
prevOutType = expanded.type
|
||||
}
|
||||
|
||||
input.prevOutScript = options.prevOutScript
|
||||
input.prevOutType = prevOutType || classify.output(options.prevOutScript)
|
||||
}
|
||||
|
||||
const vin = this.__tx.addInput(txHash, vout, options.sequence, options.scriptSig)
|
||||
this.__inputs[vin] = input
|
||||
this.__prevTxSet[prevTxOut] = true
|
||||
return vin
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.addOutput = function (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)
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.build = function () {
|
||||
return this.__build(false)
|
||||
}
|
||||
TransactionBuilder.prototype.buildIncomplete = function () {
|
||||
return this.__build(true)
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.__build = function (allowIncomplete) {
|
||||
if (!allowIncomplete) {
|
||||
if (!this.__tx.ins.length) throw new Error('Transaction has no inputs')
|
||||
if (!this.__tx.outs.length) throw new Error('Transaction has no outputs')
|
||||
}
|
||||
|
||||
const tx = this.__tx.clone()
|
||||
|
||||
// create script signatures from inputs
|
||||
this.__inputs.forEach(function (input, i) {
|
||||
if (!input.prevOutType && !allowIncomplete) throw new Error('Transaction is not complete')
|
||||
|
||||
const result = build(input.prevOutType, input, allowIncomplete)
|
||||
if (!result) {
|
||||
if (!allowIncomplete && input.prevOutType === SCRIPT_TYPES.NONSTANDARD) throw new Error('Unknown input type')
|
||||
if (!allowIncomplete) throw new Error('Not enough information')
|
||||
return
|
||||
}
|
||||
|
||||
tx.setInputScript(i, result.input)
|
||||
tx.setWitness(i, result.witness)
|
||||
})
|
||||
|
||||
if (!allowIncomplete) {
|
||||
// do not rely on this, its merely a last resort
|
||||
if (this.__overMaximumFees(tx.virtualSize())) {
|
||||
throw new Error('Transaction has absurd fees')
|
||||
}
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
function canSign (input) {
|
||||
function canSign (input: TxbInput): boolean {
|
||||
return input.signScript !== undefined &&
|
||||
input.signType !== undefined &&
|
||||
input.pubkeys !== undefined &&
|
||||
|
@ -646,140 +816,6 @@ function canSign (input) {
|
|||
)
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.sign = function (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.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, witnessValue, 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(function (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')
|
||||
}
|
||||
|
||||
function signatureHashType (buffer) {
|
||||
function signatureHashType (buffer: Buffer): number {
|
||||
return buffer.readUInt8(buffer.length - 1)
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.__canModifyInputs = function () {
|
||||
return (this.__inputs || []).every(function (input) {
|
||||
if (!input.signatures) return true
|
||||
|
||||
return (input.signatures || []).every(function (signature) {
|
||||
if (!signature) return true
|
||||
const hashType = signatureHashType(signature)
|
||||
|
||||
// if SIGHASH_ANYONECANPAY is set, signatures would not
|
||||
// be invalidated by more inputs
|
||||
return hashType & Transaction.SIGHASH_ANYONECANPAY
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.__needsOutputs = function (signingHashType) {
|
||||
if (signingHashType === Transaction.SIGHASH_ALL) {
|
||||
return this.__tx.outs.length === 0
|
||||
}
|
||||
|
||||
// if inputs are being signed with SIGHASH_NONE, we don't strictly need outputs
|
||||
// .build() will fail, but .buildIncomplete() is OK
|
||||
return (this.__tx.outs.length === 0) && this.__inputs.some((input) => {
|
||||
if (!input.signatures) return false
|
||||
|
||||
return input.signatures.some((signature) => {
|
||||
if (!signature) return false // no signature, no issue
|
||||
const hashType = signatureHashType(signature)
|
||||
if (hashType & Transaction.SIGHASH_NONE) return false // SIGHASH_NONE doesn't care about outputs
|
||||
return true // SIGHASH_* does care
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.__canModifyOutputs = function () {
|
||||
const nInputs = this.__tx.ins.length
|
||||
const nOutputs = this.__tx.outs.length
|
||||
|
||||
return this.__inputs.every(function (input) {
|
||||
if (input.signatures === undefined) return true
|
||||
|
||||
return input.signatures.every(function (signature) {
|
||||
if (!signature) return true
|
||||
const hashType = signatureHashType(signature)
|
||||
|
||||
const hashTypeMod = hashType & 0x1f
|
||||
if (hashTypeMod === Transaction.SIGHASH_NONE) return true
|
||||
if (hashTypeMod === Transaction.SIGHASH_SINGLE) {
|
||||
// if SIGHASH_SINGLE is set, and nInputs > nOutputs
|
||||
// some signatures would be invalidated by the addition
|
||||
// of more outputs
|
||||
return nInputs <= nOutputs
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
TransactionBuilder.prototype.__overMaximumFees = function (bytes) {
|
||||
// not all inputs will have .value defined
|
||||
const incoming = this.__inputs.reduce(function (a, x) { return a + (x.value >>> 0) }, 0)
|
||||
|
||||
// but all outputs do, and if we have any input value
|
||||
// we can immediately determine if the outputs are too small
|
||||
const outgoing = this.__tx.outs.reduce(function (a, x) { return a + x.value }, 0)
|
||||
const fee = incoming - outgoing
|
||||
const feeRate = fee / bytes
|
||||
|
||||
return feeRate > this.maximumFeeRate
|
||||
}
|
||||
|
||||
module.exports = TransactionBuilder
|
||||
export {}
|
||||
|
|
|
@ -6,7 +6,7 @@ const payments = require('../dist/src/payments')
|
|||
|
||||
const ECPair = require('../dist/src/ecpair')
|
||||
const Transaction = require('..').Transaction
|
||||
const TransactionBuilder = require('../dist/src/transaction_builder')
|
||||
const TransactionBuilder = require('..').TransactionBuilder
|
||||
const NETWORKS = require('../dist/src/networks')
|
||||
|
||||
const fixtures = require('./fixtures/transaction_builder')
|
||||
|
|
Loading…
Add table
Reference in a new issue