Skip to main content

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
    }
}
@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