Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

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