|
|
@ -2,7 +2,7 @@ import * as baddress from './address'; |
|
|
|
import { reverseBuffer } from './bufferutils'; |
|
|
|
import * as classify from './classify'; |
|
|
|
import * as bcrypto from './crypto'; |
|
|
|
import { ECPairInterface } from './ecpair'; |
|
|
|
import { Signer } from './ecpair'; |
|
|
|
import * as ECPair from './ecpair'; |
|
|
|
import { Network } from './networks'; |
|
|
|
import * as networks from './networks'; |
|
|
@ -16,6 +16,27 @@ const typeforce = require('typeforce'); |
|
|
|
|
|
|
|
const SCRIPT_TYPES = classify.types; |
|
|
|
|
|
|
|
const PREVOUT_TYPES: Set<string> = new Set([ |
|
|
|
// Raw
|
|
|
|
'p2pkh', |
|
|
|
'p2pk', |
|
|
|
'p2wpkh', |
|
|
|
'p2ms', |
|
|
|
// P2SH wrapped
|
|
|
|
'p2sh-p2pkh', |
|
|
|
'p2sh-p2pk', |
|
|
|
'p2sh-p2wpkh', |
|
|
|
'p2sh-p2ms', |
|
|
|
// P2WSH wrapped
|
|
|
|
'p2wsh-p2pkh', |
|
|
|
'p2wsh-p2pk', |
|
|
|
'p2wsh-p2ms', |
|
|
|
// P2SH-P2WSH wrapper
|
|
|
|
'p2sh-p2wsh-p2pkh', |
|
|
|
'p2sh-p2wsh-p2pk', |
|
|
|
'p2sh-p2wsh-p2ms', |
|
|
|
]); |
|
|
|
|
|
|
|
type MaybeBuffer = Buffer | undefined; |
|
|
|
type TxbSignatures = Buffer[] | MaybeBuffer[]; |
|
|
|
type TxbPubkeys = MaybeBuffer[]; |
|
|
@ -50,6 +71,24 @@ interface TxbOutput { |
|
|
|
maxSignatures?: number; |
|
|
|
} |
|
|
|
|
|
|
|
interface TxbSignArg { |
|
|
|
prevOutScriptType: string; |
|
|
|
vin: number; |
|
|
|
keyPair: Signer; |
|
|
|
redeemScript?: Buffer; |
|
|
|
hashType?: number; |
|
|
|
witnessValue?: number; |
|
|
|
witnessScript?: Buffer; |
|
|
|
} |
|
|
|
|
|
|
|
function tfMessage(type: any, value: any, message: string): void { |
|
|
|
try { |
|
|
|
typeforce(type, value); |
|
|
|
} catch (err) { |
|
|
|
throw new Error(message); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function txIsString(tx: Buffer | string | Transaction): tx is string { |
|
|
|
return typeof tx === 'string' || tx instanceof String; |
|
|
|
} |
|
|
@ -197,92 +236,28 @@ export class TransactionBuilder { |
|
|
|
} |
|
|
|
|
|
|
|
sign( |
|
|
|
vin: number, |
|
|
|
keyPair: ECPairInterface, |
|
|
|
signParams: number | TxbSignArg, |
|
|
|
keyPair?: Signer, |
|
|
|
redeemScript?: Buffer, |
|
|
|
hashType?: number, |
|
|
|
witnessValue?: number, |
|
|
|
witnessScript?: Buffer, |
|
|
|
): void { |
|
|
|
// 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, |
|
|
|
trySign( |
|
|
|
getSigningData( |
|
|
|
this.network, |
|
|
|
this.__INPUTS, |
|
|
|
this.__needsOutputs.bind(this), |
|
|
|
this.__TX, |
|
|
|
signParams, |
|
|
|
keyPair, |
|
|
|
redeemScript, |
|
|
|
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, this.__USE_LOW_R); |
|
|
|
input.signatures![i] = bscript.signature.encode(signature, hashType!); |
|
|
|
return true; |
|
|
|
}); |
|
|
|
|
|
|
|
if (!signed) throw new Error('Key pair cannot sign for this input'); |
|
|
|
witnessValue, |
|
|
|
witnessScript, |
|
|
|
this.__USE_LOW_R, |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
private __addInputUnsafe( |
|
|
@ -976,3 +951,361 @@ function canSign(input: TxbInput): boolean { |
|
|
|
function signatureHashType(buffer: Buffer): number { |
|
|
|
return buffer.readUInt8(buffer.length - 1); |
|
|
|
} |
|
|
|
|
|
|
|
function checkSignArgs(inputs: TxbInput[], signParams: TxbSignArg): void { |
|
|
|
if (!PREVOUT_TYPES.has(signParams.prevOutScriptType)) { |
|
|
|
throw new TypeError( |
|
|
|
`Unknown prevOutScriptType "${signParams.prevOutScriptType}"`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.Number, |
|
|
|
signParams.vin, |
|
|
|
`sign must include vin parameter as Number (input index)`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
types.Signer, |
|
|
|
signParams.keyPair, |
|
|
|
`sign must include keyPair parameter as Signer interface`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.maybe(typeforce.Number), |
|
|
|
signParams.hashType, |
|
|
|
`sign hashType parameter must be a number`, |
|
|
|
); |
|
|
|
const prevOutType = (inputs[signParams.vin] || []).prevOutType; |
|
|
|
const posType = signParams.prevOutScriptType; |
|
|
|
switch (posType) { |
|
|
|
case 'p2pkh': |
|
|
|
if (prevOutType && prevOutType !== 'pubkeyhash') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type p2pkh: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires NO witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires NO redeemScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires NO witnessValue`, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 'p2pk': |
|
|
|
if (prevOutType && prevOutType !== 'pubkey') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type p2pk: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires NO witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires NO redeemScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires NO witnessValue`, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 'p2wpkh': |
|
|
|
if (prevOutType && prevOutType !== 'witnesspubkeyhash') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type p2wpkh: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires NO witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires NO redeemScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
types.Satoshi, |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires witnessValue`, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 'p2ms': |
|
|
|
if (prevOutType && prevOutType !== 'multisig') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type p2ms: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires NO witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires NO redeemScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires NO witnessValue`, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 'p2sh-p2wpkh': |
|
|
|
if (prevOutType && prevOutType !== 'scripthash') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type p2sh-p2wpkh: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires NO witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.Buffer, |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires redeemScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
types.Satoshi, |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires witnessValue`, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 'p2sh-p2ms': |
|
|
|
case 'p2sh-p2pk': |
|
|
|
case 'p2sh-p2pkh': |
|
|
|
if (prevOutType && prevOutType !== 'scripthash') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires NO witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.Buffer, |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires redeemScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires NO witnessValue`, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 'p2wsh-p2ms': |
|
|
|
case 'p2wsh-p2pk': |
|
|
|
case 'p2wsh-p2pkh': |
|
|
|
if (prevOutType && prevOutType !== 'witnessscripthash') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.Buffer, |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.value(undefined), |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires NO redeemScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
types.Satoshi, |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires witnessValue`, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 'p2sh-p2wsh-p2ms': |
|
|
|
case 'p2sh-p2wsh-p2pk': |
|
|
|
case 'p2sh-p2wsh-p2pkh': |
|
|
|
if (prevOutType && prevOutType !== 'scripthash') { |
|
|
|
throw new TypeError( |
|
|
|
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`, |
|
|
|
); |
|
|
|
} |
|
|
|
tfMessage( |
|
|
|
typeforce.Buffer, |
|
|
|
signParams.witnessScript, |
|
|
|
`${posType} requires witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
typeforce.Buffer, |
|
|
|
signParams.redeemScript, |
|
|
|
`${posType} requires witnessScript`, |
|
|
|
); |
|
|
|
tfMessage( |
|
|
|
types.Satoshi, |
|
|
|
signParams.witnessValue, |
|
|
|
`${posType} requires witnessScript`, |
|
|
|
); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function trySign({ |
|
|
|
input, |
|
|
|
ourPubKey, |
|
|
|
keyPair, |
|
|
|
signatureHash, |
|
|
|
hashType, |
|
|
|
useLowR, |
|
|
|
}: SigningData): void { |
|
|
|
// enforce in order signing of public keys
|
|
|
|
let signed = false; |
|
|
|
for (const [i, pubKey] of input.pubkeys!.entries()) { |
|
|
|
if (!ourPubKey.equals(pubKey!)) continue; |
|
|
|
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, useLowR); |
|
|
|
input.signatures![i] = bscript.signature.encode(signature, hashType); |
|
|
|
signed = true; |
|
|
|
} |
|
|
|
|
|
|
|
if (!signed) throw new Error('Key pair cannot sign for this input'); |
|
|
|
} |
|
|
|
|
|
|
|
interface SigningData { |
|
|
|
input: TxbInput; |
|
|
|
ourPubKey: Buffer; |
|
|
|
keyPair: Signer; |
|
|
|
signatureHash: Buffer; |
|
|
|
hashType: number; |
|
|
|
useLowR: boolean; |
|
|
|
} |
|
|
|
|
|
|
|
type HashTypeCheck = (hashType: number) => boolean; |
|
|
|
|
|
|
|
function getSigningData( |
|
|
|
network: Network, |
|
|
|
inputs: TxbInput[], |
|
|
|
needsOutputs: HashTypeCheck, |
|
|
|
tx: Transaction, |
|
|
|
signParams: number | TxbSignArg, |
|
|
|
keyPair?: Signer, |
|
|
|
redeemScript?: Buffer, |
|
|
|
hashType?: number, |
|
|
|
witnessValue?: number, |
|
|
|
witnessScript?: Buffer, |
|
|
|
useLowR?: boolean, |
|
|
|
): SigningData { |
|
|
|
let vin: number; |
|
|
|
if (typeof signParams === 'number') { |
|
|
|
console.warn( |
|
|
|
'DEPRECATED: TransactionBuilder sign method arguments ' + |
|
|
|
'will change in v6, please use the TxbSignArg interface', |
|
|
|
); |
|
|
|
vin = signParams; |
|
|
|
} else if (typeof signParams === 'object') { |
|
|
|
checkSignArgs(inputs, signParams); |
|
|
|
({ |
|
|
|
vin, |
|
|
|
keyPair, |
|
|
|
redeemScript, |
|
|
|
hashType, |
|
|
|
witnessValue, |
|
|
|
witnessScript, |
|
|
|
} = signParams); |
|
|
|
} else { |
|
|
|
throw new TypeError( |
|
|
|
'TransactionBuilder sign first arg must be TxbSignArg or number', |
|
|
|
); |
|
|
|
} |
|
|
|
if (keyPair === undefined) { |
|
|
|
throw new Error('sign requires keypair'); |
|
|
|
} |
|
|
|
// TODO: remove keyPair.network matching in 4.0.0
|
|
|
|
if (keyPair.network && keyPair.network !== network) |
|
|
|
throw new TypeError('Inconsistent network'); |
|
|
|
if (!inputs[vin]) throw new Error('No input at index: ' + vin); |
|
|
|
|
|
|
|
hashType = hashType || Transaction.SIGHASH_ALL; |
|
|
|
if (needsOutputs(hashType)) throw new Error('Transaction needs outputs'); |
|
|
|
|
|
|
|
const input = 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 && 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 = tx.hashForWitnessV0( |
|
|
|
vin, |
|
|
|
input.signScript as Buffer, |
|
|
|
input.value as number, |
|
|
|
hashType, |
|
|
|
); |
|
|
|
} else { |
|
|
|
signatureHash = tx.hashForSignature( |
|
|
|
vin, |
|
|
|
input.signScript as Buffer, |
|
|
|
hashType, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
input, |
|
|
|
ourPubKey, |
|
|
|
keyPair, |
|
|
|
signatureHash, |
|
|
|
hashType, |
|
|
|
useLowR: !!useLowR, |
|
|
|
}; |
|
|
|
} |
|
|
|