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 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 a raw message

Use signRawMessage to sign raw bytes without the EIP-191 message prefix. This is useful when you need to sign pre-hashed data or raw byte payloads.
import 'dart:typed_data';
import 'package:dynamic_sdk/dynamic_sdk.dart';

final sdk = DynamicSDK.instance;

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

// Usage
final wallet = sdk.wallets.userWallets.first;
final rawBytes = Uint8List.fromList([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
final signature = await signRawMessage(
  wallet: wallet,
  rawMessage: rawBytes,
);

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