Skip to main content
Version: 0.4.7

Action Extensions

Viem action extensions that add ZKP capabilities to clients.

zkpPublicActions

On-chain read operations. Works with any PublicClient.

import { zkpPublicActions } from '@cardinal-cryptography/core'
client.extend(zkpPublicActions({ tokens?: TokenMap }))
MethodParametersReturnsDescription
getEncryptedBalanceCiphertext{ token, zkpAddress }ElGamalCiphertextRead encrypted balance
getAutoEncryptZkpAddress{ token, address }ZkpAddress | nullReverse lookup: EVM address to ZK address
getQuorumPublicKey{ token }GrumpkinPublicKeyQuorum encryption public key
getRegistration{ token, zkpAddress | epk }EvmAddress | nullController registered for an EPK on the token's ZKHub, or null if unregistered
isRegistered{ token, zkpAddress | epk }booleanWhether an EPK is registered (non-zero controller)
getPermitNonce{ token, owner }bigintNext permit nonce for controller auth
getAutoEncrypt{ token, address }booleanIs auto-encrypt enabled?
getEncryptedBalanceLogs{ token, zkpAddress, fromBlock, toBlock?, events? }EncryptedBalanceLog[]Raw event history. fromBlock is required; caller paginates with successive [fromBlock, toBlock] windows to stay under hosted-RPC block-range caps.
getTokenName{ token }stringToken contract name (for EIP-712 domain)
getPublicTokenBalance{ token, owner }bigintERC-20 balanceOf on the token contract (public L1 balance)
getPublicTokenBalances{ owner, tokenKeys? }Record<Token, bigint>Batch balanceOf for default or configured tokens. Requires client.chain when using baked-in token maps.

Amount helpers (formatAmount, parseAmount, safeParseAmount, TOKEN_DECIMALS) live in @cardinal-cryptography/core — 6 decimal places; parseAmount rejects negatives and rounds excess fractional digits (viem parseUnits behavior). safeParseAmount(value) is the non-throwing variant for per-keystroke input validation: it returns { value: bigint } on success or { error: string } (the message parseAmount would have thrown) instead of raising.

Error handling

A failed send* throws a decoded error — SendRevertError for a mined revert (carries .transactionHash and a best-effort .decoded) or UserOpRevertError for a bundler pre-submission reject (see Transaction Lifecycle). So for a caught send* failure you read err.decoded, not a separate decoder. A user declining in their wallet is not a failure: it surfaces as viem's standard UserRejectedRequestError (EIP-1193 4001), which viem nests under a higher-level error — so detect it with viem's own error.walk cause-chain helper, not by inspecting the top-level error.

import { BaseError, UserRejectedRequestError } from 'viem'
import { SendRevertError } from '@cardinal-cryptography/core'

try {
await client.sendEncryptedTransfer({ token, amount, to })
} catch (err) {
// User declined in their wallet — not an error.
if (err instanceof BaseError && err.walk(e => e instanceof UserRejectedRequestError)) return
// Reverted on-chain — read the best-effort decoded reason.
if (err instanceof SendRevertError) {
console.error(err.decoded?.summary ?? err.message)
return
}
throw err
}

To decode raw revert bytes you hold from elsewhere (a UserOperationRevertReason event, a manual eth_call), pass them to decodeUserOpError(bytesOrError) — it resolves them against all ZK contract errors (EntryPoint + SharedAccount + ZKEMT + ZKHub), see Gas sponsorship — decoding reverts.

Consumer-relevant error names: EpkAlreadyRegistered (hub) and AutoEncryptEpkAlreadySet (token) are distinct errors (controller already registered vs. auto-encrypt EPK already set); also InvalidProof, NonceAlreadyUsed, SameSenderRecipient, InvalidAuthorizationSignature.


zkpEvmActions

EVM write transactions. Requires WalletClient or BundlerLike.

import { zkpEvmActions } from '@cardinal-cryptography/core'
client.extend(zkpEvmActions({ tokens?: TokenMap, account?: LocalAccount }))

