Documentation

Rust

View as Markdown

Rust CLI

The standalone paralock binary from paralock-cli. Install it with cargo or the installer, configure paralock.toml, use the local devnet, run quote, swap, pay, swaps, resume, refund and a maker, and script it with --json and exit codes.

paralock-cli builds paralock, a single binary on the Rust SDK: swap ZEC for stablecoins, pay exact dollar amounts, recover saved swaps and run a market maker, with no Node.js process. It speaks the same protocol as the TypeScript SDK, so it trades with TypeScript makers and serves TypeScript takers, in htlc and private mode.

It complements the npm CLI rather than replacing it. npx paralock runs the whole local devnet (Zcash regtest, anvils, contracts, a maker); the Rust binary takes its configuration from a paralock.toml file, so it can point at any network, or at that devnet with --devnet.

Install

Shell
curl -fsSL https://paralock.xyz/install | sh                               # macOS and Linux, x86_64 and arm64
curl -fsSL https://paralock.xyz/install | PARALOCK_VERSION=cli-v0.1.0 sh   # a given release
cargo install paralock-cli                                                  # from source, Rust 1.95 or newer

The installer is a short POSIX shell script; read it before you pipe it to sh. It:

  1. picks the build for your machine: aarch64-apple-darwin, x86_64-apple-darwin, x86_64-unknown-linux-gnu or aarch64-unknown-linux-gnu (Linux builds need glibc 2.35 or newer), and the native build under Rosetta;
  2. downloads paralock-<target>.tar.gz and SHA256SUMS from the GitHub release (the latest one, or PARALOCK_VERSION);
  3. refuses to install unless the tarball's SHA-256 matches SHA256SUMS;
  4. installs the binary to ${PARALOCK_HOME:-$HOME/.paralock}/bin/paralock, replacing an older one atomically, and prints the line to add that directory to your PATH.

The release binaries are built by GitHub Actions from the cli-v* tag. Windows has no prebuilt binary and isn't tested.

Shell
paralock version     # paralock 0.1.0 (paralock-cli, protocol v1.1; experimental, not audited)
paralock --help

Try it on the local devnet

--devnet reads devnet.json from the same state directory as the npm CLI: <checkout>/.paralock inside a Paralock checkout, else $PARALOCK_HOME, else ~/.paralock. Starting and stopping the devnet stays in the npm CLI (npx paralock dev, or pnpm paralock dev from a checkout).

Shell
npx paralock dev                                            # Zcash regtest, anvils, contracts and the TypeScript maker
paralock --devnet doctor
paralock --devnet swap --zec 0.1                            # htlc on Tempo, against the TypeScript maker on :7420
paralock --devnet swap --zec 0.1 --chain base --private
paralock --devnet --evm-account 5 maker --port 7424         # a Rust maker on anvil account 5 (fund it first)
paralock --devnet --maker-url http://127.0.0.1:7424 swap --zec 0.1
paralock --devnet swap --zec 0.1 --private --refund-demo    # lock, let the maker vanish, refund

On the devnet the taker uses its own Zcash key (<state>/rust-cli/taker-zcash.key), funded from the ths faucet as needed, and the devnet's taker and relayer accounts on the EVM chains; --evm-account N picks anvil dev account N instead. --lightwalletd reaches Zcash through lightwalletd. The Rust maker mines a regtest block only when the chain has stalled for 6 seconds (when no TypeScript maker is mining); --no-mine turns that off. The devnet price falls back to a fixed value when CoinGecko is unreachable, and anvil's dev keys are public.

Configuration: paralock.toml

Without --devnet, the binary reads paralock.toml from --config <file>, else $PARALOCK_CONFIG, else ./paralock.toml, else <state>/paralock.toml. This is the complete, commented example shipped with the crate (paralock.example.toml):

paralock.toml
# paralock.toml: the configuration of the `paralock` binary.
#
# Where it is read from: --config <file>, else $PARALOCK_CONFIG, else ./paralock.toml, else
# <state dir>/paralock.toml (<checkout>/.paralock in a Paralock checkout, else $PARALOCK_HOME, else ~/.paralock).
# For the local devnet, skip this file and use `paralock --devnet …`.
#
# Keys: every private key comes from an environment variable (key_env) or a file only you can read (key_file,
# `chmod 600`; paralock warns about looser permissions). A key written here by value (key = "…") is refused unless
# allow_inline_keys = true, which is meant for throwaway testnet keys.
#
# Paralock is experimental and not audited. The Zcash leg is transparent on chain. Start small.

