Skip to content

nocktoshi/atomic-nock

Repository files navigation

image

Atomic Nock

Trustless, non-custodial swap of native NOCK on Nockchain ↔ USDC or wNOCK on Base mainnet, using hash-time-locked contracts on Base and Hax lock primitaves on Nockchain. No custodian, no KYC, no shared secret in the browser.

How it works

A swap is anchored by a random preimage the seller generates locally. From it: H_evm = keccak256(jam) (the Base hashlock) and H_nock = hasPreimage(jam) (the Nockchain hashlock). The preimage never leaves the seller's browser until step 5, where revealing it on Base is what lets the buyer claim NOCK.

Swaps are posted open — nobody types an address; addresses always come from the connected wallets:

  1. Seller posts an open swap — NOCK amount + quote amount (USDC or wNOCK). Seller's Nockchain + Base addresses come from their wallets. Shareable link: /swap/<id>. The swap also appears on the public Marketplace.
  2. Buyer claims — opens the link (or finds the swap on /market), connects wallets, clicks Claim. Their Nockchain pkh is taken from their authenticated session (can't be spoofed) and committed once.
  3. Seller locks NOCK in the Nockchain HTLC (claim branch = buyer pkh + hax preimage; refund branch = seller pkh after a block height).
  4. Buyer locks USDC/wNOCK in AtomicNock on Base against H_evm.
  5. Seller withdraws with withdraw(swapId, preimageJam) → the preimage is now public in the Base tx calldata.
  6. Buyer claims NOCK — reads the preimage from that Base withdraw tx and claims on Nockchain.

If either side stalls, timelocks let each party refund their own leg.

Architecture

Layer What
Web (src/) React + React Router SPA (Vite). Iris (Nockchain) + MetaMask (Base) connect from a wallet bar; per-step wizards drive each side of the swap.
Swap API (worker/) A Cloudflare Worker. Owns the swap state machine, authorizes writes by Iris-signed session, and verifies Nockchain signatures with rose-wasm.
Storage Swap metadata in Cloudflare KV (non-secret; 30-day TTL, refreshed on each write). The preimage stays client-side in IndexedDB — never on the server.
Contract (contracts/) Foundry HTLC AtomicNock.sol on Base: lock, withdraw(id, preimage), refund. One instance per quote token (USDC, wNOCK).

iris-wasm / rose-wasm ship as npm packages (@nockbox/iris-sdk, @nockchain/rose-wasm) — no source build. Nockchain txs are built in-browser and signed in the Iris extension (nock_connect, nock_signTx, nock_signMessage); Base txs use MetaMask.

Security model

  • No shared write secret in the browser. Writes are authorized by an Iris-signed session: the user signs one challenge with their wallet, the Worker verifies the Nockchain signature (rose-wasm) and binds it to the pkh, then issues a 7-day token (persisted in localStorage — sign once).
  • The Worker enforces integrity, so a malicious client can't tamper with an in-flight swap: immutable economic terms, per-party field writes, single-commit buyer claim, and protocol ordering invariants (e.g. you can't lock USDC before NOCK is locked).
  • Rate limiting via Workers native rate-limiting bindings (60 reads/min per IP, 20 writes/min per session, 10 auth attempts/min per IP). Exceeding any limit returns 429 with Retry-After: 60.
  • The on-chain HTLCs are the ultimate guard — the KV record is just coordination metadata.

Features

Multi-asset swaps

The seller picks USDC or wNOCK (0x9B5E262cF9bb04869ab40b19AF91D2dc85761722) as the quote token. Both use the same AtomicNock.sol bytecode deployed at different addresses. Existing USDC swaps are unaffected — token absent in a swap record defaults to USDC.

Quote Contract Decimals
USDC 0x5ac37e7A63b107d226d0b88129B8EB8b07172B75 6
wNOCK set VITE_HTLC_ADDRESS_WNOCK after deploying 16

Marketplace (front page)

