feat(cashu): C3 — minimal wallet UI (balance, receive, export) - #237
feat(cashu): C3 — minimal wallet UI (balance, receive, export)#237grunch wants to merge 11 commits into
Conversation
…end in About Closes the C1 phase of docs/cashu/README.md. Behaviour on a Lightning node is unchanged: with no `escrow_mode` tag the resolution stays Unknown, every Cashu path stays shut, and the About screen renders exactly what it did before. Rust - `Storage` gains a generic k/v accessor (`get_setting`/`set_setting`/ `delete_setting`) over the `settings` table that already existed. SQLite implements it for real and the two named active-node accessors become thin wrappers over it; IndexedDB follows its existing stub contract, so on web the overrides apply for the session only (tracked in #233). - `escrow_mode` now stores what the node advertised and the overrides separately, and resolves on read. Flipping an override therefore takes effect immediately instead of waiting for the next relay fetch, and there is no second copy of the answer that can go stale. A node switch clears the tags but keeps the overrides — forcing Cashu is a statement about this build, not about a node. Every mutator emits on a broadcast channel. - New `api/escrow.rs`: `get_escrow_mode`, `set_escrow_mode_override`, `set_cashu_mint_url_override`, `rehydrate_escrow_overrides` and an `on_escrow_mode_changed` stream. Mint URLs are validated as http(s) with a host, on write and again on rehydrate. `EscrowModeInfo.mode` is a stable marker; Dart maps it to a localized string. Dart - `MostroInstance.fromTags` parses `escrow_mode` and the three `cashu_*` tags, tri-state and parameter-gated exactly like `BondPolicy`. - About reports what the node advertises, so the backends are mutually exclusive on screen: a Cashu node gets a "Cashu escrow" section instead of the Lightning Network one. The mint renders in full rather than through the copyable row, whose 20-character abbreviation would hide the host. - `escrowModeProvider` / `isCashuAvailableProvider` expose the resolution that gates behaviour; the override controls live in a `kDebugMode`-only settings card that shows the effective resolution next to the toggle. - Strings translated in all five locales. Tests - Rust: k/v round-trip, override re-resolution without a fetch, node switch keeps the override, every mutator notifies, mint-URL rejection leaves the previous value in place. - Dart: tag parsing incl. gating and malformed values; widget tests asserting no trace of Cashu on a Lightning or silent node, and no Lightning section on a Cashu one.
…stence' into feat/cashu-c3-wallet-ui # Conflicts: # rust/src/api/types.rs # rust/src/frb_generated.rs
Phase C3 of docs/cashu/README.md. Deliberately minimal: the wallet exists to fund and drain escrows against the node's mint, not to be a general Cashu wallet. Melt/mint to Lightning, multiple mints and backup UX are later phases. Invisible unless the node runs Cashu: the Settings entry is gated on `isCashuAvailableProvider`, which needs the active node to have advertised Cashu *and* a usable mint. On a Lightning node the feature does not exist as far as the user is concerned — and an entry point leading to a permanently empty wallet would be worse than none. - `lib/features/cashu/providers/cashu_wallet_provider.dart` — a stream over the C2 status broadcast plus a thin command object. Every method is one Rust call; no crypto, no mint traffic and no gating logic in Dart. - `lib/features/cashu/screens/cashu_wallet_screen.dart` — balance, mint, receive (QR scan on device, paste on web, reusing PlatformAwareQrScanner), export (amount dialog bounded by the balance, then QR + copyable token), and a proof-state check for tokens nobody redeemed. - Route + gated Settings entry; strings in all five locales. Two details worth their code: - Rust markers are mapped to localized messages and an unknown marker falls back to a generic one, so an internal string can never surface to a user. - The exported token carries a warning that it is bearer money. A user who reads it as a receipt and sends it twice loses the funds. Tests: balance and mint rendered when connected; the disconnected state stated rather than shown as an empty wallet; sending disabled with a zero balance and enabled with funds; a known marker localized and an unknown one falling back, with neither leaking the raw string. Stacked: this branch also carries C1b (#234) and C2 (#235), whose providers and bridge it uses.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (35)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Review — C3 (strict pass)
Scope reviewed: 🔴 Major1. An exported token is shown exactly once, in a dismissible dialog —
Three things, in order of value:
2. No The scanner's raw output goes straight to 3. The balance renders an unknown value as
🟡 Minor4. Amounts are unformatted. 5. 6. No 🔵 Nit7. 8. ✅ What is right
Test gaps
|
… write Addresses the strict review on #234. Major - `set_escrow_mode_override` / `set_cashu_mint_url_override` read the overrides and wrote them back as a whole struct, so two writes from the same screen interleaved and the second silently discarded the first. Both now go through `escrow_mode::update_overrides`, which mutates under one write lock. Covered by `setting_one_override_never_clobbers_the_other`. - The dev card assigned `TextEditingController.text` during `build()`, which notifies listeners in the middle of laying the field out and also wiped in-progress typing on any unrelated escrow event. Seeding now happens in a post-frame callback and updates in `ref.listen`. Minor - `is_overridden` was true whenever the override was on, including when the node genuinely advertises Cashu — so the dev card labelled a real Cashu node as forced, which is the confusion the flag exists to prevent. Now `forcing && from_tags != Cashu`. - The broadcast carried a `ResolvedEscrowMode` that the stream discarded and rebuilt, resolving twice per change. The channel is now `Sender<()>`: a bare wake-up, with subscribers reading the globals as before. - Every mutator emitted unconditionally, so a node whose capability fetch keeps failing produced a stream of identical "still unknown" events. Each now compares before/after and only notifies on a real change. - `indexeddb::delete_setting` returned Ok silently while its siblings warned. A silent no-op in a storage layer reads like working storage in a log. Tests - `overrides_survive_a_restart_through_the_settings_store` — the persist and rehydrate halves end to end against a real store, which nothing covered. - `a_change_that_changes_nothing_does_not_wake_subscribers`. - `forcing_cashu_on_a_cashu_node_is_not_flagged_as_an_override`.
Addresses the strict review on #237. Major - The exported token was shown once in a dismissible dialog. Tapping outside it — the fastest gesture on the screen — lost the only copy, with the proofs already reserved and the balance already down. The dialog is now non-dismissible, the token is kept for the session, and a reminder offers "show it again" until the user says they have sent it. - The balance rendered an unknown value as `0`. Following C2's `Option<u64>`, it now renders "—", and sending is disabled while it is unknown rather than offering a send we cannot size. - `cashu:` / `cashu://` scanner payloads are handled in C2's `normalize_token`, so the scanner's raw output works as pasted. Minor - Amounts are grouped (`1,234 Satoshis`), matching the About screen. - `_busy` is checked before opening the receive sheet and the amount dialog, not only around the call that follows them. - The marker→message chain moved to `cashu_error_messages.dart`. It was already duplicated once and C5 would have made three copies. Also fixes a genuine layout bug the new test surfaced: `AlertDialog` asks its content for intrinsic dimensions and `QrImageView` lays out through a `LayoutBuilder`, which cannot answer. The token dialog now has a bounded width; without it the dialog throws on a narrow screen. Tests: unknown balance rendered as unknown; send disabled while unknown; and the export flow end to end — dialog, dismissal, and the token still reachable afterwards.
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
… doc CodeRabbit raised these against #234, where the files live. They had been fixed one branch too far down the stack (on the C5 branch) and so never reached this PR; applied here instead, and the stack will carry them forward. - `MostroInstance.parseEscrowMode` used `getOptional`, so a *present but blank* `escrow_mode` tag read as `unknown` while Rust's `parse_tags` reads the same event as `Lightning`. Two parsers, one event, different answers — About and the Cashu gate could disagree about the same daemon. Only an absent (or value-less) tag is unknown now. - The dev card's post-frame seed captured `mintUrlOverride` during build. An override arriving in that gap is applied by `ref.listen` first, and the captured copy then overwrote it with the older value. The callback reads the current provider value instead. - The C1 "Done when" still said flipping the override flips "the About section", contradicting the bullet directly above it. About reports what the node advertised; the override moves the resolved mode and the dev card's effective state, nothing else. Two more from the same pass, not raised inline but the same class: - `api::escrow::snapshot` derived `mode` from one `get_resolved()` and `is_cashu_available` from `is_cashu_mode()`, which reads the globals again; a node switch between the two produced a snapshot whose mode and gate disagreed. `ResolvedEscrowMode::is_cashu_usable()` expresses the gate against a value the caller already holds. - `set_from_tags` logged unconditionally while only notifying on a change, so a reconnect that confirmed what we already knew still wrote an info line. Tests: blank / whitespace / value-less / absent `escrow_mode` pinned against the Rust behaviour, and a widget suite for the dev card covering seeding, a newer override winning, and in-progress typing surviving a no-op event.
|
@coderabbitai Review |
✅ Action performedReview finished.
|
codaMW
left a comment
There was a problem hiding this comment.
Reviewed C3, code pass over lib/features/cashu/**, the Settings gating, and the
widget tests, plus a full hands-on run against a local nutshell mint with the C1b
dev override. Approving.
The gate verified by hand (the headline property):
- Lightning / no override: no Cashu entry in Settings.
- Force Cashu on, no mint: entry stays hidden, "Cashu cannot run without a mint"
the gate holds onis_cashu_usable, notmode == cashu. - Force Cashu +
http://localhost:3338: the "Cashu wallet" entry appears; toggle
off and it's gone.
The money path verified live against a real mint:
- Received a real 16-sat token -> balance rose to 15 (the mint's 1-sat swap fee,
matching the note in the PR body, good that it's not hidden). - Sent 2 sats -> exported the token -> redeemed it in an independent CLI wallet
(cashu receive), which credited 1 sat after the fee. A real bearer-token
round-trip out of the app and back into another wallet. - The export dialog carries the "anyone who redeems this keeps the funds" warning
and offers explicit Copy / Done rather than relying on dismissal.
Your three Majors verified (live + code + tests):
- Token-loss
_TokenDialogisbarrierDismissible: false, and_lastTokenkeeps
the token re-showable via the "show it again" reminder (visible on the wallet
screen after an export). Covered byan exported token stays retrievable until dismissed. cashu:prefix C3 passes the raw string to Rust'sreceiveToken, which
normalizes it (C2) the right layer.- Unknown balance
_BalanceCardrenders " " whenbalanceSats == null, never
"0 Satoshis";_fmtSatsformats larger amounts. Covered byan unreadable balance renders as unknown, never as zero.
One finding, non-blocking, pre-existing: on Linux desktop, tapping Receive dims
the screen and does nothing. PlatformAwareQrScanner routes every non-web platform
to MobileScanner, but mobile_scanner has no Linux/Windows plugin (confirmed
it's absent from the Linux plugin registrant), so it throws an unhandled
MissingPluginException instead of falling back to paste. It's shared code C3
didn't introduce and Cashu's real target is mobile, but C3 is the first screen to
hit it on desktop, and the failure is a silent dim rather than a graceful "paste
instead". Worth extending the paste fallback to desktop (kIsWeb || isDesktop) or
handling the missing plugin. (I verified the receive/send/export flow above by
locally enabling the paste fallback on Linux reverted, not part of any commit.)
cargo test 169 passed / flutter analyze clean. (The escrow_mode_dev_card
ListTile assertions locally are the known Flutter 3.44-vs-CI-3.38.2 gap from C1b,
not C3.)
Phase C3 of
docs/cashu/README.md— the user-facing half of the embeddedwallet.
Invisible unless the node runs Cashu. The Settings entry is gated on
isCashuAvailableProvider— the node must have advertised Cashu and a usablemint. On a Lightning node the feature does not exist as far as the user is
concerned, and an entry point leading to a permanently empty wallet would be
worse than no entry point.
Scope
Balance, redeem a token, export a token, and a proof-state check. That is all it
should be: the wallet funds and drains escrows against the node's mint. Melting
to Lightning, multiple mints and backup UX are later phases (C10), and a
"general Cashu wallet" is an explicit non-goal of the plan.
cashu_wallet_provider.dart— a stream over C2's status broadcast plus athin command object. Each method is a single Rust call: no crypto, no mint
traffic and no gating logic on the Dart side.
cashu_wallet_screen.dart— balance and mint; receive viaPlatformAwareQrScanner(camera on device, paste on web, already in therepo); export via an amount dialog bounded by the balance, then a QR plus
copyable token; and a "check for unredeemed tokens" action that reclaims
proofs left reserved by an interrupted send.
Two details worth their code:
back to a generic one — an internal string can never surface to a user.
reads it as a receipt and sends it twice loses the funds.
Tests
test/features/cashu/screens/cashu_wallet_screen_test.dart, with the bridgefaked so nothing calls Rust:
the raw string.
Verification
Test plan
paste a faucet token, see the balance rise, export part of it, redeem the
exported token in another wallet.
the reserved amount.
the wallet as unavailable rather than crashing (storage is stubbed, Web: IndexedDB storage backend is a stub — nothing persists across a reload #233).
Manual verification
This is the first PR in the series a reviewer can actually use: with a local
mint and the C1b developer override, the whole wallet is reachable by hand.
Already run on this branch
A · Setup — a mint and a token to paste
The image ships a wallet CLI, so you can mint sats and produce a real token to
paste into the app — no second wallet app needed:
Both commands are verified working against nutshell 0.20.3.
B · Turn the feature on (debug build)
flutter run -d linux(or your device).http://localhost:3338and apply.Effective mint: http://localhost:3338, with no red warning line.
— that is the gate, and it is the single most important thing in this PR.
C · The wallet
0 Satoshis, mint shown, no error.cashuB…token from step A (the scanner sheet takespasted text on desktop/web, camera on mobile). Expect "Received 16 sats"
and the balance to update.
cashu:— it must work identically.That prefix is what QR payloads normally carry.
message, never a raw marker like
CashuReceiveFailed."You only have N sats". Enter
8→ a QR plus the token text appears.fastest way to lose an exported token. Close it with Done.
Show it again. Tap it — the same token comes back. This is what makes an
accidental dismissal survivable.
reclaim".
D · Lightning regression — the part that must not move
About. Everything must behave as on
main.or hot-reload) → the screen reports the wallet as not connected and every
action refuses. It never half-works.
Notes
proof store is a stub there (Web: IndexedDB storage backend is a stub — nothing persists across a reload #233). That is expected in this PR, not a bug.
—rather than0when it cannot be read. To see it, stopthe mint container mid-session and reopen the wallet.