network = "mainnet"                      # mainnet | testnet | regtest
maker_url = "https://maker.example"      # the maker that `quote`, `swap` and `pay` trade with (or --maker-url)
# store_dir = "~/.paralock/rust-mainnet" # swap stores (<dir>/taker, <dir>/maker): back them up, they hold keys
# allow_inline_keys = false

[zcash]
rpc = "http://127.0.0.1:8232"            # a Zebra / Zakura JSON-RPC endpoint…
# lightwalletd = "https://zec.rocks:443" # …or a lightwalletd / Zaino server (no full node needed)
# rpc_user = "paralock"                  # basic auth: with rpc_password_env or rpc_password_file,
# rpc_password_env = "ZEBRA_RPC_PASSWORD"
# rpc_cookie_file = "~/.cache/zebra/.cookie"  # or Zebra's cookie file
key_env = "PARALOCK_ZCASH_KEY"           # the transparent key that funds swaps (32 bytes of hex)
# key_file = "~/.paralock/zcash.key"
# return_address = "t1…"                 # change and refunds (default: the key's own address)

# One section per EVM chain: tempo, base, ethereum, arbitrum, robinhood, hyperevm. On a testnet whose chain id
# Paralock has deployed to, htlc, adaptor_escrow, from_block and the token default to that deployment.
[evm.base]
rpc = "https://mainnet.base.org"
# verify_rpc = "https://base.other-provider.example"  # taker: a second, independent endpoint; the last check
#                                                      # before claiming needs both to report the same lock
chain_id = 8453
htlc = "0x…"                             # ParalockHTLC
adaptor_escrow = "0x…"                   # ParalockAdaptorEscrow: enables --private
from_block = 0                           # the deployment block (event scans start here)
key_env = "PARALOCK_EVM_KEY"             # your account on this chain (receives the stablecoins; pays claim gas)
# relayer_key_env = "PARALOCK_RELAYER_KEY"  # a second account that submits claims for `--relay`
# tokens = ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"]  # taker: stablecoins you accept (default: known ones)
token = { address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol = "USDC", decimals = 6 }  # maker: quoted
# fee_token = "0x20c0…"                  # Tempo only: the TIP-20 that pays fees
# rpc_timeout_seconds = 10

[price]
source = "coingecko"                     # quotes are checked against this price (and makers quote from it)
# url = "https://…"                      # another endpoint answering { "zcash": { "usd": <price> } }
# fixed = 50.0                           # regtest / testnet only

[taker]
# max_slippage_bps = 200                 # refuse quotes more than 2 % below your price
# min_claim_window_seconds = 30          # time left on the maker's lock before claiming (the floor is 30)
# min_evm_confirmations = 1
# max_zcash_lock_blocks = 288            # longest your ZEC may stay locked if the maker vanishes
# max_zec = "1000"

# `paralock maker`. A maker on the open internet: keep the defaults, run it behind TLS, back up its store.
[maker]
listen = "127.0.0.1:7420"
# payout_address = "t1…"                 # where claimed ZEC goes (default: the maker key's address)
# zcash_key_env = "PARALOCK_MAKER_ZCASH_KEY"  # default: [zcash].key_*
spread_bps = 50
policy = "mainnet"                       # mainnet | devnet (devnet is refused on mainnet)
min_zec = "0.01"
max_zec = "10"
max_concurrent_swaps = 10
quote_ttl_seconds = 60
# check_inventory = true
# cors = ["https://app.example"]         # or ["*"]
# expose_swap_list = false               # GET /swaps lets anyone link swaps
# request_timeout_seconds = 30           # 0 disables
# max_concurrent_requests = 256
# rate_limit_per_minute = 120            # per client, on GET /quote and the POSTs; rate_limit = false disables
# rate_limit_burst = 30
# client_ip_header = "x-forwarded-for"   # behind a reverse proxy that sets it
# max_connections = 1024
# idle_timeout_seconds = 75
# finished_swaps_in_memory = 256

