Skip to main content

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

Send ERC-20 Tokens

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

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

TokenDecimalsNotes
ETH, WETH18Most ERC-20 tokens use 18
USDC6Circle’s USD Coin
USDT6Tether USD
WBTC8Wrapped Bitcoin
DAI18MakerDAO stablecoin

Best Practices

1. Validate Token Address

Always validate the token contract address before sending:
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

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

class GasLimits {
  static const int erc20Transfer = 65000; // ERC-20 token transfer
  static const int erc20Approve = 50000; // ERC-20 approve
}

Handle Token Metadata

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