Overview
This guide covers sending ETH transactions including transaction creation, signing, and sending with the Dynamic Flutter SDK using the web3dart package.Prerequisites
- Dynamic SDK initialized (see Installation Guide)
- User authenticated (see Authentication Guide)
- Wallets available (see Wallet Creation)
- Network configured (see Networks)
dynamic_sdk_web3dartpackage installed
Send ETH Transaction
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> 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
Copy
Ask AI
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:Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
- ERC-20 Token Transfers - Send ERC-20 tokens
- Smart Contract Interactions - Interact with smart contracts
- Gas Management - Gas estimation and optimization
- Message Signing - Sign messages and typed data