Skip to content
home/guides/rust sdk

zrelay-sdk

The native Rust client. Same three operations as the TypeScript SDK — shield, attest, track — for services that settle on a schedule rather than in a browser tab. It talks directly to the public Nitro RPC and a public lightwalletd; there is no Z-Relay server in the path.

Where this belongs
Use the TypeScript SDK in the browser so proving stays on the user's device. Reach for this crate when the work is server-side — a payout scheduler, a treasury service, an indexer — and you control the host holding the keys.

Installation

Cargo.toml
# Cargo.toml
[dependencies]
zrelay-sdk = "0.1"
tokio = { version = "1", features = ["full"] }
alloy = { version = "0.8", features = ["signer-local"] }
Crate
zrelay-sdk
MSRV
1.79
Runtime
tokio (multi-thread or current-thread)
Signing
alloy — any Signer implementation
Proving
arkworks, BN254, CPU
Network calls
Public RPC + public lightwalletd only

Building a client

client.rs
1use zrelay_sdk::{Client, Pool};2use alloy::signers::local::PrivateKeySigner;3 4let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY")?.parse()?;5 6let client = Client::builder()7    .chain_id(4663)                          // Robinhood Chain mainnet8    .signer(signer)9    .rpc_url("https://rpc.robinhoodchain.com")   // optional override10    .lightwalletd("https://zec.rocks:443")       // optional override11    .timeout(std::time::Duration::from_secs(180))12    .build()?;

Quoting

A quote prices the DEX hop, the relayer fee and the expected Orchard output. Quotes are short-lived by design — an expired quote fails loudly rather than settling at a price you did not agree to.

quote.rs
1let quote = client2    .quote(QuoteParams {3        token: usdg,4        amount: 2_500_000_000u128,5        pool: Pool::Orchard,6    })7    .await?;8 9println!("{}", quote.estimated_zec);   // "79.12840000"10println!("{}", quote.relayer_fee);     // "0.0015"11println!("{}", quote.expires_at);      // unix seconds — quotes are short-lived

Shielding

Handles the approval, the DEX route, the escrow deposit and the gas surcharge, then resolves once the Orchard output lands in a finalised block.

main.rs
1use zrelay_sdk::{ShieldParams, Pool};2 3#[tokio::main]4async fn main() -> anyhow::Result<()> {5    // One call: swap, escrow, threshold-sign, mint into Orchard.6    let settlement = client7        .shield(ShieldParams {8            token: "0xUSDG_ADDRESS".parse()?,9            amount: 2_500_000_000u128,               // 2,500 USDG (6 decimals)10            recipient_z_address: "u1xyz...orchard_unified_address".into(),11            memo: Some("Quarterly stock-token yield".into()),12            pool: Pool::Orchard,13            max_slippage_bps: Some(50),              // 0.50%14        })15        .await?;16 17    println!("{}", settlement.zcash_tx_id);18    println!("{:?}", settlement.status);             // Status::Settled19    Ok(())20}

Attesting

Builds a Groth16 proof about shielded holdings. The viewing key is read from your secret store into process memory and never transmitted — but it is on a machine you operate, which is a materially different risk profile from the browser prover.

attest.rs
1use zrelay_sdk::{AttestParams, Condition};2 3// The witness is built in-process. The viewing key never leaves the host.4let attestation = client5    .attest(AttestParams {6        viewing_key,                             // read from your secret store7        condition: Condition::MinZecBalance {8            amount: "100.0".into(),9            at_block_height: Some(2_654_900),10        },11        destination_contract: vault_address,12    })13    .await?;14 15println!("{:?}", attestation.claim_hash);16println!("{} bytes", attestation.proof.len());   // ~19217 18// Cheap local check before you pay gas to submit it.19assert!(client.verify_attestation(&attestation).await?);
Server-side proving moves the risk, it does not remove it
A compromised host holding a full viewing key in memory discloses the whole wallet history. Prefer an incoming viewing key (ivk) where the predicate only concerns received value, scope the secret to a single process, and zero it as soon as the proof exists.

Tracking

Settlement stages arrive as a stream, so a worker can record progress instead of polling.

track.rs
1use futures::StreamExt;2 3let mut stream = client.track(&settlement.tracking_id).await?;4 5while let Some(update) = stream.next().await {6    let update = update?;7    println!("{:?} at {}", update.stage, update.at);8    // Stage::EvmSequenced | FrostSigned | OrchardIncluded | Settled9}

Errors

Failures are a single Error enum. Variants match the TypeScript ZRelayError.code values one-for-one, so a service that already handles the TS errors needs no new logic.

errors.rs
1use zrelay_sdk::Error;2 3match client.shield(params).await {4    Ok(settlement) => println!("{}", settlement.zcash_tx_id),5    Err(Error::QuoteExpired { quote_id, .. })   => { /* re-quote, do not retry */ }6    Err(Error::TransparentReceiverOnly { .. })  => { /* z-addr has no Orchard receiver */ }7    Err(Error::InsufficientGasCoverage { .. })  => { /* attach the surcharge */ }8    Err(Error::SlippageExceeded { .. })         => { /* DEX route moved */ }9    Err(Error::ThresholdNotMet { tracking_id }) => { /* ring offline; reclaim later */ }10    Err(e) => return Err(e.into()),11}

Cargo features

Cargo.toml
# Default: client + server-side proving via arkworks.
zrelay-sdk = "0.1"

# Read-only: quoting, tracking and verification, no prover, no signer.
zrelay-sdk = { version = "0.1", default-features = false, features = ["read"] }

# Bring your own runtime (the crate is runtime-agnostic under the hood).
zrelay-sdk = { version = "0.1", default-features = false, features = ["prover"] }

Related