Building a Transaction Manually
Creating a transaction manually allows you to have full control over every step, from selecting UTXOs to crafting the calldata and signing the transaction. This guide covers the process of manually building a transaction for interacting with a contract on OP_NET.
Manually building transactions can be error-prone due to gas miscalculations or incorrect UTXO usage. Prefer using the Simulation Method, which automates gas estimation and UTXO management, ensuring reliability and efficiency.
Steps to Build a Transaction
1. Obtaining Proper UTXOs
Before building a transaction, you need to gather the required UTXOs for funding. Use the utxoManager
from the JSONRpcProvider
to fetch UTXOs associated with your address.
const utxos = await provider.utxoManager.getUTXOs({
address: wallet.p2tr,
});
Ensure you have enough UTXOs to cover the transaction inputs, gas fees, and any additional outputs.
Learn more about UTXO Manager.
2. Estimating Gas
Gas estimation is crucial for ensuring your transaction executes successfully. While the provider or simulation result often helps with gas estimation, adding a buffer (e.g., 15%) is recommended to account for fluctuations.
const gasParameters = await provider.gasParameters();
const gasEstimate = gasParameters.baseGas * 2n; // Double the base gas
3. Encoding Calldata
Use your contract instance to encode the calldata for the specific function you want to call. The encodeCalldata
method helps prepare the necessary data for contract interaction.
const contractInstance = getContract<IContract>(
"contract-address",
"contract-abi",
provider,
networks.regtest,
wallet.address
);
// Call the function to get the calldata
const call = await contractInstance.foo("arg1", 123);
const calldata = call.calldata;
// Optionally, you can also get the calldata without calling the function
// const calldata = contractInstance.encodeCalldata(
// "foo", // Function name
// ["arg1", 123] // Function arguments
// );
4. Setting Interaction Parameters
The IInteractionParameters
object defines the details of the interaction, including the sender, contract address, UTXOs, gas settings, and calldata.
const interactionParameters: IInteractionParameters = {
from: wallet.p2tr, // Sender's address
to: "contract-address", // Contract address
utxos, // UTXOs for funding
signer: wallet.keypair, // Wallet keypair
network: networks.regtest, // Target network
feeRate: Number(gasEstimate), // Gas fee rate (satoshis per byte)
priorityFee: 5000n, // Priority fee for faster processing
calldata: calldata as Buffer, // Encoded calldata
};
5. Signing the Transaction
The TransactionFactory
allows you to sign the transaction with your wallet’s private key, ensuring the transaction is ready for broadcasting.
const transactionFactory = new TransactionFactory();
const signedTransaction = await transactionFactory.signInteraction(
interactionParameters
);
console.log("Signed Transaction:", signedTransaction);
Full Example
import { networks } from "@btc-vision/bitcoin";
import {
IInteractionParameters,
TransactionFactory,
Wallet,
} from "@btc-vision/transaction";
import { getContract, JSONRpcProvider } from "opnet";
// Initialize wallet and provider
const wallet = Wallet.fromWif("your-private-key-wif", networks.regtest);
const provider = new JSONRpcProvider(
"https://regtest.opnet.org",
networks.regtest
);
// Fetch UTXOs for funding the transaction
const utxos = await provider.utxoManager.getUTXOs({
address: wallet.p2tr,
});
// Define contract instance
const contractInstance = getContract<IContract>(
"contract-address",
"contract-abi",
provider,
networks.regtest,
wallet.address
);
// Call the function to get the calldata
const call = await contractInstance.foo("arg1", 123);
const calldata = call.calldata;
// Optionally, you can also get the calldata without calling the function
// const calldata = contractInstance.encodeCalldata(
// "foo", // Function name
// ["arg1", 123] // Function arguments
// );
// Define interaction parameters
const interactionParameters: IInteractionParameters = {
from: wallet.p2tr, // Sender's address
to: "contract-address", // Contract address
utxos, // UTXOs for funding
signer: wallet.keypair, // Wallet keypair
network: networks.regtest, // Target network
feeRate: Number((await provider.gasParameters()).baseGas * 2n), // Gas fee rate (satoshis per byte)
priorityFee: 5000n, // Priority fee for faster processing
calldata: calldata as Buffer, // Encoded calldata
};
// Create and sign the transaction
const transactionFactory = new TransactionFactory();
const signedTransaction = await transactionFactory.signInteraction(
interactionParameters
);
console.log("Signed Transaction:", signedTransaction);
Transaction Outputs
The signInteraction
method returns an array with the following structure:
[0]
: The first transaction to send.[1]
: The second transaction to send.[2]
: An array of newly created UTXOs.
You must broadcast the transactions in the order they appear in the output array. (i.e., [0]
first, then [1]
).
Learn how to send a manually built transaction on OP_NET.
Best Practices
- Always fetch UTXOs that meet the requirements of your transaction and validate their sufficiency.
- Ensure your gas estimate includes a buffer to prevent transaction failures due to fluctuating network conditions.
- Verify the calldata matches the intended contract interaction to avoid unexpected behavior.