The marketplace is the app's home (/). At the top sits a Uniswap-style swap box: pick what you pay and what you receive — one side is always native NOCK — and post the order. Below it, the order book lists both sides, browsable without signing in:

  • Asks (sell NOCK): the existing open swaps. Filling one is the buyer claim flow — you pay USDC/wNOCK and receive native NOCK.
  • Bids (buy NOCK): buy orders posted by USDC/wNOCK holders. Filling one (/bid/<id>) makes you the swap's seller: your device generates the secret, you lock NOCK first, and the bidder — pre-committed as the buyer, including their Base address — locks the quote token next. Same protocol, same contracts, reversed initiator: this is how you swap wNOCK/USDC into native NOCK.

Tabs filter by side (Buy NOCK / Sell NOCK), plus token filter (USDC / wNOCK) and sorting by age, price (USD-equivalent across tokens), or NOCK amount. Your own orders show a badge and can be cancelled in place; any swap can be cancelled by either participant while nothing is locked on-chain. Your in-flight swaps live at /dashboard.

Notifications

Users opt in per-channel from /settings. Notifications fire on every swap state transition (claim, NOCK locked, quote locked, preimage revealed, complete, refunds) via ctx.waitUntil — failures never block swap writes.

Channel How
Telegram Deep-link flow: generate a code in Settings → open t.me/<bot>?start=<code> → bot confirms and binds.
Browser push Service worker + Web Push (RFC-8291 VAPID). Enable in Settings → browser permission prompt.
Email Resend. Add address in Settings → 6-digit verification code → verified address receives mail.

User profiles

A per-wallet profile (Cloudflare KV, no TTL) stores notification prefs and settings. Accessed via GET/PUT /profile with the signed session.

Custom Nockchain RPC: Settings lets you point the app at your own Nockchain gRPC-Web node. The URL is stored in localStorage for immediate effect and synced to your profile for cross-device access. Reset to default at any time.

Repo layout

src/            React app (Vite)
  ui/           wallet bar, dashboard, seller/buyer wizards, marketplace, settings
  app/          session/auth, swap repo (KV reads + authed write API), push, settings
  nock/  evm/   Nockchain + Base tx building / signing
  actions/      swap steps (generate / lock / withdraw / claim / refund)
public/         static assets including sw.js (service worker for push notifications)
worker/         Cloudflare Worker — swap API, sessions, integrity, notifications, profiles
contracts/      Foundry HTLC (AtomicNock.sol)

Local dev

Prerequisites: Node ≥ 20.19 (Vite 8), the Iris wallet extension, and MetaMask on Base mainnet (chain id 8453).

# 1. Install
npm install

# 2. Env — defaults are fine for local dev
cp .env.example .env
#    VITE_KV_URL=http://localhost:8787   → use the local dev worker (below)
#    VITE_KV_URL=                        → ephemeral in-memory store, no worker needed

# 3. Terminal A — local swap API (isolated local KV; never touches production)
npm --prefix worker run dev      # http://localhost:8787

# 4. Terminal B — web app
npm run dev                      # http://localhost:5173

Then connect Iris and MetaMask; you'll sign in once at connect, and can create / claim swaps.

The Nockchain gRPC-Web calls are proxied by the Vite dev server to VITE_NOCK_GRPC_UPSTREAM (default rpc.nockchain.net) — no Envoy or local node required. You can override this per-session in Settings → Custom RPC.

The dev worker uses worker/wrangler.dev.toml + worker/.dev.vars (SESSION_SECRET). wrangler dev runs in local mode and simulates KV under worker/.wrangler/state — it never touches the production namespace. Delete worker/.wrangler/ to reset dev data.

Environment

Frontend (.env)

