- 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>
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
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 / returnsfederationId.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 towallet/fedimint_federations.json, returns{federation_id}.wallet.fedimint-leave{federation_id}→ untracks locally.wallet.fedimint-balance→ total sats across federations (from clientdinfo).
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.tarlist, and a core-create block inscripts/first-boot-containers.sh; marktier: "core"inapp-catalog/catalog.json. - Unbundled ISO:
first-boot-containers.shexits 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.share current. - Image: build from source (no official image;
flake.nixonly) → push to vps2146.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:
- Protocol-tag accepted mints.
accepted_mints: Vec<String>→ carry protocol, e.g.cashu:https://mint…/fedimint:<federation_id>. Migratewallet/accepted_mints.jsonwith a back-compat reader (bare URL ⇒cashu:). MintClient::new()is the bottleneck (~10 call sites). Introduce aMintBackendtrait withCashuBackend(wraps current code) andFedimintBackend(callsFedimintClient).TokenenumCashu(CashuToken) | Fedimint(notes); serialize/verify by variant.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.- Cross-protocol settlement (Cashu↔Fedimint) bridges over Lightning (BOLT11) — both sides already
have mint/melt (Cashu) and
ln/invoice+ln/pay(Fedimint). Web5NetworkingProfitsSettings.vue: per-service payout protocol/mint selector.
Phases
- P0 FE tabbed Wallet Settings modal + gear (Cashu live, Fedimint tab structured).
- P1
fedimint-clientdcontainer manifest + ports. - P2
FedimintClientHTTP 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/Tokenrefactor 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).