UNPKG

@btc-vision/transaction

Version:

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

1,236 lines 46.7 kB
import { Logger } from '@btc-vision/logger'; import { address as bitAddress, applySignaturesToPsbt, crypto as bitCrypto, fromHex, getFinalScripts, isP2A, isP2MR, isP2MS, isP2PK, isP2PKH, isP2SHScript, isP2TR, isP2WPKH, isP2WSHScript, isUnknownSegwitVersion, opcodes, payments, PaymentType, prepareSigningTasks, Psbt, script, toXOnly, Transaction, varuint, WorkerSigningPool, } from '@btc-vision/bitcoin'; import { isUniversalSigner, TweakedSigner, } from '../../signer/TweakedSigner.js'; import {} from '@btc-vision/ecpair'; import { UnisatSigner } from '../browser/extensions/UnisatSigner.js'; import { canSignNonTaprootInput, isTaprootInput, pubkeyInScript, } from '../../signer/SignerUtils.js'; import { witnessStackToScriptWitness } from '../utils/WitnessUtils.js'; import { P2WDADetector } from '../../p2wda/P2WDADetector.js'; import { MessageSigner } from '../../keypair/MessageSigner.js'; import {} from '../../signer/AddressRotation.js'; import { toTweakedParallelKeyPair } from '../../signer/ParallelSignerAdapter.js'; /** * The transaction sequence */ export var TransactionSequence; (function (TransactionSequence) { TransactionSequence[TransactionSequence["REPLACE_BY_FEE"] = 4294967293] = "REPLACE_BY_FEE"; TransactionSequence[TransactionSequence["FINAL"] = 4294967295] = "FINAL"; })(TransactionSequence || (TransactionSequence = {})); export var CSVModes; (function (CSVModes) { CSVModes[CSVModes["BLOCKS"] = 0] = "BLOCKS"; CSVModes[CSVModes["TIMESTAMPS"] = 1] = "TIMESTAMPS"; })(CSVModes || (CSVModes = {})); /** * @description PSBT Transaction processor. * */ export class TweakedTransaction extends Logger { logColor = '#00ffe1'; finalized = false; /** * @description Was the transaction signed? */ signer; /** * @description Tweaked signer */ tweakedSigner; /** * @description The network of the transaction */ network; /** * @description Was the transaction signed? */ signed = false; /** * @description The sighash types of the transaction * @protected */ sighashTypes; /** * @description The script data of the transaction */ scriptData = null; /** * @description The tap data of the transaction */ tapData = null; /** * @description The inputs of the transaction */ inputs = []; /** * @description The sequence of the transaction * @protected */ sequence = TransactionSequence.REPLACE_BY_FEE; /** * The tap leaf script * @protected */ tapLeafScript = null; /** * Add a non-witness utxo to the transaction * @protected */ nonWitnessUtxo; /** * Is the transaction being generated inside a browser? * @protected */ isBrowser = false; /** * Track which inputs contain CSV scripts * @protected */ csvInputIndices = new Set(); anchorInputIndices = new Set(); regenerated = false; ignoreSignatureErrors = false; noSignatures = false; unlockScript; txVersion = 2; _mldsaSigner = null; _hashedPublicKey = null; /** * Whether address rotation mode is enabled. * When true, each UTXO can be signed by a different signer. */ addressRotationEnabled = false; /** * Map of addresses to their respective signers for address rotation mode. */ signerMap = new Map(); /** * Map of input indices to their signers (resolved from UTXOs or signerMap). * Populated during input addition. */ inputSignerMap = new Map(); /** * Cache of tweaked signers per input for address rotation mode. */ tweakedSignerCache = new Map(); /** * Parallel signing configuration using worker threads. * When set, key-path taproot inputs are signed in parallel via workers. */ parallelSigningConfig; /** * Whether to use P2MR (Pay-to-Merkle-Root, BIP 360) instead of P2TR. */ useP2MR = false; constructor(data) { super(); this.signer = data.signer; this.network = data.network; this.noSignatures = data.noSignatures || false; if (data.nonWitnessUtxo !== undefined) { this.nonWitnessUtxo = data.nonWitnessUtxo; } if (data.unlockScript !== undefined) { this.unlockScript = data.unlockScript; } this.isBrowser = typeof window !== 'undefined'; if (data.txVersion) { this.txVersion = data.txVersion; } if (data.mldsaSigner) { this._mldsaSigner = data.mldsaSigner; this._hashedPublicKey = MessageSigner.sha256(this._mldsaSigner.publicKey); } // Initialize address rotation if (data.addressRotation?.enabled) { this.addressRotationEnabled = true; this.signerMap = data.addressRotation.signerMap; } if (data.parallelSigning) { this.parallelSigningConfig = data.parallelSigning; } if (data.useP2MR) { this.useP2MR = true; } } /** * Get the MLDSA signer * @protected */ get mldsaSigner() { if (!this._mldsaSigner) { throw new Error('MLDSA Signer is not set'); } return this._mldsaSigner; } /** * Get the hashed public key * @protected */ get hashedPublicKey() { if (!this._hashedPublicKey) { throw new Error('Hashed public key is not set'); } return this._hashedPublicKey; } /** * Whether parallel signing can be used for this transaction. * Requires parallelSigningConfig and excludes browser, address rotation, and no-signature modes. */ get canUseParallelSigning() { return !!this.parallelSigningConfig && !this.addressRotationEnabled && !this.noSignatures; } /** * Read witnesses * @protected */ static readScriptWitnessToWitnessStack(buffer) { let offset = 0; function readSlice(n) { const slice = new Uint8Array(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(); } /** * Pre-estimate the transaction fees for a Taproot transaction * @param {bigint} feeRate - The fee rate in satoshis per virtual byte * @param {bigint} numInputs - The number of inputs * @param {bigint} numOutputs - The number of outputs * @param {bigint} numWitnessElements - The number of witness elements (e.g., number of control blocks and witnesses) * @param {bigint} witnessElementSize - The average size of each witness element in bytes * @param {bigint} emptyWitness - The amount of empty witnesses * @param {bigint} [taprootControlWitnessSize=139n] - The size of the control block witness in bytes * @param {bigint} [taprootScriptSize=32n] - The size of the taproot script in bytes * @returns {bigint} - The estimated transaction fees */ static preEstimateTaprootTransactionFees(feeRate, // satoshis per virtual byte numInputs, numOutputs, numWitnessElements, witnessElementSize, emptyWitness, taprootControlWitnessSize = 32n, taprootScriptSize = 139n) { const txHeaderSize = 10n; const inputBaseSize = 41n; const outputSize = 68n; const taprootWitnessBaseSize = 1n; // Base witness size per input (without signatures and control blocks) // Base transaction size (excluding witness data) const baseTxSize = txHeaderSize + inputBaseSize * numInputs + outputSize * numOutputs; // Witness data size for Taproot const witnessSize = numInputs * taprootWitnessBaseSize + numWitnessElements * witnessElementSize + taprootControlWitnessSize * numInputs + taprootScriptSize * numInputs + emptyWitness; // Total weight and virtual size 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); } /** * Calculate the sign hash number * @description Calculates the sign hash * @protected * @returns {number} */ 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; } [Symbol.dispose]() { this.inputs.length = 0; this.scriptData = null; this.tapData = null; this.tapLeafScript = null; delete this.tweakedSigner; this.csvInputIndices.clear(); this.anchorInputIndices.clear(); this.inputSignerMap.clear(); this.tweakedSignerCache.clear(); delete this.parallelSigningConfig; } /** * Check if address rotation mode is enabled. */ isAddressRotationEnabled() { return this.addressRotationEnabled; } ignoreSignatureError() { this.ignoreSignatureErrors = true; } /** * @description Returns the script address * @returns {string} */ getScriptAddress() { if (!this.scriptData || !this.scriptData.address) { throw new Error('Tap data is required'); } return this.scriptData.address; } /** * @description Returns the transaction * @returns {Transaction} */ getTransaction() { return this.transaction.extractTransaction(false); } /** * @description Returns the tap address * @returns {string} * @throws {Error} - If tap data is not set */ getTapAddress() { if (!this.tapData || !this.tapData.address) { throw new Error('Tap data is required'); } return this.tapData.address; } /** * @description Disables replace by fee on the transaction */ disableRBF() { if (this.signed) throw new Error('Transaction is already signed'); this.sequence = TransactionSequence.FINAL; for (const input of this.inputs) { // This would disable CSV! You need to check if the input has CSV if (this.csvInputIndices.has(this.inputs.indexOf(input))) { continue; } input.sequence = TransactionSequence.FINAL; } } /** * Get the tweaked hash * @private * * @returns {Uint8Array | undefined} The tweaked hash */ getTweakerHash() { return this.tapData?.hash; } /** * Pre-estimate the transaction fees * @param {bigint} feeRate - The fee rate * @param {bigint} numInputs - The number of inputs * @param {bigint} numOutputs - The number of outputs * @param {bigint} numSignatures - The number of signatures * @param {bigint} numPubkeys - The number of public keys * @returns {bigint} - The estimated transaction fees */ preEstimateTransactionFees(feeRate, // satoshis per byte numInputs, numOutputs, numSignatures, numPubkeys) { const txHeaderSize = 10n; const inputBaseSize = 41n; const outputSize = 68n; const signatureSize = 144n; const pubkeySize = 34n; // Base transaction size (excluding witness data) const baseTxSize = txHeaderSize + inputBaseSize * numInputs + outputSize * numOutputs; // Witness data size const redeemScriptSize = 1n + numPubkeys * (1n + pubkeySize) + 1n + numSignatures; const witnessSize = numSignatures * signatureSize + numPubkeys * pubkeySize + redeemScriptSize; // Total weight and virtual size const weight = baseTxSize * 3n + (baseTxSize + witnessSize); const vSize = weight / 4n; return vSize * feeRate; } /** * Get the signer for a specific input index. * Returns the input-specific signer if in rotation mode, otherwise the default signer. * @param inputIndex - The index of the input */ getSignerForInput(inputIndex) { if (this.addressRotationEnabled) { const inputSigner = this.inputSignerMap.get(inputIndex); if (inputSigner) { return inputSigner; } } return this.signer; } /** * Register a signer for a specific input index. * Called during UTXO processing to map each input to its signer. * @param inputIndex - The index of the input * @param utxo - The UTXO being added */ registerInputSigner(inputIndex, utxo) { if (!this.addressRotationEnabled) { return; } // Priority 1: UTXO has an explicit signer attached if (utxo.signer) { this.inputSignerMap.set(inputIndex, utxo.signer); return; } // Priority 2: Look up signer from signerMap by address const address = utxo.scriptPubKey?.address; if (address && this.signerMap.has(address)) { const signer = this.signerMap.get(address); if (signer) { this.inputSignerMap.set(inputIndex, signer); return; } } // Fallback: Use default signer (no entry in inputSignerMap) } /** * Get the x-only public key for a specific input's signer. * Used for taproot inputs in address rotation mode. * @param inputIndex - The index of the input */ internalPubKeyToXOnlyForInput(inputIndex) { const signer = this.getSignerForInput(inputIndex); return toXOnly(signer.publicKey); } /** * Get the tweaked signer for a specific input. * Caches the result for efficiency. * @param inputIndex - The index of the input * @param useTweakedHash - Whether to use the tweaked hash */ getTweakedSignerForInput(inputIndex, useTweakedHash = false) { if (!this.addressRotationEnabled) { // Fall back to original behavior if (useTweakedHash) { this.tweakSigner(); return this.tweakedSigner; } return this.getTweakedSigner(useTweakedHash); } // Check cache const cacheKey = inputIndex * 2 + (useTweakedHash ? 1 : 0); if (this.tweakedSignerCache.has(cacheKey)) { return this.tweakedSignerCache.get(cacheKey); } const signer = this.getSignerForInput(inputIndex); const tweaked = this.getTweakedSigner(useTweakedHash, signer); this.tweakedSignerCache.set(cacheKey, tweaked); return tweaked; } generateTapData() { if (this.useP2MR) { return { network: this.network, name: PaymentType.P2MR, }; } return { internalPubkey: this.internalPubKeyToXOnly(), network: this.network, name: PaymentType.P2TR, }; } /** * Generates the script address. * @protected * @returns {TapPayment} */ generateScriptAddress() { if (this.useP2MR) { return { network: this.network, name: PaymentType.P2MR, }; } return { internalPubkey: this.internalPubKeyToXOnly(), network: this.network, name: PaymentType.P2TR, }; } /** * Returns the signer key. * @protected * @returns {Signer | UniversalSigner} */ getSignerKey() { return this.signer; } /** * Signs an input of the transaction. * @param {Psbt} transaction - The transaction to sign * @param {PsbtInput} input - The input to sign * @param {number} i - The index of the input * @param {Signer} signer - The signer to use * @param {boolean} [reverse=false] - Should the input be signed in reverse * @param {boolean} [errored=false] - Was there an error * @protected */ async signInput(transaction, input, i, signer, reverse = false, errored = false) { if (this.anchorInputIndices.has(i)) return; 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 { // Non-Taproot input 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; } /** * Signs all the inputs of the transaction. * @param {Psbt} transaction - The transaction to sign * @protected * @returns {Promise<void>} */ async signInputs(transaction) { if ('multiSignPsbt' in this.signer) { await this.signInputsWalletBased(transaction); return; } await this.signInputsNonWalletBased(transaction); } async signInputsNonWalletBased(transaction) { if (!this.noSignatures) { if (this.canUseParallelSigning && isUniversalSigner(this.signer)) { let parallelSignedIndices = new Set(); try { const result = await this.signKeyPathInputsParallel(transaction); if (result.success) { parallelSignedIndices = new Set(result.signatures.keys()); } } catch (e) { this.error(`Parallel signing failed, falling back to sequential: ${e.message}`); } // Sign remaining inputs (script-path, non-taproot, etc.) sequentially await this.signRemainingInputsSequential(transaction, parallelSignedIndices); } else { await this.signInputsSequential(transaction); } } for (let i = 0; i < transaction.data.inputs.length; i++) { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } this.finalized = true; } /** * Signs all inputs sequentially in batches of 20. * This is the original signing logic, used as fallback when parallel signing is unavailable. */ async signInputsSequential(transaction) { const txs = transaction.data.inputs; const batchSize = 20; const batches = this.splitArray(txs, batchSize); 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 { // Use per-input signer in address rotation mode const inputSigner = this.getSignerForInput(index); promises.push(this.signInput(transaction, input, index, inputSigner)); } catch (e) { this.log(`Failed to sign input ${index}: ${e.stack}`); } } await Promise.all(promises); } } /** * Signs inputs that were not handled by parallel signing. * After parallel key-path signing, script-path taproot inputs, non-taproot inputs, * and any inputs that failed parallel signing need sequential signing. */ async signRemainingInputsSequential(transaction, signedIndices) { const txs = transaction.data.inputs; const unsignedIndices = []; for (let i = 0; i < txs.length; i++) { if (!signedIndices.has(i)) { unsignedIndices.push(i); } } if (unsignedIndices.length === 0) return; const batchSize = 20; const batches = this.splitArray(unsignedIndices, batchSize); for (const batch of batches) { const promises = []; for (const index of batch) { const input = txs[index]; try { const inputSigner = this.getSignerForInput(index); promises.push(this.signInput(transaction, input, index, inputSigner)); } catch (e) { this.log(`Failed to sign input ${index}: ${e.stack}`); } } await Promise.all(promises); } } /** * Converts the public key to x-only. * @protected * @returns {Uint8Array} */ internalPubKeyToXOnly() { return toXOnly(this.signer.publicKey); } /** * Internal init. * @protected */ internalInit() { const scriptParams = this.generateScriptAddress(); const tapParams = this.generateTapData(); if (scriptParams.name === PaymentType.P2MR) { this.scriptData = payments.p2mr(scriptParams); } else { this.scriptData = payments.p2tr(scriptParams); } if (tapParams.name === PaymentType.P2MR) { this.tapData = payments.p2mr(tapParams); } else { this.tapData = payments.p2tr(tapParams); } } /** * Tweak the signer for the interaction * @protected */ tweakSigner() { if (this.tweakedSigner) return; // tweaked p2tr signer. const tweaked = this.getTweakedSigner(true); if (tweaked !== undefined) { this.tweakedSigner = tweaked; } } /** * Get the tweaked signer * @private * @returns {UniversalSigner} The tweaked signer */ getTweakedSigner(useTweakedHash = false, signer = this.signer) { const settings = { network: this.network, }; if (useTweakedHash) { const tweakHash = this.getTweakerHash(); if (tweakHash !== undefined) { settings.tweakHash = tweakHash; } } if (!isUniversalSigner(signer)) { return; } return TweakedSigner.tweakSigner(signer, settings); } /** * Signs key-path taproot inputs in parallel using worker threads. * @param transaction - The PSBT to sign * @param excludeIndices - Input indices to skip (e.g., script-path inputs already signed) * @returns The parallel signing result */ async signKeyPathInputsParallel(transaction, excludeIndices) { const signer = this.signer; // Get the tweaked signer for key-path const tweakedSigner = this.getTweakedSigner(true); if (!tweakedSigner) { throw new Error('Cannot create tweaked signer for parallel signing'); } // Create hybrid adapter: untweaked pubkey (for PSBT matching) + tweaked privkey const adapter = toTweakedParallelKeyPair(signer, tweakedSigner); // Prepare tasks from PSBT const allTasks = prepareSigningTasks(transaction, adapter); // Filter out excluded indices const tasks = excludeIndices ? allTasks.filter((t) => !excludeIndices.has(t.inputIndex)) : allTasks; if (tasks.length === 0) { return { success: true, signatures: new Map(), errors: new Map(), durationMs: 0, }; } // Get or create pool let pool; let shouldShutdown = false; if (this.parallelSigningConfig && 'signBatch' in this.parallelSigningConfig) { pool = this.parallelSigningConfig; } else { pool = WorkerSigningPool.getInstance(this.parallelSigningConfig); if (!pool.isPreservingWorkers) shouldShutdown = true; } try { await pool.initialize(); const result = await pool.signBatch(tasks, adapter); if (result.success) { applySignaturesToPsbt(transaction, result, adapter); } else { const errorEntries = [...result.errors.entries()]; const errorMsg = errorEntries .map(([idx, err]) => `Input ${idx}: ${err}`) .join(', '); this.error(`Parallel signing had errors: ${errorMsg}`); } return result; } finally { if (shouldShutdown) await pool.shutdown(); } } generateP2SHRedeemScript(customWitnessScript) { const p2wsh = payments.p2wsh({ redeem: { output: customWitnessScript }, network: this.network, }); // Wrap the P2WSH inside a P2SH (Pay-to-Script-Hash) 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 }, // Use the custom redeem script network: this.network, }); // Wrap the P2WSH in a P2SH const p2sh = payments.p2sh({ redeem: p2wsh, // The P2WSH is wrapped inside the P2SH 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, inputIndex) { // Use per-input signer in address rotation mode const signer = this.addressRotationEnabled && inputIndex !== undefined ? this.getSignerForInput(inputIndex) : this.signer; const pubkey = signer.publicKey; 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; } /** * Generate the PSBT input extended, supporting various script types * @param {UTXO} utxo The UTXO * @param {number} i The index of the input * @param {UTXO} _extra Extra UTXO * @protected * @returns {PsbtInputExtended} The PSBT input extended */ generatePsbtInputExtended(utxo, i, _extra = false) { const scriptPub = fromHex(utxo.scriptPubKey.hex); const input = { hash: utxo.transactionId, index: utxo.outputIndex, sequence: this.sequence, witnessUtxo: { value: utxo.value, script: scriptPub, }, }; // Handle P2PKH (Legacy) if (isP2PKH(scriptPub)) { // Legacy input requires nonWitnessUtxo if (utxo.nonWitnessUtxo) { //delete input.witnessUtxo; input.nonWitnessUtxo = utxo.nonWitnessUtxo instanceof Uint8Array ? utxo.nonWitnessUtxo : fromHex(utxo.nonWitnessUtxo); } else { throw new Error('Missing nonWitnessUtxo for P2PKH UTXO'); } } // Handle P2WPKH (SegWit) else if (isP2WPKH(scriptPub) || isUnknownSegwitVersion(scriptPub)) { // No redeemScript required for pure P2WPKH // witnessUtxo is enough, no nonWitnessUtxo needed. } // Handle P2WSH (SegWit) else if (isP2WSHScript(scriptPub)) { this.processP2WSHInput(utxo, input, i); } // Handle P2SH (Can be legacy or wrapping segwit) else if (isP2SHScript(scriptPub)) { // Redeem script is required for P2SH let redeemScriptBuf; if (utxo.redeemScript) { redeemScriptBuf = utxo.redeemScript instanceof Uint8Array ? utxo.redeemScript : fromHex(utxo.redeemScript); } else { // Attempt to generate a redeem script if missing 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, i); if (!legacyScripts) { throw new Error('Missing redeemScript for P2SH UTXO and unable to regenerate'); } redeemScriptBuf = legacyScripts.redeemScript; } input.redeemScript = redeemScriptBuf; // Check if redeemScript is wrapping segwit (like P2SH-P2WPKH or P2SH-P2WSH) 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 = utxo.nonWitnessUtxo instanceof Uint8Array ? utxo.nonWitnessUtxo : fromHex(utxo.nonWitnessUtxo); } if (isP2WPKH(redeemOutput)) { // P2SH-P2WPKH // Use witnessUtxo + redeemScript Reflect.deleteProperty(input, 'nonWitnessUtxo'); // ensure we do NOT have nonWitnessUtxo // witnessScript is not needed } else if (isP2WSHScript(redeemOutput)) { // P2SH-P2WSH // Use witnessUtxo + redeemScript + witnessScript Reflect.deleteProperty(input, 'nonWitnessUtxo'); // ensure we do NOT have nonWitnessUtxo this.processP2WSHInput(utxo, input, i); } else { // Legacy P2SH // Use nonWitnessUtxo Reflect.deleteProperty(input, 'witnessUtxo'); // ensure we do NOT have witnessUtxo } } // Handle P2TR (Taproot) else if (isP2TR(scriptPub)) { // Taproot inputs do not require nonWitnessUtxo, witnessUtxo is sufficient. // If there's a configured sighash type if (this.sighashTypes) { const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes); if (inputSign) input.sighashType = inputSign; } // Taproot internal key - use per-input signer in address rotation mode if (this.addressRotationEnabled) { input.tapInternalKey = this.internalPubKeyToXOnlyForInput(i); } else { this.tweakSigner(); input.tapInternalKey = this.internalPubKeyToXOnly(); } } // Handle P2MR (Pay-to-Merkle-Root, BIP 360, SegWit v2) else if (isP2MR(scriptPub)) { // P2MR inputs do not require nonWitnessUtxo, witnessUtxo is sufficient. // P2MR has no internal pubkey (no key-path spend). if (this.sighashTypes) { const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes); if (inputSign) input.sighashType = inputSign; } // For the transaction's own script-path input (index 0), set tapMerkleRoot // from this transaction's tap data. External P2MR UTXOs at other indices // do not need tapMerkleRoot set here — their tapLeafScript is sufficient. if (i === 0 && this.tapData?.hash) { input.tapMerkleRoot = this.tapData.hash; } } // Handle P2A (Any SegWit version, future versions) else if (isP2A(scriptPub)) { this.anchorInputIndices.add(i); input.isPayToAnchor = true; } // Handle P2PK (legacy) or P2MS (bare multisig) else if (isP2PK(scriptPub) || isP2MS(scriptPub)) { // These are legacy scripts, need nonWitnessUtxo if (utxo.nonWitnessUtxo) { input.nonWitnessUtxo = utxo.nonWitnessUtxo instanceof Uint8Array ? utxo.nonWitnessUtxo : fromHex(utxo.nonWitnessUtxo); } 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) { // TapLeafScript if available if (this.tapLeafScript) { input.tapLeafScript = [this.tapLeafScript]; } if (this.nonWitnessUtxo) { input.nonWitnessUtxo = this.nonWitnessUtxo; } } return input; } processP2WSHInput(utxo, input, i) { // P2WSH requires a witnessScript if (!utxo.witnessScript) { // Can't just invent a witnessScript out of thin air. If not provided, it's an error. throw new Error('Missing witnessScript for P2WSH UTXO'); } input.witnessScript = utxo.witnessScript instanceof Uint8Array ? utxo.witnessScript : fromHex(utxo.witnessScript); // No nonWitnessUtxo needed for segwit const decompiled = script.decompile(input.witnessScript); if (decompiled && this.isCSVScript(decompiled)) { const decompiled = script.decompile(input.witnessScript); if (decompiled && this.isCSVScript(decompiled)) { this.csvInputIndices.add(i); // Extract CSV value from witness script const csvBlocks = this.extractCSVBlocks(decompiled); // Use the setCSVSequence method to properly set the sequence input.sequence = this.setCSVSequence(csvBlocks, this.sequence); } } } secondsToCSVTimeUnits(seconds) { return Math.floor(seconds / 512); } createTimeBasedCSV(seconds) { const timeUnits = this.secondsToCSVTimeUnits(seconds); if (timeUnits > 0xffff) { throw new Error(`Time units ${timeUnits} exceeds maximum of 65,535`); } return timeUnits | (1 << 22); } isCSVEnabled(sequence) { return (sequence & (1 << 31)) === 0; } extractCSVValue(sequence) { return sequence & 0x0000ffff; } customFinalizerP2SH = (inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH, _canRunChecks) => { 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, }; } if (this.anchorInputIndices.has(inputIndex)) { return { finalScriptSig: undefined, finalScriptWitness: Uint8Array.from([0]), }; } if (isP2WSH && isSegwit && input.witnessScript) { if (!input.partialSig || input.partialSig.length === 0) { throw new Error(`No signatures for P2WSH input #${inputIndex}`); } const isP2WDA = P2WDADetector.isP2WDAWitnessScript(input.witnessScript); if (isP2WDA) { return this.finalizeSecondaryP2WDA(inputIndex, input); } // Check if this is a CSV input const isCSVInput = this.csvInputIndices.has(inputIndex); if (isCSVInput) { // For CSV P2WSH, the witness stack should be: [signature, witnessScript] const witnessStack = [ input.partialSig[0].signature, input.witnessScript, ]; return { finalScriptSig: undefined, finalScriptWitness: witnessStackToScriptWitness(witnessStack), }; } // For non-CSV P2WSH, use default finalization } return getFinalScripts(inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH, true, this.unlockScript); }; /** * Finalize secondary P2WDA inputs with empty data */ finalizeSecondaryP2WDA(inputIndex, input) { if (!input.partialSig || input.partialSig.length === 0) { throw new Error(`No signature for P2WDA input #${inputIndex}`); } if (!input.witnessScript) { throw new Error(`No witness script for P2WDA input #${inputIndex}`); } const witnessStack = P2WDADetector.createSimpleP2WDAWitness(input.partialSig[0].signature, input.witnessScript); return { finalScriptSig: undefined, finalScriptWitness: witnessStackToScriptWitness(witnessStack), }; } async signInputsWalletBased(transaction) { const signer = this.signer; // then, we sign all the remaining inputs with the wallet signer. await signer.multiSignPsbt([transaction]); // Then, we finalize every input. for (let i = 0; i < transaction.data.inputs.length; i++) { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } this.finalized = true; } isCSVScript(decompiled) { return decompiled.some((op) => op === opcodes.OP_CHECKSEQUENCEVERIFY); } setCSVSequence(csvBlocks, currentSequence) { if (this.txVersion < 2) { throw new Error('CSV requires transaction version 2 or higher'); } if (csvBlocks > 0xffff) { throw new Error(`CSV blocks ${csvBlocks} exceeds maximum of 65,535`); } // Layout of nSequence field (32 bits) when CSV is active (bit 31 = 0): // Bit 31: Must be 0 (CSV enable flag) // Bits 23-30: Unused by BIP68 (available for custom use) // Bit 22: Time flag (0 = blocks, 1 = time) // Bits 16-21: Unused by BIP68 (available for custom use) // Bits 0-15: CSV lock-time value // Extract the time flag if it's set in csvBlocks const isTimeBased = (csvBlocks & (1 << 22)) !== 0; // Start with the CSV value let sequence = csvBlocks & 0x0000ffff; // Preserve the time flag if set if (isTimeBased) { sequence |= 1 << 22; } if (currentSequence === TransactionSequence.REPLACE_BY_FEE) { // Set bit 25 as our explicit RBF flag // This is in the unused range (bits 23-30) when CSV is active sequence |= 1 << 25; // We could use other unused bits for version/features // sequence |= (1 << 26); // Could indicate tx flags if we wanted } // Final safety check: ensure bit 31 is 0 (CSV enabled) sequence = sequence & 0x7fffffff; return sequence; } getCSVType(csvValue) { // Bit 22 determines if it's time-based (1) or block-based (0) return csvValue & (1 << 22) ? CSVModes.TIMESTAMPS : CSVModes.BLOCKS; } extractCSVBlocks(decompiled) { for (let i = 0; i < decompiled.length; i++) { if (decompiled[i] === opcodes.OP_CHECKSEQUENCEVERIFY && i > 0) { const csvValue = decompiled[i - 1]; if (csvValue instanceof Uint8Array) { return script.number.decode(csvValue); } else if (typeof csvValue === 'number') { // Handle OP_N directly if (csvValue === opcodes.OP_0 || csvValue === opcodes.OP_FALSE) { return 0; } else if (csvValue === opcodes.OP_1NEGATE) { return -1; } else if (csvValue >= opcodes.OP_1 && csvValue <= opcodes.OP_16) { return csvValue - opcodes.OP_1 + 1; } else { // For other numbers, they should have been Buffers // This shouldn't happen in properly decompiled scripts throw new Error(`Unexpected raw number in script: ${csvValue}`); } } } } return 0; } 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}.`, { cause: e, }); } 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) { // Check if the signer's public key is involved in any tapLeafScript for (const tapLeafScript of input.tapLeafScript) { if (pubkeyInScript(publicKey, tapLeafScript.script)) { // The public key is in the script; it's a script spend 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); //tapLeafHash } } async signNonTaprootInput(signer, transaction, i) { if ('signInput' in signer) { await signer.signInput(transaction, i); } else { transaction.signInput(i, signer); } } } //# sourceMappingURL=TweakedTransaction.js.map