Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.privacyboost.io/llms.txt

Use this file to discover all available pages before exploring further.

Wallet Integration

The WalletDelegate interface is the bridge between the Privacy Boost SDK and your wallet. This guide covers implementing it for various wallet types, expanding on the brief examples in Authentication.

Interface Definition

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

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

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

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

    /** Send a transaction */
    suspend 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

Suspend Behavior

All WalletDelegate methods are suspend functions. The SDK calls them within a coroutine context, so your implementation can use Kotlin coroutines natively.

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