archy/docs/dual-ecash-design.md
archipelago 705e2436ba chore(ops,docs): first-boot containers, image versions, design docs, android remote-input
- first-boot-containers + image-versions for fmcd/fedimint
- dual-ecash, meshroller-integration, and remaining-issues design docs
- Android remote-input two-finger scroll + external-open handling

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:22:02 -04:00

7.9 KiB

Dual-ecash: Cashu + Fedimint, seamlessly

Status: in progress (2026-06-17). FE scaffolding + Fedimint HTTP bridge landed and compile-checked; live federation round-trip and networking-sats routing are not yet validated.

Why

Today the node's wallet (core/archipelago/src/wallet/ecash.rs, mint_client.rs, cashu.rs) speaks only the Cashu NUT HTTP protocol (BDHKE, cashuA… tokens). There is no Fedimint clientapps/fedimint is only the guardian server, and the "local Fedimint" default mint at 127.0.0.1:8175 is just the guardian UI nginx, which does not expose the Cashu NUT API. So:

  • The node can hold/spend generic Cashu tokens, but cannot hold Fedimint ecash or join federations.
  • "Networking sats" (streaming/seeding revenue) is hardcoded to the Cashu wallet.

Goal: support both ecash protocols seamlessly — hold balances in either, join arbitrary federations, and let networking-sats be paid/received over whichever protocol the peer accepts.

Architecture decision

Containerized fedimint-clientd + thin HTTP bridge (chosen over linking the native fedimint-client Rust SDK into the binary, and over a Lightning-only bridge).

