feat(wallet,content,seed): Fedimint dual-ecash, paid content streaming, seed ceremony

- Fedimint ecash alongside Cashu: fedimint-clientd (fmcd) HTTP bridge,
  fedimint_client, fedimint RPC, wallet wiring
- Paid peer content: content invoices + streaming content server + content RPCs
- Seed-phrase ceremony/reveal RPCs and CLI ceremony tool
- LND wallet, mesh status/messaging, app-stack (netbird HTTPS), and
  decoupled-update wiring; Fedimint Client core app in catalog

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-17 19:21:07 -04:00
co-authored by Claude Opus 4.8
parent c10f2ac22e
commit bd567cd165
34 changed files with 2677 additions and 68 deletions
+41 -11
View File
@@ -17,6 +17,10 @@ pub struct IncomingMessage {
/// Sender's node name (for display in group chat).
#[serde(default)]
pub from_name: Option<String>,
/// Sender-assigned unique id for the message. Used to dedup reliably even
/// when a slow-Tor retry/redelivery arrives outside the time window (#31).
#[serde(default)]
pub msg_id: Option<String>,
pub message: String,
pub timestamp: String,
/// "sent" or "received"
@@ -141,18 +145,34 @@ fn persist() {
}
/// Store a received message (called from HTTP handler).
pub fn store_received_sync(from_pubkey: &str, message: &str, from_name: Option<&str>) {
pub fn store_received_sync(
from_pubkey: &str,
message: &str,
from_name: Option<&str>,
msg_id: Option<&str>,
) {
let ts = chrono::Utc::now().to_rfc3339();
let mut guard = store().lock().unwrap_or_else(|e| e.into_inner());
// Deduplication: skip if same pubkey + message within last 30 seconds
let dominated = guard.messages.iter().rev().take(20).any(|m| {
m.from_pubkey == from_pubkey
&& m.message == message
&& m.direction == "received"
&& within_seconds(&m.timestamp, &ts, 30)
});
if dominated {
// Deduplication. When the sender supplied a unique id, dedup on
// (from_pubkey, msg_id) across all retained history — this is robust even
// when a slow-Tor redelivery arrives well outside any time window (#31).
// Older senders send no id; fall back to the legacy same-pubkey+message
// within-30s heuristic.
let duplicate = if let Some(id) = msg_id {
guard
.messages
.iter()
.any(|m| m.from_pubkey == from_pubkey && m.msg_id.as_deref() == Some(id))
} else {
guard.messages.iter().rev().take(20).any(|m| {
m.from_pubkey == from_pubkey
&& m.message == message
&& m.direction == "received"
&& within_seconds(&m.timestamp, &ts, 30)
})
};
if duplicate {
return;
}
@@ -160,6 +180,7 @@ pub fn store_received_sync(from_pubkey: &str, message: &str, from_name: Option<&
from_pubkey: from_pubkey.to_string(),
from_onion: None,
from_name: from_name.map(|s| s.to_string()),
msg_id: msg_id.map(|s| s.to_string()),
message: message.to_string(),
timestamp: ts,
direction: "received".to_string(),
@@ -169,8 +190,13 @@ pub fn store_received_sync(from_pubkey: &str, message: &str, from_name: Option<&
persist();
}
pub async fn store_received(from_pubkey: &str, message: &str, from_name: Option<&str>) {
store_received_sync(from_pubkey, message, from_name);
pub async fn store_received(
from_pubkey: &str,
message: &str,
from_name: Option<&str>,
msg_id: Option<&str>,
) {
store_received_sync(from_pubkey, message, from_name, msg_id);
}
/// Store a sent message (for display in Archipelago channel).
@@ -180,6 +206,7 @@ pub fn store_sent(message: &str) {
from_pubkey: "me".to_string(),
from_onion: None,
from_name: None,
msg_id: None,
message: message.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
direction: "sent".to_string(),
@@ -335,6 +362,9 @@ pub async fn send_to_peer(
"message": payload_message,
"timestamp": chrono::Utc::now().to_rfc3339(),
"encrypted": encrypted,
// Unique per-message id so receivers can dedup reliably even across
// slow-Tor retries/redeliveries (#31). Old receivers ignore it.
"msg_id": uuid::Uuid::new_v4().to_string(),
});
if let Some(name) = from_name {
body["from_name"] = serde_json::Value::String(name.to_string());