Skip to main content

Overview

This guide covers how to interact with smart contracts on EVM chains, including reading contract state and executing contract functions using the web3dart package.

Prerequisites

Write to Contract

Execute state-changing functions on a smart contract:
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> writeContract({
  required BaseWallet wallet,
  required String contractAddress,
  required String abiJson,
  required String functionName,
  required List<dynamic> parameters,
}) async {
  // 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 contract
  final contract = DeployedContract(
    ContractAbi.fromJson(abiJson, 'Contract'),
    EthereumAddress.fromHex(contractAddress),
  );

  final function = contract.function(functionName);

  // Create transaction
  final transaction = Transaction.callContract(
    contract: contract,
    function: function,
    parameters: parameters,
    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('Contract interaction successful!');
  print('Hash: $txHash');
  return txHash;
}

Read Contract Data

Query contract state without sending a transaction:
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<List<dynamic>> readContract({
  required int chainId,
  required String contractAddress,
  required String abiJson,
  required String functionName,
  List<dynamic> parameters = const [],
}) async {
  final client = sdk.web3dart.createPublicClient(chainId: chainId);

  final contract = DeployedContract(
    ContractAbi.fromJson(abiJson, 'Contract'),
    EthereumAddress.fromHex(contractAddress),
  );

  final function = contract.function(functionName);

  final result = await client.call(
    contract: contract,
    function: function,
    params: parameters,
  );

  print('Contract value: $result');
  return result;
}

Complete Contract Interaction 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 ContractInteractionWidget extends StatefulWidget {
  final BaseWallet wallet;
  final String contractAddress;
  final int chainId;

  const ContractInteractionWidget({
    Key? key,
    required this.wallet,
    required this.contractAddress,
    required this.chainId,
  }) : super(key: key);

  @override
  State<ContractInteractionWidget> createState() => _ContractInteractionWidgetState();
}

class _ContractInteractionWidgetState extends State<ContractInteractionWidget> {
  final sdk = DynamicSDK.instance;
  final _inputValueController = TextEditingController();

  String? currentValue;
  String? txHash;
  bool isLoading = false;
  String? error;

  // Example ABI for a simple storage contract
  final contractAbi = '''[
    {
      "constant": false,
      "inputs": [{"name": "value", "type": "uint256"}],
      "name": "setValue",
      "outputs": [],
      "type": "function"
    },
    {
      "constant": true,
      "inputs": [],
      "name": "getValue",
      "outputs": [{"name": "", "type": "uint256"}],
      "type": "function"
    }
  ]''';

  @override
  void initState() {
    super.initState();
    _readValue();
  }

  @override
  void dispose() {
    _inputValueController.dispose();
    super.dispose();
  }

  Future<void> _readValue() async {
    try {
      final client = sdk.web3dart.createPublicClient(chainId: widget.chainId);

      final contract = DeployedContract(
        ContractAbi.fromJson(contractAbi, 'Storage'),
        EthereumAddress.fromHex(widget.contractAddress),
      );

      final function = contract.function('getValue');

      final result = await client.call(
        contract: contract,
        function: function,
        params: [],
      );

      setState(() => currentValue = result.first.toString());
    } catch (e) {
      setState(() => error = 'Failed to read: $e');
    }
  }

  Future<void> _setValue() async {
    setState(() {
      isLoading = true;
      error = null;
      txHash = null;
    });

    try {
      final value = _inputValueController.text.trim();
      if (value.isEmpty) {
        throw Exception('Value is required');
      }

      // Get gas price
      final client = sdk.web3dart.createPublicClient(chainId: widget.chainId);
      final gasPrice = await client.getGasPrice();

      // Create contract
      final contract = DeployedContract(
        ContractAbi.fromJson(contractAbi, 'Storage'),
        EthereumAddress.fromHex(widget.contractAddress),
      );

      final function = contract.function('setValue');

      // Create transaction
      final transaction = Transaction.callContract(
        contract: contract,
        function: function,
        parameters: [BigInt.parse(value)],
        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);

      // Refresh the value after transaction
      await Future.delayed(const Duration(seconds: 2));
      await _readValue();
    } catch (e) {
      setState(() => error = 'Failed to set value: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Read section
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.grey[200],
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'Read Contract',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8),
                if (currentValue != null)
                  Row(
                    children: [
                      const Text('Current Value: '),
                      Text(
                        currentValue!,
                        style: const TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                const SizedBox(height: 8),
                OutlinedButton(
                  onPressed: _readValue,
                  child: const Text('Refresh Value'),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),
          const Divider(),
          const SizedBox(height: 24),
          // Write section
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.grey[200],
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'Write to Contract',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8),
                TextField(
                  controller: _inputValueController,
                  decoration: const InputDecoration(
                    labelText: 'New value',
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.number,
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  onPressed: isLoading ? null : _setValue,
                  child: isLoading
                      ? const SizedBox(
                          height: 20,
                          width: 20,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Text('Set Value'),
                ),
                if (txHash != null) ...[
                  const SizedBox(height: 8),
                  Container(
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.green.withOpacity(0.1),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          'Transaction sent!',
                          style: TextStyle(color: Colors.green),
                        ),
                        Text(
                          'Hash: $txHash',
                          style: const TextStyle(fontSize: 12),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ],
                    ),
                  ),
                ],
                if (error != null) ...[
                  const SizedBox(height: 8),
                  Text(
                    error!,
                    style: const TextStyle(color: Colors.red, fontSize: 12),
                  ),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Working with Contract ABIs

Defining ABIs

ABIs (Application Binary Interfaces) define how to interact with smart contracts:
// Simple storage contract ABI
const storageAbi = '''[
  {
    "constant": false,
    "inputs": [{"name": "value", "type": "uint256"}],
    "name": "store",
    "outputs": [],
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "retrieve",
    "outputs": [{"name": "", "type": "uint256"}],
    "type": "function"
  }
]''';

// ERC-721 NFT contract functions
const nftAbi = '''[
  {
    "constant": false,
    "inputs": [
      {"name": "to", "type": "address"},
      {"name": "tokenId", "type": "uint256"}
    ],
    "name": "mint",
    "outputs": [],
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{"name": "owner", "type": "address"}],
    "name": "balanceOf",
    "outputs": [{"name": "", "type": "uint256"}],
    "type": "function"
  }
]''';

Advanced Contract Interactions

NFT Minting

Future<String> mintNFT({
  required BaseWallet wallet,
  required String nftContractAddress,
  required String recipient,
  required String tokenId,
  required int chainId,
}) async {
  const nftAbi = '''[
    {
      "constant": false,
      "inputs": [
        {"name": "to", "type": "address"},
        {"name": "tokenId", "type": "uint256"}
      ],
      "name": "mint",
      "outputs": [],
      "type": "function"
    }
  ]''';

  final client = DynamicSDK.instance.web3dart.createPublicClient(chainId: chainId);
  final gasPrice = await client.getGasPrice();

  final contract = DeployedContract(
    ContractAbi.fromJson(nftAbi, 'NFT'),
    EthereumAddress.fromHex(nftContractAddress),
  );

  final mintFunction = contract.function('mint');

  final transaction = Transaction.callContract(
    contract: contract,
    function: mintFunction,
    parameters: [
      EthereumAddress.fromHex(recipient),
      BigInt.parse(tokenId),
    ],
    maxFeePerGas: EtherAmount.inWei(
      gasPrice.getValueInUnitBI(EtherUnit.wei) * BigInt.from(2),
    ),
    maxPriorityFeePerGas: EtherAmount.inWei(
      gasPrice.getValueInUnitBI(EtherUnit.wei),
    ),
  );

  return await DynamicSDK.instance.web3dart.sendTransaction(
    transaction: transaction,
    wallet: wallet,
  );
}

Check Token Allowance

Future<BigInt> checkAllowance({
  required String tokenAddress,
  required String ownerAddress,
  required String spenderAddress,
  required int chainId,
}) async {
  const erc20Abi = '''[
    {
      "constant": true,
      "inputs": [
        {"name": "_owner", "type": "address"},
        {"name": "_spender", "type": "address"}
      ],
      "name": "allowance",
      "outputs": [{"name": "", "type": "uint256"}],
      "type": "function"
    }
  ]''';

  final client = DynamicSDK.instance.web3dart.createPublicClient(chainId: chainId);

  final contract = DeployedContract(
    ContractAbi.fromJson(erc20Abi, 'ERC20'),
    EthereumAddress.fromHex(tokenAddress),
  );

  final allowanceFunction = contract.function('allowance');

  final result = await client.call(
    contract: contract,
    function: allowanceFunction,
    params: [
      EthereumAddress.fromHex(ownerAddress),
      EthereumAddress.fromHex(spenderAddress),
    ],
  );

  return result.first as BigInt;
}

Best Practices

1. Validate Contract Addresses

bool isValidEthereumAddress(String address) {
  final pattern = RegExp(r'^0x[a-fA-F0-9]{40}$');
  return pattern.hasMatch(address);
}

// Usage
if (!isValidEthereumAddress(contractAddress)) {
  throw Exception('Invalid contract address');
}

2. Handle Contract Errors

Future<String?> callContractSafely({
  required BaseWallet wallet,
  required String contractAddress,
  required String abiJson,
  required String functionName,
  required List<dynamic> parameters,
}) async {
  try {
    return await writeContract(
      wallet: wallet,
      contractAddress: contractAddress,
      abiJson: abiJson,
      functionName: functionName,
      parameters: parameters,
    );
  } catch (e) {
    final errorDesc = e.toString().toLowerCase();

    if (errorDesc.contains('revert')) {
      print('Contract reverted. Check contract requirements.');
    } else if (errorDesc.contains('gas')) {
      print('Out of gas. Increase gas limit.');
    } else {
      print('Contract call failed: $e');
    }
    return null;
  }
}

3. Use Try-Catch for Read Operations

Future<String?> safeReadContract({
  required int chainId,
  required String contractAddress,
  required String abiJson,
  required String functionName,
}) async {
  try {
    final result = await readContract(
      chainId: chainId,
      contractAddress: contractAddress,
      abiJson: abiJson,
      functionName: functionName,
    );
    return result.first.toString();
  } catch (e) {
    print('Failed to read contract: $e');
    return null;
  }
}

Next Steps