Documentation

Getting started

View as Markdown

Quickstart

Run a complete ZEC-to-stablecoin swap on a local devnet, then drive the same flow from your own code with the SDK.

This page takes you from a fresh clone to a completed swap on your machine. The local devnet runs a Zcash regtest chain, three EVM chains by default (six with --evm all), both escrow contracts and a maker, so nothing touches a public network and no real funds are involved.

Prerequisites

Install these first. pnpm paralock doctor checks every one of them and prints the same fix commands.

ToolWhyInstall
Node.js 20 or newerBuilds and runs the packages and the CLIFrom nodejs.org, or nvm install --lts
pnpmThe workspace's package managercorepack enable pnpm, or npm install -g pnpm
Docker or OrbStackRuns the Zcash regtest node; the daemon must be runningbrew install orbstack on macOS (or Docker Desktop); Docker Engine on Linux, usable without sudo
Foundry 1.8 or neweranvil runs the local EVM chains (including anvil --network tempo); forge deploys the contractscurl -L https://foundry.paradigm.xyz | bash && foundryup
thsA one-command Zcash regtest (Zakura plus lightwalletd) with a faucet and a dashboardcurl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/zcashlabs/thus-spoke-zakura/main/install.sh | sh

The commands, ready to copy:

Shell
nvm install --lts                     # or install Node 20+ from nodejs.org
corepack enable pnpm                  # or: npm install -g pnpm
brew install orbstack                 # macOS; or Docker Desktop / Docker Engine
curl -L https://foundry.paradigm.xyz | bash && foundryup
curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/zcashlabs/thus-spoke-zakura/main/install.sh | sh

1. Clone and build

Shell
git clone https://github.com/bitfalt/Paralock && cd Paralock
pnpm install && pnpm build

pnpm build compiles every package, including the CLI that pnpm paralock runs. Run all the commands below from the repository root.

2. Check your machine

Shell
pnpm paralock doctor

doctor checks Node, pnpm, the Docker daemon, ths, Foundry (and that its anvil supports --network tempo), that dependencies are installed, packages built and contract libraries present, and that every port is free or already used by the matching Paralock service. Each failed check prints the command that fixes it. Fix them and run doctor again until all required checks pass.

3. Start everything

Shell
pnpm paralock dev --web

This one command:

  1. runs doctor and stops if a required check fails;
  2. starts Zcash regtest through ths (a Zakura node plus lightwalletd);
  3. starts one anvil per EVM chain: by default Tempo, Base and Ethereum. Add --evm all for all six, which adds Arbitrum, Robinhood Chain and HyperEVM, or pick chains with a comma-separated list such as --evm tempo,arbitrum;
  4. deploys ParalockHTLC, ParalockAdaptorEscrow and a local stablecoin on each EVM chain (an OUSD stand-in on Tempo, a 6-decimal MockUSD "USDC" elsewhere) and writes every endpoint, address and key to .paralock/devnet.json;
  5. starts the maker in the background, which quotes every devnet chain in both modes and mines a regtest block every 3 seconds;
  6. with --web, starts the web app on http://localhost:3000 (change the port with --web-port).

It reuses anything that is already running, so you can run it again at any time, for example with --evm all to add chains. When it finishes, it prints the endpoints and the next commands to try. Logs of the background processes are in .paralock/maker.log and .paralock/web.log.

What's running

ServiceAddress
Zcash RPC (Zakura regtest)http://127.0.0.1:18232
lightwalletd127.0.0.1:9067
ths dashboard (wallet balances, faucet)http://127.0.0.1:32805
Tempo, Base, Ethereum anvilsports 8545, 8546, 8547 (chain ids 31337, 31338, 31339)
Arbitrum, Robinhood Chain, HyperEVM anvils (--evm all)ports 8548, 8549, 8550 (chain ids 31340, 31341, 31342)
Makerhttp://127.0.0.1:7420
Web app (--web)http://localhost:3000

pnpm paralock devnet status prints the chain heights and contract addresses.

The devnet assigns these roles:

