Skip to main content

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