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>
This commit is contained in:
parent
87769cbfbf
commit
705e2436ba
@ -35,6 +35,13 @@ class InputWebSocket(
|
|||||||
/** Player ID for arcade mode (0 = broadcast, 1 = P1, 2 = P2) */
|
/** Player ID for arcade mode (0 = broadcast, 1 = P1, 2 = P2) */
|
||||||
var playerId: Int = 0
|
var playerId: Int = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the kiosk asks us to open a URL in the phone's default
|
||||||
|
* browser ({"t":"o","url":"…"}). "Open in external browser" apps can't be
|
||||||
|
* usefully opened on the kiosk, so the kiosk forwards them here.
|
||||||
|
*/
|
||||||
|
var onExternalOpen: ((String) -> Unit)? = null
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ConnectionState.DISCONNECTED)
|
private val _state = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||||
val state: StateFlow<ConnectionState> = _state
|
val state: StateFlow<ConnectionState> = _state
|
||||||
|
|
||||||
@ -127,6 +134,20 @@ class InputWebSocket(
|
|||||||
reconnectAttempt = 0
|
reconnectAttempt = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
// The only inbound message we act on is an external-open request
|
||||||
|
// forwarded from the kiosk: {"t":"o","url":"https://…"}.
|
||||||
|
try {
|
||||||
|
val obj = org.json.JSONObject(text)
|
||||||
|
if (obj.optString("t") == "o") {
|
||||||
|
val url = obj.optString("url")
|
||||||
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
|
onExternalOpen?.invoke(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
_state.value = ConnectionState.ERROR
|
_state.value = ConnectionState.ERROR
|
||||||
scheduleReconnect()
|
scheduleReconnect()
|
||||||
|
|||||||
@ -63,6 +63,21 @@ fun RemoteInputScreen(onBack: () -> Unit) {
|
|||||||
|
|
||||||
val ws = remember { InputWebSocket(scope) }
|
val ws = remember { InputWebSocket(scope) }
|
||||||
|
|
||||||
|
// When the kiosk forwards an "open in external browser" app, launch it in
|
||||||
|
// the phone's default browser.
|
||||||
|
DisposableEffect(ws) {
|
||||||
|
ws.onExternalOpen = { url ->
|
||||||
|
try {
|
||||||
|
val intent = android.content.Intent(
|
||||||
|
android.content.Intent.ACTION_VIEW,
|
||||||
|
android.net.Uri.parse(url),
|
||||||
|
).apply { addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
onDispose { ws.onExternalOpen = null }
|
||||||
|
}
|
||||||
|
|
||||||
fun togglePlayer() {
|
fun togglePlayer() {
|
||||||
playerId = when (playerId) { 0 -> 1; 1 -> 2; else -> 0 }
|
playerId = when (playerId) { 0 -> 1; 1 -> 2; else -> 0 }
|
||||||
ws.playerId = playerId
|
ws.playerId = playerId
|
||||||
|
|||||||
224
docs/REMAINING-ISSUES-PLAN.md
Normal file
224
docs/REMAINING-ISSUES-PLAN.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# Remaining issues — implementation plans
|
||||||
|
|
||||||
|
Written 2026-06-17. Covers the open Gitea issues not closeable in the single-box
|
||||||
|
dev env. Each plan lists the files to touch, the approach, and how to verify
|
||||||
|
(most need .116 + .198, a companion phone, or funded wallets). Issues #3 (VPN)
|
||||||
|
and #5 (OpenWRT/TollGate) are intentionally out of scope per the user.
|
||||||
|
|
||||||
|
Status of the rest at time of writing:
|
||||||
|
- **#31** group chat over Tor — dedup-by-`msg_id` fix already shipped (open only
|
||||||
|
for a 2-node Tor confirmation). See its Gitea comment.
|
||||||
|
- **#43** install on .70 — blocked: .70 unreachable. Plan below is a code-side
|
||||||
|
hardening that doesn't depend on .70's logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #46 — Pay for peer files (local wallet OR invoice+QR to seller)
|
||||||
|
|
||||||
|
> **Status (2026-06-17): Phase 1 DONE & compiles** (LN invoice + QR + release).
|
||||||
|
> Seller: `content_invoice.rs` entitlement store, `GET /content/{id}/invoice`
|
||||||
|
> + `/invoice-status/{hash}`, invoice-paid path in `serve_content`
|
||||||
|
> (`X-Invoice-Hash`), LND `create_invoice`/`invoice_is_settled`. Buyer:
|
||||||
|
> `content.request-invoice` / `.invoice-status` / `.download-peer-invoice` +
|
||||||
|
> `PeerFiles.vue` picker modal + QR + poll. Phases 2 (on-chain) and 3 (local
|
||||||
|
> LN/on-chain methods) remain; needs live funded-wallet verify. Issue left open.
|
||||||
|
|
||||||
|
**Goal.** At the paid-download step in Cloud → peer files, let the buyer choose
|
||||||
|
how to pay: (a) their local wallet (ecash today; LN/on-chain later), or (b) get
|
||||||
|
an invoice with a QR drawn on the **selling** node's wallet, pay from any
|
||||||
|
external wallet, and have the file release on confirmation.
|
||||||
|
|
||||||
|
**What exists already**
|
||||||
|
- Buyer ecash auto-pay: `content.download-peer-paid` (mints ecash, downloads
|
||||||
|
atomically) — wired in `neode-ui/src/views/PeerFiles.vue` `downloadFile()`.
|
||||||
|
- Payer-side builder: `streaming.prepare-payment` RPC + `wallet/ecash.rs`
|
||||||
|
(`build_payment_token`, cross-mint), `swarm/payment.rs`.
|
||||||
|
- Free streaming download: `/api/peer-content/:onion/:id` (Range-capable).
|
||||||
|
- LND invoice RPC: `lnd.createinvoice`; ecash balance: `wallet.ecash-balance`.
|
||||||
|
|
||||||
|
**Backend work**
|
||||||
|
1. **Seller-side invoice RPC** (new), e.g. `content.request-invoice`
|
||||||
|
`{ onion, content_id }` → asks the *selling* node (over the existing
|
||||||
|
`/archipelago/...` peer transport, same path machinery as
|
||||||
|
`content.download-peer-paid`) to produce a payment request for `price_sats`:
|
||||||
|
- LN: `lnd.createinvoice` on the seller, return `bolt11` + `payment_hash`.
|
||||||
|
- on-chain: `lnd.newaddress` on the seller, return `address` + `amount`.
|
||||||
|
- Seller records a pending entitlement keyed by `payment_hash`/address →
|
||||||
|
content_id → buyer.
|
||||||
|
2. **Payment confirmation + release**: seller polls its own LND
|
||||||
|
(`lnd.lookup-invoice` / address watch); on settle, marks the entitlement
|
||||||
|
paid. Buyer side polls `content.invoice-status { payment_hash }` → when paid,
|
||||||
|
downloads via the existing `/api/peer-content` (gate now passes because the
|
||||||
|
entitlement is satisfied). Reuse the streaming gate in `streaming/` — add an
|
||||||
|
"invoice-paid" path alongside the ecash-token path.
|
||||||
|
3. Keep `content.download-peer-paid` (local-ecash) as the (a) fast path.
|
||||||
|
|
||||||
|
**Frontend work** (`PeerFiles.vue`)
|
||||||
|
1. Before a paid download, open a small **payment-method picker** modal:
|
||||||
|
- "Pay from this node's wallet" → existing ecash flow (show balance; if
|
||||||
|
insufficient, the LN/on-chain local options when those land).
|
||||||
|
- "Pay from another wallet (QR)" → call `content.request-invoice`, render the
|
||||||
|
`bolt11`/address as a **QR** (add a tiny QR lib or reuse one already in the
|
||||||
|
bundle — check `package.json`), show amount + a live "waiting for
|
||||||
|
payment…" state polling `content.invoice-status`, then auto-download.
|
||||||
|
2. Reuse the existing `purchaseError`/`downloading` state + `triggerDownload`.
|
||||||
|
|
||||||
|
**Verify**: .116 (seller) + .198 (buyer), a funded regtest/LN wallet. Buyer
|
||||||
|
picks QR, pays from a 3rd wallet, file releases. Then the local-ecash path.
|
||||||
|
|
||||||
|
**Effort**: large (multi-day). Phase it: (1) LN-invoice + QR + release, (2)
|
||||||
|
on-chain, (3) local LN/on-chain methods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #18 — Companion app: "open in external browser" apps don't work
|
||||||
|
|
||||||
|
> **Status (2026-06-17): DONE & compiles (Rust + TS); Android unbuilt here.**
|
||||||
|
> Reverse relay hop added: `external_open_tx` channel, kiosk publishes
|
||||||
|
> `{"t":"o","url"}` on `/ws/remote-relay` (URL-validated), forwarded to the
|
||||||
|
> companion's `/ws/remote-input`. `requestExternalOpen()` in `remote-relay.ts`
|
||||||
|
> wired into all four `appLauncher.ts` external-open sites; `InputWebSocket.kt`
|
||||||
|
> + `RemoteInputScreen.kt` open it via `ACTION_VIEW`. Issue closed; live pairing
|
||||||
|
> test pending.
|
||||||
|
|
||||||
|
**Goal.** Apps configured to open in a new/external browser should launch on the
|
||||||
|
**phone** when driven from the companion controller, using the phone-default-
|
||||||
|
browser request pattern.
|
||||||
|
|
||||||
|
**What exists**
|
||||||
|
- Relay protocol in `neode-ui/src/api/remote-relay.ts` — message cases `m`
|
||||||
|
(move cursor), `c` (click), `s` (scroll, just fixed in #7). Click resolves the
|
||||||
|
element under the virtual cursor via `deepElementFromPoint`.
|
||||||
|
- The kiosk side runs the dashboard; "open external" apps currently try to
|
||||||
|
`window.open` on the **kiosk**, which the phone never sees.
|
||||||
|
|
||||||
|
**Approach**
|
||||||
|
1. **Detect external-open intent on the kiosk**: when a click lands on an
|
||||||
|
element that would open externally (anchor with `target=_blank` / an app
|
||||||
|
flagged `opensExternally`, or an intercepted `window.open`), instead of
|
||||||
|
opening locally, send a new relay message to the phone:
|
||||||
|
`{ t: 'open-url', url }` over the `/ws/remote-relay` channel (the kiosk is the
|
||||||
|
relay server side — find where it sends frames back to the companion).
|
||||||
|
2. **Companion (phone) side** handles `open-url` by doing `window.open(url,
|
||||||
|
'_blank')` / `location.href = url` so it opens in the phone's default browser.
|
||||||
|
- If the companion is the **Android APK** (separate codebase, see
|
||||||
|
`Android/` + memory `feedback_companion_apk_not_in_update`), add an
|
||||||
|
intent-based handler there; if it's a mobile web client, handle in JS.
|
||||||
|
3. Intercept `window.open` on the kiosk dashboard globally (a small shim that,
|
||||||
|
when remote-relay is active, forwards to the phone instead of opening).
|
||||||
|
|
||||||
|
**Verify**: phone + kiosk paired; tap an "open external" app from the companion;
|
||||||
|
it opens in the phone browser.
|
||||||
|
|
||||||
|
**Effort**: medium; needs the companion device + possibly an APK change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #50 — Integrate Meshroller into our mesh features
|
||||||
|
|
||||||
|
> **Decision made 2026-06-17: seam (a) — Rust-native lift.** Full design with
|
||||||
|
> verified seam anchors (message types, dispatch, send API, event/trust gates,
|
||||||
|
> Ollama call) is in **`docs/meshroller-integration-design.md`**. Summary below.
|
||||||
|
|
||||||
|
Source: https://gitea.l484.com/clasko/Meshroller
|
||||||
|
|
||||||
|
**Phase 0 — review (DONE 2026-06-17)**
|
||||||
|
- Reviewed. Meshroller is a single ~29KB Python script (`meshroller.py`): a
|
||||||
|
daemon that bridges a **Meshtastic** radio (via the `meshtastic` Python serial
|
||||||
|
module, `SerialInterface`) to an **Ollama** LLM (`qwen2.5-coder`). It has
|
||||||
|
trusted-node auth, scheduled/queued messaging, and command handling on mesh
|
||||||
|
channels. It is a **daemon**, not firmware or a library.
|
||||||
|
- **License**: in-house (our own developer) — no third-party license blocker.
|
||||||
|
- **Hardware/transport reality**: it rides **Meshtastic serial + a local
|
||||||
|
Ollama**. Our radio is **Meshcore** (Heltec V3) and our mesh stack targets
|
||||||
|
meshcore. The `meshtastic` module does NOT speak meshcore, so the script
|
||||||
|
cannot drive our radio unmodified.
|
||||||
|
- **Decision needed (architecture)**: per user, integration **must work with
|
||||||
|
meshcore**. Two seams:
|
||||||
|
- (a) Lift Meshroller's *behaviors* (LLM bridge, trusted-node auth, scheduled
|
||||||
|
messaging, command parser) into our Rust mesh stack as typed message kinds —
|
||||||
|
native to meshcore, no Python/Meshtastic dependency. Preferred for meshcore.
|
||||||
|
- (b) Package the Python daemon as a container app and add a meshcore serial
|
||||||
|
backend to it (keeps the script, but requires writing meshcore I/O the
|
||||||
|
`meshtastic` module doesn't provide).
|
||||||
|
This choice is the remaining gate; the rest of Phase 1 below stands.
|
||||||
|
|
||||||
|
**Phase 1 — choose the seam**
|
||||||
|
- Our mesh stack: `core/archipelago/src/mesh/` (`mod.rs` `MeshService`,
|
||||||
|
`listener/`, `protocol.rs`, `types.rs`). Decide:
|
||||||
|
- If Meshroller is a *protocol/feature on the same radio* → implement it as a
|
||||||
|
typed message kind in our `MeshMessageType` + `listener/dispatch.rs`
|
||||||
|
(mirrors how block headers / alerts are handled).
|
||||||
|
- If it's a *separate transport/daemon* → wrap it behind our transport router
|
||||||
|
(`transport/`) like FIPS/LAN/Tor.
|
||||||
|
- Reuse the event seam (`MeshEvent`) so the UI gets pushes (same path we just
|
||||||
|
wired for #48).
|
||||||
|
|
||||||
|
**Phase 2 — UX** (ties into `project_mesh_telegram_plan`)
|
||||||
|
- A dead-simple onboarding + usage flow in the Mesh tab. Define the 1–2 killer
|
||||||
|
actions and design the setup wizard.
|
||||||
|
|
||||||
|
**Verify**: 2 radios (the .116 Meshcore + a second).
|
||||||
|
|
||||||
|
**Effort**: multi-day; gated on the Phase 0 review + a license/architecture
|
||||||
|
decision.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #15 — netbird app doesn't work (LOW PRIORITY)
|
||||||
|
|
||||||
|
> **Status (2026-06-17): DIAGNOSED LIVE on .198 + FIXED (option A shipped); login works.**
|
||||||
|
> THE real blocker: the dashboard needs a **secure context** —
|
||||||
|
> `window.crypto.subtle is unavailable` over plain http, so OIDC PKCE threw
|
||||||
|
> before login. Fix: proxy now serves **HTTPS** (self-signed cert at install,
|
||||||
|
> `8087:443`, all origins `https://`); frontend opens netbird in a **new tab**
|
||||||
|
> (self-signed-HTTPS iframe is blocked). Layered fixes also in `stacks.rs`:
|
||||||
|
> nginx `resolver <gateway>` + variable upstreams (IP-cache 502; `resolver
|
||||||
|
> local=on`/`${NGINX_LOCAL_RESOLVERS}` FAIL on nginx:1.27-alpine), LAN-IP
|
||||||
|
> canonical origin + CORS + multi-origin redirect URIs, `/nb-auth`+`/nb-silent-auth`
|
||||||
|
> SPA fallback (were 404), and a stale-store note (wipe to re-init). Also found:
|
||||||
|
> `conmon died` zombie containers (recreate fixes; #53). Validated on .198,
|
||||||
|
> registration+login succeed. Trusted-cert/iframe (option B) = #56;
|
||||||
|
> registry-app migration = #52. Existing nodes need a clean reinstall.
|
||||||
|
|
||||||
|
**Diagnose first** (likely a container/config issue, like other app fixes):
|
||||||
|
1. On a node: `podman logs <netbird container>` — capture the actual failure.
|
||||||
|
2. Check the app manifest + install path (`container/` install, env, ports,
|
||||||
|
the four iframe-sync places per memory `feedback_gitea_iframe_setup` if it
|
||||||
|
has a UI).
|
||||||
|
3. netbird needs a management URL / setup key — confirm whether the app expects
|
||||||
|
config we don't provide, or a host capability (TUN device / NET_ADMIN) the
|
||||||
|
rootless-podman setup lacks.
|
||||||
|
|
||||||
|
**Likely fix**: either supply the missing env/setup-key UI, or add the required
|
||||||
|
container capability. Low priority — schedule after the above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #43 — Install errors at DID-creation + password screens (.70); FIPS slow
|
||||||
|
|
||||||
|
`.70` is unreachable, so we can't read its logs. Code-side hardening that helps
|
||||||
|
regardless:
|
||||||
|
> **Status (2026-06-17): hardening DONE & compiles.** Root cause was a
|
||||||
|
> non-idempotent `seed.generate` that overwrote node keys under the client's
|
||||||
|
> retry storm on slow first boot. Fixed: idempotent generate + retry-safe
|
||||||
|
> verify (`seed_rpc.rs`), transient-vs-genuine error handling in
|
||||||
|
> `OnboardingSeedGenerate/Verify.vue`, and a non-blocking FIPS status on
|
||||||
|
> `OnboardingDone.vue`. Issue closed; full closure wants a fresh install on a
|
||||||
|
> reachable node + re-test on .70.
|
||||||
|
|
||||||
|
1. **Onboarding error surfacing** — in the seed/DID + password onboarding views
|
||||||
|
(`OnboardingSeed*`, the password step) and their RPC handlers
|
||||||
|
(`seed.generate` / `seed.verify` / `auth.setup`), make a *successful*
|
||||||
|
operation never show an error toast, and make genuinely-failed ops show the
|
||||||
|
real message + a retry — so cosmetic errors (op actually succeeded) stop
|
||||||
|
alarming users. Audit the promise/catch paths for races where a slow backend
|
||||||
|
resolves after a timeout fires.
|
||||||
|
2. **FIPS start delay** — confirm `spawn_post_onboarding_fips_activate`
|
||||||
|
(`api/rpc/seed_rpc.rs`) isn't blocking onboarding; it already runs detached.
|
||||||
|
Consider surfacing "FIPS starting…" status instead of letting it look stuck.
|
||||||
|
|
||||||
|
**Verify**: a fresh ISO install on a reachable node (.198 or a scratch box),
|
||||||
|
watch the DID + password screens; then re-test on .70 once reachable.
|
||||||
|
|
||||||
|
**Effort**: small–medium (the hardening); full closure needs a repro node.
|
||||||
135
docs/dual-ecash-design.md
Normal file
135
docs/dual-ecash-design.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# 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).
|
||||||
169
docs/meshroller-integration-design.md
Normal file
169
docs/meshroller-integration-design.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# Meshroller → Rust-native mesh assistant (issue #50)
|
||||||
|
|
||||||
|
**Decision (2026-06-17): seam (a) — lift Meshroller's *behaviors* into our Rust
|
||||||
|
mesh stack as typed message kinds.** We do NOT package the Python/Meshtastic
|
||||||
|
daemon. Meshroller rides Meshtastic-serial + a local Ollama; our radio is
|
||||||
|
**meshcore** (Heltec V3) and the `meshtastic` Python module cannot drive it. So
|
||||||
|
we reimplement its four behaviors natively against `core/archipelago/src/mesh/`,
|
||||||
|
drop the Python + Meshtastic dependency, and reuse our existing event/transport
|
||||||
|
seams.
|
||||||
|
|
||||||
|
Meshroller's behaviors (from the Phase-0 review of `meshroller.py`):
|
||||||
|
1. **LLM bridge** — relay an inbound mesh message to a local LLM, send the reply
|
||||||
|
back on the mesh.
|
||||||
|
2. **Trusted-node auth** — only trusted senders may invoke commands.
|
||||||
|
3. **Scheduled / queued messaging** — send messages at a future time; queue for
|
||||||
|
peers that are currently offline.
|
||||||
|
4. **On-channel command parser** — recognise commands in channel traffic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where this plugs in (verified seam map)
|
||||||
|
|
||||||
|
| Concern | File / type | Anchor |
|
||||||
|
|---|---|---|
|
||||||
|
| Wire message kinds | `mesh/message_types.rs` `MeshMessageType` (`#[repr(u8)]`) | 28–73 |
|
||||||
|
| Envelope (CBOR, `0x02` marker, `seq`, `sig`) | `mesh/message_types.rs` `TypedEnvelope` | 183–197 |
|
||||||
|
| Inbound dispatch match | `mesh/listener/dispatch.rs` `handle_typed_envelope_direct()` | 80–691 |
|
||||||
|
| Outbound send | `mesh/mod.rs` `send_typed_wire()` / `send_channel_typed_wire()` | 848 / 1152 |
|
||||||
|
| Radio I/O command channel | `mesh/listener/mod.rs` `MeshCommand` (`SendText`/`BroadcastChannel`) | 55–73 |
|
||||||
|
| Frame chunking (≤160 B/frame, transparent) | `mesh/listener/session.rs` `send_dm_via_channel()` | — |
|
||||||
|
| UI push | `mesh/types.rs` `MeshEvent` (broadcast on `state.event_tx`, cap 64) | 125–164 |
|
||||||
|
| Trust gate | `federation/types.rs` `TrustLevel::Trusted` on `FederatedNode`; `federation::load_nodes()` | 5–52 |
|
||||||
|
| Block on user-blocklist | `mesh/listener/mod.rs` `ContactEntry.blocked` (`state.contacts`) | 110 |
|
||||||
|
| Local model | Ollama container, port **11434** (`port_allocator.rs:11`); call via `reqwest` (already a dep) | — |
|
||||||
|
|
||||||
|
No in-Rust LLM exists yet; we call the **local Ollama HTTP API** (the same model
|
||||||
|
Meshroller used) so nothing new is baked into the binary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 — the assistant on the wire
|
||||||
|
|
||||||
|
### 1.1 New typed message kinds (`message_types.rs`)
|
||||||
|
Add two variants (next free tag = 24):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
AssistQuery = 24, // "ask the node's AI" — prompt + optional model
|
||||||
|
AssistResponse = 25, // reply — request_id + text + done flag
|
||||||
|
```
|
||||||
|
Wire the four spots the enum requires (`from_u8` 76–104, `from_label` 109–137,
|
||||||
|
`label()` 139–166, plus the variant) — mirror the `Invoice` variant exactly.
|
||||||
|
|
||||||
|
Payloads (CBOR via `encode_payload`/`decode_payload`):
|
||||||
|
```rust
|
||||||
|
pub struct AssistQueryPayload { pub req_id: u64, pub prompt: String, pub model: Option<String> }
|
||||||
|
pub struct AssistResponsePayload { pub req_id: u64, pub text: String, pub seq: u16, pub done: bool }
|
||||||
|
```
|
||||||
|
`seq`/`done` let a long reply span multiple `AssistResponse` messages without
|
||||||
|
relying solely on frame reassembly (radio airtime is scarce — see §1.4 cap).
|
||||||
|
|
||||||
|
### 1.2 Inbound handler (`listener/dispatch.rs`)
|
||||||
|
Add a match arm for `AssistQuery`, mirroring the **`TxRelay`** arm (169–207):
|
||||||
|
validate → **gate** → spawn background work (never block the radio loop).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Some(MeshMessageType::AssistQuery) => {
|
||||||
|
let payload = decode_payload::<AssistQueryPayload>(&envelope.v)?;
|
||||||
|
if !assistant_enabled(state) { return; } // kill switch (config)
|
||||||
|
if !sender_is_allowed(state, sender_contact_id).await { warn!(..); return; }
|
||||||
|
if !rate_limit_ok(state, sender_contact_id).await { return; } // 1 in-flight / sender
|
||||||
|
let _ = state.event_tx.send(MeshEvent::AssistQueryReceived { from_contact_id, prompt });
|
||||||
|
let st = Arc::clone(state);
|
||||||
|
tokio::spawn(async move { run_assist(&st, sender_contact_id, payload).await; });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`run_assist`: POST `http://localhost:11434/api/generate`
|
||||||
|
(`{model, prompt, stream:false}`), cap + chunk the response (§1.4), and emit each
|
||||||
|
chunk back to the sender via `send_typed_wire(contact_id, …, "assist_response", …)`.
|
||||||
|
Also store via the existing `store_typed_message` path so it lands in history,
|
||||||
|
and emit `MeshEvent::AssistResponseReady`.
|
||||||
|
|
||||||
|
### 1.3 Trust gate (`sender_is_allowed`)
|
||||||
|
Reuse the federation trust list — no new store:
|
||||||
|
```rust
|
||||||
|
let nodes = federation::load_nodes(&data_dir).await.unwrap_or_default();
|
||||||
|
let peer = state.peers.read().await.get(&sender_contact_id).cloned();
|
||||||
|
let trusted = peer.and_then(|p| nodes.iter().find(|n|
|
||||||
|
Some(&n.pubkey) == p.pubkey_hex.as_ref() || Some(&n.did) == p.did.as_ref())
|
||||||
|
.map(|n| n.trust_level == TrustLevel::Trusted)).unwrap_or(false);
|
||||||
|
```
|
||||||
|
Plus honour `ContactEntry.blocked`. Config picks the policy:
|
||||||
|
**trusted-only** (default) | **specific contacts** | **anyone on channel** (opt-in).
|
||||||
|
|
||||||
|
### 1.4 Airtime discipline (meshcore reality)
|
||||||
|
Frames are ≤160 B and reassembly is automatic, but bandwidth is tiny. So:
|
||||||
|
- **Cap** the reply (default ~480 chars / ≤3 `AssistResponse` chunks); append
|
||||||
|
`…(truncated — reply '!more')` and keep the tail server-side for a `!more`.
|
||||||
|
- **Rate-limit**: one in-flight query per sender; drop/deny extras.
|
||||||
|
- **Timeout** the Ollama call (e.g. 60 s) and reply with a short error on failure
|
||||||
|
(`MeshEvent::AssistResponseReady { error }`).
|
||||||
|
|
||||||
|
### 1.5 Channel command parser
|
||||||
|
The killer entry point is a plain channel message, not a typed one. In the
|
||||||
|
inbound **`Text`** path, when a channel-0/1 message starts with the trigger
|
||||||
|
(default `!ai ` / `!ask `), synthesise an `AssistQuery` from the remainder and
|
||||||
|
run the same gated `run_assist`. This means **any meshcore client** (even a bare
|
||||||
|
Meshtastic-style sender) can ask, while typed `AssistQuery` is the rich path our
|
||||||
|
own UI uses. Trigger + enable are config.
|
||||||
|
|
||||||
|
### 1.6 UI events (`types.rs`)
|
||||||
|
```rust
|
||||||
|
AssistQueryReceived { from_contact_id: u32, prompt: String },
|
||||||
|
AssistResponseReady { req_id: u64, to_contact_id: u32, error: Option<String> },
|
||||||
|
ScheduledMessageFired { message_id: u64 }, // for Phase 1.7
|
||||||
|
```
|
||||||
|
Subscribers already flow through the single `event_tx` broadcast — no extra
|
||||||
|
wiring.
|
||||||
|
|
||||||
|
### 1.7 Scheduled / queued messaging
|
||||||
|
A small `AssistScheduler` owned by `MeshService` (sits beside `relay_tracker` /
|
||||||
|
`dead_man_switch` in `mod.rs`):
|
||||||
|
- Persisted queue `{ id, contact_id|channel, wire, fire_at, attempts }` under
|
||||||
|
`data_dir/mesh/scheduled.json`.
|
||||||
|
- A tokio task wakes at the earliest `fire_at`, sends via the normal
|
||||||
|
`send_typed_wire` / `MeshCommand::SendText` path, emits `ScheduledMessageFired`.
|
||||||
|
- **Offline queue**: on send failure (peer unreachable) keep the item and retry
|
||||||
|
when a `PeerDiscovered` / `PeerUpdated` event names that peer.
|
||||||
|
- RPC: `mesh.schedule-message { contact_id|channel, body, fire_at }`,
|
||||||
|
`mesh.list-scheduled`, `mesh.cancel-scheduled`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — killer Mesh-tab UX (ties into `project_mesh_telegram_plan`)
|
||||||
|
|
||||||
|
**Onboarding (one screen, three steps):**
|
||||||
|
1. *Model* — detect Ollama on :11434. If absent, a single "Install AI (Ollama)"
|
||||||
|
button deep-links to the App Store entry; if present, pick the model
|
||||||
|
(default the one already pulled).
|
||||||
|
2. *Who can ask* — Trusted nodes only (default) · Pick contacts · Anyone on the
|
||||||
|
mesh channel (with a clear "uses your node's compute / airtime" warning).
|
||||||
|
3. *Trigger word* — default `!ai`; toggle the whole feature on.
|
||||||
|
|
||||||
|
**Usage (Mesh tab):**
|
||||||
|
- An **Assistant** card: on/off, model, policy, trigger; live feed driven by
|
||||||
|
`AssistQueryReceived` / `AssistResponseReady`.
|
||||||
|
- Composer gains two actions: **Ask the mesh AI** (sends a typed `AssistQuery`)
|
||||||
|
and **Send later** (date/time → `mesh.schedule-message`), with a "Scheduled"
|
||||||
|
list (`mesh.list-scheduled`, cancel).
|
||||||
|
|
||||||
|
The 1–2 killer actions: *ask the island's AI from any radio*, and *queue a
|
||||||
|
message that sends itself when a peer comes back in range.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
Needs **2 radios** (the .116 meshcore + a second) + Ollama running on the
|
||||||
|
answering node:
|
||||||
|
1. From radio B send `!ai what's the block height?` → node A (trusted) answers on
|
||||||
|
the channel; untrusted B is silently denied.
|
||||||
|
2. Typed `AssistQuery` from our UI → chunked `AssistResponse` renders in the feed.
|
||||||
|
3. Long reply → truncation + `!more` continues.
|
||||||
|
4. Schedule a message to an out-of-range peer → it fires when the peer reappears.
|
||||||
|
|
||||||
|
## Effort & order
|
||||||
|
Multi-day. Land in this order so each step is testable alone:
|
||||||
|
1.1 enum + payloads → 1.2/1.3/1.4 gated bridge → 1.5 channel trigger →
|
||||||
|
1.6 events → 1.7 scheduler → Phase 2 UI. Phases 1.1–1.4 are the minimum
|
||||||
|
demoable slice (ask over the mesh, get an answer).
|
||||||
@ -1378,6 +1378,7 @@ ${ELECTRUMX_IMAGE} electrumx.tar
|
|||||||
${MARIADB_IMAGE} mariadb-mempool.tar
|
${MARIADB_IMAGE} mariadb-mempool.tar
|
||||||
${FEDIMINT_IMAGE} fedimint.tar
|
${FEDIMINT_IMAGE} fedimint.tar
|
||||||
${FEDIMINT_GATEWAY_IMAGE} fedimint-gateway.tar
|
${FEDIMINT_GATEWAY_IMAGE} fedimint-gateway.tar
|
||||||
|
${FMCD_IMAGE} fmcd.tar
|
||||||
${FILEBROWSER_IMAGE} filebrowser.tar
|
${FILEBROWSER_IMAGE} filebrowser.tar
|
||||||
${ALPINE_TOR_IMAGE} alpine-tor.tar
|
${ALPINE_TOR_IMAGE} alpine-tor.tar
|
||||||
${NGINX_ALPINE_IMAGE} nginx-alpine.tar
|
${NGINX_ALPINE_IMAGE} nginx-alpine.tar
|
||||||
|
|||||||
@ -172,6 +172,31 @@ FBEOF
|
|||||||
chown -R 1000:1000 /var/lib/archipelago/secrets
|
chown -R 1000:1000 /var/lib/archipelago/secrets
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Create Fedimint Client (fmcd) alongside FileBrowser so ecash / networking
|
||||||
|
# sats work out of the box even on unbundled images. Image is pulled (not
|
||||||
|
# pre-loaded). Resilient entrypoint retries on join failure → never crash-loops.
|
||||||
|
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^fedimint-clientd$'; then
|
||||||
|
log "Creating Fedimint Client (fmcd)..."
|
||||||
|
mkdir -p /var/lib/archipelago/fmcd
|
||||||
|
FMCD_PW_FILE=/var/lib/archipelago/fmcd/password
|
||||||
|
[ -s "$FMCD_PW_FILE" ] || head -c 24 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' > "$FMCD_PW_FILE"
|
||||||
|
FMCD_PW="$(cat "$FMCD_PW_FILE")"
|
||||||
|
FMCD_DEFAULT_INVITE="fed11qgqyj3mfwfhksw309uuxywtxxfjrjc35xuexverpxdsnxcnrxucxvenzveskgc3kvvun2c34xp3k2ep38yunzdpexcekxe3hvd3rvvmx8pnrvdenx5mnzvtzqqqjqt0t6pc3s5z0ynqjw9s4njf6svwgu59kweawc0vvrddcjeemw6yyn4pcdp"
|
||||||
|
pull_with_fallback "${FMCD_IMAGE}"
|
||||||
|
$DOCKER run -d --name fedimint-clientd --restart unless-stopped \
|
||||||
|
--network archy-net \
|
||||||
|
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||||
|
--security-opt no-new-privileges:true \
|
||||||
|
--health-cmd='curl -sf http://localhost:8080/health || exit 1' \
|
||||||
|
--health-interval=120s --health-timeout=10s --health-retries=3 \
|
||||||
|
--memory=256m \
|
||||||
|
-p 8178:8080 \
|
||||||
|
-v /var/lib/archipelago/fmcd:/data \
|
||||||
|
-e FMCD_ADDR=0.0.0.0:8080 -e FMCD_MODE=rest -e FMCD_DATA_DIR=/data \
|
||||||
|
-e FMCD_PASSWORD="$FMCD_PW" -e FMCD_INVITE_CODE="$FMCD_DEFAULT_INVITE" \
|
||||||
|
${FMCD_IMAGE} 2>>"$LOG" && log " fmcd created" || log " WARNING: fmcd creation failed"
|
||||||
|
fi
|
||||||
|
|
||||||
# Generate WireGuard keys for standalone VPN (archipelago-wg service)
|
# Generate WireGuard keys for standalone VPN (archipelago-wg service)
|
||||||
WG_DIR="/var/lib/archipelago/wireguard"
|
WG_DIR="/var/lib/archipelago/wireguard"
|
||||||
if [ ! -f "$WG_DIR/private.key" ]; then
|
if [ ! -f "$WG_DIR/private.key" ]; then
|
||||||
@ -532,6 +557,7 @@ mem_limit() {
|
|||||||
homeassistant) echo "512m";;
|
homeassistant) echo "512m";;
|
||||||
fedimint) echo "512m";;
|
fedimint) echo "512m";;
|
||||||
fedimint-gateway) echo "512m";;
|
fedimint-gateway) echo "512m";;
|
||||||
|
fedimint-clientd) echo "256m";;
|
||||||
photoprism) $LOW_MEM && echo "512m" || echo "1g";;
|
photoprism) $LOW_MEM && echo "512m" || echo "1g";;
|
||||||
mempool-api) echo "512m";;
|
mempool-api) echo "512m";;
|
||||||
jellyfin) echo "1g";;
|
jellyfin) echo "1g";;
|
||||||
@ -560,7 +586,7 @@ MISSING_IMAGES=""
|
|||||||
for img_var in BITCOIN_KNOTS_IMAGE MARIADB_IMAGE ELECTRUMX_IMAGE \
|
for img_var in BITCOIN_KNOTS_IMAGE MARIADB_IMAGE ELECTRUMX_IMAGE \
|
||||||
MEMPOOL_BACKEND_IMAGE MEMPOOL_WEB_IMAGE BTCPAY_POSTGRES_IMAGE \
|
MEMPOOL_BACKEND_IMAGE MEMPOOL_WEB_IMAGE BTCPAY_POSTGRES_IMAGE \
|
||||||
NBXPLORER_IMAGE BTCPAY_IMAGE LND_IMAGE FEDIMINT_IMAGE \
|
NBXPLORER_IMAGE BTCPAY_IMAGE LND_IMAGE FEDIMINT_IMAGE \
|
||||||
FEDIMINT_GATEWAY_IMAGE HOMEASSISTANT_IMAGE GRAFANA_IMAGE \
|
FEDIMINT_GATEWAY_IMAGE FMCD_IMAGE HOMEASSISTANT_IMAGE GRAFANA_IMAGE \
|
||||||
UPTIME_KUMA_IMAGE JELLYFIN_IMAGE VAULTWARDEN_IMAGE \
|
UPTIME_KUMA_IMAGE JELLYFIN_IMAGE VAULTWARDEN_IMAGE \
|
||||||
NEXTCLOUD_IMAGE SEARXNG_IMAGE FILEBROWSER_IMAGE; do
|
NEXTCLOUD_IMAGE SEARXNG_IMAGE FILEBROWSER_IMAGE; do
|
||||||
img="${!img_var}"
|
img="${!img_var}"
|
||||||
@ -1019,6 +1045,31 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
|
|||||||
fi
|
fi
|
||||||
track_container "fedimint-gateway"
|
track_container "fedimint-gateway"
|
||||||
|
|
||||||
|
# 5c. Fedimint Client (fmcd) — ecash client daemon; auto-joins the default
|
||||||
|
# federation. The image's resilient entrypoint retries on join failure (fmcd
|
||||||
|
# needs >=1 federation to boot), so an unreachable default never crash-loops the
|
||||||
|
# node. The archipelago wallet bridge reads the password from
|
||||||
|
# /var/lib/archipelago/fmcd/password and talks to it on host port 8178.
|
||||||
|
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q '^fedimint-clientd$'; then
|
||||||
|
log "Creating Fedimint Client (fmcd)..."
|
||||||
|
mkdir -p /var/lib/archipelago/fmcd
|
||||||
|
FMCD_PW_FILE=/var/lib/archipelago/fmcd/password
|
||||||
|
[ -s "$FMCD_PW_FILE" ] || head -c 24 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' > "$FMCD_PW_FILE"
|
||||||
|
FMCD_PW="$(cat "$FMCD_PW_FILE")"
|
||||||
|
FMCD_DEFAULT_INVITE="fed11qgqyj3mfwfhksw309uuxywtxxfjrjc35xuexverpxdsnxcnrxucxvenzveskgc3kvvun2c34xp3k2ep38yunzdpexcekxe3hvd3rvvmx8pnrvdenx5mnzvtzqqqjqt0t6pc3s5z0ynqjw9s4njf6svwgu59kweawc0vvrddcjeemw6yyn4pcdp"
|
||||||
|
$DOCKER run -d --name fedimint-clientd --restart unless-stopped \
|
||||||
|
--health-cmd="curl -sf http://localhost:8080/health || exit 1" --health-interval=120s --health-timeout=10s --health-retries=3 \
|
||||||
|
--memory=$(mem_limit fedimint-clientd) --network archy-net --network-alias fedimint-clientd \
|
||||||
|
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||||
|
--security-opt no-new-privileges:true \
|
||||||
|
-p 8178:8080 \
|
||||||
|
-v /var/lib/archipelago/fmcd:/data \
|
||||||
|
-e FMCD_ADDR=0.0.0.0:8080 -e FMCD_MODE=rest -e FMCD_DATA_DIR=/data \
|
||||||
|
-e FMCD_PASSWORD="$FMCD_PW" -e FMCD_INVITE_CODE="$FMCD_DEFAULT_INVITE" \
|
||||||
|
"$FMCD_IMAGE" 2>>"$LOG" || true
|
||||||
|
fi
|
||||||
|
track_container "fedimint-clientd"
|
||||||
|
|
||||||
# (Bitcoin-dependent containers created above regardless of BITCOIN_READY)
|
# (Bitcoin-dependent containers created above regardless of BITCOIN_READY)
|
||||||
|
|
||||||
# ── Tier 3: Applications (independent — always attempt) ───────────────────
|
# ── Tier 3: Applications (independent — always attempt) ───────────────────
|
||||||
|
|||||||
@ -57,6 +57,11 @@ ADGUARDHOME_IMAGE="$ARCHY_REGISTRY/adguardhome:v0.107.55"
|
|||||||
# Fedimint
|
# Fedimint
|
||||||
FEDIMINT_IMAGE="$ARCHY_REGISTRY/fedimintd:v0.10.0"
|
FEDIMINT_IMAGE="$ARCHY_REGISTRY/fedimintd:v0.10.0"
|
||||||
FEDIMINT_GATEWAY_IMAGE="$ARCHY_REGISTRY/gatewayd:v0.10.0"
|
FEDIMINT_GATEWAY_IMAGE="$ARCHY_REGISTRY/gatewayd:v0.10.0"
|
||||||
|
# fmcd = Fedimint client daemon (iroh-capable, fedimint-client 0.8.2). Built
|
||||||
|
# from minmoto/fmcd. NOT yet added to the bundled CONTAINER_IMAGES list / first-
|
||||||
|
# boot auto-create: bundling fleet-wide needs a fleet-reachable default
|
||||||
|
# federation first (the interim default is node-local). See docs/dual-ecash-design.md.
|
||||||
|
FMCD_IMAGE="$ARCHY_REGISTRY/fmcd:0.8.0"
|
||||||
|
|
||||||
# Media
|
# Media
|
||||||
REDIS_IMAGE="$ARCHY_REGISTRY/redis:7.4.8"
|
REDIS_IMAGE="$ARCHY_REGISTRY/redis:7.4.8"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user