account is required on the bundler path (and ignored on the WalletClient path, where viem's client.account plays the same role). It's used to sign EIP-712 permits for publicToEncryptedTransferWithAuth.

MethodParametersReturnsDescription
preparePublicToEncryptedTransfer{ token, amount, zkpAddress }PreparedTransactionPrepare public-to-encrypted transfer (plain variant, WalletClient path)
preparePublicToEncryptedTransferWithAuth{ token, amount, zkpAddress, owner, deadline, nonce? }UnsignedTransactionPrepare permit variant — returned shape needs signing before submission
prepareSetAutoEncrypt{ token, enabled, zkpAddress }PreparedTransactionPrepare auto-encrypt initial bind (sets the EPK and the enabled flag). Reverts on-chain if the address has already bound an EPK.
prepareToggleAutoEncrypt{ token, enabled }PreparedTransactionPrepare auto-encrypt on/off flip for an already-bound address. Does not change the bound EPK.
sendPublicToEncryptedTransfer{ token, amount, zkpAddress, account?, nonce?, deadline? }0x${string}Auto-routes: plain variant on WalletClient, WithAuth on bundler (prepare → sign → send under the hood)
sendSetAutoEncrypt{ token, enabled, zkpAddress, account?, nonce?, deadline? }0x${string}Auto-routes: plain variant on WalletClient, WithAuth on bundler. Initial bind only — reverts on-chain if the address has already bound an EPK. account?, nonce?, deadline? apply to the bundler path only.
sendToggleAutoEncrypt{ token, enabled, account?, nonce?, deadline? }0x${string}Auto-routes: plain variant on WalletClient, WithAuth on bundler. Flips the enabled flag for an already-bound address; on the bundler path the SDK reads the stored EPK from chain to construct the digest.
signAuthorizationUnsignedTransactionPreparedTransactionSign an EIP-712 permit with the bound account. Named distinctly from zkpWalletActions.signTransaction so both can coexist on a full client.
toSignedTransaction(UnsignedTransaction, HexSignature)PreparedTransactionAssemble a PreparedTransaction from an externally-produced signature (custody, HSM, etc.)

Permit prepare/sign/submit flow

publicToEncryptedTransferWithAuth takes an EIP-712 permit as its last argument. Use preparePublicToEncryptedTransferWithAuth (returns UnsignedTransaction) + signAuthorization + sendPreparedTransaction. For external signers (custody, Ledger, Fireblocks), use toSignedTransaction to wrap an externally-produced signature.

See Transaction Lifecycle — EVM transactions with an EIP-712 permit for the three-step code example.


erc3009Actions

EIP-3009 transferWithAuthorization — plain ERC-20 transfers via a recipient-bound off-chain authorization. The signed digest commits to to, so anyone (the SharedAccount bundler in practice) may forward the call without being able to redirect funds. Bundler-only — on a WalletClient call transfer directly.

import { erc3009Actions } from '@cardinal-cryptography/core'
client.extend(erc3009Actions({ tokens?: TokenMap, account?: LocalAccount }))

account is the EOA whose tokens are transferred — also the EIP-712 signer (from = account.address). Required for sendTransferWithAuthorization; prepare* and reads don't need it.

MethodParametersReturnsDescription
prepareTransferWithAuthorization{ token, from, to, value, validAfter?, validBefore?, nonce? }UnsignedTransactionBuild EIP-712 typed data + calldata. Caller signs typedData and assembles a PreparedTransaction via signTransaction / toSignedTransaction.
sendTransferWithAuthorization{ token, to, value, validAfter?, validBefore?, nonce? }0x${string}prepare → sign with the bound account → submit via the bundler.
getAuthorizationState{ token, authorizer, nonce }booleanHas (authorizer, nonce) been consumed (used or canceled)? Mirrors EIP-3009's authorizationState view.

Defaults: validAfter = 0n, validBefore = 20 min ahead, nonce is a fresh random bytes32.


zkpDecryptActions

Decrypt balances and history. Requires a client with zkpPublicActions methods.

import { zkpDecryptActions } from '@cardinal-cryptography/core'
client.extend(zkpDecryptActions({ zkpAccount: ZkpReadAccount }))
MethodParametersReturnsDescription
getDecryptedBalance{ token, zkpAccount? }bigintRead + decrypt balance
getDecryptedBalanceLogs{ token, fromBlock, toBlock?, events?, zkpAccount? }DecryptedBalanceLog[]Decrypt history. fromBlock is required (forwarded to getEncryptedBalanceLogs).

The optional zkpAccount parameter overrides the account from config (useful for multi-account views).


zkpWalletActions

Prepare, sign, and send encrypted transfers. Requires a client with zkpPublicActions + zkpPreparedActions methods.

import { zkpWalletActions } from '@cardinal-cryptography/core'
client.extend(zkpWalletActions({ zkpAccount: ZkpAccount }))
MethodParametersReturnsDescription
prepareEncryptedTransfer{ token, amount, to, deadline?, nonce?, onProgress?, zkpAccount? }UnsignedTransactionPrepare private transfer
prepareEncryptedToPublicTransfer{ token, amount, recipient, deadline?, nonce?, onProgress?, zkpAccount? }UnsignedTransactionPrepare encrypted-to-public transfer
sendEncryptedTransferSame as prepare0x${string}Full flow: prepare + sign + send
sendEncryptedToPublicTransferSame as prepare0x${string}Full flow: prepare + sign + send
prepareRegisterEpk{ token, controller, onProgress?, zkpAccount? }PreparedTransactionBind a controller EVM address to this account's EPK on the token contract. Required first transaction for every new ZKP account.
sendRegisterEpkSame as prepare0x${string}Full flow: prove + submit
signTransactionUnsignedTransactionPreparedTransactionSign with the controller key (ZKP actions). Permit signing lives under zkpEvmActions.signAuthorization.
getDecryptedBalance{ token, zkpAccount? }bigintInherited from decrypt actions
getDecryptedBalanceLogs{ token, ... }DecryptedBalanceLog[]Inherited from decrypt actions

zkpPreparedActions

Submit pre-signed transactions. Handles routing between WalletClient and BundlerClient.

import { zkpPreparedActions } from '@cardinal-cryptography/core'
client.extend(zkpPreparedActions())
MethodParametersReturnsDescription
sendPreparedTransactionPreparedTransaction, options?: { waitForReceipt?: boolean }SendReceipt | PendingSendSubmit a signed transaction. Waits for inclusion and resolves { transactionHash } by default (a reverted tx throws SendRevertError); { waitForReceipt: false } resolves immediately with { hash, waitForReceipt() } (a userOp hash on the bundler path) — confirm later via the returned waitForReceipt().

waitForReceipt(client, hash) (exported from @cardinal-cryptography/core) waits until a submitted transaction is mined and resolves a { transactionHash } receipt — dispatching to waitForUserOperationReceipt (bundler) or waitForTransactionReceipt (WalletClient). The convenience send* methods take the same { waitForReceipt?: boolean } option and, when false, return a { hash, waitForReceipt() } pending send. A reverted transaction throws SendRevertError (carrying transactionHash, path, and a best-effort decoded reason) on both the default wait and the deferred waitForReceipt().