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
Sign messages to prove wallet ownership for authentication without transaction fees.
Prerequisites
Sign a Message
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.BaseWallet
val sdk = DynamicSDK.getInstance()
suspend fun signMessage(wallet: BaseWallet, message: String): String {
return try {
val signature = sdk.wallets.signMessage(wallet, message)
println("Message signed successfully!")
println("Signature: $signature")
signature
} catch (e: Exception) {
println("Failed to sign message: ${e.message}")
throw e
}
}
// Usage
val wallet = sdk.wallets.userWallets.first { it.chain.uppercase() == "SOL" }
val signature = signMessage(wallet, "Hello, Dynamic!")
Common Use Cases
Authentication
import java.util.UUID
data class AuthenticationRequest(
val message: String,
val nonce: String,
val timestamp: Long
)
suspend fun authenticateWithSignature(wallet: BaseWallet): String {
val nonce = UUID.randomUUID().toString()
val timestamp = System.currentTimeMillis()
val message = "Sign this message to authenticate with MyApp.\n\nNonce: $nonce\nTimestamp: $timestamp\n\nThis request will not trigger a blockchain transaction or cost any fees."
return sdk.wallets.signMessage(wallet, message)
}
Sign User Actions
data class UserAction(
val action: String,
val walletAddress: String,
val timestamp: Long
)
suspend fun signUserAction(wallet: BaseWallet, action: String): String {
val timestamp = System.currentTimeMillis()
val message = "Action: $action\nWallet: ${wallet.address}\nTimestamp: $timestamp"
return sdk.wallets.signMessage(wallet, message)
}
Proof of Ownership
suspend fun proveWalletOwnership(wallet: BaseWallet): Pair<String, String> {
val timestamp = System.currentTimeMillis()
val message = "I own wallet ${wallet.address}\n\nTimestamp: $timestamp\n\nThis signature proves ownership without revealing my private key."
val signature = sdk.wallets.signMessage(wallet, message)
return Pair(message, signature)
}
Off-Chain Signatures
data class PermitSignature(
val spender: String,
val amount: String,
val deadline: Long,
val signature: String
)
suspend fun signOffChainPermit(
wallet: BaseWallet,
spender: String,
amount: String,
deadline: Long
): PermitSignature {
val message = "Permit:\nSpender: $spender\nAmount: $amount\nDeadline: $deadline\n\nThis is an off-chain signature and will not incur transaction fees."
val signature = sdk.wallets.signMessage(wallet, message)
return PermitSignature(spender, amount, deadline, signature)
}
Verify Signatures
While signature verification typically happens on the backend, here’s how to structure the verification data:
data class SignatureData(
val message: String,
val signature: String,
val signerAddress: String,
val timestamp: Long,
val chain: String = "SOL"
) {
fun toJson(): Map<String, Any> {
return mapOf(
"message" to message,
"signature" to signature,
"signer" to signerAddress,
"timestamp" to timestamp,
"chain" to chain
)
}
}
// Usage
suspend fun createSignatureData(wallet: BaseWallet, message: String): SignatureData {
val signature = sdk.wallets.signMessage(wallet, message)
return SignatureData(
message = message,
signature = signature,
signerAddress = wallet.address,
timestamp = System.currentTimeMillis()
)
}
// Send to backend for verification
val signatureData = createSignatureData(wallet, "Hello, Dynamic!")
val jsonData = signatureData.toJson()
Best Practices
- Always handle errors gracefully
- Include clear context in messages (wallet address, nonce, timestamp, purpose)
- Show loading states while waiting for signature
- Clear sensitive data when done
- Validate message format (not empty, reasonable length)
- Explain that signing is free and doesn’t create transactions
Error Handling
sealed class SignatureError {
data class UserRejected(val message: String) : SignatureError()
data class UnsupportedWallet(val message: String) : SignatureError()
data class NetworkError(val message: String) : SignatureError()
data class InvalidMessage(val message: String) : SignatureError()
data class Unknown(val message: String) : SignatureError()
}
fun parseSignatureError(e: Exception): SignatureError {
val errorMessage = e.message?.lowercase() ?: ""
return when {
errorMessage.contains("rejected") || errorMessage.contains("denied") ->
SignatureError.UserRejected("User rejected the signature request")
errorMessage.contains("unsupported") ->
SignatureError.UnsupportedWallet("Wallet does not support message signing")
errorMessage.contains("network") ->
SignatureError.NetworkError("Network error occurred")
errorMessage.contains("invalid") ->
SignatureError.InvalidMessage("Invalid message format")
else ->
SignatureError.Unknown("Signing failed: ${e.message}")
}
}
// Usage in ViewModel
try {
val signed = sdk.wallets.signMessage(wallet, _message.value)
_signedMessage.value = signed
} catch (e: Exception) {
val error = parseSignatureError(e)
_errorMessage.value = when (error) {
is SignatureError.UserRejected -> error.message
is SignatureError.UnsupportedWallet -> error.message
is SignatureError.NetworkError -> error.message
is SignatureError.InvalidMessage -> error.message
is SignatureError.Unknown -> error.message
}
}
User Consent Dialog
@Composable
fun SignatureConsentDialog(
message: String,
walletAddress: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Sign Message") },
text = {
Column {
Text(
"You are about to sign the following message with your Solana wallet:",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Text(
text = message,
modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodySmall
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
"Wallet: ${walletAddress.take(8)}...${walletAddress.takeLast(8)}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"This action is free and will not send a transaction.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
confirmButton = {
Button(onClick = onConfirm) {
Text("Sign")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
Signature Explanation UI
@Composable
fun SignatureExplanation() {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Why sign this message?",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Signing proves you own this wallet without revealing your private keys or spending any SOL. It's free and doesn't create a blockchain transaction.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
Troubleshooting
User rejection: Explain why signing is needed (verifies ownership, free, no transaction, private key stays secure)
Unsupported wallet: Check if wallet supports signing before requesting
Long messages: Warn users to review carefully if message exceeds 500 characters
Security Considerations
- Never sign messages you don’t understand
- Always display message content clearly before signing
- Malicious signatures could be used to impersonate you
What’s Next