@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
1,236 lines • 46.7 kB
JavaScript
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