UNPKG

@btc-vision/transaction

Version:

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

309 lines (261 loc) 10.1 kB
import { TransactionType } from '../enums/TransactionType.js'; import { type FinalScriptsFunc, fromHex, type P2MRPayment, type P2TRPayment, PaymentType, Psbt, type PsbtInput, type TapScriptSig, type Taptree, } from '@btc-vision/bitcoin'; import { TransactionBuilder } from './TransactionBuilder.js'; import type { TapPayment } from '../shared/TweakedTransaction.js'; import type { TapLeafScript } from '../interfaces/Tap.js'; import type { ICancelTransactionParameters } from '../interfaces/ICancelTransactionParameters.js'; import { UnisatSigner } from '../browser/extensions/UnisatSigner.js'; import type { SharedInteractionParameters } from '../interfaces/ITransactionParameters.js'; import { isUniversalSigner } from '../../signer/TweakedSigner.js'; export class CancelTransaction extends TransactionBuilder<TransactionType.CANCEL> { public type: TransactionType.CANCEL = TransactionType.CANCEL; /** * The tap leaf script for spending */ protected override tapLeafScript: TapLeafScript | null = null; protected readonly compiledTargetScript: Uint8Array; protected readonly scriptTree: Taptree; protected readonly contractSecret: Uint8Array; protected leftOverFundsScriptRedeem: P2TRPayment | null = null; public constructor(parameters: ICancelTransactionParameters) { super({ ...parameters, gasSatFee: 1n, isCancellation: true, priorityFee: 1n, calldata: new Uint8Array(0), } as unknown as SharedInteractionParameters); this.contractSecret = new Uint8Array(0); if (parameters.compiledTargetScript instanceof Uint8Array) { this.compiledTargetScript = parameters.compiledTargetScript; } else { this.compiledTargetScript = fromHex(parameters.compiledTargetScript); } // Generate the minimal script tree needed for recovery this.scriptTree = this.getMinimalScriptTree(); this.internalInit(); } protected override async buildTransaction(): Promise<void> { if (!this.from) { throw new Error('From address is required'); } if (!this.leftOverFundsScriptRedeem) { throw new Error('Left over funds script redeem is required'); } if (!this.leftOverFundsScriptRedeem.redeemVersion) { throw new Error('Left over funds script redeem version is required'); } if (!this.leftOverFundsScriptRedeem.output) { throw new Error('Left over funds script redeem output is required'); } // Set up the tap leaf script for spending this.tapLeafScript = { leafVersion: this.leftOverFundsScriptRedeem.redeemVersion, script: this.leftOverFundsScriptRedeem.output, controlBlock: this.getWitness(), }; this.addInputsFromUTXO(); await this.addRefundOutput(0n, true); if (!this.feeOutput) { throw new Error('Must add extra UTXOs to cancel this transaction'); } } /*protected override async buildTransaction(): Promise<void> { if (!this.from) { throw new Error('From address is required'); } // For key-path spend, we don't need the tap leaf script this.tapLeafScript = null; this.addInputsFromUTXO(); await this.addRefundOutput(0n); }*/ /** * Sign the inputs * @param {Psbt} transaction The transaction to sign * @protected */ /*protected async signInputs(transaction: Psbt): Promise<void> { for (let i = 0; i < transaction.data.inputs.length; i++) { if (i === 0) { transaction.signInput(0, this.getSignerKey()); transaction.finalizeInput(0, this.customFinalizer.bind(this)); } else { await super.signInputs(transaction); } } }*/ /** * Generate the script address (for verification purposes) */ protected override generateScriptAddress(): TapPayment { if (this.useP2MR) { return { network: this.network, scriptTree: this.scriptTree, name: PaymentType.P2MR, } as P2MRPayment; } return { internalPubkey: this.internalPubKeyToXOnly(), network: this.network, scriptTree: this.scriptTree, name: PaymentType.P2TR, }; } /** * Generate the tap data for spending */ protected override generateTapData(): TapPayment { const selectedRedeem = this.leftOverFundsScriptRedeem; if (!selectedRedeem) { throw new Error('Left over funds script redeem is required'); } if (!this.scriptTree) { throw new Error('Script tree is required'); } if (this.useP2MR) { return { network: this.network, scriptTree: this.scriptTree, redeem: selectedRedeem, name: PaymentType.P2MR, } as P2MRPayment; } return { internalPubkey: this.internalPubKeyToXOnly(), network: this.network, scriptTree: this.scriptTree, redeem: selectedRedeem, name: PaymentType.P2TR, }; } /** * Custom finalizer for the tap script spend */ protected customFinalizer = (_inputIndex: number, input: PsbtInput) => { if (!this.tapLeafScript) { throw new Error('Tap leaf script is required'); } if (!input.tapScriptSig || input.tapScriptSig.length === 0) { throw new Error('Tap script signature is required'); } // For the simple lock script, we only need the signature const scriptSolution = [(input.tapScriptSig[0] as TapScriptSig).signature]; const witness = scriptSolution .concat(this.tapLeafScript.script) .concat(this.tapLeafScript.controlBlock); return { finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness), }; }; protected override async signInputs(transaction: Psbt): Promise<void> { if ('multiSignPsbt' in this.signer) { await this.signInputsWalletBased(transaction); } else { await this.signInputsNonWalletBased(transaction); } } protected override 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++) { if (i === 0) { transaction.finalizeInput(i, this.customFinalizer.bind(this)); } else { try { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } catch (e) { transaction.finalizeInput(i); } } } } protected override async signInputsNonWalletBased(transaction: Psbt): Promise<void> { // Input 0: always sequential (script-path with custom finalizer) await this.signInput( transaction, transaction.data.inputs[0] as PsbtInput, 0, this.getSignerKey(), ); transaction.finalizeInput(0, this.customFinalizer.bind(this)); // Inputs 1+: parallel key-path if available, then sequential for remaining let parallelSignedIndices = new Set<number>(); if (this.canUseParallelSigning && isUniversalSigner(this.signer)) { try { const result = await this.signKeyPathInputsParallel(transaction, new Set([0])); if (result.success) { parallelSignedIndices = new Set(result.signatures.keys()); } } catch (e) { this.error( `Parallel signing failed, falling back to sequential: ${(e as Error).message}`, ); } } for (let i = 1; i < transaction.data.inputs.length; i++) { if (!parallelSignedIndices.has(i)) { await this.signInput( transaction, transaction.data.inputs[i] as PsbtInput, i, this.signer, ); } } // Finalize inputs 1+ for (let i = 1; i < transaction.data.inputs.length; i++) { try { transaction.finalizeInput( i, this.customFinalizerP2SH.bind(this) as FinalScriptsFunc, ); } catch { transaction.finalizeInput(i); } } } /** * Generate the minimal script tree needed for recovery * This only includes the leftover funds script */ private getMinimalScriptTree(): Taptree { this.generateLeftoverFundsRedeem(); if (!this.leftOverFundsScriptRedeem || !this.leftOverFundsScriptRedeem.output) { throw new Error('Failed to generate leftover funds redeem script'); } return [ { output: this.compiledTargetScript, version: 192, }, { output: this.leftOverFundsScriptRedeem.output, version: 192, }, ]; } /** * Generate the leftover funds redeem script */ private generateLeftoverFundsRedeem(): void { // Use the same LOCK_LEAF_SCRIPT from the parent class this.leftOverFundsScriptRedeem = { name: PaymentType.P2TR, output: this.LOCK_LEAF_SCRIPT, redeemVersion: 192, }; } }