|
|
@ -1,5 +1,5 @@ |
|
|
|
import { Psbt as PsbtBase } from 'bip174'; |
|
|
|
import { PsbtInput } from 'bip174/src/lib/interfaces'; |
|
|
|
import { PartialSig, PsbtInput } from 'bip174/src/lib/interfaces'; |
|
|
|
import { checkForInput } from 'bip174/src/lib/utils'; |
|
|
|
import { hash160 } from './crypto'; |
|
|
|
import { Signer } from './ecpair'; |
|
|
@ -7,6 +7,7 @@ import { Network } from './networks'; |
|
|
|
import * as payments from './payments'; |
|
|
|
import * as bscript from './script'; |
|
|
|
import { Transaction } from './transaction'; |
|
|
|
const varuint = require('varuint-bitcoin'); |
|
|
|
|
|
|
|
export class Psbt extends PsbtBase { |
|
|
|
// protected __TX: Transaction;
|
|
|
@ -25,41 +26,84 @@ export class Psbt extends PsbtBase { |
|
|
|
// });
|
|
|
|
} |
|
|
|
|
|
|
|
canFinalize(inputIndex: number): boolean { |
|
|
|
extractTransaction(): Transaction { |
|
|
|
if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); |
|
|
|
const tx = Transaction.fromBuffer(this.globalMap.unsignedTx!); |
|
|
|
this.inputs.forEach((input, idx) => { |
|
|
|
if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; |
|
|
|
if (input.finalScriptWitness) { |
|
|
|
const decompiled = bscript.decompile(input.finalScriptWitness); |
|
|
|
if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled); |
|
|
|
} |
|
|
|
}); |
|
|
|
return tx; |
|
|
|
} |
|
|
|
|
|
|
|
finalizeAllInputs(): { |
|
|
|
result: boolean; |
|
|
|
inputResults: boolean[]; |
|
|
|
} { |
|
|
|
const inputResults = range(this.inputs.length).map(idx => |
|
|
|
this.finalizeInput(idx), |
|
|
|
); |
|
|
|
const result = inputResults.every(val => val === true); |
|
|
|
return { |
|
|
|
result, |
|
|
|
inputResults, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
finalizeInput(inputIndex: number): boolean { |
|
|
|
const input = checkForInput(this.inputs, inputIndex); |
|
|
|
const script = getScriptFromInput( |
|
|
|
const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( |
|
|
|
inputIndex, |
|
|
|
input, |
|
|
|
this.globalMap.unsignedTx!, |
|
|
|
); |
|
|
|
if (!script) return false; |
|
|
|
|
|
|
|
const scriptType = classifyScript(script); |
|
|
|
if (!canFinalize(input, script, scriptType)) return false; |
|
|
|
|
|
|
|
const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { |
|
|
|
if (!partialSig) return false; |
|
|
|
if (partialSig.length > neededSigs) |
|
|
|
throw new Error('Too many signatures'); |
|
|
|
return partialSig.length === neededSigs; |
|
|
|
}; |
|
|
|
let finalScriptSig: Buffer | undefined; |
|
|
|
let finalScriptWitness: Buffer | undefined; |
|
|
|
|
|
|
|
// Wow, the payments API is very handy
|
|
|
|
const payment: payments.Payment = getPayment( |
|
|
|
script, |
|
|
|
scriptType, |
|
|
|
input.partialSig!, |
|
|
|
); |
|
|
|
const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); |
|
|
|
const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); |
|
|
|
|
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkey': |
|
|
|
return hasSigs(1, input.partialSig); |
|
|
|
case 'pubkeyhash': |
|
|
|
return hasSigs(1, input.partialSig); |
|
|
|
case 'multisig': |
|
|
|
const p2ms = payments.p2ms({ output: script }); |
|
|
|
return hasSigs(p2ms.m!, input.partialSig); |
|
|
|
case 'witnesspubkeyhash': |
|
|
|
return hasSigs(1, input.partialSig); |
|
|
|
default: |
|
|
|
return false; |
|
|
|
if (isSegwit) { |
|
|
|
if (p2wsh) { |
|
|
|
finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); |
|
|
|
} else { |
|
|
|
finalScriptWitness = witnessStackToScriptWitness(payment.witness!); |
|
|
|
} |
|
|
|
if (p2sh) { |
|
|
|
finalScriptSig = bscript.compile([p2sh.redeem!.output!]); |
|
|
|
} |
|
|
|
} else { |
|
|
|
finalScriptSig = payment.input; |
|
|
|
} |
|
|
|
|
|
|
|
if (finalScriptSig) |
|
|
|
this.addFinalScriptSigToInput(inputIndex, finalScriptSig); |
|
|
|
if (finalScriptWitness) |
|
|
|
this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); |
|
|
|
if (!finalScriptSig && !finalScriptWitness) return false; |
|
|
|
|
|
|
|
this.clearFinalizedInput(inputIndex); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
signInput(inputIndex: number, keyPair: Signer): Psbt { |
|
|
|
const input = this.inputs[inputIndex]; |
|
|
|
if (input === undefined) throw new Error(`No input #${inputIndex}`); |
|
|
|
const input = checkForInput(this.inputs, inputIndex); |
|
|
|
if (!keyPair || !keyPair.publicKey) |
|
|
|
throw new Error('Need Signer to sign input'); |
|
|
|
const { hash, sighashType, script } = getHashForSig( |
|
|
|
inputIndex, |
|
|
|
input, |
|
|
@ -67,21 +111,8 @@ export class Psbt extends PsbtBase { |
|
|
|
); |
|
|
|
|
|
|
|
const pubkey = keyPair.publicKey; |
|
|
|
const pubkeyHash = hash160(keyPair.publicKey); |
|
|
|
|
|
|
|
const decompiled = bscript.decompile(script); |
|
|
|
if (decompiled === null) throw new Error('Unknown script error'); |
|
|
|
|
|
|
|
const hasKey = decompiled.some(element => { |
|
|
|
if (typeof element === 'number') return false; |
|
|
|
return element.equals(pubkey) || element.equals(pubkeyHash); |
|
|
|
}); |
|
|
|
|
|
|
|
if (!hasKey) { |
|
|
|
throw new Error( |
|
|
|
`Can not sign for this input with the key ${pubkey.toString('hex')}`, |
|
|
|
); |
|
|
|
} |
|
|
|
checkScriptForPubkey(pubkey, script); |
|
|
|
|
|
|
|
const partialSig = { |
|
|
|
pubkey, |
|
|
@ -92,6 +123,93 @@ export class Psbt extends PsbtBase { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Helper functions
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
|
|
|
|
function isFinalized(input: PsbtInput): boolean { |
|
|
|
return !!input.finalScriptSig || !!input.finalScriptWitness; |
|
|
|
} |
|
|
|
|
|
|
|
function getPayment( |
|
|
|
script: Buffer, |
|
|
|
scriptType: string, |
|
|
|
partialSig: PartialSig[], |
|
|
|
): payments.Payment { |
|
|
|
let payment: payments.Payment; |
|
|
|
switch (scriptType) { |
|
|
|
case 'multisig': |
|
|
|
payment = payments.p2ms({ |
|
|
|
output: script, |
|
|
|
signatures: partialSig.map(ps => ps.signature), |
|
|
|
}); |
|
|
|
break; |
|
|
|
case 'pubkey': |
|
|
|
payment = payments.p2pk({ |
|
|
|
output: script, |
|
|
|
signature: partialSig[0].signature, |
|
|
|
}); |
|
|
|
break; |
|
|
|
case 'pubkeyhash': |
|
|
|
payment = payments.p2pkh({ |
|
|
|
output: script, |
|
|
|
pubkey: partialSig[0].pubkey, |
|
|
|
signature: partialSig[0].signature, |
|
|
|
}); |
|
|
|
break; |
|
|
|
case 'witnesspubkeyhash': |
|
|
|
payment = payments.p2wpkh({ |
|
|
|
output: script, |
|
|
|
pubkey: partialSig[0].pubkey, |
|
|
|
signature: partialSig[0].signature, |
|
|
|
}); |
|
|
|
break; |
|
|
|
} |
|
|
|
return payment!; |
|
|
|
} |
|
|
|
|
|
|
|
function canFinalize( |
|
|
|
input: PsbtInput, |
|
|
|
script: Buffer, |
|
|
|
scriptType: string, |
|
|
|
): boolean { |
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkey': |
|
|
|
case 'pubkeyhash': |
|
|
|
case 'witnesspubkeyhash': |
|
|
|
return hasSigs(1, input.partialSig); |
|
|
|
case 'multisig': |
|
|
|
const p2ms = payments.p2ms({ output: script }); |
|
|
|
return hasSigs(p2ms.m!, input.partialSig); |
|
|
|
default: |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void { |
|
|
|
const pubkeyHash = hash160(pubkey); |
|
|
|
|
|
|
|
const decompiled = bscript.decompile(script); |
|
|
|
if (decompiled === null) throw new Error('Unknown script error'); |
|
|
|
|
|
|
|
const hasKey = decompiled.some(element => { |
|
|
|
if (typeof element === 'number') return false; |
|
|
|
return element.equals(pubkey) || element.equals(pubkeyHash); |
|
|
|
}); |
|
|
|
|
|
|
|
if (!hasKey) { |
|
|
|
throw new Error( |
|
|
|
`Can not sign for this input with the key ${pubkey.toString('hex')}`, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
interface HashForSigData { |
|
|
|
script: Buffer; |
|
|
|
hash: Buffer; |
|
|
@ -239,32 +357,86 @@ const classifyScript = (script: Buffer): string => { |
|
|
|
return 'nonstandard'; |
|
|
|
}; |
|
|
|
|
|
|
|
interface GetScriptReturn { |
|
|
|
script: Buffer | null; |
|
|
|
isSegwit: boolean; |
|
|
|
isP2SH: boolean; |
|
|
|
isP2WSH: boolean; |
|
|
|
} |
|
|
|
function getScriptFromInput( |
|
|
|
inputIndex: number, |
|
|
|
input: PsbtInput, |
|
|
|
_unsignedTx: Buffer, |
|
|
|
): Buffer | undefined { |
|
|
|
let script: Buffer; |
|
|
|
): GetScriptReturn { |
|
|
|
const res: GetScriptReturn = { |
|
|
|
script: null, |
|
|
|
isSegwit: false, |
|
|
|
isP2SH: false, |
|
|
|
isP2WSH: false, |
|
|
|
}; |
|
|
|
if (input.nonWitnessUtxo) { |
|
|
|
if (input.redeemScript) { |
|
|
|
script = input.redeemScript; |
|
|
|
res.isP2SH = true; |
|
|
|
res.script = input.redeemScript; |
|
|
|
} else { |
|
|
|
const unsignedTx = Transaction.fromBuffer(_unsignedTx); |
|
|
|
const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); |
|
|
|
const prevoutIndex = unsignedTx.ins[inputIndex].index; |
|
|
|
script = nonWitnessUtxoTx.outs[prevoutIndex].script; |
|
|
|
res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; |
|
|
|
} |
|
|
|
} else if (input.witnessUtxo) { |
|
|
|
res.isSegwit = true; |
|
|
|
res.isP2SH = !!input.redeemScript; |
|
|
|
res.isP2WSH = !!input.witnessScript; |
|
|
|
if (input.witnessScript) { |
|
|
|
script = input.witnessScript; |
|
|
|
res.script = input.witnessScript; |
|
|
|
} else if (input.redeemScript) { |
|
|
|
script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output!; |
|
|
|
res.script = payments.p2pkh({ |
|
|
|
hash: input.redeemScript.slice(2), |
|
|
|
}).output!; |
|
|
|
} else { |
|
|
|
script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) |
|
|
|
.output!; |
|
|
|
res.script = payments.p2pkh({ |
|
|
|
hash: input.witnessUtxo.script.slice(2), |
|
|
|
}).output!; |
|
|
|
} |
|
|
|
} else { |
|
|
|
return; |
|
|
|
} |
|
|
|
return script; |
|
|
|
return res; |
|
|
|
} |
|
|
|
|
|
|
|
const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { |
|
|
|
if (!partialSig) return false; |
|
|
|
if (partialSig.length > neededSigs) throw new Error('Too many signatures'); |
|
|
|
return partialSig.length === neededSigs; |
|
|
|
}; |
|
|
|
|
|
|
|
function witnessStackToScriptWitness(witness: Buffer[]): Buffer { |
|
|
|
let buffer = Buffer.allocUnsafe(0); |
|
|
|
|
|
|
|
function writeSlice(slice: Buffer): void { |
|
|
|
buffer = Buffer.concat([buffer, Buffer.from(slice)]); |
|
|
|
} |
|
|
|
|
|
|
|
function writeVarInt(i: number): void { |
|
|
|
const currentLen = buffer.length; |
|
|
|
const varintLen = varuint.encodingLength(i); |
|
|
|
|
|
|
|
buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); |
|
|
|
varuint.encode(i, buffer, currentLen); |
|
|
|
} |
|
|
|
|
|
|
|
function writeVarSlice(slice: Buffer): void { |
|
|
|
writeVarInt(slice.length); |
|
|
|
writeSlice(slice); |
|
|
|
} |
|
|
|
|
|
|
|
function writeVector(vector: Buffer[]): void { |
|
|
|
writeVarInt(vector.length); |
|
|
|
vector.forEach(writeVarSlice); |
|
|
|
} |
|
|
|
|
|
|
|
writeVector(witness); |
|
|
|
|
|
|
|
return buffer; |
|
|
|
} |
|
|
|
|
|
|
|
const range = (n: number): number[] => [...Array(n).keys()]; |
|
|
|