Installation
pip install zrelay
# with the bundled prover wheel (needed for attest())
pip install "zrelay[prover]"
# or with uv
uv add "zrelay[prover]"- Package
- zrelay
- Python
- 3.10+
- Concurrency
- asyncio native; zrelay.sync for blocking
- Signing
- eth-account
- Proving
- bundled wheel (manylinux, macOS, Windows)
- Typing
- PEP 561 inline hints, no stub package
Constructing a client
1import os2from zrelay import Client3 4client = Client(5 chain_id=4663, # Robinhood Chain mainnet6 private_key=os.environ["PRIVATE_KEY"],7 rpc_url="https://rpc.robinhoodchain.com", # optional override8 lightwalletd="https://zec.rocks:443", # optional override9 timeout=180.0,10)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.
1from zrelay import QuoteParams, Pool2 3quote = await client.quote(4 QuoteParams(token=usdg, amount=2_500_000_000, pool=Pool.ORCHARD)5)6 7print(quote.estimated_zec) # "79.12840000"8print(quote.relayer_fee) # "0.0015"9print(quote.expires_at) # unix seconds — quotes are short-livedShielding
Handles the approval, the DEX route, the escrow deposit and the gas surcharge, then resolves once the Orchard output lands in a finalised block.
1import asyncio2from zrelay import Client, ShieldParams, Pool3 4async def main() -> None:5 # One call: swap, escrow, threshold-sign, mint into Orchard.6 settlement = await client.shield(7 ShieldParams(8 token="0xUSDG_ADDRESS",9 amount=2_500_000_000, # 2,500 USDG (6 decimals)10 recipient_z_address="u1xyz...orchard_unified_address",11 memo="Quarterly stock-token yield",12 pool=Pool.ORCHARD,13 max_slippage_bps=50, # 0.50%14 )15 )16 17 print(settlement.zcash_tx_id)18 print(settlement.status) # Status.SETTLED19 20asyncio.run(main())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.
1from zrelay import AttestParams, MinZecBalance2 3# The witness is built in-process. The viewing key never leaves the host.4attestation = await client.attest(5 AttestParams(6 viewing_key=viewing_key, # read from your secret store7 condition=MinZecBalance(8 amount="100.0",9 at_block_height=2_654_900,10 ),11 destination_contract=vault_address,12 )13)14 15print(attestation.claim_hash)16print(len(attestation.proof)) # ~19217 18# Cheap local check before you pay gas to submit it.19assert await client.verify_attestation(attestation)ivk) where the predicate only concerns received value, scope the secret to a single process, and clear it as soon as the proof exists.Tracking
Settlement stages arrive as an async iterator, so a worker can record progress instead of polling.
async for update in client.track(settlement.tracking_id):
print(update.stage, update.at)
# Stage.EVM_SEQUENCED | FROST_SIGNED | ORCHARD_INCLUDED | SETTLEDBlocking API
Not every codebase is asyncio-native. zrelay.sync mirrors the full surface with blocking calls and ordinary iterators.
1# Every async method has a blocking twin under zrelay.sync, for scripts,2# cron jobs and frameworks that are not asyncio-native.3from zrelay.sync import Client4 5client = Client(chain_id=4663, private_key=os.environ["PRIVATE_KEY"])6settlement = client.shield(params) # blocks until settled7 8for update in client.track(settlement.tracking_id):9 print(update.stage)Errors
Every failure subclasses ZRelayError and carries a stable .code. Match on the code or the class, not the message — messages change, codes do not.
1from zrelay.errors import (2 ZRelayError,3 QuoteExpired,4 TransparentReceiverOnly,5 InsufficientGasCoverage,6 SlippageExceeded,7 ThresholdNotMet,8)9 10try:11 settlement = await client.shield(params)12except QuoteExpired:13 ... # re-quote, do not retry14except TransparentReceiverOnly:15 ... # the unified address has no Orchard receiver16except InsufficientGasCoverage:17 ... # attach the surcharge18except SlippageExceeded:19 ... # the DEX route moved20except ThresholdNotMet as e:21 print(e.tracking_id) # ring offline; reclaim after the escrow timeout22except ZRelayError as e:23 print(e.code, e.detail) # every error carries a stable .codeType checking
# The package ships inline type hints (PEP 561) — no stubs to install.
from zrelay import Client, Settlement, Attestation
async def payout(client: Client, amount: int) -> Settlement:
...
# mypy and pyright both resolve these without configuration.Related
- Rust SDK — the same API for Rust services.
- TypeScript SDK — the browser client and the React hook.
- Smart contracts — what the attestation looks like on the receiving end.