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
- Dynamic SDK initialized (see Quickstart)
- User authenticated (see Authentication)
- Solana wallet available (see Wallet Creation)
dynamic_sdk_solanapackage installed
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
- Send Solana Transactions - Transfer SOL
- Solana Connection - Connection setup
- EVM Message Signing - Sign with EVM wallets