archipelago binary
   ├─ CashuMintClient  ──HTTP (NUT /v1/*)──▶  cashu mint
   └─ FedimintClient   ──REST (/v2/*)─────▶  fedimint-clientd container ──▶ federation guardians

Rationale: keeps the heavy, fast-moving Fedimint SDK out of the main binary (no compile-time coupling, no rebuild bloat, OTA-friendly), and fits the existing app/container architecture (apps/fedimint, apps/fedimint-gateway). The Rust side is just a reqwest client, mirroring MintClient.

fedimint-clientd REST surface (v0.3.x)

  • Auth: Authorization: Bearer <password>. Default port 8080 (we map it to host 8178 because 8080 is LND REST). Base path /v2/....
  • GET /v2/admin/info — per-federation balances (totalAmountMsat, denominations, meta).
  • POST /v2/admin/join{ "inviteCode": "fed1…", "useManualSecret": false } → joins / returns federationId.
  • POST /v2/mint/spend{ federationId, amountMsat } → serialized notes (ecash to send).
  • POST /v2/mint/reissue{ federationId, notes } → redeem received notes; returns reissued amount.
  • POST /v2/ln/invoice / POST /v2/ln/pay — Lightning in/out (used for cross-protocol swaps).
  • GET /health.

Multi-federation: requests carry a federationId; clientd's multimint manages many clients. Exact JSON field names must be pinned to the clientd image tag we vendor — code defensively.

Components

1. Container app — apps/fedimint-clientd/manifest.yml

Sidecar running fedimint-clientd, mirroring apps/fedimint-gateway. Host port 8178 → container 8080. Password from node secret fedimint-clientd-password. State volume at /var/lib/archipelago/fedimint-clientd. Added to RESERVED_PORTS (port_allocator.rs) and fallback_package_port() (server.rs).

2. Rust bridge — core/archipelago/src/wallet/fedimint_client.rs

Thin reqwest client: info(), join(), spend(), reissue(), ln_invoice(), ln_pay(). from_node(data_dir) resolves base URL + password (env FEDIMINT_CLIENTD_URL / FEDIMINT_CLIENTD_PASSWORD, else defaults + secret file). Tor-proxy support via with_client, mirroring MintClient.

3. RPCs — core/archipelago/src/api/rpc/fedimint.rs

  • wallet.fedimint-list → joined federations + balances ({federation_id, name, balance_sats}[]).
  • wallet.fedimint-join {invite_code} → joins via clientd, persists to wallet/fedimint_federations.json, returns {federation_id}.
  • wallet.fedimint-leave {federation_id} → untracks locally.
  • wallet.fedimint-balance → total sats across federations (from clientd info).

Local registry wallet/fedimint_federations.json = { federations: [{federation_id, name}] } so the list survives clientd being temporarily down; balances are live from clientd.

4. Frontend — WalletSettingsModal.vue

Tabbed: Cashu Mints (live: streaming.list-mints / streaming.configure-mints) and Fedimint Federations (wallet.fedimint-list / -join / -leave). Gear icon on HomeWalletCard. fedimintBackendReady flips to true once the RPCs ship; join degrades gracefully with a clear error if the clientd app isn't installed.

4b. Default federation (zero-touch)

clientd auto-joins a default federation at boot via FEDIMINT_CLIENTD_INVITE_CODE (manifest), and the Rust bridge ensure_default_federation() idempotently joins + tracks it (called from wallet.fedimint-list) so already-running nodes pick it up too. Constant DEFAULT_FEDERATION_INVITE in fedimint_client.rs is the single source on the Rust side; keep it in sync with the manifest env. clientd is a client, not the guardian — it needs no local fedimintd, so it bundles standalone.

4c. Bundling on every node

  • Bundled ISO: add image to scripts/image-versions.sh, the ISO bundle .tar list, and a core-create block in scripts/first-boot-containers.sh; mark tier: "core" in app-catalog/catalog.json.
  • Unbundled ISO: first-boot-containers.sh exits after FileBrowser only — add clientd to that early-exit block so unbundled nodes also get it out of the box.
  • CAVEAT: confirm the current ISO assembler before editing the bundle list — the one found is under image-recipe/_archived/ (likely stale); first-boot-containers.sh/image-versions.sh are current.
  • Image: build from source (no official image; flake.nix only) → push to vps2 146.59.87.168:3000/lfg2025/fedimint-clientd:v0.4.0.

5. Unified balance

HomeWalletCard ecash row = Cashu wallet.ecash-balance + Fedimint wallet.fedimint-balance. (Home already calls wallet.ecash-balance; add fedimint and sum.)

Networking sats — dual protocol (phase 6, NOT yet wired)

The economic layer (streaming.rs, streaming/gate.rs, sessions, pricing, metering) is already protocol-agnostic — it just calls into the wallet. The injection points are:

  1. Protocol-tag accepted mints. accepted_mints: Vec<String> → carry protocol, e.g. cashu:https://mint… / fedimint:<federation_id>. Migrate wallet/accepted_mints.json with a back-compat reader (bare URL ⇒ cashu:).
  2. MintClient::new() is the bottleneck (~10 call sites). Introduce a MintBackend trait with CashuBackend (wraps current code) and FedimintBackend (calls FedimintClient).
  3. Token enum Cashu(CashuToken) | Fedimint(notes); serialize/verify by variant.
  4. build_payment_token() picks a (backend, id) the peer accepts; verify_and_receive_payment() auto-detects the token variant and reissues/swaps on the right backend.
  5. Cross-protocol settlement (Cashu↔Fedimint) bridges over Lightning (BOLT11) — both sides already have mint/melt (Cashu) and ln/invoice+ln/pay (Fedimint).
  6. Web5NetworkingProfitsSettings.vue: per-service payout protocol/mint selector.

Phases

  • P0 FE tabbed Wallet Settings modal + gear (Cashu live, Fedimint tab structured).
  • P1 fedimint-clientd container manifest + ports.
  • P2 FedimintClient HTTP bridge + wallet.fedimint-* RPCs (compiles).
  • P3 Validate join / balance / spend / reissue against a live clientd + real federation on a scratch node.
  • P4 Unified ecash balance in the wallet card (Cashu + Fedimint).
  • P5 Flip FE fully live; surface "install Fedimint client app" when clientd unreachable.
  • P6 Networking-sats dual-protocol routing (the MintBackend/Token refactor above).

Validation (per project testing discipline)

clientd image + a real federation are required; cannot be validated from the dev tree. Validate on a scratch node: install Fedimint app + clientd, join a known test federation, confirm wallet.fedimint-balance, then a spend→reissue round-trip between two nodes, then networking-sats payment over Fedimint. Heavy/iterative work belongs in a worktree (see CLAUDE.md memory).