The example is written for mainnet so every field appears, but Paralock has no mainnet deployment: the 0x… escrow addresses stay placeholders until there is one. On a testnet, rpc and chain_id are enough for a chain Paralock has deployed to. For example, against Base Sepolia through a public lightwalletd server (paralock doctor then checks the server, both escrows' code, your balances, the maker and the price):

paralock.toml
network = "testnet"
maker_url = "https://maker.example"

[zcash]
lightwalletd = "https://testnet.zec.rocks:443"
key_env = "PARALOCK_ZCASH_KEY"

[evm.base]
rpc = "https://sepolia.base.org"
chain_id = 84532                         # htlc, adaptor_escrow, from_block and USDC come from the deployment
key_env = "PARALOCK_EVM_KEY"
  • Keys come from environment variables (key_env) or files (key_file); a key file readable by other users gets a warning, and a key written in the file by value is refused unless allow_inline_keys = true.
  • Testnet defaults. On a chain id with a Paralock deployment, htlc, adaptor_escrow, from_block and the token default to it (Contracts and deployments lists them).
  • Price. CoinGecko by default. A fixed price, or a fallback, is refused on mainnet.
  • [taker] sets the slippage, the claim window and the longest acceptable Zcash lock. [maker] sets the listen address, payout address, spread, policy, limits and the HTTP server's limits.
  • Stores live in store_dir (default <state>/rust-<network>), under taker/ and maker/. They hold secrets and per-swap keys: back them up and keep them private. Their format differs from the TypeScript SDK's, so finish a swap with the tool that started it.

Commands

CommandWhat it does
paralock doctor [--maker]Checks the Zcash backend (network, height, branch id), each EVM RPC (chain id), the contracts' code, your keys and balances, the maker's /health and the price feed. With --maker, the maker side: its keys, inventory and listen address. Exit 1 if a required check fails
paralock quote --zec 0.5 [--chain base] [--private]A quote, checked as a swap would check it (keys, chain id, token, windows, price). --usd 25 asks for an exact-out quote. Nothing is locked
paralock swap --zec 0.5 [options]The whole swap. Also --fresh-recipient, --relay, --standard-only, --jitter <ms>, --refund-demo
paralock pay --usd 25 --to 0x… [--label "Acme Coffee"] [options]Pays exactly 25 dollars of the chain's stablecoin to an address, from ZEC
paralock swaps [--all]Lists the swaps in the taker's store, unfinished ones by default. Works while a swap runs
paralock resume <id> [--no-relay]Continues a saved swap after a crash or restart. --no-relay claims from your own account even if the swap was started with --relay
paralock refund <id> [--no-wait]Refunds a saved swap's ZEC once its refund height is reached. Works with the EVM RPC down
paralock maker [--listen <addr> | --port <port>] [--no-mine]Runs a maker: the protocol's HTTP API with the SDK's server (request timeout, concurrency and per-client rate limits, connection caps) and the settlement loops. Unfinished swaps resume on restart
paralock versionThe version and protocol version

swap and pay share --chain (default tempo, or the only configured chain), --private, --relay, --no-refund and --yes. Options for every command: --config <file>, --devnet, --maker-url <url>, --json, -v / -vv, and on the devnet --evm-account <n> and --lightwalletd.

Progress is printed as the swap moves, one line per step with its transaction:

Quote: 0.1 ZEC → 127.55 OUSD on tempo @ $1276.85/ZEC
  Mode htlc (a shared sha256 hashlock on both chains)
  Zcash refund window 40 blocks · tempo lock 45s · quote 9e1ff79b-…
Swapping:
  [zcash   ] zcash_locked    Locked 0.1 ZEC in the Zcash HTLC  17b1e6e9…
  [tempo   ] evm_locked      Maker locked 127.55 OUSD; terms verified  0x723f21f7…
  [tempo   ] evm_claimed     Received 127.55 OUSD; the secret is now public  0xc53ef4cd…
  [zcash   ] completed       Maker claimed the ZEC with the secret  0dbfee45…

If a swap fails after its ZEC was locked, swap and pay wait for the refund height and refund it, unless --no-refund. The swap is saved either way, so paralock refund <id> also works after a restart. On mainnet they ask for confirmation unless --yes.

Scripts: --json and exit codes

--json prints exactly one JSON document on stdout: { "ok": true, … } with the quote, the swap or the checks, or { "ok": false, "error": { "code", "message", "hint", "exitCode" }, "swap"? }. paralock maker --json prints newline-delimited records instead: listening, then one swap record per status change, then stopped. Warnings and logs (-v, -vv, RUST_LOG) go to stderr.

Exit codeMeaning
0Success: a swap completed, a refund landed, every required doctor check passed
1Failure, and nothing irreversible happened: an unreachable node, a refused quote…
2Usage or configuration error
3A swap failed after locking ZEC: refunded, or refundable with paralock refund <id>
Shell
paralock --json quote --zec 0.1 --chain base | jq -r '.usd'      # the dollars quoted, as a decimal string
paralock --json swap --zec 0.1 --chain base > swap.json || echo "exit $?: see .error.hint in swap.json"

Running a maker

paralock maker serves the protocol with the Rust SDK's server and runs the settlement loops in the same process. A maker on the open internet should keep the [maker] defaults, sit behind TLS (a reverse proxy, with client_ip_header set so the per-client rate limit sees real clients), use its own full node rather than someone else's lightwalletd, and keep its store backed up. Run a maker covers pricing, limits, inventory and what a maker earns and risks; those apply to the binary too.