@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
972 lines (841 loc) • 30.7 kB
text/typescript
import { Logger } from '@btc-vision/logger';
import {
address as bitAddress,
crypto as bitCrypto,
getFinalScripts,
isP2MS,
isP2PK,
isP2PKH,
isP2SHScript,
isP2TR,
isP2WPKH,
isP2WSHScript,
isUnknownSegwitVersion,
Network,
opcodes,
P2TRPayment,
Payment,
payments,
PaymentType,
Psbt,
PsbtInput,
PsbtInputExtended,
script,
Signer,
toXOnly,
Transaction,
varuint,
} from '@btc-vision/bitcoin';
import { TweakedSigner, TweakSettings } from '../../signer/TweakedSigner.js';
import { ECPairInterface } from 'ecpair';
import { UTXO } from '../../utxo/interfaces/IUTXO.js';
import { TapLeafScript } from '../interfaces/Tap.js';
import { ChainId } from '../../network/ChainId.js';
import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
import {
canSignNonTaprootInput,
isTaprootInput,
pubkeyInScript,
} from '../../signer/SignerUtils.js';
export interface ITweakedTransactionData {
readonly signer: Signer | ECPairInterface | UnisatSigner;
readonly network: Network;
readonly chainId?: ChainId;
readonly nonWitnessUtxo?: Buffer;
readonly noSignatures?: boolean;
readonly unlockScript?: Buffer[];
}
/**
* The transaction sequence
*/
export enum TransactionSequence {
REPLACE_BY_FEE = 0xfffffffd,
FINAL = 0xffffffff,
}
/**
* @description PSBT Transaction processor.
* */
export abstract class TweakedTransaction extends Logger {
public readonly logColor: string = '#00ffe1';
public finalized: boolean = false;
/**
* @description Was the transaction signed?
*/
protected signer: Signer | ECPairInterface | UnisatSigner;
/**
* @description Tweaked signer
*/
protected tweakedSigner?: ECPairInterface;
/**
* @description The network of the transaction
*/
protected network: Network;
/**
* @description Was the transaction signed?
*/
protected signed: boolean = false;
/**
* @description The transaction
* @protected
*/
protected abstract readonly transaction: Psbt;
/**
* @description The sighash types of the transaction
* @protected
*/
protected sighashTypes: number[] | undefined;
/**
* @description The script data of the transaction
*/
protected scriptData: P2TRPayment | null = null;
/**
* @description The tap data of the transaction
*/
protected tapData: P2TRPayment | null = null;
/**
* @description The inputs of the transaction
*/
protected readonly inputs: PsbtInputExtended[] = [];
/**
* @description The sequence of the transaction
* @protected
*/
protected sequence: number = TransactionSequence.REPLACE_BY_FEE;
/**
* The tap leaf script
* @protected
*/
protected tapLeafScript: TapLeafScript | null = null;
/**
* Add a non-witness utxo to the transaction
* @protected
*/
protected nonWitnessUtxo?: Buffer;
/**
* Is the transaction being generated inside a browser?
* @protected
*/
protected readonly isBrowser: boolean = false;
protected regenerated: boolean = false;
protected ignoreSignatureErrors: boolean = false;
protected noSignatures: boolean = false;
protected unlockScript: Buffer[] | undefined;
protected constructor(data: ITweakedTransactionData) {
super();
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';
}
/**
* Read witnesses
* @protected
*/
public static readScriptWitnessToWitnessStack(Buffer: Buffer): Buffer[] {
let offset = 0;
function readSlice(n: number): Buffer {
const slice = Buffer.subarray(offset, offset + n);
offset += n;
return slice;
}
function readVarInt(): number {
const varint = varuint.decode(Buffer, offset);
offset += varint.bytes;
return varint.numberValue || 0;
}
function readVarSlice(): Buffer {
const len = readVarInt();
return readSlice(len);
}
function readVector(): Buffer[] {
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
*/
public static preEstimateTaprootTransactionFees(
feeRate: bigint, // satoshis per virtual byte
numInputs: bigint,
numOutputs: bigint,
numWitnessElements: bigint,
witnessElementSize: bigint,
emptyWitness: bigint,
taprootControlWitnessSize: bigint = 32n,
taprootScriptSize: bigint = 139n,
): bigint {
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;
}
protected static signInput(
transaction: Psbt,
input: PsbtInput,
i: number,
signer: Signer | ECPairInterface,
sighashTypes: number[],
): void {
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}
*/
protected static calculateSignHash(sighashTypes: number[]): number {
if (!sighashTypes) {
throw new Error('Sighash types are required');
}
let signHash: number = 0;
for (const sighashType of sighashTypes) {
signHash |= sighashType;
}
return signHash || 0;
}
public ignoreSignatureError(): void {
this.ignoreSignatureErrors = true;
}
/**
* @description Returns the script address
* @returns {string}
*/
public getScriptAddress(): string {
if (!this.scriptData || !this.scriptData.address) {
throw new Error('Tap data is required');
}
return this.scriptData.address;
}
/**
* @description Returns the transaction
* @returns {Transaction}
*/
public getTransaction(): Transaction {
return this.transaction.extractTransaction(false);
}
/**
* @description Returns the tap address
* @returns {string}
* @throws {Error} - If tap data is not set
*/
public getTapAddress(): string {
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
*/
public disableRBF(): void {
if (this.signed) throw new Error('Transaction is already signed');
this.sequence = TransactionSequence.FINAL;
for (const input of this.inputs) {
input.sequence = TransactionSequence.FINAL;
}
}
/**
* Get the tweaked hash
* @private
*
* @returns {Buffer | undefined} The tweaked hash
*/
public getTweakerHash(): Buffer | undefined {
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
*/
public preEstimateTransactionFees(
feeRate: bigint, // satoshis per byte
numInputs: bigint,
numOutputs: bigint,
numSignatures: bigint,
numPubkeys: bigint,
): bigint {
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;
}
protected generateTapData(): P2TRPayment {
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
name: PaymentType.P2TR,
};
}
/**
* Generates the script address.
* @protected
* @returns {Payment}
*/
protected generateScriptAddress(): P2TRPayment {
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
name: PaymentType.P2TR,
};
}
/**
* Returns the signer key.
* @protected
* @returns {Signer | ECPairInterface}
*/
protected getSignerKey(): Signer | ECPairInterface {
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
*/
protected async signInput(
transaction: Psbt,
input: PsbtInput,
i: number,
signer: Signer | ECPairInterface,
reverse: boolean = false,
errored: boolean = false,
): Promise<void> {
const publicKey = signer.publicKey;
let isTaproot = isTaprootInput(input);
if (reverse) {
isTaproot = !isTaproot;
}
let signed: boolean = false;
let didError: boolean = 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 as Error).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 as Error).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.`);
}
}
}
protected splitArray<T>(arr: T[], chunkSize: number): T[][] {
if (chunkSize <= 0) {
throw new Error('Chunk size must be greater than 0.');
}
const result: T[][] = [];
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>}
*/
protected async signInputs(transaction: Psbt): Promise<void> {
if ('multiSignPsbt' in this.signer) {
await this.signInputsWalletBased(transaction);
return;
}
await this.signInputsNonWalletBased(transaction);
}
protected async signInputsNonWalletBased(transaction: Psbt): Promise<void> {
// non web based signing.
const txs: PsbtInput[] = transaction.data.inputs;
const batchSize: number = 20;
const batches = this.splitArray(txs, batchSize);
if (!this.noSignatures) {
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const promises: Promise<void>[] = [];
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 as Error).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;
}
/**
* Converts the public key to x-only.
* @protected
* @returns {Buffer}
*/
protected internalPubKeyToXOnly(): Buffer {
return toXOnly(Buffer.from(this.signer.publicKey));
}
/**
* Internal init.
* @protected
*/
protected internalInit(): void {
this.scriptData = payments.p2tr(this.generateScriptAddress());
this.tapData = payments.p2tr(this.generateTapData());
}
/**
* Tweak the signer for the interaction
* @protected
*/
protected tweakSigner(): void {
if (this.tweakedSigner) return;
// tweaked p2tr signer.
this.tweakedSigner = this.getTweakedSigner(true);
}
/**
* Get the tweaked signer
* @private
* @returns {ECPairInterface} The tweaked signer
*/
protected getTweakedSigner(
useTweakedHash: boolean = false,
signer: Signer | ECPairInterface = this.signer,
): ECPairInterface | undefined {
const settings: TweakSettings = {
network: this.network,
};
if (useTweakedHash) {
settings.tweakHash = this.getTweakerHash();
}
if (!('privateKey' in signer)) {
return;
}
return TweakedSigner.tweakSigner(signer as unknown as ECPairInterface, settings);
}
protected generateP2SHRedeemScript(customWitnessScript: Buffer): Buffer | undefined {
const p2wsh = payments.p2wsh({
redeem: { output: customWitnessScript },
network: this.network,
});
// Step 2: Wrap the P2WSH inside a P2SH (Pay-to-Script-Hash)
const p2sh = payments.p2sh({
redeem: p2wsh,
network: this.network,
});
return p2sh.output;
}
protected generateP2SHRedeemScriptLegacy(inputAddr: string):
| {
redeemScript: Buffer;
outputScript: Buffer;
}
| undefined {
const pubKeyHash = bitCrypto.hash160(this.signer.publicKey);
const redeemScript: Buffer = 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,
});
// Step 3: 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;
}
protected generateP2SHP2PKHRedeemScript(inputAddr: string):
| {
redeemScript: Buffer;
outputScript: Buffer;
}
| undefined {
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;
}
/**
* 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
*/
protected generatePsbtInputExtended(
utxo: UTXO,
i: number,
_extra: boolean = false,
): PsbtInputExtended {
const script = Buffer.from(utxo.scriptPubKey.hex, 'hex');
const input: PsbtInputExtended = {
hash: utxo.transactionId,
index: utxo.outputIndex,
sequence: this.sequence,
witnessUtxo: {
value: Number(utxo.value),
script,
},
};
// Handle P2PKH (Legacy)
if (isP2PKH(script)) {
// Legacy input requires nonWitnessUtxo
if (utxo.nonWitnessUtxo) {
//delete input.witnessUtxo;
input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
? utxo.nonWitnessUtxo
: Buffer.from(utxo.nonWitnessUtxo, 'hex');
} else {
throw new Error('Missing nonWitnessUtxo for P2PKH UTXO');
}
}
// Handle P2WPKH (SegWit)
else if (isP2WPKH(script) || isUnknownSegwitVersion(script)) {
// No redeemScript required for pure P2WPKH
// witnessUtxo is enough, no nonWitnessUtxo needed.
}
// Handle P2WSH (SegWit)
else if (isP2WSHScript(script)) {
// 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 = Buffer.isBuffer(utxo.witnessScript)
? utxo.witnessScript
: Buffer.from(utxo.witnessScript, 'hex');
// No nonWitnessUtxo needed for segwit
}
// Handle P2SH (Can be legacy or wrapping segwit)
else if (isP2SHScript(script)) {
// Redeem script is required for P2SH
let redeemScriptBuf: Buffer | undefined;
if (utxo.redeemScript) {
redeemScriptBuf = Buffer.isBuffer(utxo.redeemScript)
? utxo.redeemScript
: Buffer.from(utxo.redeemScript, 'hex');
} 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);
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 = Buffer.isBuffer(utxo.nonWitnessUtxo)
? utxo.nonWitnessUtxo
: Buffer.from(utxo.nonWitnessUtxo, 'hex');
}
if (isP2WPKH(redeemOutput)) {
// P2SH-P2WPKH
// Use witnessUtxo + redeemScript
delete input.nonWitnessUtxo; // ensure we do NOT have nonWitnessUtxo
// witnessScript is not needed
} else if (isP2WSHScript(redeemOutput)) {
// P2SH-P2WSH
// Use witnessUtxo + redeemScript + witnessScript
delete input.nonWitnessUtxo; // ensure we do NOT have nonWitnessUtxo
if (!input.witnessScript) {
throw new Error('Missing witnessScript for P2SH-P2WSH UTXO');
}
} else {
// Legacy P2SH
// Use nonWitnessUtxo
delete input.witnessUtxo; // ensure we do NOT have witnessUtxo
}
}
// Handle P2TR (Taproot)
else if (isP2TR(script)) {
// 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
this.tweakSigner();
input.tapInternalKey = this.internalPubKeyToXOnly();
}
// Handle P2PK (legacy) or P2MS (bare multisig)
else if (isP2PK(script) || isP2MS(script)) {
// These are legacy scripts, need nonWitnessUtxo
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) {
// TapLeafScript if available
if (this.tapLeafScript) {
input.tapLeafScript = [this.tapLeafScript];
}
if (this.nonWitnessUtxo) {
input.nonWitnessUtxo = this.nonWitnessUtxo;
}
}
/*if (utxo.nonWitnessUtxo && extra) {
const witness = Buffer.isBuffer(utxo.nonWitnessUtxo)
? utxo.nonWitnessUtxo
: typeof utxo.nonWitnessUtxo === 'string'
? Buffer.from(utxo.nonWitnessUtxo, 'hex')
: (utxo.nonWitnessUtxo as unknown) instanceof Uint8Array
? Buffer.from(utxo.nonWitnessUtxo)
: undefined;
input.nonWitnessUtxo = witness;
}*/
return input;
}
protected customFinalizerP2SH = (
inputIndex: number,
input: PsbtInput,
scriptA: Buffer,
isSegwit: boolean,
isP2SH: boolean,
isP2WSH: boolean,
): {
finalScriptSig: Buffer | undefined;
finalScriptWitness: Buffer | undefined;
} => {
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,
);
};
protected async signInputsWalletBased(transaction: Psbt): Promise<void> {
const signer: UnisatSigner = this.signer as UnisatSigner;
// 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;
}
private async attemptSignTaproot(
transaction: Psbt,
input: PsbtInput,
i: number,
signer: Signer | ECPairInterface,
publicKey: Buffer,
): Promise<void> {
const isScriptSpend = this.isTaprootScriptSpend(input, publicKey);
if (isScriptSpend) {
await this.signTaprootInput(signer, transaction, i);
} else {
let tweakedSigner: ECPairInterface | undefined;
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}.`);
}
}
}
private isTaprootScriptSpend(input: PsbtInput, publicKey: Buffer): boolean {
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;
}
private async signTaprootInput(
signer: Signer | ECPairInterface,
transaction: Psbt,
i: number,
tapLeafHash?: Buffer,
): Promise<void> {
if ('signTaprootInput' in signer) {
try {
await (
signer.signTaprootInput as (
tx: Psbt,
i: number,
tapLeafHash?: Buffer,
) => Promise<void>
)(transaction, i, tapLeafHash);
} catch {
throw new Error('Failed to sign Taproot input with provided signer.');
}
} else {
transaction.signTaprootInput(i, signer); //tapLeafHash
}
}
private async signNonTaprootInput(
signer: Signer | ECPairInterface,
transaction: Psbt,
i: number,
): Promise<void> {
if ('signInput' in signer) {
await (signer.signInput as (tx: Psbt, i: number) => Promise<void>)(transaction, i);
} else {
transaction.signInput(i, signer);
}
}
}