Overview
Message signing allows users to prove ownership of their wallet by signing arbitrary messages. This is commonly used for authentication and verification.Prerequisites
- Dynamic SDK initialized (see Installation Guide)
- User authenticated (see Authentication Guide)
- EVM wallet available (see Wallet Creation)
dynamic_sdk_web3dartpackage installed
Sign a 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 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
Copy
Ask AI
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
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 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
Copy
Ask AI
/// 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
Copy
Ask AI
/// 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:Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
// 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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
- Typed Data Signing - Sign structured data (EIP-712)
- Send ETH Transactions - Send transactions
- Smart Contract Interactions - Interact with contracts