Skip to main content

Overview

Message signing allows users to prove ownership of their wallet by signing arbitrary messages. This is commonly used for authentication and verification.

Prerequisites

Sign a Message

import 'package:dynamic_sdk/dynamic_sdk.dart';

final sdk = DynamicSDK.instance;

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

// Usage
final wallet = sdk.wallets.userWallets.first;
final signature = await signMessage(
  wallet: wallet,
  message: 'Hello, Dynamic!',
);

Sign Message Widget

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

class SignMessageWidget extends StatefulWidget {
  final BaseWallet wallet;

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

  @override
  State<SignMessageWidget> createState() => _SignMessageWidgetState();
}

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

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

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

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

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

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

    try {
      final sig = await sdk.wallets.signMessage(
        wallet: widget.wallet,
        message: message,
      );
      setState(() => signature = sig);
    } catch (e) {
      setState(() => errorMessage = 'Failed to sign: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          TextField(
            controller: _messageController,
            decoration: const InputDecoration(
              labelText: 'Enter 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.grey[200],
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Signature:',
                    style: TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                  const SizedBox(height: 8),
                  SelectableText(
                    signature!,
                    style: const TextStyle(
                      fontSize: 12,
                      fontFamily: 'monospace',
                    ),
                    maxLines: 4,
                  ),
                  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 (errorMessage != null) ...[
            const SizedBox(height: 16),
            Text(
              errorMessage!,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ],
        ],
      ),
    );
  }
}

Common Use Cases

Authentication

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

  final signature = await DynamicSDK.instance.wallets.signMessage(
    wallet: wallet,
    message: message,
  );

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

Signing User Actions

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

  return await DynamicSDK.instance.wallets.signMessage(
    wallet: wallet,
    message: message,
  );
}

Off-Chain Signatures

/// Create off-chain signature for gasless transactions
Future<String> signOffChainPermit({
  required BaseWallet wallet,
  required String spender,
  required String amount,
  required int deadline,
}) async {
  final message = '''
Permit:
Spender: $spender
Amount: $amount
Deadline: $deadline
''';

  return await DynamicSDK.instance.wallets.signMessage(
    wallet: wallet,
    message: message,
  );
}

Verify Signatures

While signature verification typically happens on the backend or smart contract, here’s how to structure the verification data:
class SignatureData {
  final String message;
  final String signature;
  final String signerAddress;

  SignatureData({
    required this.message,
    required this.signature,
    required this.signerAddress,
  });

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

// Usage
final signatureData = SignatureData(
  message: 'Hello, Dynamic!',
  signature: signature,
  signerAddress: wallet.address,
);

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

Best Practices

1. Always Handle Errors

Future<String?> signMessageSafely({
  required BaseWallet wallet,
  required String message,
}) async {
  try {
    final signature = await DynamicSDK.instance.wallets.signMessage(
      wallet: wallet,
      message: message,
    );
    return signature;
  } catch (e) {
    print('Could not sign message. Please try again.');
    return null;
  }
}

2. Include Context in Messages

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

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

Click "Sign" to prove you own this wallet.

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

3. Show Loading States

class MessageSigner extends StatefulWidget {
  final BaseWallet wallet;

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

  @override
  State<MessageSigner> createState() => _MessageSignerState();
}

class _MessageSignerState extends State<MessageSigner> {
  bool isLoading = false;

  Future<void> signMessage(String message) async {
    setState(() => isLoading = true);

    try {
      final signature = await DynamicSDK.instance.wallets.signMessage(
        wallet: widget.wallet,
        message: message,
      );
      // Process signature
    } finally {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: isLoading ? null : () => signMessage('Hello!'),
      child: isLoading
          ? const CircularProgressIndicator()
          : const Text('Sign Message'),
    );
  }
}

4. Clear Sensitive Data

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

  try {
    signature = await DynamicSDK.instance.wallets.signMessage(
      wallet: wallet,
      message: message,
    );
    // ... use signature ...
  } finally {
    signature = null; // Clear from memory
  }
}

Error Handling

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

    if (errorDesc.contains('rejected') || errorDesc.contains('denied')) {
      print('User rejected the signature request');
    } else if (errorDesc.contains('unsupported')) {
      print('Wallet does not support message signing');
    } else {
      print('Signing failed: $e');
    }
    return null;
  }
}

Complete Authentication Flow

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

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

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

  @override
  State<AuthenticationWidget> createState() => _AuthenticationWidgetState();
}

class _AuthenticationWidgetState extends State<AuthenticationWidget> {
  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 gas fees.
''';

      // Sign message
      final signature = await sdk.wallets.signMessage(
        wallet: widget.wallet,
        message: message,
      );

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

  @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: ${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),
          ),
        ],
      ],
    );
  }
}

Next Steps