Skip to main content

Overview

Sign messages to prove wallet ownership for authentication and verification without transactions.

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()
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"
    return sdk.wallets.signMessage(wallet, message)
}

Signing 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)
}

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"
    val signature = sdk.wallets.signMessage(wallet, message)
    return PermitSignature(spender, amount, deadline, signature)
}

Proof of Ownership

suspend fun proveWalletOwnership(wallet: BaseWallet): String {
    val timestamp = System.currentTimeMillis()
    val message = "I own wallet ${wallet.address} at timestamp $timestamp"

    return sdk.wallets.signMessage(wallet, message)
}

// Usage in ViewModel
fun verifyOwnership() {
    viewModelScope.launch {
        try {
            val signature = proveWalletOwnership(wallet)
            _ownershipProof.value = signature
        } catch (e: Exception) {
            _errorMessage.value = "Failed to prove ownership: ${e.message}"
        }
    }
}

Verify Signatures

While signature verification typically happens on the backend or smart contract, here’s how to structure the verification data:
data class SignatureData(
    val message: String,
    val signature: String,
    val signerAddress: String,
    val timestamp: Long
) {
    fun toJson(): Map<String, Any> {
        return mapOf(
            "message" to message,
            "signature" to signature,
            "signer" to signerAddress,
            "timestamp" to timestamp
        )
    }
}

// Usage
val signatureData = SignatureData(
    message = "Hello, Dynamic!",
    signature = signature,
    signerAddress = wallet.address,
    timestamp = System.currentTimeMillis()
)

// Send to backend for verification
val jsonData = signatureData.toJson()

Backend Verification Example

// This would run on your backend server
import org.web3j.crypto.Keys
import org.web3j.crypto.Sign
import org.web3j.utils.Numeric

fun verifySignature(
    message: String,
    signature: String,
    expectedAddress: String
): Boolean {
    try {
        // Parse signature
        val signatureBytes = Numeric.hexStringToByteArray(signature)
        val r = signatureBytes.copyOfRange(0, 32)
        val s = signatureBytes.copyOfRange(32, 64)
        val v = signatureBytes[64]

        // Hash message with Ethereum prefix
        val messageHash = Sign.getEthereumMessageHash(message.toByteArray())

        // Recover address from signature
        val signData = Sign.SignatureData(v, r, s)
        val publicKey = Sign.signedMessageHashToKey(messageHash, signData)
        val recoveredAddress = "0x" + Keys.getAddress(publicKey)

        // Compare addresses (case-insensitive)
        return recoveredAddress.equals(expectedAddress, ignoreCase = true)
    } catch (e: Exception) {
        return false
    }
}

Best Practices

  • Always handle errors gracefully
  • Include clear context in messages (nonce, timestamp, purpose)
  • Show loading states while waiting for signature
  • Clear sensitive data when done
  • Validate message format (not empty, reasonable length)
  • Never sign messages you don’t understand

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 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(6)}...${walletAddress.takeLast(4)}",
                    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")
            }
        }
    )
}

Troubleshooting

User rejection: Add clear explanation of why signing is needed (proves ownership, free, no transaction) Unsupported wallet: Check if wallet supports signing before requesting

What’s Next