Your First Contract Interaction
Overview
This guide walks you through building your first OP_NET application, from connecting to the network to executing your first smart contract transaction.
By the end of this guide, you'll be able to:
- Connect to an OP_NET node.
- Read data from a smart contract.
- Send a transaction to a smart contract.
Step 1: Provider Setup
First, establish a connection to an OP_NET node.
import { JSONRpcProvider } from 'opnet';
import { networks } from '@btc-vision/bitcoin';
// Create the provider
const provider = new JSONRpcProvider({
url: 'https://regtest.opnet.org',
network: networks.regtest
});
// Test the connection
async function testConnection() {
const blockNumber = await provider.getBlockNumber();
console.log('Connected! Current block:', blockNumber);
}RPC Endpoint URLs
| Network | URL |
|---|---|
| Mainnet | https://mainnet.opnet.org |
| Regtest | https://regtest.opnet.org |
Step 2: Create a Wallet
To interact with contracts, you need a wallet. OP_NET extends BIP32 to provide seamless ML-DSA (quantum-resistant) key management alongside traditional ECDSA keys. This means a single mnemonic seed phrase generates both key types automatically.
import {
Mnemonic,
MnemonicStrength,
MLDSASecurityLevel,
AddressTypes,
} from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';
const network = networks.regtest;
// Option 1: Generate a new mnemonic (24 words for maximum security)
const mnemonic = Mnemonic.generate(
MnemonicStrength.MAXIMUM, // 24 words (256-bit entropy)
'', // BIP39 passphrase (optional)
network, // Network
MLDSASecurityLevel.LEVEL2, // Quantum security level
);
console.log('Seed phrase:', mnemonic.phrase);
// Option 2: Import from existing seed phrase
const existingMnemonic = new Mnemonic(
'your twenty four word seed phrase goes here ...',
'', // BIP39 passphrase
network,
MLDSASecurityLevel.LEVEL2,
);
// RECOMMENDED: Use deriveOPWallet() to match OPWallet derivation
const wallet = mnemonic.deriveOPWallet(AddressTypes.P2TR, 0); // OPWallet-compatible
// Alternative: Standard derivation (different path than OPWallet)
const walletStandard = mnemonic.derive(0);
// Wallet properties (both ECDSA and ML-DSA keys derived from same mnemonic)
console.log('Taproot address:', wallet.p2tr);
console.log('SegWit address:', wallet.p2wpkh);
console.log('Keypair:', wallet.keypair); // ECDSA keypair
console.log('Address object:', wallet.address);
console.log('ML-DSA keypair:', wallet.mldsaKeypair); // Quantum-resistant keypairUsing WIF (Wallet Import Format) is NOT recommended. Always use the Mnemonic class for proper key derivation and ML-DSA support.
Why Mnemonic Over WIF?
| Feature | Mnemonic | WIF |
|---|---|---|
| ML-DSA key derivation | Automatic | Manual (error-prone) |
| OPWallet compatibility | Yes (with deriveOPWallet) | No |
| Multiple accounts | Easy (derive(0), derive(1), ...) | Requires separate keys |
| Backup | Single seed phrase | Multiple private keys |
| Quantum-resistant | Built-in | Requires separate management |
Step 3: Instantiate a Contract
Use getContract() to create a type-safe contract instance.
import {
getContract,
IOP20Contract,
JSONRpcProvider,
OP_20_ABI,
} from 'opnet';
import {
Address,
AddressTypes,
Mnemonic,
MLDSASecurityLevel,
Wallet,
} from '@btc-vision/transaction';
import { Network, networks } from '@btc-vision/bitcoin';
const network: Network = networks.regtest;
const provider: JSONRpcProvider = new JSONRpcProvider({
url: 'https://regtest.opnet.org',
network: network
});
// Your wallet (from mnemonic) - use deriveOPWallet for OPWallet compatibility
const mnemonic = new Mnemonic('your seed phrase here ...', '', network, MLDSASecurityLevel.LEVEL2);
const wallet: Wallet = mnemonic.deriveOPWallet(AddressTypes.P2TR, 0);
// Contract address (the token you want to interact with)
const tokenAddress: Address = Address.fromString(
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
);
// Create the contract instance
const token: IOP20Contract = getContract<IOP20Contract>(
tokenAddress,
OP_20_ABI,
provider,
network,
wallet.address // Optional: sender address for simulations
);
Step 4: First Contract Call (Reading Data)
Read data from the contract without spending any Bitcoin.
async function readTokenInfo() {
// Get token metadata
const nameResult = await token.name();
const symbolResult = await token.symbol();
const decimalsResult = await token.decimals();
const totalSupplyResult = await token.totalSupply();
console.log('Token Name:', nameResult.properties.name);
console.log('Symbol:', symbolResult.properties.symbol);
console.log('Decimals:', decimalsResult.properties.decimals);
console.log('Total Supply:', totalSupplyResult.properties.totalSupply);
// Check your balance
const balanceResult = await token.balanceOf(wallet.address);
console.log('Your Balance:', balanceResult.properties.balance);
}
readTokenInfo();Understanding CallResult
Every contract call returns a CallResult object.
const result = await token.balanceOf(wallet.address);
// Access decoded properties
console.log(result.properties.balance); // bigint
// Check for errors
if (result.revert) {
console.error('Call reverted:', result.revert);
}
// Gas used
console.log('Gas used:', result.gasUsed);Step 5: First Transaction (Writing Data)
Sending a transaction requires:
- Simulating the call.
- Building the transaction.
- Signing and broadcasting.
import { TransactionParameters } from 'opnet';
async function transferTokens() {
// Recipient address
const recipient = Address.fromString(
'0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'
);
// Amount to transfer (with decimals - e.g., 100 tokens with 8 decimals)
const amount = 100_00000000n; // 100 tokens
// Step 1: Simulate the transfer
const simulation = await token.transfer(recipient, amount, new Uint8Array(0));
// Check if simulation succeeded
if (simulation.revert) {
throw new Error(`Transfer would fail: ${simulation.revert}`);
}
console.log('Simulation successful!');
console.log('Gas used:', simulation.gasUsed);
// Step 2: Build and send the transaction
const params: TransactionParameters = {
signer: wallet.keypair, // ECDSA signing key
mldsaSigner: wallet.mldsaKeypair, // Quantum-resistant key (optional)
refundTo: wallet.p2tr, // Where to send change
maximumAllowedSatToSpend: 10000n, // Max sats for fees
feeRate: 10, // sat/vB (0 = automatic)
network: network,
};
const tx = await simulation.sendTransaction(params);
console.log('Transaction sent!');
console.log('Transaction ID:', tx.transactionId);
console.log('Estimated fees:', tx.estimatedFees, 'sats');
return tx;
}
transferTokens().catch(console.error);Step 6: Complete Example
import {
getContract,
IOP20Contract,
JSONRpcProvider,
OP_20_ABI,
TransactionParameters,
} from 'opnet';
import {
Address,
AddressTypes,
Mnemonic,
MLDSASecurityLevel,
} from '@btc-vision/transaction';
import { networks } from '@btc-vision/bitcoin';
async function main() {
// ============ Configuration ============
const network = networks.regtest;
const rpcUrl = 'https://regtest.opnet.org';
// ============ Setup Provider ============
const provider = new JSONRpcProvider({
url: rpcUrl,
network: network
});
// Verify connection
const blockNumber = await provider.getBlockNumber();
console.log('Connected to block:', blockNumber);
// ============ Setup Wallet ============
// IMPORTANT: Replace with your actual seed phrase
// Use deriveOPWallet for OPWallet-compatible derivation
const mnemonic = new Mnemonic(
'your twenty four word seed phrase goes here ...',
'', // BIP39 passphrase
network,
MLDSASecurityLevel.LEVEL2,
);
const wallet = mnemonic.deriveOPWallet(AddressTypes.P2TR, 0);
console.log('Wallet address:', wallet.p2tr);
// ============ Get Balance ============
const balance = await provider.getBalance(wallet.p2tr);
console.log('Bitcoin balance:', balance, 'sats');
// ============ Setup Contract ============
const tokenAddress = Address.fromString(
'0x...' // Replace with actual token address
);
const token = getContract<IOP20Contract>(
tokenAddress,
OP_20_ABI,
provider,
network,
wallet.address
);
// ============ Read Token Info ============
const [name, symbol, decimals] = await Promise.all([
token.name(),
token.symbol(),
token.decimals(),
]);
console.log('Token:', name.properties.name);
console.log('Symbol:', symbol.properties.symbol);
console.log('Decimals:', decimals.properties.decimals);
// ============ Check Balance ============
const tokenBalance = await token.balanceOf(wallet.address);
console.log('Token balance:', tokenBalance.properties.balance);
// ============ Transfer (if you have balance) ============
if (tokenBalance.properties.balance > 0n) {
const recipient = Address.fromString('0x...'); // Replace
const amount = 1000000n; // Amount to send
// Simulate first
const simulation = await token.transfer(recipient, amount, new Uint8Array(0));
if (simulation.revert) {
console.error('Transfer would fail:', simulation.revert);
return;
}
// Send transaction
const params: TransactionParameters = {
signer: wallet.keypair,
mldsaSigner: wallet.mldsaKeypair,
refundTo: wallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: network,
};
const tx = await simulation.sendTransaction(params);
console.log('Transaction sent:', tx.transactionId);
}
// ============ Cleanup ============
provider.close();
}
main().catch(console.error);Transaction Sequence Diagram
The following sequence diagram illustrates the interactions between all actors involved in the previous transfer example.
Troubleshooting
Insufficient balance
Ensure your wallet has enough BTC for fees.
const balance = await provider.getBalance(wallet.p2tr);
console.log('Balance:', balance, 'sats');No UTXOs available
Your address needs confirmed UTXOs.
const utxos = await provider.utxoManager.getUTXOs({
address: wallet.p2tr,
});
console.log('Available UTXOs:', utxos.length);Simulation reverted
Check the revert message for details.
if (simulation.revert) {
console.error('Revert reason:', simulation.revert);
}