feat(wallet): wire fmcd as core app + dual-ecash receive

Fedimint never appeared in Wallet > Settings > Fedimint because the
fmcd (fedimint-clientd) sidecar was never installed: ensure_default_
federation() needs the fmcd password to reach the daemon, found none,
and silently no-oped, leaving the registry empty.

- prod_orchestrator: add fedimint-clientd to the baseline auto-install
  set so it self-heals onto every node and auto-joins the default
  federation; generate the fmcd-password secret before secret_env
  resolves.
- fedimint_client: ensure_fmcd_password (random hex, 0600) shared with
  the container's secret_env; from_node reads the same secret (legacy
  fmcd/password kept as fallback); reissue_into_any redeems received
  notes into the first joined federation that accepts them.
- wallet.ecash-receive: dual-token — cashu* tokens redeem at the mint,
  anything else is reissued via fmcd; returns the kind + federation_id.
- UI: receive box advertises "Cashu or Fedimint" and reports which kind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-19 09:52:26 -04:00
co-authored by Claude Opus 4.8
parent 298595069d
commit c4855526fe
4 changed files with 119 additions and 14 deletions
+25 -2
View File
@@ -1,7 +1,14 @@
use super::RpcHandler;
use crate::wallet::{ecash, profits};
use crate::wallet::{ecash, fedimint_client, profits};
use anyhow::Result;
/// A Cashu token (NUT-00 `cashuA`/`cashuB`, or our legacy `cashuSend_` form)
/// always starts with `cashu`. Fedimint ecash notes never do, so a non-`cashu`
/// string is routed to the Fedimint reissue path.
fn is_cashu_token(token: &str) -> bool {
token.trim_start().starts_with("cashu")
}
impl RpcHandler {
pub(super) async fn handle_wallet_ecash_balance(&self) -> Result<serde_json::Value> {
let wallet = ecash::load_wallet(&self.config.data_dir).await?;
@@ -129,11 +136,27 @@ impl RpcHandler {
let token = params
.get("token")
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| anyhow::anyhow!("Missing token"))?;
let amount = ecash::receive_token(&self.config.data_dir, token).await?;
// Dual-ecash: one "Receive ecash" box accepts either a Cashu token
// (redeemed at the mint) or Fedimint notes (reissued via the fmcd
// sidecar). Detect by prefix and route accordingly.
if is_cashu_token(token) {
let amount = ecash::receive_token(&self.config.data_dir, token).await?;
return Ok(serde_json::json!({
"received_sats": amount,
"kind": "cashu",
}));
}
let (amount, federation_id) =
fedimint_client::reissue_into_any(&self.config.data_dir, token).await?;
Ok(serde_json::json!({
"received_sats": amount,
"kind": "fedimint",
"federation_id": federation_id,
}))
}