archy/docs/dual-ecash-design.md

136 lines
7.9 KiB
Markdown
Raw Normal View History

# 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
*client* — `apps/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
- [x] **P0** FE tabbed Wallet Settings modal + gear (Cashu live, Fedimint tab structured).
- [x] **P1** `fedimint-clientd` container manifest + ports.
- [x] **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).