@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
242 lines (239 loc) • 8.72 kB
JavaScript
import { TransactionType } from '../enums/TransactionType.js';
import { fromHex, PaymentType, Psbt, } from '@btc-vision/bitcoin';
import { TransactionBuilder } from './TransactionBuilder.js';
import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
import { isUniversalSigner } from '../../signer/TweakedSigner.js';
export class CancelTransaction extends TransactionBuilder {
type = TransactionType.CANCEL;
/**
* The tap leaf script for spending
*/
tapLeafScript = null;
compiledTargetScript;
scriptTree;
contractSecret;
leftOverFundsScriptRedeem = null;
constructor(parameters) {
super({
...parameters,
gasSatFee: 1n,
isCancellation: true,
priorityFee: 1n,
calldata: new Uint8Array(0),
});
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();
}
async buildTransaction() {
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)
*/
generateScriptAddress() {
if (this.useP2MR) {
return {
network: this.network,
scriptTree: this.scriptTree,
name: PaymentType.P2MR,
};
}
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
scriptTree: this.scriptTree,
name: PaymentType.P2TR,
};
}
/**
* Generate the tap data for spending
*/
generateTapData() {
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,
};
}
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
scriptTree: this.scriptTree,
redeem: selectedRedeem,
name: PaymentType.P2TR,
};
}
/**
* Custom finalizer for the tap script spend
*/
customFinalizer = (_inputIndex, input) => {
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].signature];
const witness = scriptSolution
.concat(this.tapLeafScript.script)
.concat(this.tapLeafScript.controlBlock);
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness),
};
};
async signInputs(transaction) {
if ('multiSignPsbt' in this.signer) {
await this.signInputsWalletBased(transaction);
}
else {
await this.signInputsNonWalletBased(transaction);
}
}
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++) {
if (i === 0) {
transaction.finalizeInput(i, this.customFinalizer.bind(this));
}
else {
try {
transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this));
}
catch (e) {
transaction.finalizeInput(i);
}
}
}
}
async signInputsNonWalletBased(transaction) {
// Input 0: always sequential (script-path with custom finalizer)
await this.signInput(transaction, transaction.data.inputs[0], 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();
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.message}`);
}
}
for (let i = 1; i < transaction.data.inputs.length; i++) {
if (!parallelSignedIndices.has(i)) {
await this.signInput(transaction, transaction.data.inputs[i], i, this.signer);
}
}
// Finalize inputs 1+
for (let i = 1; i < transaction.data.inputs.length; i++) {
try {
transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this));
}
catch {
transaction.finalizeInput(i);
}
}
}
/**
* Generate the minimal script tree needed for recovery
* This only includes the leftover funds script
*/
getMinimalScriptTree() {
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
*/
generateLeftoverFundsRedeem() {
// Use the same LOCK_LEAF_SCRIPT from the parent class
this.leftOverFundsScriptRedeem = {
name: PaymentType.P2TR,
output: this.LOCK_LEAF_SCRIPT,
redeemVersion: 192,
};
}
}
//# sourceMappingURL=CancelTransaction.js.map