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
+57 -6
View File
@@ -117,9 +117,12 @@ fn expire_stale(requests: &mut Vec<PendingPeerRequest>) {
/// or `None` if the request was deduplicated or rate-limited.
///
/// Dedup rule: if the same (from_nostr_pubkey, from_did) already has a
/// `Pending` entry, do not insert a second one — the user will see the
/// existing row and act on that. Otherwise count `Pending` entries per
/// pubkey and reject anything beyond `MAX_PENDING_PER_PUBKEY`.
/// `Pending` OR `Approved` entry, do not insert a second one. Including
/// `Approved` is what stops an already-approved peer from re-spawning a fresh
/// pending row every time their request re-syncs (the reported "approve, Poll
/// Now, see approved + a new pending" loop). `Rejected` is intentionally NOT
/// matched so a previously-rejected peer can still ask again later. Otherwise
/// count `Pending` entries per pubkey and reject beyond `MAX_PENDING_PER_PUBKEY`.
pub async fn insert_inbound(
data_dir: &Path,
from_nostr_pubkey: String,
@@ -131,13 +134,13 @@ pub async fn insert_inbound(
let mut requests = load_pending(data_dir).await?;
expire_stale(&mut requests);
let already_pending = requests.iter().any(|r| {
let already_handled = requests.iter().any(|r| {
r.from_nostr_pubkey == from_nostr_pubkey
&& r.from_did == from_did
&& matches!(r.state, PendingState::Pending)
&& matches!(r.state, PendingState::Pending | PendingState::Approved)
&& !r.outbound
});
if already_pending {
if already_handled {
save_pending(data_dir, &requests).await?;
return Ok(None);
}
@@ -271,6 +274,54 @@ mod tests {
assert!(r2.is_none(), "duplicate Pending request should be ignored");
}
#[tokio::test]
async fn test_approved_request_does_not_respawn_pending() {
// Regression for the "approve → Poll Now → approved + a fresh pending"
// loop: once a request is Approved, a re-synced inbound for the same
// peer must NOT create a new Pending row.
let dir = tempfile::tempdir().unwrap();
let r1 = insert_inbound(
dir.path(),
"npk1".into(),
"npub1".into(),
"did:key:zABC".into(),
None,
None,
)
.await
.unwrap()
.expect("first insert stored");
set_state(dir.path(), &r1.id, PendingState::Approved)
.await
.unwrap();
let r2 = insert_inbound(
dir.path(),
"npk1".into(),
"npub1".into(),
"did:key:zABC".into(),
None,
None,
)
.await
.unwrap();
assert!(
r2.is_none(),
"an already-approved peer must not re-spawn a pending request"
);
let pending = load_pending(dir.path()).await.unwrap();
assert_eq!(
pending
.iter()
.filter(|r| matches!(r.state, PendingState::Pending))
.count(),
0,
"no Pending rows should remain after approval + re-sync"
);
}
#[tokio::test]
async fn test_rate_limit() {
let dir = tempfile::tempdir().unwrap();