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
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.
Why Two Transactions?
- 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.
- Fee accuracy: The factory iteratively estimates the funding amount needed for the second transaction, accounting for fees, priority fees, and optional outputs.
- 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):
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:
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:
// 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
Complete Examples
Example 1: 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
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
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
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
// 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
});