Skip to content

feat(cashu): C1a — detect the node's escrow mode from its 38385 tags - #230

Merged
grunch merged 2 commits into
mainfrom
feat/cashu-c1-mode-detection
Jul 24, 2026
Merged

feat(cashu): C1a — detect the node's escrow mode from its 38385 tags#230
grunch merged 2 commits into
mainfrom
feat/cashu-c1-mode-detection

Conversation

@grunch

@grunch grunch commented Jul 24, 2026

Copy link
Copy Markdown
Member

Phase C1, first half of docs/cashu/README.md.

Depends on: nothing — works on mostro-core 0.13.1, so it is independent of #229 (C0) and the two can merge in any order. Blocks: C1b, then C5.

Why this is split

C1 as specified covers tag parsing, a persisted developer override, the FRB surface and the Dart About/settings UI. The override needs a generic k/v accessor on the storage trait, which means touching both the sqlite and IndexedDB backends — a different kind of change from tag parsing, and not one that belongs in the same review.

  • C1a (this PR) — Rust detection + wiring. Inert: it learns the mode and stores it; nothing reads it yet.
  • C1b (next) — settings k/v + override, FRB getters + change stream, escrowModeProvider, About section, dev-only toggle, l10n ×5.

Behaviour is unchanged either way. Against every daemon that exists today the mode resolves to Unknown, which keeps all Cashu paths shut.

The design point that matters

EscrowMode is tri-state, mirroring BondPolicy on the Dart side — and the third state earns its place:

state when is_cashu()
Unknown no tag: daemon predates it, or not fetched yet false
Lightning tag absent-but-fetched, "lightning", or anything unrecognised false
Cashu tag is "cashu" true

UnknownLightning: it lets the About screen (C1b) say "not advertised" rather than claim the node confirmed Lightning. And is_cashu() is the only way to ask, answering false for both non-Cashu states — so the gate fails safe by construction, per design principle 1 ("off by default, inert until detected").

An unrecognised backend (say a future fedimint) reads as Lightning deliberately: a node we cannot trade Cashu with is one whose Cashu paths must stay closed.

Wiring

fetch_and_set_powfetch_and_set_node_capabilities, parsing PoW and the escrow tags from the same event. One fetch: they come from the same 38385 event, and a second relay query would double the traffic for no new information.

The mode is also cleared at the start of a node switch, beside the existing order-book clear. The capability re-fetch is a network round trip; without this the old node's mode stays cached until it answers, and that window would carry one node's Cashu mode onto another. Clearing first makes the window read Unknown.

Test plan

  • cargo test124 pass (110 pre-existing unmodified, +14 new)
  • cargo clippy -- -D warnings — clean
  • cargo check --target wasm32-unknown-unknown — clean
  • No rust/src/api/ surface change (the renamed fn is pub(crate)), so no frb-generate.sh run required
  • Zero Dart change; Lightning behaviour identical

The 14 tests cover: a today's-daemon tag set resolving to Unknown; the is_cashu() fail-safe; a full Cashu tag set; explicit lightning; an unrecognised backend; case/whitespace tolerance; a malformed day count not costing us the mint URL; a blank mint URL not counting as one; override precedence both ways; a blank override not erasing the node's value; a corrupted stored override falling back to auto; and the node-switch clear.

⚠️ Pre-existing flaky test (not from this PR)

While verifying I hit api::messages::tests::mark_as_read_updates_count failing intermittently. I checked it on unmodified main: 2 failures in 6 runs (~33%). It is not caused by this change and is not touched here — filed separately so it does not ride along in a Cashu review.

Series

C0 #229 ✅ · C1a (this) · C1b · C2 wallet core (cdk 0.17.3) · C3 wallet UI · C4 escrow primitives · C5 Track A · C6–C8 · C9 wasm · C10 polish

First half of phase C1. Rust-only and inert: it learns what the active node
advertises and stores it. Nothing reads the result yet, so behaviour is
unchanged — the Dart provider, About surface and dev override are C1b.

Split because the override needs a generic k/v accessor on the storage trait,
which means touching both the sqlite and IndexedDB backends. That is a
different kind of change from tag parsing and does not belong in the same
review.

