Installation
npm install @zrelay/sdk ethersConstructing a client
1import { ZRelayClient } from "@zrelay/sdk";2 3const client = new ZRelayClient({4 /** Robinhood Chain mainnet (4663) or testnet (46630). Required. */5 chainId: 4663,6 7 /** ethers Signer or viem WalletClient. Omit for read-only use. */8 signer,9 10 /** Override the public Nitro RPC. Optional. */11 rpcUrl: "https://rpc.robinhoodchain.com",12 13 /** Override the public lightwalletd endpoint. Optional. */14 lightwalletd: "https://zec.rocks:443",15 16 /** Where proofs are generated. "browser" keeps keys on the device. */17 prover: "browser",18 19 /** Fail fast instead of waiting out a stalled ring. Default 180_000. */20 timeoutMs: 180_000,21});- Bundle size
- ≈ 41 kB gzipped (core, no prover)
- Prover WASM
- ≈ 1.8 MB, lazy-loaded on first attest()
- Runtime
- Browser, Node 18+, Bun, Deno
- Signer
- ethers v6 Signer or viem WalletClient
- Types
- Bundled, no @types package needed
- Network calls
- Public RPC + public lightwalletd only
client.shield()
Moves an EVM asset into a Zcash shielded note. Handles approval, the DEX route, the escrow deposit and the gas surcharge, then resolves once the Orchard output is included in a finalised block.
1import { ZRelayClient } from "@zrelay/sdk";2import { ethers } from "ethers";3 4const provider = new ethers.JsonRpcProvider("https://rpc.robinhoodchain.com");5const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);6 7const client = new ZRelayClient({8 chainId: 4663, // Robinhood Chain mainnet9 signer,10});11 12// One call: swap, escrow, threshold-sign, mint into Orchard.13const settlement = await client.shield({14 token: "0xUSDG_ADDRESS",15 amount: ethers.parseUnits("2500", 6),16 recipientZAddress: "u1xyz...orchard_unified_address",17 memo: "Quarterly stock-token yield",18});19 20console.log(settlement.zcashTxId); // 9f2a...c41d21console.log(settlement.status); // "SETTLED"maxSlippageBps defaults to 50 (0.50%). Widen it for thin equity-token pairs, but understand what you are agreeing to: the swap and the escrow are one transaction, so a bad route reverts rather than settling badly.client.attest()
Builds a zero-knowledge proof about shielded holdings. The viewing key is read into local memory, the witness is constructed there, and only the proof is returned.
1import { ZRelayClient } from "@zrelay/sdk";2 3const client = new ZRelayClient({ chainId: 4663 });4 5// The witness never leaves the browser — only the proof is published.6const attestation = await client.attest({7 viewingKey: "uview1qqqqq...", // held client-side, never transmitted8 condition: { minZecBalance: "100.0", atBlockHeight: 2_654_900 },9 destinationContract: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",10});11 12// Groth16 proof, ready to submit from the user's own wallet.13await contract.verifySolvencyAndBorrow(14 loanAmount,15 attestation.claimHash,16 attestation.minBalance,17 attestation.proof,18);attest() makes no request carrying the viewing key. If you are auditing this claim, watch the network tab: the only outbound traffic is the lightwalletd range fetch used to build the witness, and the note data it returns is already encrypted to your key.client.track()
An async iterator over cross-chain settlement stages. Useful for rendering real progress rather than an indeterminate spinner.
for await (const u of client.track(trackingId)) {
// u.stage: "EVM_SEQUENCED" | "FROST_SIGNED" | "ORCHARD_INCLUDED" | "SETTLED"
setStage(u.stage);
}Read-only usage
Every method that does not spend money works without a signer — useful for server-rendered pages, indexers and dashboards.
1// No signer: quote, track and verify without a wallet connected.2const reader = new ZRelayClient({ chainId: 4663 });3 4await reader.quote({ token: USDG, amount: 1_000_000n, pool: "orchard" });5await reader.status("track_44a108e");6await reader.verifyAttestation(attestation); // local check before submittingReact bindings
@zrelay/sdk/react wraps the client in a hook with stage and error state already managed.
1import { useZRelay } from "@zrelay/sdk/react";2 3export function ShieldButton({ amount }: { amount: bigint }) {4 const { shield, stage, error, isPending } = useZRelay({ chainId: 4663 });5 6 return (7 <button8 disabled={isPending}9 onClick={() =>10 shield({11 token: USDG,12 amount,13 recipientZAddress: recipient,14 memo: "Q3 distribution",15 })16 }17 >18 {isPending ? stage ?? "Shielding…" : "Shield to Zcash"}19 {error && <span role="alert">{error.code}</span>}20 </button>21 );22}Error handling
Every failure is a typed ZRelayError with a stable code. Match on the code, not the message — messages change, codes do not.
1import { ZRelayError, isZRelayError } from "@zrelay/sdk";2 3try {4 await client.shield(params);5} catch (err) {6 if (!isZRelayError(err)) throw err;7 8 switch (err.code) {9 case "QUOTE_EXPIRED": // re-quote, do not retry10 case "TRANSPARENT_RECEIVER_ONLY": // the address cannot receive shielded11 case "INSUFFICIENT_GAS_COVERAGE": // surcharge not attached12 case "SLIPPAGE_EXCEEDED": // DEX route moved; the tx reverted13 case "THRESHOLD_NOT_MET": // ring offline; reclaim after timeout14 case "PROOF_REJECTED": // verifier refused the attestation15 console.error(err.code, err.detail, err.trackingId);16 }17}Related
- Rust SDK and Python SDK — the same operations from a backend service.
- Smart contracts — what the attestation looks like on the receiving end.