Skip to main content

Overview

This guide covers sending SOL, building transactions, and signing transactions on Solana using the Flutter Solana package.

Prerequisites

Send SOL

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

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:
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):
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"
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

MethodDescription
createSigner(wallet:)Create a signer for a Solana wallet
signMessage(message:)Sign an arbitrary message
signAndSendTransaction(transaction:)Sign and broadcast a transaction

Error Handling

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

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

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

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

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