React Hooks
A wagmi-style provider and useZkStables() hook for binding a ZKP account to React tree state. Same surface on web and React Native — only the import path differs.
The provider owns one ZkpRuntime and one viem PublicClient for the tree, exposes a session state machine (idle / connecting / connected / error), and gives you connect / disconnect actions. When connected, you get a ready-to-use FullClient — the same one createFullClient returns — already bound to the account.
Mount the provider
Wrap the part of your tree that needs ZKP access. chain is the only required field — bundler, paymaster, tokens, and SharedAccount address resolve from baked-in deployment defaults when the chain is supported (see Live Deployments).
// Web
import { ZkStablesProvider } from '@cardinal-cryptography/sdk/react'
import { baseSepolia } from 'viem/chains'
export function App({ children }) {
return (
<ZkStablesProvider config={{ chain: baseSepolia }}>
{children}
</ZkStablesProvider>
)
}
// React Native — identical API, different package
import { ZkStablesProvider } from '@cardinal-cryptography/sdk-react-native/react'
import { baseSepolia } from 'viem/chains'
export function App({ children }) {
return (
<ZkStablesProvider config={{ chain: baseSepolia, prewarm: true }}>
{children}
</ZkStablesProvider>
)
}
prewarm: true calls runtime.prewarmProver() on mount so the first proof doesn't pay the one-time prover warmup (loading circuits + initializing the prover; on React Native also the SRS/VK first-use cost). Recommended on any platform where users will prove.
The provider snapshots chain, transport, runtime, prewarm, and the bundler config at mount; later changes are ignored. To swap any of them, remount: <ZkStablesProvider key={chain.id} config={…}>.
Connect
import { useZkStables } from '@cardinal-cryptography/sdk/react'
export function Connect() {
const { status, error, fromMnemonic } = useZkStables()
if (status === 'connecting') return <p>Connecting…</p>
if (status === 'error') return <p>Failed: {error.message}</p>
if (status === 'connected') return <p>Connected.</p>
return (
<button onClick={() => fromMnemonic('abandon abandon ... about')}>
Connect
</button>
)
}
fromMnemonic derives the account, builds the bundler-extended client, and atomically transitions idle → connecting → connected. If the client build throws, the session lands in error with the original Error preserved. Calling fromMnemonic again (or disconnect) mid-flight cancels the pending resolve. Each connect method resolves to a boolean — true when that attempt won and connected, false when it was superseded by a later call or landed in error — so you can gate post-connect UI on if (await fromMnemonic(...)).
Link an existing MetaMask EOA
import { ZKSTABLES_DEFAULT_APP } from '@cardinal-cryptography/sdk'
const { fromLinkedWallet } = useZkStables()
await fromLinkedWallet(walletClient, { app: ZKSTABLES_DEFAULT_APP })
// or: fromLinkedWallet(probeClient, { app: ZKSTABLES_DEFAULT_APP, account: address })
fromLinkedWallet signs the canonical wallet linking message via core, derives the account, and installs the session. The signing prompt is part of the lifecycle: status is connecting while the wallet popup is open, and a rejected signature lands in error. Same options as createAccountFromLinkedWallet: app is required and permanent (use ZKSTABLES_DEFAULT_APP for the default), with an optional account fallback.
Four setters, one per account-creation mode:
| Setter | Use |
|---|---|
fromMnemonic(mnemonic, index?) | BIP-39 mnemonic. Optional index for HD-style derivation (default 0). |
fromLinkedWallet(walletClient, { app, account? }) | MetaMask / viem EOA — sign linking message + connect (preferred over manual fromSeed). app required. |
fromSeed(seed, index?) | Raw seed bytes or hex signature when you already have the linking signature. |
fromEsk(esk, signer) | Pre-derived ESK + external ExternalSigner for delegated controller signing (MetaMask, Ledger). |
Each setter resolves true when its connect landed, false when it failed or was superseded mid-flight by a newer setter or disconnect.
See Account Creation for which to pick. For MetaMask + linking, see Wallet Linking.
Use the client
client is null until status === 'connected'. Gate on status (or use RequireConnected) before calling action methods.
import { useZkStables } from '@cardinal-cryptography/sdk/react'
export function Send() {
const { client, status } = useZkStables()
async function send() {
if (status !== 'connected') return
await client.sendEncryptedTransfer({
token: 'zkUSD',
amount: 50n,
to: 'zk1recipient…',
})
}
return <button onClick={send}>Send 50 zkUSD</button>
}
client is bundler-extended — the same surface documented in Encrypted Transfers and Read Balances.
RequireConnected
Drop-in gate so child components don't repeat the status === 'connected' check. Children render only when connected.
import { RequireConnected } from '@cardinal-cryptography/sdk/react'
<RequireConnected fallback={<Connect />}>
<Balance />
<Send />
</RequireConnected>
Pass a render function for a narrowed context (account and client non-null):
<RequireConnected fallback={<Connect />}>
{(zk) => <Send client={zk.client} account={zk.account} />}
</RequireConnected>
Or use useZkStablesConnected() inside a child that is already gated — throws if not connected.
Disconnect
const { disconnect } = useZkStables()
disconnect() // → status: 'idle'
disconnect resets the session and cancels any in-flight connect. The underlying runtime is preserved — you can call fromMnemonic again without rebuilding it.
Multichain
The provider snapshots chain at mount. To switch chains, remount with a fresh key:
<ZkStablesProvider key={chain.id} config={{ chain }}>
{children}
</ZkStablesProvider>
This tears down the old provider (and its viem client) and builds a fresh one. The session is reset; the caller is responsible for re-connecting.
Persistence
The provider is in-memory only — refreshing the page resets status to 'idle'. Persist the mnemonic / seed yourself in platform storage and re-connect on mount.
// React Native — Expo SecureStore
import { useEffect } from 'react'
import * as SecureStore from 'expo-secure-store'
import { useZkStables } from '@cardinal-cryptography/sdk-react-native/react'
function useAutoConnect() {
const { fromMnemonic, status } = useZkStables()
useEffect(() => {
if (status !== 'idle') return
SecureStore.getItemAsync('zkp-mnemonic').then((m) => {
if (m) void fromMnemonic(m)
})
}, [fromMnemonic, status])
}
The web shape is the same with localStorage.getItem in place of SecureStore.getItemAsync. Don't put a mnemonic in AsyncStorage (RN) or plain cookies (web) — both are plaintext. Use expo-secure-store / react-native-keychain on RN, an encrypted browser store on web, or skip persistence entirely.
ZkStablesConfig
| Field | Required | Default | Notes |
|---|---|---|---|
chain | yes | — | viem Chain. Determines deployment defaults. |
transport | no | http() | viem Transport for the internal PublicClient. |
bundlerUrl | no | from chain defaults | ERC-4337 bundler endpoint. |
paymaster | no | from chain defaults | { paymasterUrl, signPartnerContext }. See Gas Sponsorship. |
sharedAccountAddress | no | from chain defaults | SharedAccount contract address. |
tokens | no | from chain defaults | Custom TokenMap override. |
runtime | no | built via createRuntime({}) | Inject a pre-built runtime (tests, sharing across providers). Provider-built runtimes are destroyed on unmount in prod; injected ones are not. |
prewarm | no | false | Call runtime.prewarmProver() on mount to pay the first-proof warmup up front. Recommended on any platform where users prove. |
threads | no | host cores (4 if unavailable) | bb.js worker count for browser proving. Browsers need cross-origin isolation (COOP/COEP) to use >1; without it proving falls back to single-thread (the prover logs a warning). Ignored on React Native (native proving) and when runtime is supplied. |
Anything that createFullClient accepts in its { chain } branch (minus client and zkpAccount, which the provider supplies) is passed through verbatim — the config shape stays in lock-step with the underlying factory.
useZkStables() return
useZkStables() flattens the session onto the context value. The shape is a discriminated union on status:
| Field | Type | Notes |
|---|---|---|
runtime | ZkpRuntime | Stable across the provider's lifetime. |
viemClient | PublicClient | The provider's internal chain reader. Stable. |
chainId | number | From config.chain.id. |
tokens | TokenMap | Resolved from config.tokens or chain defaults. |
status | 'idle' | 'connecting' | 'connected' | 'error' | Session state. |
account | ZkpAccount | null | Non-null iff status === 'connected'. |
client | FullClient | null | Non-null iff status === 'connected'. |
error | Error | null | Non-null iff status === 'error'. |
fromMnemonic | (mnemonic, index?) => Promise<boolean> | Resolves true if this attempt connected, false if it was superseded or errored. |
fromLinkedWallet | (walletClient, { app, account? }) => Promise<boolean> | Link EOA + connect. |
fromSeed | (seed, index?) => Promise<boolean> | As above. |
fromEsk | (esk, signer) => Promise<boolean> | As above. |
disconnect | () => void | Reset to idle. |
Calling useZkStables() outside a <ZkStablesProvider> throws.