@zill-protocol/rn-sdk
v0.2.3
Published
React Native focused SDK for Zill/Nocturne. It re-exports the shared protocol logic from `@zill-protocol/client` and adds React Native helpers for sync, storage, and proving.
Readme
@zill-protocol/rn-sdk
React Native focused SDK for Zill/Nocturne. It re-exports the shared protocol
logic from @zill-protocol/client and adds React Native helpers for sync,
storage, and proving.
This package is internal-only for now and is opinionated toward the Zill stack.
What this SDK does
- Syncs public chain data from
/v0/syncand maintains local note state. - Decrypts notes on-device using the viewer keys.
- Maintains a local sparse Merkle prover in MMKV.
- Prepares, signs, and proves operations (joinsplit).
- Tracks operation status via the bundler endpoint.
- Exposes optional activity and tx-status HTTP clients.
How it works (high level)
createHttpSyncAdapterpolls/v0/syncand yields insertions + nullifiers.- The client decrypts notes locally and stores state in
NocturneDB. - The merkle prover is updated in MMKV.
prepareOperationbuilds a pre-sign operation from local notes.signOperationandproveOperationgenerate a submittable operation.- Your app submits the operation to the bundler and tracks status.
Quick start (recommended)
import { createReactNativeClientFromConfig } from "@zill-protocol/rn-sdk";
const { client } = createReactNativeClientFromConfig({
namespace: walletId,
syncBaseUrl: "https://insertion-writer.up.railway.app",
bundlerBaseUrl: "https://bundler.up.railway.app",
viewer,
provider,
config: "citrea-testnet",
tokenConverter,
syncOptions: {
expectedChainId: "5115",
expectedHandlerAddress: "0x...",
},
});Sync
await client.sync();
const latestIndex = await client.getLatestSyncedMerkleIndex();createHttpSyncAdapter ships with a built-in MMKV cursor store for nullifier
pagination. If you omit nullifierCursorStore, it persists the cursor under
<namespace>::sync (default namespace is default).
Retry/backoff is supported via the retry option on the sync adapter. Chain and
handler validation can be enforced via expectedChainId and
expectedHandlerAddress.
Sync adapter options (summary)
baseUrl(required): sync API base URL.fetch: custom fetch implementation.insertionLimit/nullifierLimit: page sizes.pollIntervalMs: sleep interval when idle.namespace: used by the built-in cursor store.nullifierCursorKey: override the cursor key name.nullifierCursorStore: custom store implementation.expectedChainId/expectedHandlerAddress: fail fast on misconfigured URLs.retry: retry/backoff settings for transient failures.
Retry options
{
maxRetries: 3,
baseDelayMs: 500,
maxDelayMs: 5000,
jitterRatio: 0.2,
retryOnStatuses: [429, 502, 503, 504],
timeoutMs: 10000,
}Custom cursor store
const syncAdapter = createHttpSyncAdapter({
baseUrl: "...",
nullifierCursorStore: {
get: async () => Number(await storage.getItem("nfCursor") ?? 0),
set: async (cursor) => storage.setItem("nfCursor", cursor.toString()),
},
});Operation flow (prepare -> sign -> prove)
import { createHttpBundlerClient } from "@zill-protocol/rn-sdk";
import { signOperation, proveOperation } from "@zill-protocol/client";
const preSignOp = await client.prepareOperation(opRequest, 1.1);
const signedOp = signOperation(nocturneSigner, preSignOp);
const provenOp = await proveOperation(joinSplitProver, signedOp);
const bundler = createHttpBundlerClient("https://bundler.up.railway.app");
const { id } = await bundler.submitOperation(provenOp);
const status = await bundler.getOperationStatus(id);Notes:
joinSplitProveris typicallyReactNativeJoinSplitProver.- You can submit via
createHttpBundlerClientor your own network layer.
Submitting operations
Bundler accepts a JSON payload of the form { operation } at POST /relay and
returns { id }, where id is the operation digest string. The SDK provides a
simple client wrapper:
import { createHttpBundlerClient } from "@zill-protocol/rn-sdk";
const bundler = createHttpBundlerClient("https://bundler.up.railway.app");
const { id } = await bundler.submitOperation(provenOp);
// Track status with the digest returned by the bundler.
const status = await bundler.getOperationStatus(id);Practical guidance:
- Keep the returned
idto reconcile status updates. - A 400 response indicates validation failure (nullifier conflict, revert, gas).
- A 500 response indicates bundler failure; retry with backoff.
Bundler client
createHttpBundlerClient wraps the bundler HTTP API:
submitOperation(op)→POST /relaygetOperationStatus(id)→GET /operations/:idcheckNullifier(nf)→GET /nullifiers/:nullifier
Activity and tx status
import { createHttpActivityClient, createHttpTxStatusClient } from "@zill-protocol/rn-sdk";
const activityClient = createHttpActivityClient("https://insertion-writer.up.railway.app");
const page = await activityClient.fetchActivity({ limit: 100 });
const txClient = createHttpTxStatusClient("https://bundler.up.railway.app");
const status = await txClient.getOperationStatus(opDigest);Storage adapter
The SDK ships an MMKV-backed KVStore adapter and a helper that wires both the
DB and the sparse Merkle prover.
import { createReactNativeStorage } from "@zill-protocol/rn-sdk";
const { db, merkleProver } = createReactNativeStorage({
namespace: walletId,
});createReactNativeStorage accepts optional overrides for MMKV ids, encryption
keys, or custom MMKV instances. You can also instantiate MMKVStore directly
via createMMKVStore.
JoinSplit artifact manager
JoinSplit proving requires three artifacts:
- proving key (
.zkey) - witness calculator graph (
.wcd) - verification key (base64)
The SDK ships JoinSplitArtifactManager to download, verify, and cache these
artifacts in the app sandbox. You bring a storage adapter (e.g. RNFS) and the
R2/CDN manifest (version + SHA256 hashes).
import RNFS from "react-native-fs";
import {
JoinSplitArtifactManager,
ReactNativeJoinSplitProver,
} from "@zill-protocol/rn-sdk";
const storage = {
artifactDir: `${RNFS.DocumentDirectoryPath}/nocturne-artifacts`,
ensureDir: () => RNFS.mkdir(`${RNFS.DocumentDirectoryPath}/nocturne-artifacts`),
exists: (path: string) => RNFS.exists(path),
readText: async (path: string) =>
(await RNFS.exists(path)) ? RNFS.readFile(path, "utf8") : null,
writeText: (path: string, contents: string) =>
RNFS.writeFile(path, contents, "utf8"),
readFileBase64: (path: string) => RNFS.readFile(path, "base64"),
writeFileBase64: (path: string, base64: string) =>
RNFS.writeFile(path, base64, "base64"),
};
const artifactManager = new JoinSplitArtifactManager(storage, {
baseUrl: "https://r2.example.com/nocturne/joinsplit/",
manifest: {
version: "v1",
zkey: {
path: "joinsplit.zkey",
sha256: "0x...",
},
wcd: {
path: "joinsplit.wcd",
sha256: "0x...",
},
verificationKeyBase64: "base64-vkey",
},
// Optional: add signed headers or tokens for private buckets.
// getHeaders: async () => ({ Authorization: `Bearer ${token}` }),
});
const proverConfig = await artifactManager.ensureArtifacts();
const prover = new ReactNativeJoinSplitProver(proverConfig);If you want the SDK to fetch the manifest directly:
import {
createReactNativeJoinSplitProverFromManifestUrl,
} from "@zill-protocol/rn-sdk";
const { prover } = await createReactNativeJoinSplitProverFromManifestUrl({
storage,
manifestUrl: "https://bucket.example.com/joinsplit/joinsplit-artifacts.json",
});React Native prover
ReactNativeJoinSplitProver is backed by:
@iden3/react-native-circom-witnesscalc@iden3/react-native-rapidsnark
Privacy notes
- The server only exposes public data (commitments, nullifiers).
- Notes are decrypted on-device using the viewer keys.
- Do not store decrypted note metadata server-side.
Limitations
- Activity feed is optional and not a source of truth for private state.
