Skip to main content

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.

DEPRECATED

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,
});
UTXO Management

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.
important

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.