Files
Danish AroraandClaude Opus 5 110dd61b32 feat(deploy): package swap-cli maker as a container + deploy to VPS (#88)
* refactor(lez): lift native faucet/init off the wallet CLI critical path

Moves the pinata PoW-claim and authenticated_transfer::Initialize
implementations out of lez-mcp and into src/lez/{faucet,onboard}.rs in the
app crate, so the maker bot (and later the GUI over FFI) can create an
account, initialize it on-chain, and fund it from the public faucet without
shelling out to the external LEZ `wallet` binary. lez-mcp now re-exports
Signer and the faucet module from the app crate instead of keeping its own
copy.

bot::fund_to_target is rewritten onto lez::onboard::claim_to_target, which
also lifts the wallet-mode-only restriction: --fund-to now works under
LezAuth::RawKey.

Also fixes three pre-existing clippy lints (manual_is_multiple_of,
redundant_guards, collapsible_if) unrelated to this change but needed for a
clean `cargo clippy --all-targets -D warnings` run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(lez): retry the balance read in claim_to_target on transient RPC errors

Live-testing against the public testnet hit exactly this: a single
"client error (SendRequest)" on get_account_balance aborted the whole
funding loop even though the very next read would have succeeded. A maker
bot running unattended will hit this eventually, so bound-retry the read
(3 attempts, 2s apart) instead of propagating the first transient error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(deploy): package swap-cli maker as a container + fix maker-state gitignore

Multi-stage Dockerfile (Rust 1.93 release build of swap-cli + Node 20
offer-publisher deps + slim node-based runtime, since swap-cli itself
shells out to `node`), deploy/docker-compose.yml + maker.env.example +
README documenting the runbook and the RESTRICTED-counterparty mode this
deployment starts in (public-taker support is unmerged, PR #64/#76).

Also:
- offer-publisher/watch-offers.mjs: subscribes to the offers content topic
  independently of the maker, for external liveness verification (the
  fleet runs store=false, so this is the only way to prove an offer
  actually reached it).
- examples/onboard_maker_account.rs: one-off provisioning helper using the
  native LEZ onboarding path (src/lez/onboard.rs) to generate + initialize
  + pinata-fund a fresh maker account, no scaffold/wallet binary needed.
- .gitignore: .maker-state.json (the crash-recovery journal) was never
  actually ignored despite being CWD-relative by default; the container
  image fixes the CWD-relative part (state lives on a volume) but the
  gitignore gap was separate and real.

* ci: publish the maker container image to GHCR on tag push

release-maker-image.yml — the repo's first CI artifact for the CLI (every
other release workflow publishes a Basecamp module via
logos-modules-release-action, which has no concept of a headless
container). Triggers on maker-v* tags, kept separate from swap-v*/swap_ui-v*
so a maker image cut never collides with a module release.

* fix(deploy): make image+build coexist in compose so first deploy needs no edits

Previously docker-compose.yml only had `image:` active with `build:`
commented out, so a build-on-host first deploy (before any GHCR tag exists)
would have needed manually uncommenting build and commenting image. Both
are now active together: `docker compose build` builds from source and
tags it with the same name `docker compose up`/`pull` expect, so the same
compose file works unchanged for build-on-host now and registry-pull later.

* fix(docker): add python3-dev — risc0-zkvm links pyo3 against libpython

Local validation build failed at the final swap-cli link step:
"/usr/bin/ld: cannot find -lpython3.11". risc0-zkvm's dependency tree
pulls in pyo3/pyo3-ffi, which links against libpython at compile time —
unrelated to RISC0_SKIP_BUILD (that only skips building the actual guest
ELF, not this native link requirement). python3-dev in the rust-builder
stage provides the missing library.

* fix(deploy): ETH_PRIVATE_KEY has no 0x prefix, matching docs/testnet.md

* fix(docker): copy examples/ into rust-builder stage for onboarding helper

Not shipped in the runtime image (only target/release/swap-cli is copied
out) but needed so the rust-builder stage can also compile/run
examples/onboard_maker_account.rs during deploy provisioning.

* feat(onboard): add --generate-only for placeholder taker accounts

Restricted-mode maker deployments need a well-formed LEZ_TAKER_ACCOUNT_ID
before a real public taker exists (the loop refuses to start without one).
It never has to be initialized/funded — restricted mode never pays out to
it — so skip the on-chain round-trip entirely for this case.

* fix(docker): bump Node 20 -> 22, the maker's Node sidecar crashes on 20.x

Live on the VPS: the container started, the maker loop began, but the
offer-publisher sidecar (spawned via \`node publish-offer.mjs\`) crashed
immediately with "TypeError: Promise.withResolvers is not a function"
inside @libp2p/peer-store's write-lock path (mortice -> it-queue). Node
20.x does not have Promise.withResolvers; offer-publisher/README.md's
"Node >= 20" was stale. Bumped both Node stages to node:22-bookworm-slim
and corrected the docs.

* docs(deploy): catch up to the merged public-taker work + Sepolia redeploy

PR #64/#76 merged into master mid-deploy, flipping --restrict-counterparty
from mandatory to an opt-in allowlist (public mode is now the default,
correct behavior), and a new EthHTLC (INTERFACE_VERSION=2) was deployed at
0x351B0EA07739FA9F6769213927D7836a790A5FAF, superseding
0x8636Fe66DFee166589a913140f14d5F57394834A (incompatible ABI). Point
maker.env.example at the new contract and explain both the historical
context and that this deployment still runs RESTRICT_COUNTERPARTY=true by
deliberate operator choice for the first rollout, not code necessity.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 21:22:53 +05:30
..