Building a Transaction Manually
Manually creating a transaction using WalletConnect gives you precise control over UTXO selection, calldata crafting, and transaction parameters. This guide walks you through the process of building a transaction using WalletConnect, specifically for interactions with OP_NET smart contracts.
Steps to Build a Transaction
1. Get Required Parameters
Before deploying a contract, ensure you have the following parameters:
account
: Wallet account for transaction signing.signer
: Wallet signer for transaction authorization.
import { useWallet } from "@btc-vision/walletconnect";
const { account, signer } = useWallet();
2. 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 account.provider.utxoManager.getUTXOs({
address: account.address.p2tr(account.network),
});
Ensure you have enough UTXOs to cover the transaction inputs, gas fees, and any additional outputs.
Learn more about UTXO Manager.
3. Define the Contract Instance
Use the getContract
method to define the contract you want to interact with. This step ensures you have a properly typed contract instance that can encode calldata for the desired function.
const contractAddress = "bcrt1qexampleaddress"; // Replace with a valid contract address
const contractInstance = getContract<IOP_20Contract>(
contractAddress, // Contract address
OP_20_ABI, // Contract ABI
provider: account.provider, // Provider instance
network: account.network, // Target network
address: account.address // Sender's address
);
Ensure the contract instance matches the contract you want to interact with, including the correct address and ABI.
Learn more about Interacting with Smart Contracts.
4. Encode Calldata
Prepare the calldata for the contract interaction using the contract instance's methods, ensuring it matches the function you want to call.
const approveCall = await contractInstance.approve(account.address, 100n);
// Alternatively, you can manually encode the calldata
// const approveCall = contractInstance.encodeCalldata(
// "approve", // Function name
// [account.account, 100n] // Function arguments
// );
if (!approveCall || !approveCall.calldata)
throw new Error("Approve call failed.");
5. Configure Interaction Parameters
Set up the IInteractionParameters
object, which includes details like the signer, sender, UTXOs, calldata, and gas settings.
const interactionParameters: IInteractionParameters = {
signer: signer!, // OP_WALLET does not require a signer
from: account.address.p2tr(account.network), // Sender's address (P2TR)
to: contractAddress, // Contract address
utxos, // UTXOs for funding
network: account.network, // Target network
feeRate: Number((await account.provider.gasParameters()).baseGas * 2n), // Gas fee rate (satoshis per byte)
priorityFee: 5000n, // Priority fee for faster processing
gasSatFee: 5000n, // Gas fee (satoshis)
calldata: approveCall.calldata, // Call data from the approve function call
};
6. Signing the Deployment Transaction
Use the signDeployment
method from TransactionFactory
to sign the interaction transaction.
const transactionFactory = new TransactionFactory();
const interactionResult = await transactionFactory.signInteraction(
interactionParameters
);
console.log(interactionResult);
7. Broadcasting the Transaction
Finally, broadcast the signed transaction to the network using the sendRawTransaction
method.
const transactionReceipt = await account.provider.sendRawTransaction(
interactionResult.interactionTransaction,
false
);
const txId = transactionReceipt.result;
console.log(`Transaction ID: ${txId}`);
Full Example
import {
IInteractionParameters,
TransactionFactory,
} from "@btc-vision/transaction";
import { SupportedWallets, useWallet } from "@btc-vision/walletconnect";
import { getContract, IOP_20Contract, OP_20_ABI } from "opnet";
function BuildAnApproveTransaction() {
const { account, connect, disconnect, signer } = useWallet();
async function approveRandomToken() {
if (!account) throw new Error("Connect wallet first");
const utxos = await account.provider.utxoManager.getUTXOs({
address: account.address.p2tr(account.network),
});
const contractAddress = "bcrt1qexampleaddress"; // Replace with a valid contract address
const contractInstance = getContract<IOP_20Contract>(
contractAddress, // Contract address
OP_20_ABI, // Contract ABI
provider: account.provider, // Provider instance
network: account.network, // Target network
address: account.address // Sender's address
);
const approveCall = await contractInstance.approve(account.address, 100n);
if (!approveCall || !approveCall.calldata)
throw new Error("Approve call failed.");
const interactionParameters: IInteractionParameters = {
signer: signer!, // OP_WALLET does not require a signer
from: account.address.p2tr(account.network), // Sender's address (P2TR)
to: contractAddress, // Contract address
utxos, // UTXOs for funding
network: account.network, // Target network
feeRate: Number((await account.provider.gasParameters()).baseGas * 2n), // Gas fee rate (satoshis per byte)
priorityFee: 5000n, // Priority fee for faster processing
gasSatFee: 5000n, // Gas fee (satoshis)
calldata: approveCall.calldata, // Call data from the approve function call
};
const transactionFactory = new TransactionFactory();
const interactionResult = await transactionFactory.signInteraction(
interactionParameters
);
console.log(interactionResult); // This is not sent to the blockchain yet
const transactionReceipt = await account.provider.sendRawTransaction(
interactionResult.interactionTransaction,
false
);
const txId = transactionReceipt.result;
console.log(`Transaction ID: ${txId}`);
}
return (
<div>
{account ? (
<div>
<p>Connected Address: {account.address.p2tr(account.network)}</p>
<button onClick={approveRandomToken}>Approve a random token</button>
<button onClick={disconnect}>Disconnect</button>
</div>
) : (
<button onClick={() => connect(SupportedWallets.OP_WALLET)}>
Connect Wallet
</button>
)}
</div>
);
}
export default BuildAnApproveTransaction;
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.