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:
co-authored by
Claude Opus 4.8
parent
c10f2ac22e
commit
bd567cd165
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user