EscrowMode is tri-state like BondPolicy on the Dart side, and the distinction
carries weight: a daemon that predates the tags is Unknown, not Lightning, so
the UI can say "not advertised" instead of claiming the node confirmed
anything. is_cashu() is the only way to ask and answers false for both Unknown
and Lightning, so every Cashu path stays shut unless a node positively said
otherwise. An unrecognised backend reads as Lightning for the same reason: one
we cannot trade Cashu with is one whose Cashu paths must stay closed.

fetch_and_set_pow becomes fetch_and_set_node_capabilities and now parses both
PoW and the escrow tags from the same event — one fetch, since a second relay
query would double the traffic for no new information.

The mode is also cleared at the start of a node switch, beside the order-book
clear. The capability re-fetch is a network round trip, and without this the
old node's mode stays cached until it answers; clearing first makes that
window read as Unknown rather than carrying one node's Cashu mode onto
another.

14 tests over parsing, the fail-safe defaults, override precedence and the
node-switch clear.

Unrelated, found while verifying: api::messages::tests::mark_as_read_updates_count
is flaky on unmodified main — 2 failures in 6 runs. Not touched here; filed
separately.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@grunch, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e0369d5b-18ec-48e3-a0f4-ff828798ca53

📥 Commits

Reviewing files that changed from the base of the PR and between 71061fb and 2a57773.

📒 Files selected for processing (4)
  • rust/src/api/nostr.rs
  • rust/src/api/orders.rs
  • rust/src/mostro/escrow_mode.rs
  • rust/src/mostro/mod.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cashu-c1-mode-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec140bf8b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rust/src/api/nostr.rs
Comment on lines 258 to 260
Err(e) => {
log::warn!("[nostr] failed to fetch Kind 38385 for PoW: {e}");
log::warn!("[nostr] failed to fetch Kind 38385 for node capabilities: {e}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear stale escrow mode on fetch errors

When the active node was previously resolved as Cashu, a later capability refresh that hits this error branch leaves the global RESOLVED cache untouched. For a reconnect where the 38385 fetch times out or otherwise fails, is_cashu_mode() can keep returning true from stale data even though the module treats unfetched/unreachable node info as Unknown, so the Cashu gate does not fail closed until a successful Ok(None) or node-switch clear occurs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed in 2a57773.

The Err branch now calls escrow_mode::clear(). The module documents unreachable as identical to unfetched — both are Unknown — but the code was only honouring that for Ok(None), so a reconnect whose 38385 query timed out kept answering cashu from the previous successful fetch. Clearing makes that window fail closed.

PoW is deliberately left untouched in the same branch: a stale difficulty still gets messages accepted by the daemon, whereas a stale escrow mode opens a path. The comment in the code says so, so the asymmetry does not read as an oversight later.

Comment thread rust/src/mostro/escrow_mode.rs Outdated
@grunch

grunch commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…less nodes

Two P2 findings from the Codex review, both cases where the gate stayed
open when the module's own contract says it must not.

Stale mode after a failed fetch: the Err branch of
fetch_and_set_node_capabilities left RESOLVED untouched, so a reconnect
whose 38385 query timed out kept answering "cashu" from the last
successful fetch. escrow_mode treats unreachable exactly like unfetched,
so clear it here too. PoW is deliberately left alone — a stale
difficulty still gets messages accepted, whereas a stale escrow mode
opens a path.

Cashu without a usable mint: a node advertising escrow_mode=cashu but no
cashu_mint_url produced is_cashu_mode() == true with nothing to connect
to. The global gate now also requires CashuNodeConfig::is_usable(), so
the failure lands at the gate instead of at the first mint call.
EscrowMode::is_cashu() keeps reporting the mode alone — that is what the
About screen (C1b) reads to say "cashu, no mint advertised" rather than
silently showing Lightning. The mint override (§4.3) is what makes a
forced Cashu mode usable against a daemon publishing no mint.

Tests that touch the RESOLVED global now share a mutex, so the new case
does not race the node-switch test.
@grunch
grunch merged commit 1931ac0 into main Jul 24, 2026
4 checks passed
@grunch
grunch deleted the feat/cashu-c1-mode-detection branch July 24, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant