Using Transaction Factory

Overview

The TransactionFactory class is the primary entry point for creating all OP_NET transaction types. It manages UTXO selection, fee estimation, and the two-transaction model required for contract operations. In browser environments, it automatically detects and delegates signing to the OP_WALLET extension when available.

Architecture

TransactionFactory Architecture TransactionFactory createBTCTransfer Send BTC to address createCancellableTransaction RBF-cancellable tx createCustomScriptTransaction User-defined script signDeployment Deploy contract signInteraction Call contract method signConsolidatedInteraction Anti-censorship batch without P2WDA with P2WDA FundingTransaction BTC value transfers CancelTransaction RBF fee-bump cancel CustomScriptTransaction Arbitrary script outputs DeploymentTransaction Contract bytecode deploy InteractionTransaction Contract calldata exec Consolidated InteractionTx CHCT anti-censorship InteractionTx P2WDA Data anchor wrap

The Two-Transaction Model

As mentionned in the Understanding the Protocol Design, the standard contract operations (deployment, interaction) use a two-transaction model. The first transaction funds a temporary script address; the second spends from that address to execute the contract operation.

Two-Transaction Pattern: Funding + Operation Transaction 1: Funding User UTXO 2 spendable input User UTXO 1 spendable input Script Output (to temporary address) Change Output (back to sender) Transaction 2: Operation Script Input (from TX1 output 0) Epoch Reward (330 sat minimum) Refund Output remaining sats OP_RETURN Data (optional note) creates input for

Why Two Transactions?

  1. Script isolation: The contract operation script is committed to a specific Taproot address. Funding that address in a separate transaction ensures the script hash is locked before spending.
  2. Fee accuracy: The factory iteratively estimates the funding amount needed for the second transaction, accounting for fees, priority fees, and optional outputs.
  3. Cancellation support: If the second transaction fails to confirm, the first transaction's output can be recovered using createCancellableTransaction with the compiledTargetScript.

P2WDA Exception

When the factory detects P2WDA (Pay-to-Witness-Data-Authentication) UTXOs in the inputs, it switches to a single-transaction model. P2WDA embeds operation data directly in the witness field, avoiding the need for a separate funding transaction and achieving approximately 75% cost reduction.

In P2WDA mode, the InteractionResponse will have:

  • fundingTransaction: null
  • interactionAddress: null
  • compiledTargetScript: null

CHCT System (Consolidated Interactions)

The signConsolidatedInteraction method uses a different two-transaction model called CHCT (Commitment-Hash-Commitment-Transaction):

Hash Commitment: Setup + Reveal Setup Transaction User UTXOs P2WSH Output 1 HASH160 commitments P2WSH Output 2 HASH160 commitments P2WSH Output N ... Change Reveal Transaction Spend P2WSH 1 Reveal data chunks Spend P2WSH 2 Reveal data chunks Spend P2WSH N ... Epoch Reward miner incentive Refund witness reveals data witness reveals data witness reveals data

This bypasses BIP110/Bitcoin Knots censorship by avoiding Tapscript OP_IF opcodes entirely. Data integrity is consensus-enforced: if any data is modified, HASH160(data) != committed_hash and the transaction is invalid.

Browser vs. Backend Environments

The TransactionFactory supports both browser and backend environments. The key difference is how signing is handled.

Backend Environment

In a backend (Node.js) environment, you provide a signer object directly:

typescript
Backend signing
import { EcKeyPair } from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';

const signer = EcKeyPair.fromWIF(privateKeyWIF, networks.bitcoin);

const result = await factory.signInteraction({
    signer: signer,           // Provided signer
    mldsaSigner: mldsaKey,     // Or null if no quantum signing
    network: networks.bitcoin,
    // ... other parameters
});

Browser Environment (Wallet Extensions)

In a browser environment, set signer and mldsaSigner to null. The factory automatically detects the OP_WALLET browser extension window.opnet.web3 and delegates signing to it:

typescript
Browser signing
// Browser - wallet extension handles signing
const result = await factory.signInteraction({
    // signer is OMITTED (or set to null via the WithoutSigner type)
    // mldsaSigner is OMITTED
    network: networks.bitcoin,
    utxos,
    from: walletAddress,
    to: contractAddress,
    feeRate: 10,
    // priorityFee and gasSatFee are still required
    priorityFee: 1000n,
    gasSatFee: 500n,
    calldata: encodedCall,
    // challenge is OMITTED for browser
});

The WithoutSigner type variants (InteractionParametersWithoutSigner, IDeploymentParametersWithoutSigner, etc.) automatically omit signer, mldsaSigner, and challenge from the parameter types.

Detection Flow

Signer Resolution Flow Method Called typeof window !== 'undefined'? No (Backend) Yes (Browser) window.opnet.web3 exists? Yes Delegate to OP_WALLET browser extension signs No Require signer parameter 'signer' in params? Yes Sign with provided signer local keypair signing No Throw Error signer not provided

Complete Examples

Example 1: Simple BTC Transfer

typescript
Simple BTC Transfer
import { TransactionFactory, EcKeyPair, type UTXO } from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';

async function sendBitcoin() {
    const network = networks.bitcoin;
    const factory = new TransactionFactory();

    const signer = EcKeyPair.fromWIF(process.env.PRIVATE_KEY!, network);
    const address = EcKeyPair.getTaprootAddress(signer, network);

    const utxos: UTXO[] = [
        {
            transactionId: 'abcd1234...'.padEnd(64, '0'),
            outputIndex: 0,
            value: 100_000n,
            scriptPubKey: {
                hex: '5120...',
                address: address,
            },
        },
    ];

    const result = await factory.createBTCTransfer({
        signer,
        mldsaSigner: null,
        network,
        utxos,
        from: address,
        to: 'bc1p...recipient',
        feeRate: 10,
        priorityFee: 0n,
        gasSatFee: 0n,
        amount: 50_000n,
    });

    console.log('Transaction hex:', result.tx);
    console.log('Fees paid:', result.estimatedFees, 'satoshis');
    console.log('Change UTXOs:', result.nextUTXOs);

    // Broadcast result.tx to the Bitcoin network
    // Save result.nextUTXOs for the next transaction
}

Example 2: Contract Deployment

typescript
Contract Deployment
import { TransactionFactory, EcKeyPair } from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';

async function deployContract(
    bytecode: Uint8Array,
    constructorCalldata: Uint8Array,
    challenge: IChallengeSolution,
) {
    const network = networks.bitcoin;
    const factory = new TransactionFactory();

    const signer = EcKeyPair.fromWIF(process.env.PRIVATE_KEY!, network);
    const address = EcKeyPair.getTaprootAddress(signer, network);

    const utxos = await fetchUTXOs(address);

    const result = await factory.signDeployment({
        signer,
        mldsaSigner: null,
        network,
        utxos,
        from: address,
        feeRate: 15,
        priorityFee: 1000n,
        gasSatFee: 500n,
        bytecode: bytecode,
        calldata: constructorCalldata,
        challenge: challenge,
    });

    // Broadcast BOTH transactions in order
    await broadcastTransaction(result.transaction[0]); // Funding tx first
    await broadcastTransaction(result.transaction[1]); // Then deployment tx

    console.log('Contract deployed at:', result.contractAddress);
    console.log('Contract public key:', result.contractPubKey);
    console.log('Refund UTXOs:', result.utxos);
}

Example 3: Contract Interaction

typescript
Contract Interaction
import { TransactionFactory, EcKeyPair } from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';

async function callContract(
    contractAddress: string,
    calldata: Uint8Array,
    challenge: IChallengeSolution,
) {
    const network = networks.bitcoin;
    const factory = new TransactionFactory();

    const signer = EcKeyPair.fromWIF(process.env.PRIVATE_KEY!, network);
    const address = EcKeyPair.getTaprootAddress(signer, network);

    const utxos = await fetchUTXOs(address);

    const result = await factory.signInteraction({
        signer,
        mldsaSigner: null,
        network,
        utxos,
        from: address,
        to: contractAddress,
        feeRate: 10,
        priorityFee: 1000n,
        gasSatFee: 500n,
        calldata: calldata,
        challenge: challenge,
    });

    // For standard interactions: broadcast both transactions
    if (result.fundingTransaction) {
        await broadcastTransaction(result.fundingTransaction);
    }
    await broadcastTransaction(result.interactionTransaction);

    console.log('Interaction address:', result.interactionAddress);
    console.log('Estimated fees:', result.estimatedFees, 'satoshis');
    console.log('Change UTXOs:', result.nextUTXOs);

    // Save compiledTargetScript in case cancellation is needed
    if (result.compiledTargetScript) {
        saveCancelScript(result.compiledTargetScript);
    }
}

Example 4: Cancel a Stuck Transaction

typescript
Cancel a Stuck Transaction
async function cancelStuckTransaction(
    stuckUtxos: UTXO[],
    compiledTargetScript: string,
) {
    const factory = new TransactionFactory();

    const result = await factory.createCancellableTransaction({
        signer,
        mldsaSigner: null,
        network: networks.bitcoin,
        utxos: stuckUtxos,
        from: myAddress,
        to: myAddress,
        feeRate: 20, // Higher fee to ensure confirmation
        compiledTargetScript: compiledTargetScript,
    });

    await broadcastTransaction(result.transaction);
    console.log('Funds recovered! UTXOs:', result.nextUTXOs);
}

Example 5: Send-Max with autoAdjustAmount

typescript
Send-Max with autoAdjustAmount
// Send the entire UTXO balance minus fees
const result = await factory.createBTCTransfer({
    signer,
    mldsaSigner: null,
    network,
    utxos: allMyUtxos,
    from: address,
    to: 'bc1p...recipient',
    feeRate: 10,
    priorityFee: 0n,
    gasSatFee: 0n,
    amount: totalUtxoValue,      // Set amount to total value
    autoAdjustAmount: true,      // Fees deducted from amount automatically
});