Variable Purpose
VITE_KV_URL Swap API URL. Local: http://localhost:8787. Empty: in-memory. Prod: deployed Worker URL.
VITE_HTLC_ADDRESS Deployed AtomicNock on Base for USDC swaps.
VITE_HTLC_ADDRESS_WNOCK Deployed AtomicNock for wNOCK swaps. Empty = wNOCK option hidden.
VITE_NOCK_GRPC_UPSTREAM Vite proxy target for Nockchain gRPC-Web (default https://rpc.nockchain.net).
VITE_ETH_RPC_URL Mainnet RPC for ENS lookups — must allow CORS (optional; has a default).
VITE_PRICE_URL NOCK/USD price feed (optional; empty = hidden).
VITE_VAPID_PUBLIC_KEY VAPID public key for browser push. Empty = push option hidden in Settings.

There is no VITE_KV_TOKEN — writes are authorized per-user by signature, not a shared token.

Worker secrets (wrangler secret put)

Secret Purpose
SESSION_SECRET HMAC key for Iris session tokens (32+ random bytes).
TELEGRAM_BOT_TOKEN From @BotFather — authorizes Bot API calls.
TELEGRAM_WEBHOOK_SECRET Random string registered with setWebhook; validates incoming updates.
VAPID_PRIVATE_KEY VAPID ES256 private key for browser push encryption.
RESEND_API_KEY Resend API key for sending verification and notification emails.

Worker vars (wrangler.toml or dashboard)

Var Purpose
TELEGRAM_BOT_NAME Bot username (without @) for deep-link URL construction.
VAPID_PUBLIC_KEY Must match VITE_VAPID_PUBLIC_KEY.
VAPID_SUBJECT VAPID contact (mailto: or https: URL).
EMAIL_FROM Sender address for Resend emails (must match verified Resend domain).
APP_URL Canonical app URL (e.g. https://atomicnock.com) used in notification links.

Deploy

Worker (swap API)

Requires the Cloudflare Workers Paid plan: the rose-wasm verifier is ~1 MB gzipped, over the free-tier 1 MB Worker limit.

cd worker
wrangler kv namespace create SWAPS   # once — paste the id into wrangler.toml
wrangler secret put SESSION_SECRET   # a random 32+ byte secret
wrangler deploy

Notifications setup (optional)

Telegram:

# 1. Create a bot via @BotFather → copy the token
wrangler secret put TELEGRAM_BOT_TOKEN
wrangler secret put TELEGRAM_WEBHOOK_SECRET  # any random string

# 2. Register the webhook (replace placeholders)
curl "https://api.telegram.org/bot<TOKEN>/setWebhook" \
  -d "url=https://api.atomicnock.com/tg/webhook" \
  -d "secret_token=<WEBHOOK_SECRET>"

Browser push:

npx web-push generate-vapid-keys
# → paste public key into VITE_VAPID_PUBLIC_KEY (.env) and VAPID_PUBLIC_KEY (worker var)
wrangler secret put VAPID_PRIVATE_KEY
# set VAPID_SUBJECT to mailto:you@example.com (worker var)

Email (Resend):

# 1. Create account at resend.com, verify your sending domain (SPF/DKIM)
wrangler secret put RESEND_API_KEY
# set EMAIL_FROM to noreply@yourdomain.com (worker var)

Web app

npm run build        # → dist/ (static)
# Host dist/ (e.g. Cloudflare Pages). Set VITE_KV_URL to the deployed Worker URL,
# VITE_HTLC_ADDRESS and VITE_HTLC_ADDRESS_WNOCK to the deployed contracts.

Contract (Base)

cp .env.example .env          # set DEPLOYER_PRIVATE_KEY (fund with Base ETH), BASESCAN_API_KEY
make deploy-base-dry          # simulate
make deploy-base              # broadcast → paste address into VITE_HTLC_ADDRESS

# wNOCK instance (same bytecode, different token):
TOKEN_ADDRESS=0x9B5E262cF9bb04869ab40b19AF91D2dc85761722 make deploy-base
# → paste address into VITE_HTLC_ADDRESS_WNOCK

USDC on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. wNOCK on Base: 0x9B5E262cF9bb04869ab40b19AF91D2dc85761722.

Tests

npm test                       # web app unit tests (vitest)
npm --prefix worker run test   # worker integrity / ordering-invariant / profile tests
cd contracts && forge test     # HTLC contract tests (Foundry)

Caveats

  • Not audited. This is mainnet — use small amounts.
  • The shared swap record holds hashes and params only — never the preimage. The buyer learns the secret from the seller's Base withdraw transaction.
  • A 7-day session token in localStorage is XSS-exposed (standard dApp tradeoff); it only authorizes writes to that wallet's own swaps.
  • Swap records expire 30 days after last activity. On-chain state is always authoritative — expiry only affects the coordination metadata.
  • Browser push requires HTTPS and a registered service worker. iOS Safari requires the PWA to be installed to the home screen.

About

cross chain swaps using hash timelock contracts

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages