Skip to main content
Version: 0.3.0

Getting Started — React Native

Install @cardinal-cryptography/sdk-react-native and call the mobile-native prover and solver from an Expo or bare RN app.

The first time a user triggers a ZK-proof feature, the SDK automatically fetches and stores the required cryptographic files on-device. After that, they load from local cache instantly — and you don't have to manage any of this yourself.

Expo Go is not supported

This package ships a custom native module, so it cannot run in Expo Go. Use a development build (npx expo run:ios / run:android, or EAS Build), or a bare RN app.

Prerequisites

  • React Native ≥ 0.81 (Expo SDK ≥ 54 or bare RN with expo-modules-core installed)
  • Bun 1.3+, npm, or yarn
  • Android Studio (Android target) and/or Xcode (iOS target)

The package autolinks via expo-module.config.json — no manual registration in MainApplication.kt / AppDelegate needed.

Get testnet tokens

A faucet at ebemt-faucet.vercel.app dispenses 100 of each test token (zkUSD, zkEUR, zkGBP, zkPLN) on Base Sepolia per request. Pass the EVM address of the controller wallet you'll back the account with — not your zk1... address.

curl -X POST https://ebemt-faucet.vercel.app/api/mint \
-H 'content-type: application/json' \
-d '{"address":"0xYourControllerAddress"}'

Install

The SDK is published to GitHub Packages, which requires authentication.

  1. Create a GitHub PAT with the read:packages scope.

  2. Add to ~/.npmrc (or a project-local .npmrc):

    @cardinal-cryptography:registry=https://npm.pkg.github.com
    //npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
  3. Install:

    bun add @cardinal-cryptography/sdk-react-native expo-modules-core

Polyfill — already included

The package depends on react-native-get-random-values and side-effect-imports it from its entry point. That installs globalThis.crypto.getRandomValues backed by the platform RNG. No manual import in your app's entry file needed — it flows in the moment you import anything from @cardinal-cryptography/sdk-react-native.

Create a runtime

The runtime owns two on-device services: the solver (decrypts your balance) and the prover (generates ZK proofs for transfers). Zero config — defaults are right for almost every app:

import { createRuntime } from '@cardinal-cryptography/sdk-react-native'

const runtime = createRuntime()

Both services initialize lazily on first use. createReadRuntime({ solver }) exists for read-only flows that don't need the prover.

Native cache for SRS and VKs

The prover needs two cryptographic assets to run: the Aztec SRS (~2 MiB universal trusted-setup file) and one verification key (VK) per circuit. On the first proof of each kind, the native side fetches or derives what it needs and writes it to disk; later launches load from those files and skip the work.

AssetCache path (iOS)Cache path (Android)First-use cost
SRS<applicationSupportDirectory>/EbSdk/srs/g1.dat<context.filesDir>/EbSdk/srs/g1.datDownload from crs.aztec.network (~2 MiB, once per install)
VK<…>/EbSdk/vk/<sha256-of-bytecode>.binsameDerive from circuit bytecode (once per circuit)

The cache lives in the app's persistent dir — not the OS cache dir that can be purged. On iOS the files are flagged isExcludedFromBackupKey so they don't bloat iCloud backups. If the underlying circuit changes (e.g. after an SDK upgrade), the new bytecode hash means the cache file naturally misses — no manual eviction.

Prewarm during splash. To pay the first-use cost up front instead of on the user's first action:

await runtime.prewarmProver() // resolves SRS path + derives all bundled VKs

(If you've configured custom solver tiers via solver: { tiers: [...] }, also call await runtime.solver.init() to upload them eagerly. At zero config the solver has no precomputed tiers to load and init() is a no-op.)

Custom storage. To plug in your own VK / SRS storage (MMKV, SecureStore, server-fetched VKs) or to wipe the cache, see Custom prover storage.

Wire up the high-level client

The same viem-driven client factories that ship in @cardinal-cryptography/sdk are re-exported here. Reusing the runtime from above:

import { baseSepolia } from 'viem/chains'
import { createFullClient } from '@cardinal-cryptography/sdk-react-native'

const account = runtime.createAccountFromMnemonic(MNEMONIC)

console.log('Your ZK address:', account.zkpAddress) // zk1…

const client = await createFullClient({
chain: baseSepolia,
zkpAccount: account,
})

// `to` is the recipient's `zk1...` address — share yours
// (`account.zkpAddress`) with whoever wants to pay you.
await client.sendEncryptedTransfer({
token: 'zkUSD',
amount: 50n,
to: 'zk1…',
})

Gas is sponsored by default via a bundler and paymaster — the device doesn't need ETH. See Gas Sponsorship to customize endpoints.

createDecryptClient (read-only) and createEvmClient (EVM-side, no proving) are the same surface as on Node / browser — see Concepts for what each tier covers.

Threading

All native prover and solver calls are async and run off the JS thread on a background queue — await client.sendEncryptedTransfer(...) will not freeze your UI. Proof generation is CPU-heavy (typically seconds on a modern device), so show progress UI and avoid kicking off multiple proofs in parallel from the same screen.

Cleanup

runtime.solver.destroy()
await runtime.prover.destroy()

destroy() releases the in-process caches; the on-disk caches survive across runtime lifetimes within the same app install.

What's next?