|
|
@ -1,13 +1,174 @@ |
|
|
|
import { Psbt as PsbtBase } from 'bip174'; |
|
|
|
import { PsbtInput } from 'bip174/src/lib/interfaces'; |
|
|
|
import { checkForInput } from 'bip174/src/lib/utils'; |
|
|
|
import * as classify from './classify'; |
|
|
|
// import { hash160 } from './crypto'; // TODO: used in pubkey check
|
|
|
|
import { Signer } from './ecpair'; |
|
|
|
import { Network } from './networks'; |
|
|
|
import * as payments from './payments'; |
|
|
|
import * as bscript from './script'; |
|
|
|
import { Transaction } from './transaction'; |
|
|
|
|
|
|
|
export class Psbt extends PsbtBase { |
|
|
|
constructor(public network?: Network) { |
|
|
|
super(); |
|
|
|
} |
|
|
|
|
|
|
|
canFinalize(inputIndex: number): boolean { |
|
|
|
const input = checkForInput(this.inputs, inputIndex); |
|
|
|
const script = getScriptFromInput( |
|
|
|
inputIndex, |
|
|
|
input, |
|
|
|
this.globalMap.unsignedTx!, |
|
|
|
); |
|
|
|
if (!script) return false; |
|
|
|
const scriptType = classifyScript(script); |
|
|
|
// TODO: for each type
|
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkey': |
|
|
|
return false; |
|
|
|
case 'pubkeyhash': |
|
|
|
return false; |
|
|
|
case 'multisig': |
|
|
|
return false; |
|
|
|
case 'witnesspubkeyhash': |
|
|
|
return false; |
|
|
|
default: |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
signInput(inputIndex: number, keyPair: Signer): Psbt { |
|
|
|
const input = this.inputs[inputIndex]; |
|
|
|
if (input === undefined) throw new Error(`No input #${inputIndex}`); |
|
|
|
const { |
|
|
|
hash, |
|
|
|
sighashType, |
|
|
|
// script, // TODO: use for pubkey check below
|
|
|
|
} = getHashForSig(inputIndex, input, this.globalMap.unsignedTx!); |
|
|
|
|
|
|
|
const pubkey = keyPair.publicKey; |
|
|
|
// // TODO: throw error when the pubkey or pubkey hash is not found anywhere
|
|
|
|
// // in the script
|
|
|
|
// 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')}`,
|
|
|
|
// );
|
|
|
|
// }
|
|
|
|
|
|
|
|
const partialSig = { |
|
|
|
pubkey, |
|
|
|
signature: bscript.signature.encode(keyPair.sign(hash), sighashType), |
|
|
|
}; |
|
|
|
|
|
|
|
return this.addPartialSigToInput(inputIndex, partialSig); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
interface HashForSigData { |
|
|
|
script: Buffer; |
|
|
|
hash: Buffer; |
|
|
|
sighashType: number; |
|
|
|
} |
|
|
|
|
|
|
|
const getHashForSig = ( |
|
|
|
inputIndex: number, |
|
|
|
input: PsbtInput, |
|
|
|
txBuf: Buffer, |
|
|
|
): HashForSigData => { |
|
|
|
const unsignedTx = Transaction.fromBuffer(txBuf); |
|
|
|
const sighashType = input.sighashType || Transaction.SIGHASH_ALL; |
|
|
|
let hash: Buffer; |
|
|
|
let script: Buffer; |
|
|
|
|
|
|
|
if (input.nonWitnessUtxo) { |
|
|
|
const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); |
|
|
|
|
|
|
|
const prevoutHash = unsignedTx.ins[inputIndex].hash; |
|
|
|
const utxoHash = nonWitnessUtxoTx.getHash(); |
|
|
|
|
|
|
|
// If a non-witness UTXO is provided, its hash must match the hash specified in the prevout
|
|
|
|
if (!prevoutHash.equals(utxoHash)) { |
|
|
|
throw new Error( |
|
|
|
`Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const prevoutIndex = unsignedTx.ins[inputIndex].index; |
|
|
|
const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; |
|
|
|
|
|
|
|
if (input.redeemScript) { |
|
|
|
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
|
|
checkRedeemScript(inputIndex, prevout.script, input.redeemScript); |
|
|
|
script = input.redeemScript; |
|
|
|
hash = unsignedTx.hashForSignature( |
|
|
|
inputIndex, |
|
|
|
input.redeemScript, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
} else { |
|
|
|
script = prevout.script; |
|
|
|
hash = unsignedTx.hashForSignature( |
|
|
|
inputIndex, |
|
|
|
prevout.script, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
} |
|
|
|
} else if (input.witnessUtxo) { |
|
|
|
let _script: Buffer; // so we don't shadow the `let script` above
|
|
|
|
if (input.redeemScript) { |
|
|
|
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
|
|
checkRedeemScript( |
|
|
|
inputIndex, |
|
|
|
input.witnessUtxo.script, |
|
|
|
input.redeemScript, |
|
|
|
); |
|
|
|
_script = input.redeemScript; |
|
|
|
} else { |
|
|
|
_script = input.witnessUtxo.script; |
|
|
|
} |
|
|
|
if (isP2WPKH(_script)) { |
|
|
|
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
|
|
|
const signingScript = payments.p2pkh({ hash: _script.slice(2) }).output!; |
|
|
|
hash = unsignedTx.hashForWitnessV0( |
|
|
|
inputIndex, |
|
|
|
signingScript, |
|
|
|
input.witnessUtxo.value, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
script = _script; |
|
|
|
} else { |
|
|
|
if (!input.witnessScript) |
|
|
|
throw new Error('Segwit input needs witnessScript if not P2WPKH'); |
|
|
|
checkWitnessScript(inputIndex, _script, input.witnessScript); |
|
|
|
hash = unsignedTx.hashForWitnessV0( |
|
|
|
inputIndex, |
|
|
|
_script, |
|
|
|
input.witnessUtxo.value, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
// want to make sure the script we return is the actual meaningful script
|
|
|
|
script = input.witnessScript; |
|
|
|
} |
|
|
|
} else { |
|
|
|
throw new Error('Need a Utxo input item for signing'); |
|
|
|
} |
|
|
|
return { |
|
|
|
script, |
|
|
|
sighashType, |
|
|
|
hash, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
type ScriptCheckerFunction = (idx: number, spk: Buffer, rs: Buffer) => void; |
|
|
|
|
|
|
|
const scriptCheckerFactory = ( |
|
|
@ -35,7 +196,11 @@ const checkWitnessScript = scriptCheckerFactory( |
|
|
|
'Witness script', |
|
|
|
); |
|
|
|
|
|
|
|
const isPayment = (script: Buffer, payment: any): boolean => { |
|
|
|
type isPaymentFunction = (script: Buffer) => boolean; |
|
|
|
|
|
|
|
const isPaymentFactory = (payment: any): isPaymentFunction => ( |
|
|
|
script: Buffer, |
|
|
|
): boolean => { |
|
|
|
try { |
|
|
|
payment({ output: script }); |
|
|
|
return true; |
|
|
@ -43,6 +208,18 @@ const isPayment = (script: Buffer, payment: any): boolean => { |
|
|
|
return false; |
|
|
|
} |
|
|
|
}; |
|
|
|
const isP2WPKH = isPaymentFactory(payments.p2wpkh); |
|
|
|
const isP2PKH = isPaymentFactory(payments.p2pkh); |
|
|
|
const isP2MS = isPaymentFactory(payments.p2ms); |
|
|
|
const isP2PK = isPaymentFactory(payments.p2pk); |
|
|
|
|
|
|
|
const classifyScript = (script: Buffer): string => { |
|
|
|
if (isP2WPKH(script)) return 'witnesspubkeyhash'; |
|
|
|
if (isP2PKH(script)) return 'pubkeyhash'; |
|
|
|
if (isP2MS(script)) return 'multisig'; |
|
|
|
if (isP2PK(script)) return 'pubkey'; |
|
|
|
return 'nonstandard'; |
|
|
|
}; |
|
|
|
|
|
|
|
function getScriptFromInput( |
|
|
|
inputIndex: number, |
|
|
@ -73,140 +250,3 @@ function getScriptFromInput( |
|
|
|
} |
|
|
|
return script; |
|
|
|
} |
|
|
|
|
|
|
|
export class Psbt extends PsbtBase { |
|
|
|
constructor(public network?: Network) { |
|
|
|
super(); |
|
|
|
} |
|
|
|
|
|
|
|
canFinalize(inputIndex: number): boolean { |
|
|
|
const input = checkForInput(this.inputs, inputIndex); |
|
|
|
const script = getScriptFromInput( |
|
|
|
inputIndex, |
|
|
|
input, |
|
|
|
this.globalMap.unsignedTx!, |
|
|
|
); |
|
|
|
if (!script) return false; |
|
|
|
const scriptType = classify.output(script); |
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkey': |
|
|
|
return false; |
|
|
|
case 'pubkeyhash': |
|
|
|
return false; |
|
|
|
case 'multisig': |
|
|
|
return false; |
|
|
|
case 'witnesspubkeyhash': |
|
|
|
return false; |
|
|
|
default: |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
signInput(inputIndex: number, keyPair: Signer): Psbt { |
|
|
|
// TODO: Implement BIP174 pre-sign checks:
|
|
|
|
// https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer
|
|
|
|
//
|
|
|
|
// if non_witness_utxo.exists:
|
|
|
|
// assert(sha256d(non_witness_utxo) == psbt.tx.innput[i].prevout.hash)
|
|
|
|
// if redeemScript.exists:
|
|
|
|
// assert(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey == P2SH(redeemScript))
|
|
|
|
// sign_non_witness(redeemScript)
|
|
|
|
// else:
|
|
|
|
// sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey)
|
|
|
|
// else if witness_utxo.exists:
|
|
|
|
// if redeemScript.exists:
|
|
|
|
// assert(witness_utxo.scriptPubKey == P2SH(redeemScript))
|
|
|
|
// script = redeemScript
|
|
|
|
// else:
|
|
|
|
// script = witness_utxo.scriptPubKey
|
|
|
|
// if IsP2WPKH(script):
|
|
|
|
// sign_witness(P2PKH(script[2:22]))
|
|
|
|
// else if IsP2WSH(script):
|
|
|
|
// assert(script == P2WSH(witnessScript))
|
|
|
|
// sign_witness(witnessScript)
|
|
|
|
// else:
|
|
|
|
// assert False
|
|
|
|
|
|
|
|
const input = this.inputs[inputIndex]; |
|
|
|
if (input === undefined) throw new Error(`No input #${inputIndex}`); |
|
|
|
|
|
|
|
const unsignedTx = Transaction.fromBuffer(this.globalMap.unsignedTx!); |
|
|
|
const sighashType = input.sighashType || 0x01; |
|
|
|
let hash: Buffer; |
|
|
|
|
|
|
|
if (input.nonWitnessUtxo) { |
|
|
|
const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); |
|
|
|
|
|
|
|
const prevoutHash = unsignedTx.ins[inputIndex].hash; |
|
|
|
const utxoHash = nonWitnessUtxoTx.getHash(); |
|
|
|
|
|
|
|
// If a non-witness UTXO is provided, its hash must match the hash specified in the prevout
|
|
|
|
if (!prevoutHash.equals(utxoHash)) { |
|
|
|
throw new Error( |
|
|
|
`Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const prevoutIndex = unsignedTx.ins[inputIndex].index; |
|
|
|
const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; |
|
|
|
|
|
|
|
if (input.redeemScript) { |
|
|
|
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
|
|
checkRedeemScript(inputIndex, prevout.script, input.redeemScript); |
|
|
|
hash = unsignedTx.hashForSignature( |
|
|
|
inputIndex, |
|
|
|
input.redeemScript, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
} else { |
|
|
|
hash = unsignedTx.hashForSignature( |
|
|
|
inputIndex, |
|
|
|
prevout.script, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
} |
|
|
|
} else if (input.witnessUtxo) { |
|
|
|
let script: Buffer; |
|
|
|
if (input.redeemScript) { |
|
|
|
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
|
|
checkRedeemScript( |
|
|
|
inputIndex, |
|
|
|
input.witnessUtxo.script, |
|
|
|
input.redeemScript, |
|
|
|
); |
|
|
|
script = input.redeemScript; |
|
|
|
} else { |
|
|
|
script = input.witnessUtxo.script; |
|
|
|
} |
|
|
|
if (isPayment(script, payments.p2wpkh)) { |
|
|
|
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
|
|
|
const signingScript = payments.p2pkh({ hash: script.slice(2) }).output!; |
|
|
|
hash = unsignedTx.hashForWitnessV0( |
|
|
|
inputIndex, |
|
|
|
signingScript, |
|
|
|
input.witnessUtxo.value, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
} else { |
|
|
|
if (!input.witnessScript) |
|
|
|
throw new Error('Segwit input needs witnessScript if not P2WPKH'); |
|
|
|
checkWitnessScript(inputIndex, script, input.witnessScript); |
|
|
|
hash = unsignedTx.hashForWitnessV0( |
|
|
|
inputIndex, |
|
|
|
script, |
|
|
|
input.witnessUtxo.value, |
|
|
|
sighashType, |
|
|
|
); |
|
|
|
} |
|
|
|
} else { |
|
|
|
throw new Error('Need a Utxo input item for signing'); |
|
|
|
} |
|
|
|
|
|
|
|
const partialSig = { |
|
|
|
pubkey: keyPair.publicKey, |
|
|
|
signature: bscript.signature.encode(keyPair.sign(hash), sighashType), |
|
|
|
}; |
|
|
|
|
|
|
|
return this.addPartialSigToInput(inputIndex, partialSig); |
|
|
|
} |
|
|
|
} |
|
|
|