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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
/// 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
Copy
Ask AI
/// 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:Copy
Ask AI
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
Copy
Ask AI
// 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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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