Replaces the registry host across 86 files: 309 references, covering all 40 app manifests, the orchestrator and container crates, the release and catalog scripts, both demo-images workflows, the ISO builder, demo-deploy, and the frontend marketplace data. Verified the domain actually serves the registry before rewriting anything, rather than assuming the web host implies the registry: - TLS verifies clean, HTTP/2 on the web root - an anonymous token grants a manifest fetch (HTTP 200) with no credentials - skopeo inspect --no-creds resolves an image and lists its tags That last check is the one that matters: an outside developer with no account can now pull, which was the functional blocker for publishing at all. Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop crossing the network in the clear. Deliberately NOT rewritten: - The public FIPS anchor on port 8444. It is a functional network endpoint every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded seeds than to leaked infrastructure. The domain does resolve to the same host, so it could become a hostname, but that adds a DNS dependency to the path used precisely when things are broken. Worth a deliberate decision, not a side effect of this change. - The companion APK on port 2100. The domain returns 404 for that path, so rewriting it would swap a working URL for a broken one. The Releases page does serve (200), which is where the plan already wants those binaries. - releases/app-catalog.json, releases/manifest.json and release-manifest.json. These carry `signature` and `signed_by`; editing their contents invalidates the signature and the fleet refuses artifacts that fail verification. They were rewritten in a first pass and reverted — they must be regenerated and re-signed through the signing ceremony instead, which needs the mnemonic. So the catalog still advertises the old host until that ceremony runs. Nodes resolve images through the signed catalog, not the on-disk manifests, so this commit alone does not change what a node pulls. Verified: archipelago-container 75/75; every manifest still parses with a top-level app block; no signed artifact modified. Co-Authored-By: Claude Opus 5 (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 vps2source.archipelago-foundation.org/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).