RoleZcashEVM
Takerths account 1 holds the shielded savings; ths account 2 is the swap address Paralock spends fromanvil dev account 1
Makerths account 5anvil dev account 0
Relayernoneanvil dev account 2

4. Swap ZEC for stablecoins

dev returns once everything is up; the maker and the web app keep running in the background. From the repository root, in the same terminal or a second one:

Shell
pnpm paralock swap --zec 1

This sells 1 ZEC for OUSD on Tempo in htlc mode. The CLI prints the quote (amount, rate, the 40-block Zcash refund window and the 45-second EVM lock), unshields what the swap needs (the quoted ZEC plus a small fee buffer) from the taker's shielded balance into the swap address, and then prints each step:

StatusWhat happened
zcash_lockedThe taker locked the ZEC in a Zcash HTLC
evm_lockedThe maker locked OUSD on Tempo; the taker verified recipient, token, amount and time left
evm_claimedThe taker claimed the OUSD, which made the secret public
completedThe maker used the secret to claim the ZEC

Leftover change goes back into the taker's shielded balance, and the maker shields the ZEC it claimed.

5. Try private mode

Shell
pnpm paralock swap --zec 1 --chain base --private

Same swap, settled on Base in USDC, in adaptor mode: the ZEC sits in a plain 2-of-2 multisig, and the secret travels inside adaptor signatures, so no hash appears on either chain. Add --fresh-recipient --relay to receive at a new address created for this swap, with a relayer paying the claim gas. Privacy explains each option.

6. Pay someone an exact amount

Shell
pnpm paralock pay --usd 25 --to 0x90F79bf6EB2c4f870365E785982E1f101E93b906 --chain base --label "Acme Coffee"

The recipient (here anvil dev account 3) receives exactly 25.00 USDC. The maker quotes exact-out: the dollar amount is fixed and the ZEC amount rounds up. See Payments and payment links.

7. Watch a refund

Shell
pnpm paralock swap --zec 0.5 --refund-demo

The taker locks 0.5 ZEC and the maker never responds. Once the chain reaches the refund height, the taker takes the ZEC back and shields it again. No funds are lost.

8. Use the web app

Open http://localhost:3000/app. With the devnet running, the app's API routes run the real SDK against your local chains. It has two flows: Swap (sell an amount of ZEC) and Pay (send exact dollars to an address). A payment link such as http://localhost:3000/app?pay=0x90F79bf6EB2c4f870365E785982E1f101E93b906&usd=25&chain=base&label=Acme%20Coffee opens Pay with everything filled in.

9. Recover a swap

Every swap the CLI or the web app starts is saved in .paralock/taker before anything is broadcast:

Shell
pnpm paralock swaps          # unfinished swaps
pnpm paralock resume <id>    # continue one after a crash or restart
pnpm paralock refund <id>    # take the ZEC back once its refund height is reached

Persistence and recovery covers what is stored and how the engines resume.

10. Stop the devnet

Shell
pnpm paralock devnet down

This stops the maker and web app that dev started, runs ths stop, stops the anvils Paralock started, and deletes .paralock/devnet.json. Processes Paralock didn't start are left running. The swap stores in .paralock/taker and .paralock/maker are kept.

Step by step instead of dev

dev combines these commands, which you can also run yourself:

Shell
pnpm paralock devnet up --evm all     # Zcash regtest + EVM anvils + contracts, writes .paralock/devnet.json
pnpm paralock maker                   # terminal 1: the maker on :7420 (stays in the foreground)
pnpm paralock swap --zec 1            # terminal 2
pnpm --filter web dev                 # the web app (Next.js allows one dev server per checkout)

Use the SDK in your own code

The CLI is a thin layer over @paralock/sdk. The examples/quickstart app is the "hello world": about 60 lines that build a taker with createTaker, get a validated quote, swap, and demonstrate the refund path. Run it against the devnet:

Shell
pnpm paralock dev --evm all                                     # from the repo root
pnpm --filter @paralock/example-quickstart start               # quote, swap, done (htlc mode)
pnpm --filter @paralock/example-quickstart start -- --private  # adaptor mode
pnpm --filter @paralock/example-quickstart start -- --refund   # the maker never locks: refund

