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

Message signing on Solana allows users to prove wallet ownership by signing arbitrary messages using the Flutter Solana package.

Prerequisites

Sign Message

import 'package:dynamic_sdk/dynamic_sdk.dart';

final sdk = DynamicSDK.instance;

Future<String> signMessage({
  required BaseWallet wallet,
  required String message,
}) async {
  try {
    final signer = sdk.solana.createSigner(wallet: wallet);
    final signature = await signer.signMessage(message: message);
    print('Signature: $signature');
    return signature;
  } catch (e) {
    print('Failed to sign message: $e');
    rethrow;
  }
}

// Usage
final wallet = sdk.wallets.userWallets.firstWhere(
  (w) => w.chain.toUpperCase() == 'SOL',
);
final signature = await signMessage(
  wallet: wallet,
  message: 'Hello, Solana!',
);

Flutter Widget Example

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

class SolanaSignWidget extends StatefulWidget {
  final BaseWallet wallet;

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

  @override
  State<SolanaSignWidget> createState() => _SolanaSignWidgetState();
}

class _SolanaSignWidgetState extends State<SolanaSignWidget> {
  final sdk = DynamicSDK.instance;
  final _messageController = TextEditingController();

  String? signature;
  bool isLoading = false;
  String? error;

  @override
  void dispose() {
    _messageController.dispose();
    super.dispose();
  }

  String formatAddress(String address) {
    if (address.length <= 10) return address;
    return '${address.substring(0, 6)}...${address.substring(address.length - 4)}';
  }

  Future<void> _signMessage() async {
    final message = _messageController.text.trim();

    if (message.isEmpty) {
      setState(() => error = 'Please enter a message');
      return;
    }

    setState(() {
      isLoading = true;
      error = null;
      signature = null;
    });

    try {
      final signer = sdk.solana.createSigner(wallet: widget.wallet);
      final sig = await signer.signMessage(message: message);
      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(
            'Wallet: ${formatAddress(widget.wallet.address)}',
            style: const TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _messageController,
            decoration: const InputDecoration(
              labelText: 'Message to sign',
              border: OutlineInputBorder(),
            ),
            maxLines: 3,
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: isLoading ? null : _signMessage,
            child: isLoading
                ? const SizedBox(
                    height: 20,
                    width: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Text('Sign Message'),
          ),
          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(
                    'Signature:',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  SelectableText(
                    signature!,
                    style: const TextStyle(
                      fontSize: 12,
                      fontFamily: 'monospace',
                    ),
                  ),
                  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'),
                  ),
                ],
              ),
            ),
          ],
          if (error != null) ...[
            const SizedBox(height: 16),
            Text(
              error!,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ],
        ],
      ),
    );
  }
}

Authentication Use Case

/// Sign a message to prove wallet ownership
Future<String> authenticateWithSignature(BaseWallet wallet) async {
  final nonce = DateTime.now().millisecondsSinceEpoch.toString();
  final message = 'Sign to authenticate: $nonce';

  final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
  final signature = await signer.signMessage(message: message);

  // Send signature to your backend for verification
  return signature;
}

Sign Action Confirmation

/// Sign a message to confirm user action
Future<String> signActionConfirmation({
  required BaseWallet wallet,
  required String action,
  required DateTime timestamp,
}) async {
  final message = '''
Action: $action
Wallet: ${wallet.address}
Timestamp: ${timestamp.millisecondsSinceEpoch}
''';

  final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
  return await signer.signMessage(message: message);
}

Verify Signature Data

Structure for sending to backend:
class SolanaSignatureData {
  final String message;
  final String signature;
  final String publicKey;

  SolanaSignatureData({
    required this.message,
    required this.signature,
    required this.publicKey,
  });

  Map<String, String> toJson() {
    return {
      'message': message,
      'signature': signature,
      'publicKey': publicKey,
    };
  }
}

// Usage
final signatureData = SolanaSignatureData(
  message: 'Hello, Solana!',
  signature: signature,
  publicKey: wallet.address,
);

final jsonData = signatureData.toJson();
// Send to backend for verification

Best Practices

1. Include Context

// Bad: Unclear message
const message = '12345';

// Good: Clear message with context
String createClearMessage(BaseWallet wallet) {
  return '''
Welcome to MyApp!

Sign to prove ownership of this wallet.

Wallet: ${wallet.address}
Nonce: ${DateTime.now().millisecondsSinceEpoch}
Timestamp: ${DateTime.now().toIso8601String()}
''';
}

