opnet
Version:
The perfect library for building Bitcoin-based applications.
489 lines (397 loc) • 12.9 kB
Markdown
# OP20 Token Examples
This guide provides comprehensive examples for working with OP20 (fungible) tokens.
## Table of Contents
- [Overview](#overview)
- [Setup](#setup)
- [Method Data Encoding](#method-data-encoding)
- [Transfer Example](#transfer-example)
- [Allowance/TransferFrom Pattern](#allowancetransferfrom-pattern)
- [Airdrop Example (Batch Transfers)](#airdrop-example-batch-transfers)
- [Balance Checking with Decimals](#balance-checking-with-decimals)
- [Complete Token Service](#complete-token-service)
## Overview
OP20 is OPNet's fungible token standard, similar to ERC20 on Ethereum.
```mermaid
classDiagram
class IOP20Contract {
+name() string
+symbol() string
+decimals() number
+totalSupply() bigint
+balanceOf(owner) bigint
+transfer(to, amount)
+transferFrom(from, to, amount)
+safeTransfer(to, amount, data)
+increaseAllowance(spender, amount)
+decreaseAllowance(spender, amount)
+allowance(owner, spender) bigint
+burn(amount)
}
```
## Setup
```typescript
import {
getContract,
IOP20Contract,
JSONRpcProvider,
OP_20_ABI,
TransactionParameters,
BitcoinUtils,
} 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,
});
const mnemonic = new Mnemonic('your seed phrase here ...', '', network, MLDSASecurityLevel.LEVEL2);
const wallet: Wallet = mnemonic.deriveUnisat(AddressTypes.P2TR, 0); // OPWallet-compatible
const tokenAddress: Address = Address.fromString('0x...');
const token: IOP20Contract = getContract<IOP20Contract>(
tokenAddress,
OP_20_ABI,
provider,
network,
wallet.address
);
```
## Method Data Encoding
Manually encode calldata for contract methods:
```typescript
// Encode transfer calldata
const transferCalldata = token.encodeCalldata('transfer', [
Address.fromString('0x...'), // recipient
1000000000n, // amount
]);
console.log('Calldata:', toHex(transferCalldata));
// Encode increaseAllowance calldata
const allowanceCalldata = token.encodeCalldata('increaseAllowance', [
Address.fromString('0x...'), // spender
BigInt('0xffffffffffffffff'), // amount to increase by
]);
```
## Transfer Example
### Basic Transfer
```typescript
async function transferTokens(
token: IOP20Contract,
recipient: Address,
amount: bigint,
wallet: Wallet
): Promise<string> {
// Simulate first
const simulation = await token.transfer(recipient, amount);
// Send transaction
const params: TransactionParameters = {
signer: wallet.keypair,
mldsaSigner: wallet.mldsaKeypair,
refundTo: wallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: network,
};
const receipt = await simulation.sendTransaction(params);
return receipt.transactionId;
}
// Usage
const txId = await transferTokens(
token,
Address.fromString('0x...'),
100_00000000n, // 100 tokens with 8 decimals
wallet
);
console.log('Transfer TX:', txId);
```
### Safe Transfer (with data)
```typescript
async function safeTransfer(
token: IOP20Contract,
recipient: Address,
amount: bigint,
data: Uint8Array,
wallet: Wallet
): Promise<string> {
// safeTransfer allows passing additional data
// The simulation call throws if the contract reverts
const simulation = await token.safeTransfer(recipient, amount, data);
const params: TransactionParameters = {
signer: wallet.keypair,
mldsaSigner: wallet.mldsaKeypair,
refundTo: wallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: network,
};
const receipt = await simulation.sendTransaction(params);
return receipt.transactionId;
}
// Usage with empty data
try {
const txId = await safeTransfer(
token,
recipient,
100_00000000n,
new Uint8Array(),
wallet
);
console.log('Safe transfer TX:', txId);
} catch (error) {
console.error('Safe transfer failed:', error);
}
```
## Allowance/TransferFrom Pattern
### Increase Allowance
OP20 uses `increaseAllowance` and `decreaseAllowance` instead of a single `approve` method.
```typescript
async function increaseSpenderAllowance(
token: IOP20Contract,
spender: Address,
amount: bigint,
wallet: Wallet
): Promise<string> {
// The simulation call throws if the contract reverts
const simulation = await token.increaseAllowance(spender, amount);
const params: TransactionParameters = {
signer: wallet.keypair,
mldsaSigner: wallet.mldsaKeypair,
refundTo: wallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: network,
};
const receipt = await simulation.sendTransaction(params);
return receipt.transactionId;
}
// Increase allowance by a large amount
const largeAmount = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
await increaseSpenderAllowance(token, spenderAddress, largeAmount, wallet);
```
### Transfer From Another Account
```typescript
async function transferFrom(
token: IOP20Contract,
from: Address,
to: Address,
amount: bigint,
spenderWallet: Wallet
): Promise<string> {
// Create contract with spender as sender
const spenderToken = getContract<IOP20Contract>(
token.address,
OP_20_ABI,
provider,
network,
spenderWallet.address
);
// Check allowance first
const allowance = await spenderToken.allowance(from, spenderWallet.address);
if (allowance.properties.remaining < amount) {
throw new Error('Insufficient allowance');
}
// Execute transferFrom (throws if contract reverts)
const simulation = await spenderToken.transferFrom(from, to, amount);
const params: TransactionParameters = {
signer: spenderWallet.keypair,
mldsaSigner: spenderWallet.mldsaKeypair,
refundTo: spenderWallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: network,
};
const receipt = await simulation.sendTransaction(params);
return receipt.transactionId;
}
```
## Airdrop Example (Batch Transfers)
Distribute tokens to multiple recipients:
```typescript
interface AirdropRecipient {
address: Address;
amount: bigint;
}
async function airdropTokens(
token: IOP20Contract,
recipients: AirdropRecipient[],
wallet: Wallet
): Promise<string[]> {
const txIds: string[] = [];
let currentUtxos: UTXO[] | undefined;
for (const recipient of recipients) {
try {
// Simulate transfer (throws on revert)
const simulation = await token.transfer(recipient.address, recipient.amount);
const params: TransactionParameters = {
signer: wallet.keypair,
mldsaSigner: wallet.mldsaKeypair,
refundTo: wallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: network,
utxos: currentUtxos, // Use previous UTXOs
};
const receipt = await simulation.sendTransaction(params);
txIds.push(receipt.transactionId);
// Update UTXOs for next transaction
currentUtxos = receipt.newUTXOs;
console.log(`Sent ${recipient.amount} to ${recipient.address.toHex()}`);
} catch (error) {
console.error(`Skip ${recipient.address.toHex()}:`, error);
continue;
}
}
return txIds;
}
// Usage
const recipients: AirdropRecipient[] = [
{ address: Address.fromString('0x...'), amount: 100_00000000n },
{ address: Address.fromString('0x...'), amount: 200_00000000n },
{ address: Address.fromString('0x...'), amount: 150_00000000n },
];
const txIds = await airdropTokens(token, recipients, wallet);
console.log('Airdrop complete:', txIds.length, 'transfers');
```
## Balance Checking with Decimals
```typescript
async function getFormattedBalance(
token: IOP20Contract,
address: Address
): Promise<string> {
// Get decimals
const decimalsResult = await token.decimals();
const decimals = decimalsResult.properties.decimals;
// Get balance
const balanceResult = await token.balanceOf(address);
const balance = balanceResult.properties.balance;
// Format with decimals
return BitcoinUtils.formatUnits(balance, decimals);
}
// Usage
const formattedBalance = await getFormattedBalance(token, myAddress);
console.log('Balance:', formattedBalance, 'tokens');
```
### Parse Amount with Decimals
```typescript
async function parseTokenAmount(
token: IOP20Contract,
amountString: string
): Promise<bigint> {
const decimalsResult = await token.decimals();
const decimals = decimalsResult.properties.decimals;
return BitcoinUtils.expandToDecimals(amountString, decimals);
}
// Usage
const amount = await parseTokenAmount(token, '100.5');
console.log('Amount in base units:', amount); // 10050000000n (for 8 decimals)
```
## Complete Token Service
```typescript
class TokenService {
private token: IOP20Contract;
private wallet: Wallet;
private network: Network;
private decimals: number | undefined;
constructor(
tokenAddress: Address,
provider: JSONRpcProvider,
wallet: Wallet,
network: Network
) {
this.wallet = wallet;
this.network = network;
this.token = getContract<IOP20Contract>(
tokenAddress,
OP_20_ABI,
provider,
network,
wallet.address
);
}
async getInfo(): Promise<{
name: string;
symbol: string;
decimals: number;
totalSupply: bigint;
}> {
const [name, symbol, decimals, totalSupply] = await Promise.all([
this.token.name(),
this.token.symbol(),
this.token.decimals(),
this.token.totalSupply(),
]);
this.decimals = decimals.properties.decimals;
return {
name: name.properties.name,
symbol: symbol.properties.symbol,
decimals: decimals.properties.decimals,
totalSupply: totalSupply.properties.totalSupply,
};
}
async getBalance(address: Address): Promise<bigint> {
const result = await this.token.balanceOf(address);
return result.properties.balance;
}
async getFormattedBalance(address: Address): Promise<string> {
if (!this.decimals) {
await this.getInfo();
}
const balance = await this.getBalance(address);
return BitcoinUtils.formatUnits(balance, this.decimals!);
}
async transfer(to: Address, amount: bigint): Promise<string> {
// Simulation throws if contract reverts
const simulation = await this.token.transfer(to, amount);
const receipt = await simulation.sendTransaction({
signer: this.wallet.keypair,
mldsaSigner: this.wallet.mldsaKeypair,
refundTo: this.wallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: this.network,
});
return receipt.transactionId;
}
async increaseAllowance(spender: Address, amount: bigint): Promise<string> {
// Simulation throws if contract reverts
const simulation = await this.token.increaseAllowance(spender, amount);
const receipt = await simulation.sendTransaction({
signer: this.wallet.keypair,
mldsaSigner: this.wallet.mldsaKeypair,
refundTo: this.wallet.p2tr,
maximumAllowedSatToSpend: 10000n,
feeRate: 10,
network: this.network,
});
return receipt.transactionId;
}
}
// Usage
const tokenService = new TokenService(tokenAddress, provider, wallet, network);
const info = await tokenService.getInfo();
console.log(`Token: ${info.name} (${info.symbol})`);
const balance = await tokenService.getFormattedBalance(wallet.address);
console.log(`Balance: ${balance}`);
const txId = await tokenService.transfer(recipient, 100_00000000n);
console.log(`Transfer TX: ${txId}`);
```
## Next Steps
- [OP721 NFT Examples](./op721-examples.md) - NFT operations
- [Advanced Swaps](./advanced-swaps.md) - DEX interactions
- [Contract Deployment](./deployment-examples.md) - Deploy contracts
[← Previous: Offline Signing](../contracts/offline-signing.md) | [Next: OP721 Examples →](./op721-examples.md)