Skip to main content
Version: develop

Read & Decrypt Balances

Balances in the encrypted balance system are stored on-chain as ciphertexts -- the chain can verify transfers without seeing amounts, but you need your encryption key to read your own balance. This guide shows how to decrypt your balance and transaction history.

Decryption only requires a viewing key -- you don't need your spending key. This means you can safely share viewing access (e.g., with a portfolio tracker or auditor) without giving them the ability to spend. See Keys & Addresses for details on the key hierarchy.

Unlike the full client, you only need a read-only runtime here -- no proving or transfer capabilities, just decryption.

Setup

import { createDecryptClient, createReadRuntime } from '@cardinal-cryptography/sdk'
import { baseSepolia } from 'viem/chains'
import { http } from 'viem'

// A read-only runtime provides decryption services only (no ZK proving)
// -- lighter than a full runtime since it doesn't load the prover
const runtime = await createReadRuntime()

// A read-only account can decrypt but can't sign or generate proofs
const account = runtime.createReadAccountFromMnemonic('your mnemonic here')

const client = createDecryptClient({
chain: baseSepolia,
transport: http(),
zkpAccount: account,
})

Decrypt current balance

The SDK fetches the encrypted balance from the contract, then decrypts it locally using your encryption key.

const balance = await client.getDecryptedBalance({ token: 'zkUSD' })
console.log(`Decrypted balance: ${balance}`)

Decrypt transaction history

Each event in your history is decrypted individually. The type field tells you what happened:

const logs = await client.getDecryptedBalanceLogs({
token: 'zkUSD',
fromBlock: 1000n,
})

for (const log of logs) {
switch (log.type) {
case 'deposit':
console.log(`Deposited ${log.amount} at block ${log.blockNumber}`)
break
case 'withdrawal':
console.log(`Withdrew ${log.amount} to ${log.recipient}`)
break
case 'transfer_in':
console.log(`Received ${log.amount} (transfer ${log.transferId})`)
break
case 'transfer_out':
console.log(`Sent ${log.amount} (transfer ${log.transferId})`)
break
}
}

Filter by event type

const deposits = await client.getDecryptedBalanceLogs({
token: 'zkUSD',
fromBlock: 1000n,
events: ['deposit', 'transfer_in'],
})

Cleanup

runtime.destroy()

How decryption works

Encrypted balances use ElGamal encryption -- a scheme that lets the contract perform math on encrypted values (like adding a transfer amount to your balance) without decrypting them. To read your balance, the SDK fetches the ciphertext and decrypts it client-side using your encryption key. No private data leaves your machine.