UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

516 lines (515 loc) 19.6 kB
import { Logger } from '@btc-vision/logger'; import { address as bitAddress, crypto as bitCrypto, getFinalScripts, isP2MS, isP2PK, isP2PKH, isP2SHScript, isP2TR, isP2WPKH, isP2WSHScript, isUnknownSegwitVersion, opcodes, payments, PaymentType, script, toXOnly, varuint, } from '@btc-vision/bitcoin'; import { TweakedSigner } from '../../signer/TweakedSigner.js'; import { canSignNonTaprootInput, isTaprootInput, pubkeyInScript, } from '../../signer/SignerUtils.js'; export var TransactionSequence; (function (TransactionSequence) { TransactionSequence[TransactionSequence["REPLACE_BY_FEE"] = 4294967293] = "REPLACE_BY_FEE"; TransactionSequence[TransactionSequence["FINAL"] = 4294967295] = "FINAL"; })(TransactionSequence || (TransactionSequence = {})); export class TweakedTransaction extends Logger { constructor(data) { super(); this.logColor = '#00ffe1'; this.finalized = false; this.signed = false; this.scriptData = null; this.tapData = null; this.inputs = []; this.sequence = TransactionSequence.REPLACE_BY_FEE; this.tapLeafScript = null; this.isBrowser = false; this.regenerated = false; this.ignoreSignatureErrors = false; this.noSignatures = false; this.customFinalizerP2SH = (inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH) => { const inputDecoded = this.inputs[inputIndex]; if (isP2SH && input.partialSig && inputDecoded && inputDecoded.redeemScript) { const signatures = input.partialSig.map((sig) => sig.signature) || []; const scriptSig = script.compile([...signatures, inputDecoded.redeemScript]); return { finalScriptSig: scriptSig, finalScriptWitness: undefined, }; } return getFinalScripts(inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH, true, this.unlockScript); }; this.signer = data.signer; this.network = data.network; this.noSignatures = data.noSignatures || false; this.nonWitnessUtxo = data.nonWitnessUtxo; this.unlockScript = data.unlockScript; this.isBrowser = typeof window !== 'undefined'; } static readScriptWitnessToWitnessStack(Buffer) { let offset = 0; function readSlice(n) { const slice = Buffer.subarray(offset, offset + n); offset += n; return slice; } function readVarInt() { const varint = varuint.decode(Buffer, offset); offset += varint.bytes; return varint.numberValue || 0; } function readVarSlice() { const len = readVarInt(); return readSlice(len); } function readVector() { const count = readVarInt(); const vector = []; for (let i = 0; i < count; i++) { vector.push(readVarSlice()); } return vector; } return readVector(); } static preEstimateTaprootTransactionFees(feeRate, numInputs, numOutputs, numWitnessElements, witnessElementSize, emptyWitness, taprootControlWitnessSize = 32n, taprootScriptSize = 139n) { const txHeaderSize = 10n; const inputBaseSize = 41n; const outputSize = 68n; const taprootWitnessBaseSize = 1n; const baseTxSize = txHeaderSize + inputBaseSize * numInputs + outputSize * numOutputs; const witnessSize = numInputs * taprootWitnessBaseSize + numWitnessElements * witnessElementSize + taprootControlWitnessSize * numInputs + taprootScriptSize * numInputs + emptyWitness; const weight = baseTxSize * 3n + (baseTxSize + witnessSize); const vSize = weight / 4n; return vSize * feeRate; } static signInput(transaction, input, i, signer, sighashTypes) { if (sighashTypes && sighashTypes[0]) input.sighashType = sighashTypes[0]; transaction.signInput(i, signer, sighashTypes.length ? sighashTypes : undefined); } static calculateSignHash(sighashTypes) { if (!sighashTypes) { throw new Error('Sighash types are required'); } let signHash = 0; for (const sighashType of sighashTypes) { signHash |= sighashType; } return signHash || 0; } ignoreSignatureError() { this.ignoreSignatureErrors = true; } getScriptAddress() { if (!this.scriptData || !this.scriptData.address) { throw new Error('Tap data is required'); } return this.scriptData.address; } getTransaction() { return this.transaction.extractTransaction(false); } getTapAddress() { if (!this.tapData || !this.tapData.address) { throw new Error('Tap data is required'); } return this.tapData.address; } disableRBF() { if (this.signed) throw new Error('Transaction is already signed'); this.sequence = TransactionSequence.FINAL; for (const input of this.inputs) { input.sequence = TransactionSequence.FINAL; } } getTweakerHash() { return this.tapData?.hash; } preEstimateTransactionFees(feeRate, numInputs, numOutputs, numSignatures, numPubkeys) { const txHeaderSize = 10n; const inputBaseSize = 41n; const outputSize = 68n; const signatureSize = 144n; const pubkeySize = 34n; const baseTxSize = txHeaderSize + inputBaseSize * numInputs + outputSize * numOutputs; const redeemScriptSize = 1n + numPubkeys * (1n + pubkeySize) + 1n + numSignatures; const witnessSize = numSignatures * signatureSize + numPubkeys * pubkeySize + redeemScriptSize; const weight = baseTxSize * 3n + (baseTxSize + witnessSize); const vSize = weight / 4n; return vSize * feeRate; } generateTapData() { return { internalPubkey: this.internalPubKeyToXOnly(), network: this.network, name: PaymentType.P2TR, }; } generateScriptAddress() { return { internalPubkey: this.internalPubKeyToXOnly(), network: this.network, name: PaymentType.P2TR, }; } getSignerKey() { return this.signer; } async signInput(transaction, input, i, signer, reverse = false, errored = false) { const publicKey = signer.publicKey; let isTaproot = isTaprootInput(input); if (reverse) { isTaproot = !isTaproot; } let signed = false; let didError = false; if (isTaproot) { try { await this.attemptSignTaproot(transaction, input, i, signer, publicKey); signed = true; } catch (e) { this.error(`Failed to sign Taproot script path input ${i} (reverse: ${reverse}): ${e.message}`); didError = true; } } else { if (!reverse ? canSignNonTaprootInput(input, publicKey) : true) { try { await this.signNonTaprootInput(signer, transaction, i); signed = true; } catch (e) { this.error(`Failed to sign non-Taproot input ${i}: ${e.stack}`); didError = true; } } } if (!signed) { if (didError && errored) { throw new Error(`Failed to sign input ${i} with the provided signer.`); } try { await this.signInput(transaction, input, i, signer, true, didError); } catch { throw new Error(`Cannot sign input ${i} with the provided signer.`); } } } splitArray(arr, chunkSize) { if (chunkSize <= 0) { throw new Error('Chunk size must be greater than 0.'); } const result = []; for (let i = 0; i < arr.length; i += chunkSize) { result.push(arr.slice(i, i + chunkSize)); } return result; } async signInputs(transaction) { if ('multiSignPsbt' in this.signer) { await this.signInputsWalletBased(transaction); return; } await this.signInputsNonWalletBased(transaction); } async signInputsNonWalletBased(transaction) { const txs = transaction.data.inputs; const batchSize = 20; const batches = this.splitArray(txs, batchSize); if (!this.noSignatures) { for (let i = 0; i < batches.length; i++) { const batch = batches[i]; const promises = []; const offset = i * batchSize; for (let j = 0; j < batch.length; j++) { const index = offset + j; const input = batch[j]; try { promises.push(this.signInput(transaction, input, index, this.signer)); } catch (e) { this.log(`Failed to sign input ${index}: ${e.stack}`); } } await Promise.all(promises); } } for (let i = 0; i < transaction.data.inputs.length; i++) { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } this.finalized = true; } internalPubKeyToXOnly() { return toXOnly(Buffer.from(this.signer.publicKey)); } internalInit() { this.scriptData = payments.p2tr(this.generateScriptAddress()); this.tapData = payments.p2tr(this.generateTapData()); } tweakSigner() { if (this.tweakedSigner) return; this.tweakedSigner = this.getTweakedSigner(true); } getTweakedSigner(useTweakedHash = false, signer = this.signer) { const settings = { network: this.network, }; if (useTweakedHash) { settings.tweakHash = this.getTweakerHash(); } if (!('privateKey' in signer)) { return; } return TweakedSigner.tweakSigner(signer, settings); } generateP2SHRedeemScript(customWitnessScript) { const p2wsh = payments.p2wsh({ redeem: { output: customWitnessScript }, network: this.network, }); const p2sh = payments.p2sh({ redeem: p2wsh, network: this.network, }); return p2sh.output; } generateP2SHRedeemScriptLegacy(inputAddr) { const pubKeyHash = bitCrypto.hash160(this.signer.publicKey); const redeemScript = script.compile([ opcodes.OP_DUP, opcodes.OP_HASH160, pubKeyHash, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG, ]); const redeemScriptHash = bitCrypto.hash160(redeemScript); const outputScript = script.compile([ opcodes.OP_HASH160, redeemScriptHash, opcodes.OP_EQUAL, ]); const p2wsh = payments.p2wsh({ redeem: { output: redeemScript }, network: this.network, }); const p2sh = payments.p2sh({ redeem: p2wsh, network: this.network, }); const address = bitAddress.fromOutputScript(outputScript, this.network); if (address === inputAddr && p2sh.redeem && p2sh.redeem.output) { return { redeemScript, outputScript: p2sh.redeem.output, }; } return; } generateP2SHP2PKHRedeemScript(inputAddr) { const pubkey = Buffer.isBuffer(this.signer.publicKey) ? this.signer.publicKey : Buffer.from(this.signer.publicKey, 'hex'); const w = payments.p2wpkh({ pubkey: pubkey, network: this.network, }); const p = payments.p2sh({ redeem: w, network: this.network, }); const address = p.address; const redeemScript = p.redeem?.output; if (!redeemScript) { throw new Error('Failed to generate P2SH-P2WPKH redeem script'); } if (address === inputAddr && p.redeem && p.redeem.output && p.output) { return { redeemScript: p.redeem.output, outputScript: p.output, }; } return; } generatePsbtInputExtended(utxo, i, _extra = false) { const script = Buffer.from(utxo.scriptPubKey.hex, 'hex'); const input = { hash: utxo.transactionId, index: utxo.outputIndex, sequence: this.sequence, witnessUtxo: { value: Number(utxo.value), script, }, }; if (isP2PKH(script)) { if (utxo.nonWitnessUtxo) { input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo) ? utxo.nonWitnessUtxo : Buffer.from(utxo.nonWitnessUtxo, 'hex'); } else { throw new Error('Missing nonWitnessUtxo for P2PKH UTXO'); } } else if (isP2WPKH(script) || isUnknownSegwitVersion(script)) { } else if (isP2WSHScript(script)) { if (!utxo.witnessScript) { throw new Error('Missing witnessScript for P2WSH UTXO'); } input.witnessScript = Buffer.isBuffer(utxo.witnessScript) ? utxo.witnessScript : Buffer.from(utxo.witnessScript, 'hex'); } else if (isP2SHScript(script)) { let redeemScriptBuf; if (utxo.redeemScript) { redeemScriptBuf = Buffer.isBuffer(utxo.redeemScript) ? utxo.redeemScript : Buffer.from(utxo.redeemScript, 'hex'); } else { if (!utxo.scriptPubKey.address) { throw new Error('Missing redeemScript and no address to regenerate it for P2SH UTXO'); } const legacyScripts = this.generateP2SHP2PKHRedeemScript(utxo.scriptPubKey.address); if (!legacyScripts) { throw new Error('Missing redeemScript for P2SH UTXO and unable to regenerate'); } redeemScriptBuf = legacyScripts.redeemScript; } input.redeemScript = redeemScriptBuf; const payment = payments.p2sh({ redeem: { output: input.redeemScript } }); if (!payment.redeem) { throw new Error('Failed to extract redeem script from P2SH UTXO'); } const redeemOutput = payment.redeem.output; if (!redeemOutput) { throw new Error('Failed to extract redeem output from P2SH UTXO'); } if (utxo.nonWitnessUtxo) { input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo) ? utxo.nonWitnessUtxo : Buffer.from(utxo.nonWitnessUtxo, 'hex'); } if (isP2WPKH(redeemOutput)) { delete input.nonWitnessUtxo; } else if (isP2WSHScript(redeemOutput)) { delete input.nonWitnessUtxo; if (!input.witnessScript) { throw new Error('Missing witnessScript for P2SH-P2WSH UTXO'); } } else { delete input.witnessUtxo; } } else if (isP2TR(script)) { if (this.sighashTypes) { const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes); if (inputSign) input.sighashType = inputSign; } this.tweakSigner(); input.tapInternalKey = this.internalPubKeyToXOnly(); } else if (isP2PK(script) || isP2MS(script)) { if (utxo.nonWitnessUtxo) { input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo) ? utxo.nonWitnessUtxo : Buffer.from(utxo.nonWitnessUtxo, 'hex'); } else { throw new Error('Missing nonWitnessUtxo for P2PK or P2MS UTXO'); } } else { this.error(`Unknown or unsupported script type for output: ${utxo.scriptPubKey.hex}`); } if (i === 0) { if (this.tapLeafScript) { input.tapLeafScript = [this.tapLeafScript]; } if (this.nonWitnessUtxo) { input.nonWitnessUtxo = this.nonWitnessUtxo; } } return input; } async signInputsWalletBased(transaction) { const signer = this.signer; await signer.multiSignPsbt([transaction]); for (let i = 0; i < transaction.data.inputs.length; i++) { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } this.finalized = true; } async attemptSignTaproot(transaction, input, i, signer, publicKey) { const isScriptSpend = this.isTaprootScriptSpend(input, publicKey); if (isScriptSpend) { await this.signTaprootInput(signer, transaction, i); } else { let tweakedSigner; if (signer !== this.signer) { tweakedSigner = this.getTweakedSigner(true, signer); } else { if (!this.tweakedSigner) this.tweakSigner(); tweakedSigner = this.tweakedSigner; } if (tweakedSigner) { try { await this.signTaprootInput(tweakedSigner, transaction, i); } catch (e) { tweakedSigner = this.getTweakedSigner(false, this.signer); if (!tweakedSigner) { throw new Error(`Failed to obtain tweaked signer for input ${i}.`); } await this.signTaprootInput(tweakedSigner, transaction, i); } } else { this.error(`Failed to obtain tweaked signer for input ${i}.`); } } } isTaprootScriptSpend(input, publicKey) { if (input.tapLeafScript && input.tapLeafScript.length > 0) { for (const tapLeafScript of input.tapLeafScript) { if (pubkeyInScript(publicKey, tapLeafScript.script)) { return true; } } } return false; } async signTaprootInput(signer, transaction, i, tapLeafHash) { if ('signTaprootInput' in signer) { try { await signer.signTaprootInput(transaction, i, tapLeafHash); } catch { throw new Error('Failed to sign Taproot input with provided signer.'); } } else { transaction.signTaprootInput(i, signer); } } async signNonTaprootInput(signer, transaction, i) { if ('signInput' in signer) { await signer.signInput(transaction, i); } else { transaction.signInput(i, signer); } } }