Skip to main content

Overview

This guide covers sending ETH transactions including transaction creation, signing, and sending with the Dynamic Flutter SDK using the web3dart package.

Prerequisites

Send ETH 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<String> sendTransaction({
  required BaseWallet wallet,
  required String recipientAddress,
  required double amountInEth,
}) async {
  // Convert ETH to Wei (1 ETH = 10^18 Wei)
  final amountInWei = (amountInEth * BigInt.from(10).pow(18).toDouble()).toInt();

  // Create transaction
  final transaction = Transaction(
    from: EthereumAddress.fromHex(wallet.address),
    to: EthereumAddress.fromHex(recipientAddress),
    value: EtherAmount.inWei(BigInt.from(amountInWei)),
  );

  // Send transaction
  final txHash = await sdk.web3dart.sendTransaction(
    transaction: transaction,
    wallet: wallet,
  );

  print('Transaction sent!');
  print('Hash: $txHash');
  return txHash;
}

Complete Send Transaction Widget

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_web3dart/dynamic_sdk_web3dart.dart';
import 'package:web3dart/web3dart.dart';

class SendTransactionWidget extends StatefulWidget {
  final BaseWallet wallet;

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

  @override
  State<SendTransactionWidget> createState() => _SendTransactionWidgetState();
}

class _SendTransactionWidgetState extends State<SendTransactionWidget> {
  final sdk = DynamicSDK.instance;
  final _recipientController = TextEditingController();
  final _amountController = TextEditingController();

  String? txHash;
  bool isLoading = false;
  String? errorMessage;

  @override
  void dispose() {
    _recipientController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  Future<void> _sendTransaction() async {
    final recipient = _recipientController.text.trim();
    final amount = _amountController.text.trim();

    if (recipient.isEmpty || amount.isEmpty) {
      setState(() => errorMessage = 'Please fill all fields');
      return;
    }

    final amountDouble = double.tryParse(amount);
    if (amountDouble == null) {
      setState(() => errorMessage = 'Invalid amount');
      return;
    }

    setState(() {
      isLoading = true;
      errorMessage = null;
      txHash = null;
    });

    try {
      // Convert ETH to Wei
      final amountInWei = (amountDouble * BigInt.from(10).pow(18).toDouble()).toInt();

      // Create transaction
      final transaction = Transaction(
        from: EthereumAddress.fromHex(widget.wallet.address),
        to: EthereumAddress.fromHex(recipient),
        value: EtherAmount.inWei(BigInt.from(amountInWei)),
      );

      // Send transaction
      final hash = await sdk.web3dart.sendTransaction(
        transaction: transaction,
        wallet: widget.wallet,
      );

      setState(() => txHash = hash);
    } catch (e) {
      setState(() => errorMessage = 'Failed: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          TextField(
            controller: _recipientController,
            decoration: const InputDecoration(
              labelText: 'Recipient address',
              border: OutlineInputBorder(),
            ),
            autocorrect: false,
            enableSuggestions: false,
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _amountController,
            decoration: const InputDecoration(
              labelText: 'Amount (ETH)',
              border: OutlineInputBorder(),
            ),
            keyboardType: const TextInputType.numberWithOptions(decimal: true),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: isLoading ? null : _sendTransaction,
            child: isLoading
                ? const SizedBox(
                    height: 20,
                    width: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Text('Send Transaction'),
          ),
          if (txHash != null) ...[
            const SizedBox(height: 16),
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.green.withOpacity(0.1),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Transaction Sent!',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      color: Colors.green,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Hash: $txHash',
                    style: const TextStyle(
                      fontSize: 12,
                      fontFamily: 'monospace',
                    ),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 8),
                  TextButton(
                    onPressed: () {
                      Clipboard.setData(ClipboardData(text: txHash!));
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('Hash copied!')),
                      );
                    },
                    child: const Text('Copy Hash'),
                  ),
                ],
              ),
            ),
          ],
          if (errorMessage != null) ...[
            const SizedBox(height: 16),
            Text(
              errorMessage!,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ],
        ],
      ),
    );
  }
}

Sign Transaction (Without Sending)

To sign a transaction without broadcasting it, you can use web3dart directly:
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:web3dart/web3dart.dart';

Future<void> signTransaction(BaseWallet wallet) async {
  final transaction = Transaction(
    from: EthereumAddress.fromHex(wallet.address),
    to: EthereumAddress.fromHex('0x...'),
    value: EtherAmount.inWei(BigInt.from(1000000000000000)), // 0.001 ETH
  );

  // Sign using DynamicSDK.instance.wallets.signMessage for custom implementations
  print('Transaction prepared');
}

