Overview
The Dynamic SDK provides methods to send ERC-20 tokens using the web3dart package. You can interact with token contracts to transfer tokens between addresses.Prerequisites
- Dynamic SDK initialized (see Installation Guide)
- User authenticated (see Authentication Guide)
- EVM wallet available (see Wallet Creation)
dynamic_sdk_web3dartpackage installed
Send ERC-20 Tokens
Copy
Ask AI
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_web3dart/dynamic_sdk_web3dart.dart';
import 'package:web3dart/web3dart.dart';
final sdk = DynamicSDK.instance;
Future<String> sendERC20({
required BaseWallet wallet,
required String tokenAddress,
required String recipient,
required String amount, // Human-readable amount (e.g., "1.5")
int decimals = 18,
}) async {
// Convert human-readable amount to base units
final baseUnits = parseDecimalToBaseUnits(amount, decimals: decimals);
// Get network information
final network = await sdk.wallets.getNetwork(wallet: wallet);
final chainId = network.intValue()!;
// Create public client
final client = sdk.web3dart.createPublicClient(chainId: chainId);
// Get gas price
final gasPrice = await client.getGasPrice();
// Create ERC-20 contract
final contract = DeployedContract(
ContractAbi.fromJson(
'''[
{
"constant": false,
"inputs": [
{"name": "_to", "type": "address"},
{"name": "_value", "type": "uint256"}
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
}
]''',
'ERC20',
),
EthereumAddress.fromHex(tokenAddress),
);
final transferFunction = contract.function('transfer');
// Create transaction
final transaction = Transaction.callContract(
contract: contract,
function: transferFunction,
parameters: [
EthereumAddress.fromHex(recipient),
baseUnits,
],
maxFeePerGas: EtherAmount.inWei(
gasPrice.getValueInUnitBI(EtherUnit.wei) * BigInt.from(2),
),
maxPriorityFeePerGas: EtherAmount.inWei(
gasPrice.getValueInUnitBI(EtherUnit.wei),
),
);
// Send transaction
final txHash = await sdk.web3dart.sendTransaction(
transaction: transaction,
wallet: wallet,
);
print('ERC20 transfer sent!');
print('Hash: $txHash');
return txHash;
}
// Helper function to convert decimal string to base units
BigInt parseDecimalToBaseUnits(String value, {required int decimals}) {
if (decimals < 0 || decimals > 77) {
throw Exception('Token decimals must be between 0 and 77');
}
final parts = value.split('.');
final wholePart = parts[0].isEmpty ? '0' : parts[0];
final fracPartRaw = parts.length == 2 ? parts[1] : '';
final whole = BigInt.tryParse(wholePart);
if (whole == null) {
throw Exception('Invalid token amount');
}
// Trim fractional part to token's decimal places
final trimmedFrac = fracPartRaw.substring(
0,
fracPartRaw.length > decimals ? decimals : fracPartRaw.length,
);
final fracPadded = trimmedFrac.padRight(decimals, '0');
final frac = fracPadded.isEmpty ? BigInt.zero : (BigInt.tryParse(fracPadded) ?? BigInt.zero);
return whole * BigInt.from(10).pow(decimals) + frac;
}
Complete ERC20 Transfer Widget
Copy
Ask AI
import 'package:flutter/material.dart';
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_web3dart/dynamic_sdk_web3dart.dart';
import 'package:web3dart/web3dart.dart';
class SendERC20Widget extends StatefulWidget {
final BaseWallet wallet;
const SendERC20Widget({Key? key, required this.wallet}) : super(key: key);
@override
State<SendERC20Widget> createState() => _SendERC20WidgetState();
}
class _SendERC20WidgetState extends State<SendERC20Widget> {
final sdk = DynamicSDK.instance;
final _tokenAddressController = TextEditingController();
final _recipientController = TextEditingController();
final _amountController = TextEditingController();
final _decimalsController = TextEditingController(text: '18');
String? txHash;
bool isLoading = false;
String? error;
@override
void dispose() {
_tokenAddressController.dispose();
_recipientController.dispose();
_amountController.dispose();
_decimalsController.dispose();
super.dispose();
}
Future<void> _sendTokens() async {
setState(() {
isLoading = true;
error = null;
txHash = null;
});
try {
final tokenAddress = _tokenAddressController.text.trim();
final recipient = _recipientController.text.trim();
final amount = _amountController.text.trim();
final decimals = int.parse(_decimalsController.text);
final baseUnits = parseDecimalToBaseUnits(amount, decimals: decimals);
// Get network information
final network = await sdk.wallets.getNetwork(wallet: widget.wallet);
final chainId = network.intValue()!;
// Create public client
final client = sdk.web3dart.createPublicClient(chainId: chainId);
// Get gas price
final gasPrice = await client.getGasPrice();
// Create ERC-20 contract
final contract = DeployedContract(
ContractAbi.fromJson(
'''[
{
"constant": false,
"inputs": [
{"name": "_to", "type": "address"},
{"name": "_value", "type": "uint256"}
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
}
]''',
'ERC20',
),
EthereumAddress.fromHex(tokenAddress),
);
final transferFunction = contract.function('transfer');
// Create transaction
final transaction = Transaction.callContract(
contract: contract,
function: transferFunction,
parameters: [
EthereumAddress.fromHex(recipient),
baseUnits,
],
maxFeePerGas: EtherAmount.inWei(
gasPrice.getValueInUnitBI(EtherUnit.wei) * BigInt.from(2),
),
maxPriorityFeePerGas: EtherAmount.inWei(
gasPrice.getValueInUnitBI(EtherUnit.wei),
),
);
// Send transaction
final hash = await sdk.web3dart.sendTransaction(
transaction: transaction,
wallet: widget.wallet,
);
setState(() => txHash = hash);
} catch (e) {
setState(() => error = e.toString());
} finally {
setState(() => isLoading = false);
}
}
BigInt parseDecimalToBaseUnits(String value, {required int decimals}) {
final parts = value.split('.');
final whole = BigInt.tryParse(parts[0]) ?? BigInt.zero;
final fracRaw = parts.length == 2 ? parts[1] : '';
final fracPadded = fracRaw.substring(0, fracRaw.length > decimals ? decimals : fracRaw.length).padRight(decimals, '0');
final frac = BigInt.tryParse(fracPadded) ?? BigInt.zero;
return whole * BigInt.from(10).pow(decimals) + frac;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _tokenAddressController,
decoration: const InputDecoration(
labelText: 'Token Contract (0x...)',
border: OutlineInputBorder(),
),
autocorrect: false,
),
const SizedBox(height: 16),
TextField(
controller: _recipientController,
decoration: const InputDecoration(
labelText: 'Recipient (0x...)',
border: OutlineInputBorder(),
),
autocorrect: false,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller: _amountController,
decoration: const InputDecoration(
labelText: 'Amount',
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _decimalsController,
decoration: const InputDecoration(
labelText: 'Decimals',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: isLoading ? null : _sendTokens,
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Send Tokens'),
),
if (txHash != null) ...[
const SizedBox(height: 16),
Text(
'Success: $txHash',
style: const TextStyle(fontSize: 12, color: Colors.green),
),
],
if (error != null) ...[
const SizedBox(height: 16),
Text(
error!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
);
}
}
Common Token Decimals
| Token | Decimals | Notes |
|---|---|---|
| ETH, WETH | 18 | Most ERC-20 tokens use 18 |
| USDC | 6 | Circle’s USD Coin |
| USDT | 6 | Tether USD |
| WBTC | 8 | Wrapped Bitcoin |
| DAI | 18 | MakerDAO stablecoin |
Best Practices
1. Validate Token Address
Always validate the token contract address before sending:Copy
Ask AI
bool isValidEthereumAddress(String address) {
final pattern = RegExp(r'^0x[a-fA-F0-9]{40}$');
return pattern.hasMatch(address);
}
// Usage
if (!isValidEthereumAddress(tokenAddress)) {
throw Exception('Invalid token address');
}
2. Check Token Balance Before Transfer
Copy
Ask AI
import 'package:web3dart/web3dart.dart';
Future<BigInt> getTokenBalance({
required BaseWallet wallet,
required String tokenAddress,
required int chainId,
}) async {
final client = DynamicSDK.instance.web3dart.createPublicClient(chainId: chainId);
final contract = DeployedContract(
ContractAbi.fromJson(
'''[
{
"constant": true,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
}
]''',
'ERC20',
),
EthereumAddress.fromHex(tokenAddress),
);
final balanceFunction = contract.function('balanceOf');
final result = await client.call(
contract: contract,
function: balanceFunction,
params: [EthereumAddress.fromHex(wallet.address)],
);
return result.first as BigInt;
}
3. Use Appropriate Gas Limits
Copy
Ask AI
class GasLimits {
static const int erc20Transfer = 65000; // ERC-20 token transfer
static const int erc20Approve = 50000; // ERC-20 approve
}
Handle Token Metadata
Copy
Ask AI
class TokenMetadata {
final String name;
final String symbol;
final int decimals;
TokenMetadata({
required this.name,
required this.symbol,
required this.decimals,
});
}
Future<TokenMetadata> getTokenMetadata({
required String tokenAddress,
required int chainId,
}) async {
final client = DynamicSDK.instance.web3dart.createPublicClient(chainId: chainId);
final contract = DeployedContract(
ContractAbi.fromJson(
'''[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"type": "function"
}
]''',
'ERC20',
),
EthereumAddress.fromHex(tokenAddress),
);
final nameResult = await client.call(
contract: contract,
function: contract.function('name'),
params: [],
);
final symbolResult = await client.call(
contract: contract,
function: contract.function('symbol'),
params: [],
);
final decimalsResult = await client.call(
contract: contract,
function: contract.function('decimals'),
params: [],
);
return TokenMetadata(
name: nameResult.first as String,
symbol: symbolResult.first as String,
decimals: (decimalsResult.first as BigInt).toInt(),
);
}
Next Steps
- Send ETH Transactions - Basic ETH transfers
- Smart Contract Interactions - Advanced contract calls
- Gas Management - Optimize gas usage
- Token Balances - Query token balances