Overview
This guide covers sending SOL, building transactions, and signing transactions on Solana using the Flutter Solana package.Prerequisites
- Dynamic SDK initialized (see Quickstart)
- User authenticated (see Authentication)
- Solana wallet available (see Wallet Creation)
dynamic_sdk_solanapackage installed
Send SOL
Copy
Ask AI
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:solana/solana.dart';
final sdk = DynamicSDK.instance;
Future<String> sendSOL({
required BaseWallet wallet,
required String recipientAddress,
required double amount,
}) async {
try {
final signer = sdk.solana.createSigner(wallet: wallet);
final connection = sdk.solana.createConnection();
final fromPubKey = Pubkey.fromString(wallet.address);
final toPubKey = Pubkey.fromString(recipientAddress);
final BlockhashWithExpiryBlockHeight recentBlockhash =
await connection.getLatestBlockhash();
final transaction = Transaction.v0(
payer: fromPubKey,
recentBlockhash: recentBlockhash.blockhash,
instructions: [
SystemProgram.transfer(
fromPubkey: fromPubKey,
toPubkey: toPubKey,
lamports: solToLamports(amount),
),
],
);
final signature = await signer.signAndSendTransaction(
transaction: transaction,
);
print('Transaction sent with signature: $signature');
return signature;
} catch (e) {
print('Failed to send transaction: $e');
rethrow;
}
}
// Helper function to convert SOL to lamports
int solToLamports(double sol) {
return (sol * 1e9).toInt();
}
Complete Send SOL Widget
Copy
Ask AI
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:solana/solana.dart';
class SolanaSendWidget extends StatefulWidget {
final BaseWallet wallet;
const SolanaSendWidget({Key? key, required this.wallet}) : super(key: key);
@override
State<SolanaSendWidget> createState() => _SolanaSendWidgetState();
}
class _SolanaSendWidgetState extends State<SolanaSendWidget> {
final sdk = DynamicSDK.instance;
final _recipientController = TextEditingController();
final _amountController = TextEditingController();
String? signature;
bool isLoading = false;
String? error;
@override
void dispose() {
_recipientController.dispose();
_amountController.dispose();
super.dispose();
}
String formatAddress(String address) {
if (address.length <= 10) return address;
return '${address.substring(0, 6)}...${address.substring(address.length - 4)}';
}
int solToLamports(double sol) {
return (sol * 1e9).toInt();
}
Future<void> _sendTransaction() async {
final recipient = _recipientController.text.trim();
final amount = _amountController.text.trim();
if (recipient.isEmpty || amount.isEmpty) {
setState(() => error = 'Please fill all fields');
return;
}
final amountDouble = double.tryParse(amount);
if (amountDouble == null || amountDouble <= 0) {
setState(() => error = 'Invalid amount');
return;
}
setState(() {
isLoading = true;
error = null;
signature = null;
});
try {
// Create connection and signer
final connection = sdk.solana.createConnection();
final signer = sdk.solana.createSigner(wallet: widget.wallet);
// Get latest blockhash
final blockhash = await connection.getLatestBlockhash();
// Convert SOL to lamports
final lamports = solToLamports(amountDouble);
// Create transaction
final transaction = Transaction.v0(
payer: Pubkey.fromString(widget.wallet.address),
recentBlockhash: blockhash.blockhash,
instructions: [
SystemProgram.transfer(
fromPubkey: Pubkey.fromString(widget.wallet.address),
toPubkey: Pubkey.fromString(recipient),
lamports: lamports,
),
],
);
// Sign and send
final sig = await signer.signAndSendTransaction(transaction: transaction);
setState(() => signature = sig);
} catch (e) {
setState(() => error = e.toString());
} finally {
setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'From: ${formatAddress(widget.wallet.address)}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 16),
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 (SOL)',
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 SOL'),
),
if (signature != 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(
'Success!',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
signature!,
style: const TextStyle(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: signature!));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Signature copied!')),
);
},
child: const Text('Copy Signature'),
),
TextButton(
onPressed: () async {
final url = 'https://explorer.solana.com/tx/$signature?cluster=devnet';
// Use url_launcher package to open URL
},
child: const Text('View on Explorer'),
),
],
),
),
],
if (error != null) ...[
const SizedBox(height: 16),
Text(
error!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
);
}
}
Sign Transaction (Without Sending)
To sign a transaction without broadcasting it:Copy
Ask AI
Future<String> signTransaction({
required BaseWallet wallet,
required Transaction transaction,
}) async {
final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
// Note: For signing only, you would need to implement custom logic
// The standard flow is signAndSendTransaction
throw UnimplementedError('Sign-only is not directly supported');
}
Lamports Conversion
Solana uses lamports as its smallest unit (1 SOL = 10^9 lamports):Copy
Ask AI
class SolanaConverter {
/// Convert SOL to lamports
static int solToLamports(double sol) {
return (sol * 1e9).toInt();
}
/// Convert lamports to SOL
static double lamportsToSol(int lamports) {
return lamports / 1e9;
}
/// Format lamports for display
static String formatLamports(int lamports, {int decimals = 4}) {
final sol = lamportsToSol(lamports);
return '${sol.toStringAsFixed(decimals)} SOL';
}
}
// Usage
final amount = 1.5; // SOL
final lamports = SolanaConverter.solToLamports(amount);
print('$amount SOL = $lamports lamports');
final formatted = SolanaConverter.formatLamports(1500000000);
print(formatted); // "1.5000 SOL"
Transaction Explorer Links
Copy
Ask AI
class SolanaExplorer {
static String getTransactionUrl(
String signature, {
String cluster = 'devnet',
}) {
return 'https://explorer.solana.com/tx/$signature?cluster=$cluster';
}
static String getAddressUrl(
String address, {
String cluster = 'devnet',
}) {
return 'https://explorer.solana.com/address/$address?cluster=$cluster';
}
}
// Usage
final txUrl = SolanaExplorer.getTransactionUrl(
signature,
cluster: 'mainnet-beta',
);
Signer Methods
| Method | Description |
|---|---|
createSigner(wallet:) | Create a signer for a Solana wallet |
signMessage(message:) | Sign an arbitrary message |
signAndSendTransaction(transaction:) | Sign and broadcast a transaction |
Error Handling
Copy
Ask AI
Future<String?> sendSOLSafely({
required BaseWallet wallet,
required String recipient,
required double amount,
}) async {
try {
return await sendSOL(
wallet: wallet,
recipientAddress: recipient,
amount: amount,
);
} catch (e) {
final errorDesc = e.toString().toLowerCase();
if (errorDesc.contains('insufficient')) {
print('Insufficient balance');
} else if (errorDesc.contains('blockhash')) {
print('Blockhash expired, try again');
} else if (errorDesc.contains('invalid')) {
print('Invalid address or parameters');
} else {
print('Transaction failed: $e');
}
return null;
}
}
Best Practices
1. Validate Address
Copy
Ask AI
bool isValidSolanaAddress(String address) {
try {
Pubkey.fromString(address);
return true;
} catch (e) {
return false;
}
}
// Usage
if (!isValidSolanaAddress(recipientAddress)) {
throw Exception('Invalid Solana address');
}
2. Check Balance Before Sending
Copy
Ask AI
Future<bool> hasEnoughBalance({
required BaseWallet wallet,
required double amount,
}) async {
try {
final connection = DynamicSDK.instance.solana.createConnection();
final pubkey = Pubkey.fromString(wallet.address);
final balance = await connection.getBalance(pubkey);
final requiredLamports = SolanaConverter.solToLamports(amount);
// Add transaction fee (typically 5000 lamports)
final totalRequired = requiredLamports + 5000;
return balance >= totalRequired;
} catch (e) {
return false;
}
}
3. Show Confirmation Dialog
Copy
Ask AI
Future<bool> showTransactionConfirmation(
BuildContext context, {
required String recipient,
required String amount,
}) async {
return await showDialog<bool>(
context: context,
builder: (context) => 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 SOL'),
const SizedBox(height: 8),
const Text(
'This transaction cannot be reversed.',
style: TextStyle(color: Colors.orange, fontSize: 12),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirm'),
),
],
),
) ?? false;
}
Devnet Faucet
For testing, get free devnet SOL from the Solana Faucet.Complete Example with Error Handling
Copy
Ask AI
import 'package:flutter/material.dart';
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:solana/solana.dart';
Future<void> sendSOLWithValidation(
BuildContext context, {
required BaseWallet wallet,
required String recipient,
required double amount,
}) async {
// Validate address
if (!isValidSolanaAddress(recipient)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid recipient address')),
);
return;
}
// Check balance
final hasBalance = await hasEnoughBalance(wallet: wallet, amount: amount);
if (!hasBalance) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Insufficient balance')),
);
return;
}
// Show confirmation
final confirmed = await showTransactionConfirmation(
context,
recipient: recipient,
amount: amount.toString(),
);
if (!confirmed) return;
// Send transaction
try {
final signature = await sendSOL(
wallet: wallet,
recipientAddress: recipient,
amount: amount,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Transaction sent: $signature')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Transaction failed: $e')),
);
}
}
bool isValidSolanaAddress(String address) {
try {
Pubkey.fromString(address);
return true;
} catch (e) {
return false;
}
}
Future<bool> hasEnoughBalance({
required BaseWallet wallet,
required double amount,
}) async {
try {
final connection = DynamicSDK.instance.solana.createConnection();
final pubkey = Pubkey.fromString(wallet.address);
final balance = await connection.getBalance(pubkey);
final requiredLamports = (amount * 1e9).toInt();
final totalRequired = requiredLamports + 5000; // Add fee
return balance >= totalRequired;
} catch (e) {
return false;
}
}
Future<bool> showTransactionConfirmation(
BuildContext context, {
required String recipient,
required String amount,
}) async {
return await showDialog<bool>(
context: context,
builder: (context) => 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 SOL'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirm'),
),
],
),
) ?? false;
}
Next Steps
- Solana Connection - Connection setup
- Sign Solana Messages - Sign messages
- Token Balances - Get SOL balance