Best Practices

1. Validate Input

String? validateAddress(String address) {
  if (address.isEmpty) {
    return 'Address is required';
  }

  if (!address.startsWith('0x') || address.length != 42) {
    return 'Invalid Ethereum address';
  }

  return null;
}

String? validateAmount(String amount) {
  if (amount.isEmpty) {
    return 'Amount is required';
  }

  final value = double.tryParse(amount);
  if (value == null || value <= 0) {
    return 'Invalid amount';
  }

  return null;
}

2. Handle Transaction Errors

Future<String?> sendTransactionWithErrorHandling({
  required BaseWallet wallet,
  required String recipient,
  required double amount,
}) async {
  try {
    final amountInWei = (amount * BigInt.from(10).pow(18).toDouble()).toInt();

    final transaction = Transaction(
      from: EthereumAddress.fromHex(wallet.address),
      to: EthereumAddress.fromHex(recipient),
      value: EtherAmount.inWei(BigInt.from(amountInWei)),
    );

    final txHash = await DynamicSDK.instance.web3dart.sendTransaction(
      transaction: transaction,
      wallet: wallet,
    );

    return txHash;
  } catch (e) {
    final errorStr = e.toString().toLowerCase();

    if (errorStr.contains('insufficient')) {
      throw Exception('Insufficient funds for transaction');
    } else if (errorStr.contains('gas')) {
      throw Exception('Gas estimation failed. Try increasing gas limit.');
    } else if (errorStr.contains('rejected') || errorStr.contains('denied')) {
      throw Exception('Transaction was rejected');
    } else if (errorStr.contains('network')) {
      throw Exception('Network error. Please check your connection.');
    } else {
      throw Exception('Transaction failed. Please try again.');
    }
  }
}

3. Show Transaction Status

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

class TransactionStatusWidget extends StatelessWidget {
  final String txHash;
  final int chainId;

  const TransactionStatusWidget({
    Key? key,
    required this.txHash,
    required this.chainId,
  }) : super(key: key);

  String get explorerUrl {
    // Map chain ID to block explorer
    switch (chainId) {
      case 1:
        return 'https://etherscan.io/tx/$txHash';
      case 84532:
        return 'https://sepolia.basescan.org/tx/$txHash';
      case 137:
        return 'https://polygonscan.com/tx/$txHash';
      default:
        return '';
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text(
          'Transaction Submitted',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Text(
          txHash,
          style: const TextStyle(fontSize: 12),
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),
        if (explorerUrl.isNotEmpty) ...[
          const SizedBox(height: 8),
          TextButton(
            onPressed: () async {
              final uri = Uri.parse(explorerUrl);
              if (await canLaunchUrl(uri)) {
                await launchUrl(uri);
              }
            },
            child: const Text('View on Explorer'),
          ),
        ],
      ],
    );
  }
}

4. Confirm Before Sending

import 'package:flutter/material.dart';

class ConfirmTransactionDialog extends StatelessWidget {
  final String recipient;
  final String amount;
  final VoidCallback onConfirm;

  const ConfirmTransactionDialog({
    Key? key,
    required this.recipient,
    required this.amount,
    required this.onConfirm,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Confirm Transaction'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('To: $recipient'),
          const SizedBox(height: 8),
          Text('Amount: $amount ETH'),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop();
            onConfirm();
          },
          child: const Text('Confirm'),
        ),
      ],
    );
  }
}

Error Handling

Common Transaction Errors

String getErrorMessage(dynamic error) {
  final errorStr = error.toString().toLowerCase();

  if (errorStr.contains('insufficient')) {
    return 'Insufficient balance for this transaction';
  } else if (errorStr.contains('gas')) {
    return 'Gas estimation failed. Try increasing gas limit.';
  } else if (errorStr.contains('rejected') || errorStr.contains('denied')) {
    return 'Transaction was rejected';
  } else if (errorStr.contains('network')) {
    return 'Network error. Please check your connection.';
  } else {
    return 'Transaction failed. Please try again.';
  }
}

// Usage
try {
  await sendTransaction(
    wallet: wallet,
    recipientAddress: recipient,
    amountInEth: amount,
  );
} catch (e) {
  final message = getErrorMessage(e);
  print(message);
}

Next Steps