Skip to main content

WalletDelegate Guide

The WalletDelegate interface is the bridge between the Privacy Boost SDK and your wallet integration. This guide covers implementing it for various wallet types.

Interface Definition

interface WalletDelegate {
    /** Get the connected wallet address */
    fun getAddress(): String

    /** Get the current chain ID */
    fun getChainId(): ULong

    /** Sign a personal message (EIP-191) */
    fun signMessage(message: String): String

    /** Sign EIP-712 typed data */
    fun signTypedData(typedDataJson: String): String

    /** Send a transaction */
    fun sendTransaction(toAddress: String, value: String, data: String): String
}

Implementation Requirements

Return Values

  • Addresses: Checksummed hex strings with 0x prefix (42 characters)
  • Signatures: Hex strings with 0x prefix (132 characters for 65 bytes)
  • Transaction hashes: Hex strings with 0x prefix (66 characters)
  • Chain ID: ULong matching expected network

Blocking Behavior

The SDK calls delegate methods on a background thread. Your implementation must block until:
  • User approves/rejects (for signing)
  • Transaction is submitted (for sendTransaction)
  • Data is available (for getAddress/getChainId)
Use runBlocking or CountDownLatch for async wallet libraries.

WalletConnect Integration

Example using WalletConnect Kotlin SDK:
import com.walletconnect.web3.wallet.client.Web3Wallet
import kotlinx.coroutines.runBlocking
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class WalletConnectDelegate(
    private val sessionTopic: String,
    private val account: String
) : WalletDelegate {

    override fun getAddress(): String = account

    override fun getChainId(): ULong {
        // Parse from account (eip155:1:0x...)
        val parts = account.split(":")
        return parts.getOrNull(1)?.toULongOrNull() ?: 1UL
    }

    override fun signMessage(message: String): String {
        val latch = CountDownLatch(1)
        var result: String? = null
        var error: Throwable? = null

        val request = Web3Wallet.Request.PersonalSign(
            sessionTopic = sessionTopic,
            message = message,
            address = getAddress()
        )

        Web3Wallet.request(request,
            onSuccess = { signature ->
                result = signature
                latch.countDown()
            },
            onError = { e ->
                error = e
                latch.countDown()
            }
        )

        if (!latch.await(60, TimeUnit.SECONDS)) {
            throw WalletException("Signing timeout")
        }

        error?.let { throw it }
        return result ?: throw WalletException("No signature returned")
    }

    override fun signTypedData(typedDataJson: String): String {
        val latch = CountDownLatch(1)
        var result: String? = null
        var error: Throwable? = null

        val request = Web3Wallet.Request.SignTypedData(
            sessionTopic = sessionTopic,
            data = typedDataJson,
            address = getAddress()
        )

        Web3Wallet.request(request,
            onSuccess = { signature ->
                result = signature
                latch.countDown()
            },
            onError = { e ->
                error = e
                latch.countDown()
            }
        )

        if (!latch.await(60, TimeUnit.SECONDS)) {
            throw WalletException("Signing timeout")
        }

        error?.let { throw it }
        return result ?: throw WalletException("No signature returned")
    }

    override fun sendTransaction(toAddress: String, value: String, data: String): String {
        val latch = CountDownLatch(1)
        var result: String? = null
        var error: Throwable? = null

        val request = Web3Wallet.Request.SendTransaction(
            sessionTopic = sessionTopic,
            from = getAddress(),
            to = toAddress,
            value = value,
            data = data
        )

        Web3Wallet.request(request,
            onSuccess = { txHash ->
                result = txHash
                latch.countDown()
            },
            onError = { e ->
                error = e
                latch.countDown()
            }
        )

        if (!latch.await(120, TimeUnit.SECONDS)) {
            throw WalletException("Transaction timeout")
        }

        error?.let { throw it }
        return result ?: throw WalletException("No transaction hash returned")
    }
}

class WalletException(message: String) : Exception(message)

Web3j Integration

Example using Web3j with a local keystore:
import org.web3j.crypto.Credentials
import org.web3j.crypto.Sign
import org.web3j.protocol.Web3j
import org.web3j.protocol.core.DefaultBlockParameterName
import org.web3j.protocol.http.HttpService
import org.web3j.tx.RawTransactionManager
import org.web3j.utils.Numeric