Here is its core. To use it in another project, npm install @paralock/sdk @paralock/zcash and point devnetFilePath at your Paralock checkout.

TypeScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { type Swap, createTaker, devnetFilePath } from "@paralock/sdk";
import { ZcashRpc, bytesToHex, randomKeyPair } from "@paralock/zcash";

// Endpoints, contract addresses and the taker's EVM key, read from the devnet as plain config.
// Outside the Paralock checkout, pass its path: devnetFilePath("/path/to/Paralock").
const devnet = JSON.parse(readFileSync(devnetFilePath(), "utf8"));
const base = devnet.evm.base;

// A Zcash key for the swap address. Keep it: htlc refunds are signed with it.
mkdirSync(".quickstart", { recursive: true, mode: 0o700 });
if (!existsSync(".quickstart/zcash.key")) {
  writeFileSync(".quickstart/zcash.key", bytesToHex(randomKeyPair().privateKey), { mode: 0o600 });
}

const taker = createTaker({
  zcash: { rpcUrl: devnet.zcash.rpc, network: "regtest", key: readFileSync(".quickstart/zcash.key", "utf8") },
  evm: {
    base: {
      rpcUrl: base.rpc,
      chainId: base.chainId,
      htlc: base.htlc,
      adaptorEscrow: base.adaptorEscrow,
      account: devnet.taker.evmKey,
      fromBlock: base.startBlock,
      tokens: [base.tokens[base.quoteToken]], // only accept quotes that pay this stablecoin
    },
  },
  maker: devnet.maker.url,
  store: ".quickstart/swaps", // secrets and refund data, written before anything is broadcast
  limits: { minClaimWindowSeconds: 15 }, // the devnet's EVM locks last 45 s
  mineZcash: (blocks) => new ZcashRpc(devnet.zcash.rpc).generate(blocks), // devnet only
  onUpdate: (swap: Swap) => console.log(swap.status.padEnd(14), swap.events.at(-1)?.detail ?? ""),
});

// Fund the swap address. On the devnet the ths faucet pays it; elsewhere you send ZEC from your wallet.
await fetch(`${devnet.zcash.dashboard}/api/v1/faucet/address`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ address: taker.zcashAddress, amount_zatoshi: 10_050_000, idempotency_key: crypto.randomUUID() }),
});

const quote = await taker.quote(10_000_000n, "base", "htlc"); // 0.1 ZEC, validated
const swap = await taker.swap(quote); // lock ZEC, verify the maker's lock, claim
console.log(`${swap.evm.recipient} received ${Number(swap.evm.amount) / 1e6} ${quote.tokenSymbol}`);

What each part does:

  • Amounts are bigint. ZEC is in zatoshi (1 ZEC = 100,000,000 zat), so 10_000_000n is 0.1 ZEC. Dollars are in token base units with 6 decimals.
  • tokens lists the stablecoins you accept on this chain. The taker rejects any quote that pays a different token.
  • store is a directory for a FileSwapStore. The swap secret and refund data are written there before the funding transaction is broadcast, so a crash never strands your ZEC.
  • taker.quote asks the maker for a price and validates it: the maker's key, the chain id (also checked against your RPC node), the token, the lock windows and the expiry.
  • taker.swap locks the ZEC, waits for the maker's EVM lock, verifies it on-chain and only then claims.
  • mineZcash mines regtest blocks so a refund doesn't wait for real time. Leave it out anywhere else.

The refund path is two calls. lockOnly funds the Zcash lock without involving the maker, and refund returns the ZEC once the refund height is reached. It also works from a fresh process, because everything it needs is in the store.

TypeScript
const quote = await taker.quote(10_000_000n, "base", "htlc");
const locked = await taker.lockOnly(quote); // fund the Zcash lock but never involve the maker
await taker.refund(locked.id); // waits for the refund height, then returns the ZEC

On a real network you pass your own node, contracts, keys and maker URL to the same createTaker call; Where Paralock works lists the supported networks and Zcash backends. Contract addresses and start blocks for the public testnets are in contracts/deployments/<chainId>.json and on the contracts page.

Next steps