import { Psbt as PsbtBase } from 'bip174'; import * as varuint from 'bip174/src/lib/converter/varint'; import { Bip32Derivation, KeyValue, PartialSig, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, Transaction as ITransaction, TransactionFromBuffer, } from 'bip174/src/lib/interfaces'; import { checkForInput, checkForOutput } from 'bip174/src/lib/utils'; import { fromOutputScript, toOutputScript } from './address'; import { cloneBuffer, reverseBuffer } from './bufferutils'; import { hash160 } from './crypto'; import { fromPublicKey as ecPairFromPublicKey, Signer, SignerAsync, } from './ecpair'; import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; export interface TransactionInput { hash: string | Buffer; index: number; sequence?: number; } export interface PsbtTxInput extends TransactionInput { hash: Buffer; } export interface TransactionOutput { script: Buffer; value: number; } export interface PsbtTxOutput extends TransactionOutput { address: string | undefined; } /** * These are the default arguments for a Psbt instance. */ const DEFAULT_OPTS: PsbtOpts = { /** * A bitcoinjs Network object. This is only used if you pass an `address` * parameter to addOutput. Otherwise it is not needed and can be left default. */ network: btcNetwork, /** * When extractTransaction is called, the fee rate is checked. * THIS IS NOT TO BE RELIED ON. * It is only here as a last ditch effort to prevent sending a 500 BTC fee etc. */ maximumFeeRate: 5000, // satoshi per byte }; /** * Psbt class can parse and generate a PSBT binary based off of the BIP174. * There are 6 roles that this class fulfills. (Explained in BIP174) * * Creator: This can be done with `new Psbt()` * Updater: This can be done with `psbt.addInput(input)`, `psbt.addInputs(inputs)`, * `psbt.addOutput(output)`, `psbt.addOutputs(outputs)` when you are looking to * add new inputs and outputs to the PSBT, and `psbt.updateGlobal(itemObject)`, * `psbt.updateInput(itemObject)`, `psbt.updateOutput(itemObject)` * addInput requires hash: Buffer | string; and index: number; as attributes * and can also include any attributes that are used in updateInput method. * addOutput requires script: Buffer; and value: number; and likewise can include * data for updateOutput. * For a list of what attributes should be what types. Check the bip174 library. * Also, check the integration tests for some examples of usage. * Signer: There are a few methods. signAllInputs and signAllInputsAsync, which will search all input * information for your pubkey or pubkeyhash, and only sign inputs where it finds * your info. Or you can explicitly sign a specific input with signInput and * signInputAsync. For the async methods you can create a SignerAsync object * and use something like a hardware wallet to sign with. (You must implement this) * Combiner: psbts can be combined easily with `psbt.combine(psbt2, psbt3, psbt4 ...)` * the psbt calling combine will always have precedence when a conflict occurs. * Combine checks if the internal bitcoin transaction is the same, so be sure that * all sequences, version, locktime, etc. are the same before combining. * Input Finalizer: This role is fairly important. Not only does it need to construct * the input scriptSigs and witnesses, but it SHOULD verify the signatures etc. * Before running `psbt.finalizeAllInputs()` please run `psbt.validateSignaturesOfAllInputs()` * Running any finalize method will delete any data in the input(s) that are no longer * needed due to the finalized scripts containing the information. * Transaction Extractor: This role will perform some checks before returning a * Transaction object. Such as fee rate not being larger than maximumFeeRate etc. */ export class Psbt { static fromBase64(data: string, opts: PsbtOptsOptional = {}): Psbt { const buffer = Buffer.from(data, 'base64'); return this.fromBuffer(buffer, opts); } static fromHex(data: string, opts: PsbtOptsOptional = {}): Psbt { const buffer = Buffer.from(data, 'hex'); return this.fromBuffer(buffer, opts); } static fromBuffer(buffer: Buffer, opts: PsbtOptsOptional = {}): Psbt { const psbtBase = PsbtBase.fromBuffer(buffer, transactionFromBuffer); const psbt = new Psbt(opts, psbtBase); checkTxForDupeIns(psbt.__CACHE.__TX, psbt.__CACHE); return psbt; } private __CACHE: PsbtCache; private opts: PsbtOpts; constructor( opts: PsbtOptsOptional = {}, readonly data: PsbtBase = new PsbtBase(new PsbtTransaction()), ) { // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); this.__CACHE = { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], __TX_IN_CACHE: {}, __TX: (this.data.globalMap.unsignedTx as PsbtTransaction).tx, // Old TransactionBuilder behavior was to not confirm input values // before signing. Even though we highly encourage people to get // the full parent transaction to verify values, the ability to // sign non-segwit inputs without the full transaction was often // requested. So the only way to activate is to use @ts-ignore. // We will disable exporting the Psbt when unsafe sign is active. // because it is not BIP174 compliant. __UNSAFE_SIGN_NONSEGWIT: false, }; if (this.data.inputs.length === 0) this.setVersion(2); // Make data hidden when enumerating const dpew = ( obj: any, attr: string, enumerable: boolean, writable: boolean, ): any => Object.defineProperty(obj, attr, { enumerable, writable, }); dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } get inputCount(): number { return this.data.inputs.length; } get version(): number { return this.__CACHE.__TX.version; } set version(version: number) { this.setVersion(version); } get locktime(): number { return this.__CACHE.__TX.locktime; } set locktime(locktime: number) { this.setLocktime(locktime); } get txInputs(): PsbtTxInput[] { return this.__CACHE.__TX.ins.map(input => ({ hash: cloneBuffer(input.hash), index: input.index, sequence: input.sequence, })); } get txOutputs(): PsbtTxOutput[] { return this.__CACHE.__TX.outs.map(output => { let address; try { address = fromOutputScript(output.script, this.opts.network); } catch (_) {} return { script: cloneBuffer(output.script), value: output.value, address, }; }); } combine(...those: Psbt[]): this { this.data.combine(...those.map(o => o.data)); return this; } clone(): Psbt { // TODO: more efficient cloning const res = Psbt.fromBuffer(this.data.toBuffer()); res.opts = JSON.parse(JSON.stringify(this.opts)); return res; } setMaximumFeeRate(satoshiPerByte: number): void { check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw this.opts.maximumFeeRate = satoshiPerByte; } setVersion(version: number): this { check32Bit(version); checkInputsForPartialSig(this.data.inputs, 'setVersion'); const c = this.__CACHE; c.__TX.version = version; c.__EXTRACTED_TX = undefined; return this; } setLocktime(locktime: number): this { check32Bit(locktime); checkInputsForPartialSig(this.data.inputs, 'setLocktime'); const c = this.__CACHE; c.__TX.locktime = locktime; c.__EXTRACTED_TX = undefined; return this; } setInputSequence(inputIndex: number, sequence: number): this { check32Bit(sequence); checkInputsForPartialSig(this.data.inputs, 'setInputSequence'); const c = this.__CACHE; if (c.__TX.ins.length <= inputIndex) { throw new Error('Input index too high'); } c.__TX.ins[inputIndex].sequence = sequence; c.__EXTRACTED_TX = undefined; return this; } addInputs(inputDatas: PsbtInputExtended[]): this { inputDatas.forEach(inputData => this.addInput(inputData)); return this; } addInput(inputData: PsbtInputExtended): this { if ( arguments.length > 1 || !inputData || inputData.hash === undefined || inputData.index === undefined ) { throw new Error( `Invalid arguments for Psbt.addInput. ` + `Requires single object with at least [hash] and [index]`, ); } checkInputsForPartialSig(this.data.inputs, 'addInput'); if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); const c = this.__CACHE; this.data.addInput(inputData); const txIn = c.__TX.ins[c.__TX.ins.length - 1]; checkTxInputCache(c, txIn); const inputIndex = this.data.inputs.length - 1; const input = this.data.inputs[inputIndex]; if (input.nonWitnessUtxo) { addNonWitnessTxCache(this.__CACHE, input, inputIndex); } c.__FEE = undefined; c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; } addOutputs(outputDatas: PsbtOutputExtended[]): this { outputDatas.forEach(outputData => this.addOutput(outputData)); return this; } addOutput(outputData: PsbtOutputExtended): this { if ( arguments.length > 1 || !outputData || outputData.value === undefined || ((outputData as any).address === undefined && (outputData as any).script === undefined) ) { throw new Error( `Invalid arguments for Psbt.addOutput. ` + `Requires single object with at least [script or address] and [value]`, ); } checkInputsForPartialSig(this.data.inputs, 'addOutput'); const { address } = outputData as any; if (typeof address === 'string') { const { network } = this.opts; const script = toOutputScript(address, network); outputData = Object.assign(outputData, { script }); } const c = this.__CACHE; this.data.addOutput(outputData); c.__FEE = undefined; c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; } extractTransaction(disableFeeCheck?: boolean): Transaction { if (!this.data.inputs.every(isFinalized)) throw new Error('Not finalized'); const c = this.__CACHE; if (!disableFeeCheck) { checkFees(this, c, this.opts); } if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; const tx = c.__TX.clone(); inputFinalizeGetAmts(this.data.inputs, tx, c, true); return tx; } getFeeRate(): number { return getTxCacheValue( '__FEE_RATE', 'fee rate', this.data.inputs, this.__CACHE, )!; } getFee(): number { return getTxCacheValue('__FEE', 'fee', this.data.inputs, this.__CACHE)!; } finalizeAllInputs(): this { checkForInput(this.data.inputs, 0); // making sure we have at least one range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } finalizeInput( inputIndex: number, finalScriptsFunc: FinalScriptsFunc = getFinalScripts, ): this { const input = checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, this.__CACHE, ); if (!script) throw new Error(`No script found for input #${inputIndex}`); checkPartialSigSighashes(input); const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( inputIndex, input, script, isSegwit, isP2SH, isP2WSH, ); if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); if (finalScriptWitness) this.data.updateInput(inputIndex, { finalScriptWitness }); if (!finalScriptSig && !finalScriptWitness) throw new Error(`Unknown error finalizing input #${inputIndex}`); this.data.clearFinalizedInput(inputIndex); return this; } getInputType(inputIndex: number): AllScriptType { const input = checkForInput(this.data.inputs, inputIndex); const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); const result = getMeaningfulScript( script, inputIndex, 'input', input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || redeemFromFinalWitnessScript(input.finalScriptWitness), ); const type = result.type === 'raw' ? '' : result.type + '-'; const mainType = classifyScript(result.meaningfulScript); return (type + mainType) as AllScriptType; } inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean { const input = checkForInput(this.data.inputs, inputIndex); return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE); } inputHasHDKey(inputIndex: number, root: HDSigner): boolean { const input = checkForInput(this.data.inputs, inputIndex); const derivationIsMine = bip32DerivationIsMine(root); return ( !!input.bip32Derivation && input.bip32Derivation.some(derivationIsMine) ); } outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean { const output = checkForOutput(this.data.outputs, outputIndex); return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE); } outputHasHDKey(outputIndex: number, root: HDSigner): boolean { const output = checkForOutput(this.data.outputs, outputIndex); const derivationIsMine = bip32DerivationIsMine(root); return ( !!output.bip32Derivation && output.bip32Derivation.some(derivationIsMine) ); } validateSignaturesOfAllInputs(): boolean { checkForInput(this.data.inputs, 0); // making sure we have at least one const results = range(this.data.inputs.length).map(idx => this.validateSignaturesOfInput(idx), ); return results.reduce((final, res) => res === true && final, true); } validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean { const input = this.data.inputs[inputIndex]; const partialSig = (input || {}).partialSig; if (!input || !partialSig || partialSig.length < 1) throw new Error('No signatures to validate'); const mySigs = pubkey ? partialSig.filter(sig => sig.pubkey.equals(pubkey)) : partialSig; if (mySigs.length < 1) throw new Error('No signatures for this pubkey'); const results: boolean[] = []; let hashCache: Buffer; let scriptCache: Buffer; let sighashCache: number; for (const pSig of mySigs) { const sig = bscript.signature.decode(pSig.signature); const { hash, script } = sighashCache! !== sig.hashType ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), this.__CACHE, true, ) : { hash: hashCache!, script: scriptCache! }; sighashCache = sig.hashType; hashCache = hash; scriptCache = script; checkScriptForPubkey(pSig.pubkey, script, 'verify'); const keypair = ecPairFromPublicKey(pSig.pubkey); results.push(keypair.verify(hash, sig.signature)); } return results.every(res => res === true); } signAllInputsHD( hdKeyPair: HDSigner, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): this { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } const results: boolean[] = []; for (const i of range(this.data.inputs.length)) { try { this.signInputHD(i, hdKeyPair, sighashTypes); results.push(true); } catch (err) { results.push(false); } } if (results.every(v => v === false)) { throw new Error('No inputs were signed'); } return this; } signAllInputsHDAsync( hdKeyPair: HDSigner | HDSignerAsync, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): Promise { return new Promise( (resolve, reject): any => { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { return reject(new Error('Need HDSigner to sign input')); } const results: boolean[] = []; const promises: Array> = []; for (const i of range(this.data.inputs.length)) { promises.push( this.signInputHDAsync(i, hdKeyPair, sighashTypes).then( () => { results.push(true); }, () => { results.push(false); }, ), ); } return Promise.all(promises).then(() => { if (results.every(v => v === false)) { return reject(new Error('No inputs were signed')); } resolve(); }); }, ); } signInputHD( inputIndex: number, hdKeyPair: HDSigner, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): this { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } const signers = getSignersFromHD( inputIndex, this.data.inputs, hdKeyPair, ) as Signer[]; signers.forEach(signer => this.signInput(inputIndex, signer, sighashTypes)); return this; } signInputHDAsync( inputIndex: number, hdKeyPair: HDSigner | HDSignerAsync, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): Promise { return new Promise( (resolve, reject): any => { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { return reject(new Error('Need HDSigner to sign input')); } const signers = getSignersFromHD( inputIndex, this.data.inputs, hdKeyPair, ); const promises = signers.map(signer => this.signInputAsync(inputIndex, signer, sighashTypes), ); return Promise.all(promises) .then(() => { resolve(); }) .catch(reject); }, ); } signAllInputs( keyPair: Signer, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); // TODO: Add a pubkey/pubkeyhash cache to each input // as input information is added, then eventually // optimize this method. const results: boolean[] = []; for (const i of range(this.data.inputs.length)) { try { this.signInput(i, keyPair, sighashTypes); results.push(true); } catch (err) { results.push(false); } } if (results.every(v => v === false)) { throw new Error('No inputs were signed'); } return this; } signAllInputsAsync( keyPair: Signer | SignerAsync, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): Promise { return new Promise( (resolve, reject): any => { if (!keyPair || !keyPair.publicKey) return reject(new Error('Need Signer to sign input')); // TODO: Add a pubkey/pubkeyhash cache to each input // as input information is added, then eventually // optimize this method. const results: boolean[] = []; const promises: Array> = []; for (const [i] of this.data.inputs.entries()) { promises.push( this.signInputAsync(i, keyPair, sighashTypes).then( () => { results.push(true); }, () => { results.push(false); }, ), ); } return Promise.all(promises).then(() => { if (results.every(v => v === false)) { return reject(new Error('No inputs were signed')); } resolve(); }); }, ); } signInput( inputIndex: number, keyPair: Signer, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, sighashTypes, ); const partialSig = [ { pubkey: keyPair.publicKey, signature: bscript.signature.encode(keyPair.sign(hash), sighashType), }, ]; this.data.updateInput(inputIndex, { partialSig }); return this; } signInputAsync( inputIndex: number, keyPair: Signer | SignerAsync, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): Promise { return Promise.resolve().then(() => { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, sighashTypes, ); return Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = [ { pubkey: keyPair.publicKey, signature: bscript.signature.encode(signature, sighashType), }, ]; this.data.updateInput(inputIndex, { partialSig }); }); }); } toBuffer(): Buffer { checkCache(this.__CACHE); return this.data.toBuffer(); } toHex(): string { checkCache(this.__CACHE); return this.data.toHex(); } toBase64(): string { checkCache(this.__CACHE); return this.data.toBase64(); } updateGlobal(updateData: PsbtGlobalUpdate): this { this.data.updateGlobal(updateData); return this; } updateInput(inputIndex: number, updateData: PsbtInputUpdate): this { if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); this.data.updateInput(inputIndex, updateData); if (updateData.nonWitnessUtxo) { addNonWitnessTxCache( this.__CACHE, this.data.inputs[inputIndex], inputIndex, ); } return this; } updateOutput(outputIndex: number, updateData: PsbtOutputUpdate): this { this.data.updateOutput(outputIndex, updateData); return this; } addUnknownKeyValToGlobal(keyVal: KeyValue): this { this.data.addUnknownKeyValToGlobal(keyVal); return this; } addUnknownKeyValToInput(inputIndex: number, keyVal: KeyValue): this { this.data.addUnknownKeyValToInput(inputIndex, keyVal); return this; } addUnknownKeyValToOutput(outputIndex: number, keyVal: KeyValue): this { this.data.addUnknownKeyValToOutput(outputIndex, keyVal); return this; } clearFinalizedInput(inputIndex: number): this { this.data.clearFinalizedInput(inputIndex); return this; } } interface PsbtCache { __NON_WITNESS_UTXO_TX_CACHE: Transaction[]; __NON_WITNESS_UTXO_BUF_CACHE: Buffer[]; __TX_IN_CACHE: { [index: string]: number }; __TX: Transaction; __FEE_RATE?: number; __FEE?: number; __EXTRACTED_TX?: Transaction; __UNSAFE_SIGN_NONSEGWIT: boolean; } interface PsbtOptsOptional { network?: Network; maximumFeeRate?: number; } interface PsbtOpts { network: Network; maximumFeeRate: number; } interface PsbtInputExtended extends PsbtInput, TransactionInput {} type PsbtOutputExtended = PsbtOutputExtendedAddress | PsbtOutputExtendedScript; interface PsbtOutputExtendedAddress extends PsbtOutput { address: string; value: number; } interface PsbtOutputExtendedScript extends PsbtOutput { script: Buffer; value: number; } interface HDSignerBase { /** * DER format compressed publicKey buffer */ publicKey: Buffer; /** * The first 4 bytes of the sha256-ripemd160 of the publicKey */ fingerprint: Buffer; } interface HDSigner extends HDSignerBase { /** * The path string must match /^m(\/\d+'?)+$/ * ex. m/44'/0'/0'/1/23 levels with ' must be hard derivations */ derivePath(path: string): HDSigner; /** * Input hash (the "message digest") for the signature algorithm * Return a 64 byte signature (32 byte r and 32 byte s in that order) */ sign(hash: Buffer): Buffer; } /** * Same as above but with async sign method */ interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; } /** * This function is needed to pass to the bip174 base class's fromBuffer. * It takes the "transaction buffer" portion of the psbt buffer and returns a * Transaction (From the bip174 library) interface. */ const transactionFromBuffer: TransactionFromBuffer = ( buffer: Buffer, ): ITransaction => new PsbtTransaction(buffer); /** * This class implements the Transaction interface from bip174 library. * It contains a bitcoinjs-lib Transaction object. */ class PsbtTransaction implements ITransaction { tx: Transaction; constructor(buffer: Buffer = Buffer.from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0])) { this.tx = Transaction.fromBuffer(buffer); checkTxEmpty(this.tx); Object.defineProperty(this, 'tx', { enumerable: false, writable: true, }); } getInputOutputCounts(): { inputCount: number; outputCount: number; } { return { inputCount: this.tx.ins.length, outputCount: this.tx.outs.length, }; } addInput(input: any): void { if ( (input as any).hash === undefined || (input as any).index === undefined || (!Buffer.isBuffer((input as any).hash) && typeof (input as any).hash !== 'string') || typeof (input as any).index !== 'number' ) { throw new Error('Error adding input.'); } const hash = typeof input.hash === 'string' ? reverseBuffer(Buffer.from(input.hash, 'hex')) : input.hash; this.tx.addInput(hash, input.index, input.sequence); } addOutput(output: any): void { if ( (output as any).script === undefined || (output as any).value === undefined || !Buffer.isBuffer((output as any).script) || typeof (output as any).value !== 'number' ) { throw new Error('Error adding output.'); } this.tx.addOutput(output.script, output.value); } toBuffer(): Buffer { return this.tx.toBuffer(); } } 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, p2ms.pubkeys); default: return false; } } function checkCache(cache: PsbtCache): void { if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) { throw new Error('Not BIP174 compliant, can not export'); } } function hasSigs( neededSigs: number, partialSig?: any[], pubkeys?: Buffer[], ): boolean { if (!partialSig) return false; let sigs: any; if (pubkeys) { sigs = pubkeys .map(pkey => { const pubkey = ecPairFromPublicKey(pkey, { compressed: true }) .publicKey; return partialSig.find(pSig => pSig.pubkey.equals(pubkey)); }) .filter(v => !!v); } else { sigs = partialSig; } if (sigs.length > neededSigs) throw new Error('Too many signatures'); return sigs.length === neededSigs; } function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } function isPaymentFactory(payment: any): (script: Buffer) => boolean { return (script: Buffer): boolean => { try { payment({ output: script }); return true; } catch (err) { return false; } }; } const isP2MS = isPaymentFactory(payments.p2ms); const isP2PK = isPaymentFactory(payments.p2pk); const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); const isP2SHScript = isPaymentFactory(payments.p2sh); function bip32DerivationIsMine( root: HDSigner, ): (d: Bip32Derivation) => boolean { return (d: Bip32Derivation): boolean => { if (!d.masterFingerprint.equals(root.fingerprint)) return false; if (!root.derivePath(d.path).publicKey.equals(d.pubkey)) return false; return true; }; } function check32Bit(num: number): void { if ( typeof num !== 'number' || num !== Math.floor(num) || num > 0xffffffff || num < 0 ) { throw new Error('Invalid 32 bit integer'); } } function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void { const feeRate = cache.__FEE_RATE || psbt.getFeeRate(); const vsize = cache.__EXTRACTED_TX!.virtualSize(); const satoshis = feeRate * vsize; if (feeRate >= opts.maximumFeeRate) { throw new Error( `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + `fees, which is ${feeRate} satoshi per byte for a transaction ` + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + `pass true to the first arg of extractTransaction.`, ); } } function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { inputs.forEach(input => { let throws = false; let pSigs: PartialSig[] = []; if ((input.partialSig || []).length === 0) { if (!input.finalScriptSig && !input.finalScriptWitness) return; pSigs = getPsigsFromInputFinalScripts(input); } else { pSigs = input.partialSig!; } pSigs.forEach(pSig => { const { hashType } = bscript.signature.decode(pSig.signature); const whitelist: string[] = []; const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY; if (isAnyoneCanPay) whitelist.push('addInput'); const hashMod = hashType & 0x1f; switch (hashMod) { case Transaction.SIGHASH_ALL: break; case Transaction.SIGHASH_SINGLE: case Transaction.SIGHASH_NONE: whitelist.push('addOutput'); whitelist.push('setInputSequence'); break; } if (whitelist.indexOf(action) === -1) { throws = true; } }); if (throws) { throw new Error('Can not modify transaction, signatures exist.'); } }); } function checkPartialSigSighashes(input: PsbtInput): void { if (!input.sighashType || !input.partialSig) return; const { partialSig, sighashType } = input; partialSig.forEach(pSig => { const { hashType } = bscript.signature.decode(pSig.signature); if (sighashType !== hashType) { throw new Error('Signature sighash does not match input sighash type'); } }); } function checkScriptForPubkey( pubkey: Buffer, script: Buffer, action: string, ): void { if (!pubkeyInScript(pubkey, script)) { throw new Error( `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, ); } } function checkTxEmpty(tx: Transaction): void { const isEmpty = tx.ins.every( input => input.script && input.script.length === 0 && input.witness && input.witness.length === 0, ); if (!isEmpty) { throw new Error('Format Error: Transaction ScriptSigs are not empty'); } } function checkTxForDupeIns(tx: Transaction, cache: PsbtCache): void { tx.ins.forEach(input => { checkTxInputCache(cache, input); }); } function checkTxInputCache( cache: PsbtCache, input: { hash: Buffer; index: number }, ): void { const key = reverseBuffer(Buffer.from(input.hash)).toString('hex') + ':' + input.index; if (cache.__TX_IN_CACHE[key]) throw new Error('Duplicate input detected.'); cache.__TX_IN_CACHE[key] = 1; } function scriptCheckerFactory( payment: any, paymentScriptName: string, ): (idx: number, spk: Buffer, rs: Buffer, ioType: 'input' | 'output') => void { return ( inputIndex: number, scriptPubKey: Buffer, redeemScript: Buffer, ioType: 'input' | 'output', ): void => { const redeemScriptOutput = payment({ redeem: { output: redeemScript }, }).output as Buffer; if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( `${paymentScriptName} for ${ioType} #${inputIndex} doesn't match the scriptPubKey in the prevout`, ); } }; } const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); const checkWitnessScript = scriptCheckerFactory( payments.p2wsh, 'Witness script', ); type TxCacheNumberKey = '__FEE_RATE' | '__FEE'; function getTxCacheValue( key: TxCacheNumberKey, name: string, inputs: PsbtInput[], c: PsbtCache, ): number | undefined { if (!inputs.every(isFinalized)) throw new Error(`PSBT must be finalized to calculate ${name}`); if (key === '__FEE_RATE' && c.__FEE_RATE) return c.__FEE_RATE; if (key === '__FEE' && c.__FEE) return c.__FEE; let tx: Transaction; let mustFinalize = true; if (c.__EXTRACTED_TX) { tx = c.__EXTRACTED_TX; mustFinalize = false; } else { tx = c.__TX.clone(); } inputFinalizeGetAmts(inputs, tx, c, mustFinalize); if (key === '__FEE_RATE') return c.__FEE_RATE!; else if (key === '__FEE') return c.__FEE!; } /** * This function must do two things: * 1. Check if the `input` can be finalized. If it can not be finalized, throw. * ie. `Can not finalize input #${inputIndex}` * 2. Create the finalScriptSig and finalScriptWitness Buffers. */ type FinalScriptsFunc = ( inputIndex: number, // Which input is it? input: PsbtInput, // The PSBT input contents script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.) isSegwit: boolean, // Is it segwit? isP2SH: boolean, // Is it P2SH? isP2WSH: boolean, // Is it P2WSH? ) => { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; }; function getFinalScripts( inputIndex: number, input: PsbtInput, script: Buffer, isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean, ): { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; } { const scriptType = classifyScript(script); if (!canFinalize(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); return prepareFinalScripts( script, scriptType, input.partialSig!, isSegwit, isP2SH, isP2WSH, ); } function prepareFinalScripts( script: Buffer, scriptType: string, partialSig: PartialSig[], isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean, ): { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; } { let finalScriptSig: Buffer | undefined; let finalScriptWitness: Buffer | undefined; // Wow, the payments API is very handy const payment: payments.Payment = getPayment(script, scriptType, partialSig); const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); if (isSegwit) { if (p2wsh) { finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); } else { finalScriptWitness = witnessStackToScriptWitness(payment.witness!); } if (p2sh) { finalScriptSig = p2sh.input; } } else { if (p2sh) { finalScriptSig = p2sh.input; } else { finalScriptSig = payment.input; } } return { finalScriptSig, finalScriptWitness, }; } function getHashAndSighashType( inputs: PsbtInput[], inputIndex: number, pubkey: Buffer, cache: PsbtCache, sighashTypes: number[], ): { hash: Buffer; sighashType: number; } { const input = checkForInput(inputs, inputIndex); const { hash, sighashType, script } = getHashForSig( inputIndex, input, cache, false, sighashTypes, ); checkScriptForPubkey(pubkey, script, 'sign'); return { hash, sighashType, }; } function getHashForSig( inputIndex: number, input: PsbtInput, cache: PsbtCache, forValidate: boolean, sighashTypes?: number[], ): { script: Buffer; hash: Buffer; sighashType: number; } { const unsignedTx = cache.__TX; const sighashType = input.sighashType || Transaction.SIGHASH_ALL; if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { const str = sighashTypeToString(sighashType); throw new Error( `Sighash type is not allowed. Retry the sign method passing the ` + `sighashTypes array of whitelisted types. Sighash type: ${str}`, ); } let hash: Buffer; let prevout: Output; if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); 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; prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output; } else if (input.witnessUtxo) { prevout = input.witnessUtxo; } else { throw new Error('Need a Utxo input item for signing'); } const { meaningfulScript, type } = getMeaningfulScript( prevout.script, inputIndex, 'input', input.redeemScript, input.witnessScript, ); if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( inputIndex, meaningfulScript, prevout.value, sighashType, ); } else if (isP2WPKH(meaningfulScript)) { // P2WPKH uses the P2PKH template for prevoutScript when signing const signingScript = payments.p2pkh({ hash: meaningfulScript.slice(2) }) .output!; hash = unsignedTx.hashForWitnessV0( inputIndex, signingScript, prevout.value, sighashType, ); } else { // non-segwit if ( input.nonWitnessUtxo === undefined && cache.__UNSAFE_SIGN_NONSEGWIT === false ) throw new Error( `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + `${meaningfulScript.toString('hex')}`, ); if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false) console.warn( 'Warning: Signing non-segwit inputs without the full parent transaction ' + 'means there is a chance that a miner could feed you incorrect information ' + 'to trick you into paying large fees. This behavior is the same as the old ' + 'TransactionBuilder class when signing non-segwit scripts. You are not ' + 'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' + 'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' + '*********************', ); hash = unsignedTx.hashForSignature( inputIndex, meaningfulScript, sighashType, ); } return { script: meaningfulScript, sighashType, hash, }; } function getPayment( script: Buffer, scriptType: string, partialSig: PartialSig[], ): payments.Payment { let payment: payments.Payment; switch (scriptType) { case 'multisig': const sigs = getSortedSigs(script, partialSig); payment = payments.p2ms({ output: script, signatures: sigs, }); 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 getPsigsFromInputFinalScripts(input: PsbtInput): PartialSig[] { const scriptItems = !input.finalScriptSig ? [] : bscript.decompile(input.finalScriptSig) || []; const witnessItems = !input.finalScriptWitness ? [] : bscript.decompile(input.finalScriptWitness) || []; return scriptItems .concat(witnessItems) .filter(item => { return Buffer.isBuffer(item) && bscript.isCanonicalScriptSignature(item); }) .map(sig => ({ signature: sig })) as PartialSig[]; } interface GetScriptReturn { script: Buffer | null; isSegwit: boolean; isP2SH: boolean; isP2WSH: boolean; } function getScriptFromInput( inputIndex: number, input: PsbtInput, cache: PsbtCache, ): GetScriptReturn { const unsignedTx = cache.__TX; const res: GetScriptReturn = { script: null, isSegwit: false, isP2SH: false, isP2WSH: false, }; res.isP2SH = !!input.redeemScript; res.isP2WSH = !!input.witnessScript; if (input.witnessScript) { res.script = input.witnessScript; } else if (input.redeemScript) { res.script = input.redeemScript; } else { if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } else if (input.witnessUtxo) { res.script = input.witnessUtxo.script; } } if (input.witnessScript || isP2WPKH(res.script!)) { res.isSegwit = true; } return res; } function getSignersFromHD( inputIndex: number, inputs: PsbtInput[], hdKeyPair: HDSigner | HDSignerAsync, ): Array { const input = checkForInput(inputs, inputIndex); if (!input.bip32Derivation || input.bip32Derivation.length === 0) { throw new Error('Need bip32Derivation to sign with HD'); } const myDerivations = input.bip32Derivation .map(bipDv => { if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) { return bipDv; } else { return; } }) .filter(v => !!v); if (myDerivations.length === 0) { throw new Error( 'Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint', ); } const signers: Array = myDerivations.map(bipDv => { const node = hdKeyPair.derivePath(bipDv!.path); if (!bipDv!.pubkey.equals(node.publicKey)) { throw new Error('pubkey did not match bip32Derivation'); } return node; }); return signers; } function getSortedSigs(script: Buffer, partialSig: PartialSig[]): Buffer[] { const p2ms = payments.p2ms({ output: script }); // for each pubkey in order of p2ms script return p2ms .pubkeys!.map(pk => { // filter partialSig array by pubkey being equal return ( partialSig.filter(ps => { return ps.pubkey.equals(pk); })[0] || {} ).signature; // Any pubkey without a match will return undefined // this last filter removes all the undefined items in the array. }) .filter(v => !!v); } function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { let offset = 0; function readSlice(n: number): Buffer { offset += n; return buffer.slice(offset - n, offset); } function readVarInt(): number { const vi = varuint.decode(buffer, offset); offset += (varuint.decode as any).bytes; return vi; } function readVarSlice(): Buffer { return readSlice(readVarInt()); } function readVector(): Buffer[] { const count = readVarInt(); const vector: Buffer[] = []; for (let i = 0; i < count; i++) vector.push(readVarSlice()); return vector; } return readVector(); } function sighashTypeToString(sighashType: number): string { let text = sighashType & Transaction.SIGHASH_ANYONECANPAY ? 'SIGHASH_ANYONECANPAY | ' : ''; const sigMod = sighashType & 0x1f; switch (sigMod) { case Transaction.SIGHASH_ALL: text += 'SIGHASH_ALL'; break; case Transaction.SIGHASH_SINGLE: text += 'SIGHASH_SINGLE'; break; case Transaction.SIGHASH_NONE: text += 'SIGHASH_NONE'; break; } return text; } 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; } function addNonWitnessTxCache( cache: PsbtCache, input: PsbtInput, inputIndex: number, ): void { cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo!; const tx = Transaction.fromBuffer(input.nonWitnessUtxo!); cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; const self = cache; const selfIndex = inputIndex; delete input.nonWitnessUtxo; Object.defineProperty(input, 'nonWitnessUtxo', { enumerable: true, get(): Buffer { const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex]; if (buf !== undefined) { return buf; } else { const newBuf = txCache.toBuffer(); self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf; return newBuf; } }, set(data: Buffer): void { self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; }, }); } function inputFinalizeGetAmts( inputs: PsbtInput[], tx: Transaction, cache: PsbtCache, mustFinalize: boolean, ): void { let inputAmount = 0; inputs.forEach((input, idx) => { if (mustFinalize && input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (mustFinalize && input.finalScriptWitness) { tx.ins[idx].witness = scriptWitnessToWitnessStack( input.finalScriptWitness, ); } if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { const nwTx = nonWitnessUtxoTxFromCache(cache, input, idx); const vout = tx.ins[idx].index; const out = nwTx.outs[vout] as Output; inputAmount += out.value; } }); const outputAmount = (tx.outs as Output[]).reduce( (total, o) => total + o.value, 0, ); const fee = inputAmount - outputAmount; if (fee < 0) { throw new Error('Outputs are spending more than Inputs'); } const bytes = tx.virtualSize(); cache.__FEE = fee; cache.__EXTRACTED_TX = tx; cache.__FEE_RATE = Math.floor(fee / bytes); } function nonWitnessUtxoTxFromCache( cache: PsbtCache, input: PsbtInput, inputIndex: number, ): Transaction { const c = cache.__NON_WITNESS_UTXO_TX_CACHE; if (!c[inputIndex]) { addNonWitnessTxCache(cache, input, inputIndex); } return c[inputIndex]; } function getScriptFromUtxo( inputIndex: number, input: PsbtInput, cache: PsbtCache, ): Buffer { if (input.witnessUtxo !== undefined) { return input.witnessUtxo.script; } else if (input.nonWitnessUtxo !== undefined) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; } else { throw new Error("Can't find pubkey in input without Utxo data"); } } function pubkeyInInput( pubkey: Buffer, input: PsbtInput, inputIndex: number, cache: PsbtCache, ): boolean { const script = getScriptFromUtxo(inputIndex, input, cache); const { meaningfulScript } = getMeaningfulScript( script, inputIndex, 'input', input.redeemScript, input.witnessScript, ); return pubkeyInScript(pubkey, meaningfulScript); } function pubkeyInOutput( pubkey: Buffer, output: PsbtOutput, outputIndex: number, cache: PsbtCache, ): boolean { const script = cache.__TX.outs[outputIndex].script; const { meaningfulScript } = getMeaningfulScript( script, outputIndex, 'output', output.redeemScript, output.witnessScript, ); return pubkeyInScript(pubkey, meaningfulScript); } function redeemFromFinalScriptSig( finalScript: Buffer | undefined, ): Buffer | undefined { if (!finalScript) return; const decomp = bscript.decompile(finalScript); if (!decomp) return; const lastItem = decomp[decomp.length - 1]; if ( !Buffer.isBuffer(lastItem) || isPubkeyLike(lastItem) || isSigLike(lastItem) ) return; const sDecomp = bscript.decompile(lastItem); if (!sDecomp) return; return lastItem; } function redeemFromFinalWitnessScript( finalScript: Buffer | undefined, ): Buffer | undefined { if (!finalScript) return; const decomp = scriptWitnessToWitnessStack(finalScript); const lastItem = decomp[decomp.length - 1]; if (isPubkeyLike(lastItem)) return; const sDecomp = bscript.decompile(lastItem); if (!sDecomp) return; return lastItem; } function isPubkeyLike(buf: Buffer): boolean { return buf.length === 33 && bscript.isCanonicalPubKey(buf); } function isSigLike(buf: Buffer): boolean { return bscript.isCanonicalScriptSignature(buf); } function getMeaningfulScript( script: Buffer, index: number, ioType: 'input' | 'output', redeemScript?: Buffer, witnessScript?: Buffer, ): { meaningfulScript: Buffer; type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw'; } { const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); const isP2WSH = isP2WSHScript(script); if (isP2SH && redeemScript === undefined) throw new Error('scriptPubkey is P2SH but redeemScript missing'); if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) throw new Error( 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', ); let meaningfulScript: Buffer; if (isP2SHP2WSH) { meaningfulScript = witnessScript!; checkRedeemScript(index, script, redeemScript!, ioType); checkWitnessScript(index, redeemScript!, witnessScript!, ioType); checkInvalidP2WSH(meaningfulScript); } else if (isP2WSH) { meaningfulScript = witnessScript!; checkWitnessScript(index, script, witnessScript!, ioType); checkInvalidP2WSH(meaningfulScript); } else if (isP2SH) { meaningfulScript = redeemScript!; checkRedeemScript(index, script, redeemScript!, ioType); } else { meaningfulScript = script; } return { meaningfulScript, type: isP2SHP2WSH ? 'p2sh-p2wsh' : isP2SH ? 'p2sh' : isP2WSH ? 'p2wsh' : 'raw', }; } function checkInvalidP2WSH(script: Buffer): void { if (isP2WPKH(script) || isP2SHScript(script)) { throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); } } function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { const pubkeyHash = hash160(pubkey); const decompiled = bscript.decompile(script); if (decompiled === null) throw new Error('Unknown script error'); return decompiled.some(element => { if (typeof element === 'number') return false; return element.equals(pubkey) || element.equals(pubkeyHash); }); } type AllScriptType = | 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard'; type ScriptType = | 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard'; function classifyScript(script: Buffer): ScriptType { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; return 'nonstandard'; } function range(n: number): number[] { return [...Array(n).keys()]; }