2. Handle Errors

Future<String?> signMessageSafely({
  required BaseWallet wallet,
  required String message,
}) async {
  try {
    final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
    return await signer.signMessage(message: message);
  } catch (e) {
    final errorDesc = e.toString().toLowerCase();

    if (errorDesc.contains('rejected') || errorDesc.contains('denied')) {
      print('User rejected the signature request');
    } else {
      print('Signing failed: $e');
    }
    return null;
  }
}

3. Clear Sensitive Data

Future<void> signAndClear(BaseWallet wallet, String message) async {
  String? signature;

  try {
    final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
    signature = await signer.signMessage(message: message);
    // ... use signature ...
  } finally {
    signature = null; // Clear from memory
  }
}

Complete Authentication Flow

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

class SolanaAuthWidget extends StatefulWidget {
  final BaseWallet wallet;
  final Function(String signature) onAuthenticated;

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

  @override
  State<SolanaAuthWidget> createState() => _SolanaAuthWidgetState();
}

class _SolanaAuthWidgetState extends State<SolanaAuthWidget> {
  final sdk = DynamicSDK.instance;
  bool isLoading = false;
  String? error;

  Future<void> _authenticate() async {
    setState(() {
      isLoading = true;
      error = null;
    });

    try {
      // Generate nonce
      final nonce = DateTime.now().millisecondsSinceEpoch.toString();

      // Create authentication message
      final message = '''
Welcome to MyApp!

Sign this message to authenticate your wallet.

Wallet: ${widget.wallet.address}
Nonce: $nonce
Timestamp: ${DateTime.now().toIso8601String()}

This signature will not trigger any blockchain transaction or cost any fees.
''';

      // Sign message
      final signer = sdk.solana.createSigner(wallet: widget.wallet);
      final signature = await signer.signMessage(message: message);

      // Call success callback
      widget.onAuthenticated(signature);
    } catch (e) {
      setState(() => error = 'Authentication failed: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  String formatAddress(String address) {
    if (address.length <= 10) return address;
    return '${address.substring(0, 6)}...${address.substring(address.length - 4)}';
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Text(
          'Authenticate Your Wallet',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 16),
        Text(
          'Wallet: ${formatAddress(widget.wallet.address)}',
          style: const TextStyle(fontSize: 14, color: Colors.grey),
        ),
        const SizedBox(height: 32),
        ElevatedButton(
          onPressed: isLoading ? null : _authenticate,
          child: isLoading
              ? const SizedBox(
                  height: 20,
                  width: 20,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : const Text('Sign to Authenticate'),
        ),
        if (error != null) ...[
          const SizedBox(height: 16),
          Text(
            error!,
            style: const TextStyle(color: Colors.red),
          ),
        ],
      ],
    );
  }
}

Error Handling

Future<String?> signWithErrorHandling({
  required BaseWallet wallet,
  required String message,
}) async {
  try {
    final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
    final signature = await signer.signMessage(message: message);
    print('Message signed: $signature');
    return signature;
  } catch (e) {
    final errorDesc = e.toString().toLowerCase();

    if (errorDesc.contains('rejected') || errorDesc.contains('cancelled')) {
      print('User rejected the signature request');
    } else if (errorDesc.contains('network')) {
      print('Network error occurred');
    } else {
      print('Failed to sign message: $e');
    }
    return null;
  }
}

FutureBuilder Example

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

class SignMessageButton extends StatelessWidget {
  final BaseWallet wallet;
  final String message;

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

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        final signature = await _showSigningDialog(context);
        if (signature != null) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Signed: $signature')),
          );
        }
      },
      child: const Text('Sign Message'),
    );
  }

  Future<String?> _showSigningDialog(BuildContext context) async {
    return await showDialog<String>(
      context: context,
      barrierDismissible: false,
      builder: (context) => FutureBuilder<String>(
        future: _signMessage(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const AlertDialog(
              content: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 16),
                  Text('Signing message...'),
                ],
              ),
            );
          }

          if (snapshot.hasError) {
            return AlertDialog(
              title: const Text('Error'),
              content: Text('Failed to sign: ${snapshot.error}'),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('Close'),
                ),
              ],
            );
          }

          if (snapshot.hasData) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              Navigator.of(context).pop(snapshot.data);
            });
          }

          return const SizedBox.shrink();
        },
      ),
    );
  }

  Future<String> _signMessage() async {
    final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
    return await signer.signMessage(message: message);
  }
}

Next Steps