Public Account (EVM Client)
The public account is the entry point for any EVM address into the encrypted balance system. With just a standard wallet client (no ZK proving or decryption needed), you can:
- Send to encrypted address -- move public ERC-20 tokens into an encrypted balance at any ZK address
- Auto-encrypt -- link your EVM address to a ZK address and automatically encrypt incoming public transfers. Useful when interacting with users or services that don't use encrypted accounts.
This is the simplest integration level -- no ZK proving or decryption needed.
Setup
import { createEvmClient } from '@cardinal-cryptography/sdk'
import { baseSepolia } from 'viem/chains'
import { http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
const client = await createEvmClient({
chain: baseSepolia,
transport: http(),
account: privateKeyToAccount('0xYourPrivateKey...'),
})
The account is a standard viem account that signs EVM transactions -- the same one you'd pass to createWalletClient.
Send to encrypted address
Send public ERC-20 tokens to a ZK address. The tokens are burned from the sender's public balance and added to the encrypted balance at the specified ZK address.
await client.sendPublicToEncryptedTransfer({
token: 'zkUSD',
amount: 1000n,
zkpAddress: 'zk1YourZkAddress...',
})
You can send to any ZK address -- your own or someone else's. This is how users move tokens between public and encrypted accounts.
Auto-encrypt incoming public transfers
Auto-encrypt lets you receive public transfers and have them automatically encrypted -- without the sender needing to know about ZK addresses at all. It's useful when you interact with users or services that don't use encrypted accounts.
The first call links your EVM address to your ZK address on-chain — a one-time bind. From then on, the token contract automatically routes any public tokens sent to your EVM address into your encrypted balance.
await client.sendSetAutoEncrypt({
token: 'zkUSD',
enabled: true,
zkpAddress: 'zk1YourZkAddress...',
})
sendSetAutoEncrypt only does the initial bind — calling it again after an EPK is already bound reverts on-chain (AutoEncryptEpkAlreadySet). The bound EPK is permanent; only the enabled flag can be flipped afterwards:
await client.sendToggleAutoEncrypt({
token: 'zkUSD',
enabled: false,
})
sendToggleAutoEncrypt does not take zkpAddress — it operates on whatever EPK was already bound. On the bundler path the SDK reads the stored EPK from chain so the EIP-712 digest matches.
Recipient-bound ERC-20 transfer (EIP-3009)
For plain ERC-20 transfers (no encryption involved), the EVM client supports EIP-3009 transferWithAuthorization. Sign an off-chain authorization and the SharedAccount bundler forwards it — the recipient pays no gas, the sender doesn't need ETH. The signed digest commits to to, so an attacker who intercepts the signature can't redirect the funds.
await client.sendTransferWithAuthorization({
token: 'zkUSD',
to: '0xRecipient...',
value: 1000n,
})
Defaults: validAfter = 0n, validBefore = 20 minutes ahead, fresh random bytes32 nonce. Pass any of them explicitly to override.
To check whether a (signer, nonce) pair has already been consumed (used or canceled):
const used = await client.getAuthorizationState({
token: 'zkUSD',
authorizer: '0xSigner...',
nonce: '0x...',
})
This is plain ERC-20 — for moving public tokens into an encrypted balance, use sendPublicToEncryptedTransfer above.
Read on-chain state
The EVM client also has public read methods:
// Look up which ZK address is registered for an EVM account
const zkpAddr = await client.getAutoEncryptZkpAddress({
token: 'zkUSD',
address: '0xSomeEvmAddress...',
})
// Check if auto-encrypt is enabled for an address
const autoEncrypt = await client.getAutoEncrypt({
token: 'zkUSD',
address: '0xYourAddress...',
})
Prepare without sending
All write operations have a prepare variant that returns a transaction without sending it:
const prepared = client.preparePublicToEncryptedTransfer({
token: 'zkUSD',
amount: 1000n,
zkpAddress: 'zk1...',
})
// prepared: { tokenAddress, functionName, args }
This is useful for gas estimation or custom submission flows. See Transaction Lifecycle for the full prepare/sign/send pattern and Gas Sponsorship for bundler-based submission.