class Web3jDelegate(
    private val credentials: Credentials,
    private val web3j: Web3j
) : WalletDelegate {

    override fun getAddress(): String = credentials.address

    override fun getChainId(): ULong {
        return web3j.ethChainId().send().chainId.toLong().toULong()
    }

    override fun signMessage(message: String): String {
        val messageBytes = message.toByteArray()
        val prefix = "\u0019Ethereum Signed Message:\n${messageBytes.size}"
        val prefixedMessage = (prefix + message).toByteArray()

        val signature = Sign.signPrefixedMessage(prefixedMessage, credentials.ecKeyPair)

        val r = Numeric.toHexStringNoPrefix(signature.r)
        val s = Numeric.toHexStringNoPrefix(signature.s)
        val v = Numeric.toHexStringNoPrefix(byteArrayOf(signature.v[0]))

        return "0x$r$s$v"
    }

    override fun signTypedData(typedDataJson: String): String {
        // Parse and sign EIP-712 typed data
        val typedData = parseTypedData(typedDataJson)
        val structHash = hashTypedData(typedData)

        val signature = Sign.signMessage(structHash, credentials.ecKeyPair, false)

        val r = Numeric.toHexStringNoPrefix(signature.r)
        val s = Numeric.toHexStringNoPrefix(signature.s)
        val v = Numeric.toHexStringNoPrefix(byteArrayOf(signature.v[0]))

        return "0x$r$s$v"
    }

    override fun sendTransaction(toAddress: String, value: String, data: String): String {
        val nonce = web3j.ethGetTransactionCount(
            credentials.address,
            DefaultBlockParameterName.LATEST
        ).send().transactionCount

        val gasPrice = web3j.ethGasPrice().send().gasPrice
        val gasLimit = estimateGas(toAddress, value, data)

        val txManager = RawTransactionManager(web3j, credentials, getChainId().toLong())

        val txHash = txManager.sendTransaction(
            gasPrice,
            gasLimit,
            toAddress,
            data,
            value.toBigInteger()
        ).transactionHash

        return txHash
    }

    private fun estimateGas(to: String, value: String, data: String): java.math.BigInteger {
        val estimate = web3j.ethEstimateGas(
            org.web3j.protocol.core.methods.request.Transaction.createFunctionCallTransaction(
                credentials.address,
                null,
                null,
                null,
                to,
                value.toBigInteger(),
                data
            )
        ).send()
        return estimate.amountUsed.multiply(java.math.BigInteger.valueOf(120)).divide(java.math.BigInteger.valueOf(100))
    }

    private fun parseTypedData(json: String): TypedData {
        // Implementation depends on your JSON parsing library
        TODO("Parse EIP-712 typed data")
    }

    private fun hashTypedData(typedData: TypedData): ByteArray {
        // Implementation of EIP-712 hashing
        TODO("Hash typed data")
    }
}

Testing with Mock Wallet

For testing, create a mock delegate:
class MockWalletDelegate(
    private val mockAddress: String = "0x1234567890123456789012345678901234567890",
    private val mockChainId: ULong = 1UL
) : WalletDelegate {

    var signMessageHandler: ((String) -> String)? = null
    var signTypedDataHandler: ((String) -> String)? = null
    var sendTransactionHandler: ((String, String, String) -> String)? = null

    override fun getAddress(): String = mockAddress

    override fun getChainId(): ULong = mockChainId

    override fun signMessage(message: String): String {
        return signMessageHandler?.invoke(message)
            ?: "0x" + "ab".repeat(65)
    }

    override fun signTypedData(typedDataJson: String): String {
        return signTypedDataHandler?.invoke(typedDataJson)
            ?: "0x" + "cd".repeat(65)
    }

    override fun sendTransaction(toAddress: String, value: String, data: String): String {
        return sendTransactionHandler?.invoke(toAddress, value, data)
            ?: "0x" + "ef".repeat(32)
    }
}

Error Handling

Throw appropriate exceptions from delegate methods:
sealed class WalletDelegateException(message: String) : Exception(message) {
    class NotConnected : WalletDelegateException("Wallet not connected")
    class UserRejected : WalletDelegateException("User rejected the request")
    class NetworkError(message: String) : WalletDelegateException("Network error: $message")
    class InvalidSignature : WalletDelegateException("Invalid signature returned")
    class TransactionFailed(message: String) : WalletDelegateException("Transaction failed: $message")
    class Timeout : WalletDelegateException("Operation timed out")
}

Best Practices

  1. Validate inputs - Check addresses and data formats before processing
  2. Handle timeouts - Set reasonable timeouts for wallet operations
  3. User feedback - Show loading indicators during signing/transactions
  4. Error messages - Provide clear error messages for user-facing errors
  5. Thread safety - Delegate methods are called from background threads

Next Steps