Skip to content

@zrelay/sdk

A client-side library with no server behind it. Every method here either reads a public chain, signs with your own key, or builds a proof in local memory — there is no Z-Relay API to be rate-limited by or logged in.

Installation

terminal
npm install @zrelay/sdk ethers

Constructing a client

client.ts
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.

usage.ts
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"
Slippage defaults are deliberately tight
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.

usage.ts
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);
The key never crosses a network boundary
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.

track.ts
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.

read-only.ts
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 submitting

React bindings

@zrelay/sdk/react wraps the client in a hook with stage and error state already managed.

ShieldButton.tsx
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.

errors.ts
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