Skip to main content

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

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
);
Contract Interaction

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.