Skip to main content

WalletDelegate Guide

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

Protocol Definition

public protocol WalletDelegate: AnyObject {
    /// Get the connected wallet address
    func getAddress() throws -> String

    /// Get the current chain ID
    func getChainId() throws -> UInt64

    /// Sign a personal message (EIP-191)
    func signMessage(message: String) throws -> String

    /// Sign EIP-712 typed data
    func signTypedData(typedDataJson: String) throws -> String

    /// Send a transaction
    func sendTransaction(toAddress: String, value: String, data: String) throws -> 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: Integer matching expected network

Blocking Behavior

The SDK calls delegate methods synchronously. Your implementation must block until:
  • User approves/rejects (for signing)
  • Transaction is submitted (for sendTransaction)
  • Data is available (for getAddress/getChainId)
Use semaphores or similar synchronization for async wallet libraries.

WalletConnect Integration

Example using WalletConnect Swift SDK:
import WalletConnectSwift

class WalletConnectDelegate: WalletDelegate {
    private let session: Session
    private let client: Client

    init(session: Session, client: Client) {
        self.session = session
        self.client = client
    }

    func getAddress() throws -> String {
        guard let account = session.walletInfo?.accounts.first else {
            throw WalletError.notConnected
        }
        return account
    }

    func getChainId() throws -> UInt64 {
        guard let chainId = session.walletInfo?.chainId else {
            throw WalletError.notConnected
        }
        return UInt64(chainId)
    }

    func signMessage(message: String) throws -> String {
        let semaphore = DispatchSemaphore(value: 0)
        var result: Result<String, Error>?

        let address = try getAddress()

        try client.personal_sign(
            url: session.url,
            message: message,
            account: address
        ) { response in
            switch response {
            case .success(let signature):
                result = .success(signature)
            case .failure(let error):
                result = .failure(error)
            }
            semaphore.signal()
        }

        semaphore.wait()

        switch result {
        case .success(let signature):
            return signature
        case .failure(let error):
            throw error
        case .none:
            throw WalletError.timeout
        }
    }

    func signTypedData(typedDataJson: String) throws -> String {
        let semaphore = DispatchSemaphore(value: 0)
        var result: Result<String, Error>?

        let address = try getAddress()

        try client.eth_signTypedData(
            url: session.url,
            account: address,
            message: typedDataJson
        ) { response in
            switch response {
            case .success(let signature):
                result = .success(signature)
            case .failure(let error):
                result = .failure(error)
            }
            semaphore.signal()
        }

        semaphore.wait()

        switch result {
        case .success(let signature):
            return signature
        case .failure(let error):
            throw error
        case .none:
            throw WalletError.timeout
        }
    }

    func sendTransaction(toAddress: String, value: String, data: String) throws -> String {
        let semaphore = DispatchSemaphore(value: 0)
        var result: Result<String, Error>?

        let address = try getAddress()

        let transaction = Client.Transaction(
            from: address,
            to: toAddress,
            data: data,
            gas: nil,
            gasPrice: nil,
            value: value,
            nonce: nil
        )

        try client.eth_sendTransaction(
            url: session.url,
            transaction: transaction
        ) { response in
            switch response {
            case .success(let txHash):
                result = .success(txHash)
            case .failure(let error):
                result = .failure(error)
            }
            semaphore.signal()
        }

        semaphore.wait()

        switch result {
        case .success(let txHash):
            return txHash
        case .failure(let error):
            throw error
        case .none:
            throw WalletError.timeout
        }
    }
}

enum WalletError: Error {
    case notConnected
    case timeout
    case userRejected
}

Web3.swift Integration

Example using web3.swift with a local keystore:
import web3swift

class Web3Delegate: WalletDelegate {
    private let web3: web3
    private let keystoreManager: KeystoreManager
    private let address: EthereumAddress

    init(web3: web3, keystoreManager: KeystoreManager, address: EthereumAddress) {
        self.web3 = web3
        self.keystoreManager = keystoreManager
        self.address = address
    }

    func getAddress() throws -> String {
        return address.address
    }

    func getChainId() throws -> UInt64 {
        guard let chainId = web3.provider.network?.chainID else {
            throw Web3Error.connectionError
        }
        return UInt64(chainId)
    }

    func signMessage(message: String) throws -> String {
        let messageData = message.data(using: .utf8)!
        let signature = try web3.wallet.signPersonalMessage(
            messageData,
            account: address,
            password: ""  // Or prompt user
        )
        return signature.toHexString().addHexPrefix()
    }

    func signTypedData(typedDataJson: String) throws -> String {
        // Parse typed data and sign
        let data = typedDataJson.data(using: .utf8)!
        let typedData = try JSONDecoder().decode(EIP712TypedData.self, from: data)

        let signature = try web3.wallet.signTypedData(
            typedData,
            account: address,
            password: ""
        )
        return signature.toHexString().addHexPrefix()
    }

    func sendTransaction(toAddress: String, value: String, data: String) throws -> String {
        var transaction = EthereumTransaction(
            to: EthereumAddress(toAddress)!,
            value: BigUInt(value)!,
            data: Data(hex: data)
        )

        let result = try web3.eth.sendTransaction(transaction)
        return result.hash
    }
}

Testing with Mock Wallet

For testing, create a mock delegate:
class MockWalletDelegate: WalletDelegate {
    let mockAddress = "0x1234567890123456789012345678901234567890"
    let mockChainId: UInt64 = 1

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

    func getAddress() throws -> String {
        return mockAddress
    }

    func getChainId() throws -> UInt64 {
        return mockChainId
    }

    func signMessage(message: String) throws -> String {
        if let handler = signMessageHandler {
            return handler(message)
        }
        // Return a valid-looking signature
        return "0x" + String(repeating: "ab", count: 65)
    }

    func signTypedData(typedDataJson: String) throws -> String {
        if let handler = signTypedDataHandler {
            return handler(typedDataJson)
        }
        return "0x" + String(repeating: "cd", count: 65)
    }

    func sendTransaction(toAddress: String, value: String, data: String) throws -> String {
        if let handler = sendTransactionHandler {
            return handler(toAddress, value, data)
        }
        return "0x" + String(repeating: "ef", count: 32)
    }
}

Error Handling

Throw appropriate errors from delegate methods:
enum WalletDelegateError: Error, LocalizedError {
    case notConnected
    case userRejected
    case networkError(String)
    case invalidSignature
    case transactionFailed(String)

    var errorDescription: String? {
        switch self {
        case .notConnected:
            return "Wallet not connected"
        case .userRejected:
            return "User rejected the request"
        case .networkError(let message):
            return "Network error: \(message)"
        case .invalidSignature:
            return "Invalid signature returned"
        case .transactionFailed(let message):
            return "Transaction failed: \(message)"
        }
    }
}

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 - Ensure delegate methods can be called from any thread

Next Steps