feat: cashu boot + run_cashu + dispatch seam — Cashu foundation CF-5#828
feat: cashu boot + run_cashu + dispatch seam — Cashu foundation CF-5#828grunch wants to merge 2 commits into
Conversation
The integration PR of the Cashu foundation (docs/cashu/01-fundamentals.md §6). A node with `[cashu] enabled = true` now boots with NO Lightning node, connects its configured mint, and runs a Cashu event loop in which every trade action is rejected with `CantDo(InvalidAction)` — the feature tracks replace those arms one at a time. With Cashu disabled the daemon is behaviourally identical to `main`. - `main.rs`: branch on `Settings::is_cashu_enabled()`. Cashu mode skips `LndConnector::new()` and the LN status probe, connects the mint via `CashuClient::connect` (exit(1) if unreachable, mirroring LND-refusal), attaches it to the context, and returns `run_cashu(ctx)`. The spam-gate warm-up is extracted into `install_spam_gate()` and shared by both paths; the Lightning branch is otherwise unchanged. - `app.rs`: factor the transport/validation prologue and the post-dispatch error tail out of `run()` into `accept_event` / `finalize_dispatch`, shared VERBATIM by `run` and the new `run_cashu` so the two loops cannot drift. Add `dispatch_cashu` with the closed allow-list (`Orders`, `LastTradeIndex`, `RestoreSession`, `TradePubkey` → `handle_message_action_no_ln`; `AddCashuEscrow` → the TA-1 stub; everything else → `InvalidAction`). - `app/add_cashu_escrow.rs` (new): stub `add_cashu_escrow_action` returning `InvalidAction`; Track A fills the body in its own file (G-1). - `app/context.rs`: `AppContext` carries `Option<Arc<CashuClient>>` with a `with_cashu_client` setter and `cashu_client()` accessor (`None` in Lightning mode). The `escrow: Arc<dyn EscrowBackend>` field the spec sketches is deferred: `run()` still threads `&mut LndConnector` and an unused `Arc<dyn EscrowBackend>` would be dead weight — added when Track B needs it. - `scheduler.rs`: gate the LN-only jobs (cancel-orders, retry-payments, dev-fee, bond payouts, stranded-bond reconcile) behind `!is_cashu_enabled()` so they never call `LndConnector::new()` on a node with no LND; make `job_info_event_send` self-skip when `LN_STATUS` is absent instead of panicking. Tests: `dispatch_cashu` blocks every order-lifecycle action with `InvalidAction` and routes the allow-list through the no-LN handler; the CF-5 stub returns `InvalidAction`. Full suite green (1023) with Cashu off. Refs: docs/cashu/01-fundamentals.md (CF-5)
WalkthroughAdds Cashu escrow-mode startup with a mint client, dedicated event routing, shared event validation, Cashu-specific scheduling behavior, and an ChangesCashu escrow mode
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Main
participant CashuMint
participant AppContext
participant Scheduler
participant CashuLoop
Main->>CashuMint: connect to configured mint
Main->>AppContext: attach CashuClient
Main->>Scheduler: start mode-aware jobs
Main->>CashuLoop: run Cashu event loop
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07936897cd
ℹ️ 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".
| info!("Creating scheduler"); | ||
|
|
||
| // Mode-agnostic jobs run in both Lightning and Cashu mode. | ||
| job_expire_pending_older_orders(ctx.clone()).await; |
There was a problem hiding this comment.
Gate bond expiry in Cashu mode
When Cashu mode is enabled but the database still contains an expired waiting-maker-bond or pending order with active bond rows (for example after an operator switches modes or reuses an existing DB), this unconditionally spawned expiry job still runs the bond cleanup paths in job_expire_pending_older_orders. Those paths persist the order as Expired and then call the release_*_bonds... helpers, which open LndConnector::new(); because the Cashu boot path deliberately has no LND, release fails after the order leaves the statuses selected by find_order_by_date, so the bond is not retried by this job. Either gate the bond-related expiry paths in Cashu mode or avoid terminalizing those orders when no LND is available.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in b8116e3. Gated all three bond-release calls in job_expire_pending_older_orders (WaitingMakerBond, taker-bond, range maker bond) behind !Settings::is_cashu_enabled(), so a cashu node never opens LndConnector::new() here. The order still expires; the bond release is skipped (bonds are mutually exclusive with Cashu per CF-1, so this only guards a reused/mode-switched DB).
| start_scheduler(ctx.clone()).await; | ||
|
|
||
| // Run the Mostro Cashu event loop and be happy!! | ||
| return run_cashu(ctx).await; |
There was a problem hiding this comment.
Do not silently skip RPC in Cashu mode
With both [cashu].enabled = true and [rpc].enabled = true, this early return bypasses the existing RpcServer::is_enabled() startup block, so the configured admin gRPC listener never starts and there is no validation error or warning. Some exposed RPCs such as version/password validation and solver management do not require Lightning, so operators that enable RPC lose that API only in Cashu mode. If RPC is unsupported for Cashu, reject or warn on that config; otherwise start the compatible subset without constructing an LND client.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in b8116e3. The Cashu boot branch now warns when [rpc].enabled = true instead of silently skipping the admin gRPC server. Starting the LN-independent RPC subset in Cashu mode is left as a follow-up: RpcServer::start currently requires an Arc<Mutex<LndConnector>>, so serving the compatible subset needs a signature change that is out of scope for this foundation PR.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/main.rs (1)
180-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsolidate shared initialization before the Cashu block.
The initialization of
AppContextand the call toinstall_spam_gate().awaitare exactly duplicated in the Lightning path further down the file (lines 245-263). Since neither of these setups relies onLndConnectoror any branch-specific logic, you can hoist them above theSettings::is_cashu_enabled()check to keep the code DRY.♻️ Proposed refactor
+ // Install the protocol-v2 anti-spam gate and warm its active-trade-pubkey + // cache before the event loop starts (mode-agnostic). + install_spam_gate().await; + + // Build AppContext explicitly with all dependencies + let settings = Arc::new( + MOSTRO_CONFIG + .get() + .expect("MOSTRO_CONFIG not initialized") + .clone(), + ); + let ctx = AppContext::new( + get_db_pool(), + client.clone(), + settings, + MESSAGE_QUEUES.queue_order_msg.clone(), + mostro_keys.clone(), + ); + if Settings::is_cashu_enabled() { // `mint_url` non-emptiness + scheme were validated at config load // (CF-1); this expect is unreachable for a validated config. let mint_url = Settings::get_cashu() .map(|c| c.mint_url.clone()) .expect("cashu enabled but [cashu] settings missing after validation"); tracing::info!( "Starting in Cashu escrow mode — connecting mint {mint_url} (LND not initialised)" ); let cashu_client = match cashu::CashuClient::connect(&mint_url).await { Ok(client) => Arc::new(client), Err(e) => { tracing::error!( "No connection to Cashu mint {mint_url} - shutting down Mostro! ({e})" ); exit(1); } }; - // Warm the anti-spam gate exactly as the Lightning path does. - install_spam_gate().await; - - let settings = Arc::new( - MOSTRO_CONFIG - .get() - .expect("MOSTRO_CONFIG not initialized") - .clone(), - ); - let ctx = AppContext::new( - get_db_pool(), - client.clone(), - settings, - MESSAGE_QUEUES.queue_order_msg.clone(), - mostro_keys.clone(), - ) - .with_cashu_client(cashu_client); + let cashu_ctx = ctx.with_cashu_client(cashu_client); - start_scheduler(ctx.clone()).await; + start_scheduler(cashu_ctx.clone()).await; // Run the Mostro Cashu event loop and be happy!! - return run_cashu(ctx).await; + return run_cashu(cashu_ctx).await; }(You can then safely remove the duplicated
install_spam_gate().awaitandAppContextbuilding blocks currently at lines 245-263).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main.rs` around lines 180 - 202, Move the shared install_spam_gate().await call and AppContext construction out of the Cashu-specific branch and place them before the Settings::is_cashu_enabled() check. Preserve the existing AppContext inputs and retain branch-specific setup only where required, then remove the duplicated initialization from the Cashu and Lightning paths while keeping each branch’s scheduler and event-loop behavior unchanged.src/app.rs (1)
1094-1111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtend Cashu allow-list test coverage beyond
RestoreSession.
allows_restore_session_through_no_ln_routeronly provesRestoreSessionreaches the no-LN handler.dispatch_cashu's allow-list also includesOrders,LastTradeIndex, andTradePubkey(Line 561); none of those are asserted here, so a future edit that accidentally drops one of them from the match arm wouldn't be caught by this test.✅ Suggested test extension
#[tokio::test] async fn allows_restore_session_through_no_ln_router() { let ctx = create_ctx().await; let my_keys = create_test_keys(); let event = create_test_unwrapped_message(); let msg = Message::new_restore(None); let result = dispatch_cashu(&Action::RestoreSession, msg, &event, &my_keys, &ctx).await; assert!( result.is_ok(), "RestoreSession must route to the no-LN handler, got {result:?}" ); } + + #[tokio::test] + async fn allows_orders_last_trade_index_and_trade_pubkey_through_no_ln_router() { + let ctx = create_ctx().await; + let my_keys = create_test_keys(); + let event = create_test_unwrapped_message(); + + for action in [Action::Orders, Action::LastTradeIndex, Action::TradePubkey] { + let msg = create_test_message(action.clone(), None); + let result = dispatch_cashu(&action, msg, &event, &my_keys, &ctx).await; + assert!( + !is_invalid_action(result), + "{action:?} must not be blocked with InvalidAction in Cashu mode" + ); + } + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app.rs` around lines 1094 - 1111, Extend the Cashu allow-list coverage around dispatch_cashu and allows_restore_session_through_no_ln_router by adding equivalent assertions for Orders, LastTradeIndex, and TradePubkey. Construct suitable messages for each action, dispatch them through the no-LN path, and assert each result is Ok so removal from the allow-list is detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app.rs`:
- Around line 396-402: The trade-index mutation currently runs before Cashu
dispatch can reject unsupported actions. In the message handling flow around
check_trade_index and dispatch_cashu, ensure check_trade_index executes only
after dispatchability is confirmed, or bypass it for NewOrder, TakeBuy, and
TakeSell actions that Cashu always blocks, so rejected probes cannot persist or
advance trade state.
---
Nitpick comments:
In `@src/app.rs`:
- Around line 1094-1111: Extend the Cashu allow-list coverage around
dispatch_cashu and allows_restore_session_through_no_ln_router by adding
equivalent assertions for Orders, LastTradeIndex, and TradePubkey. Construct
suitable messages for each action, dispatch them through the no-LN path, and
assert each result is Ok so removal from the allow-list is detected.
In `@src/main.rs`:
- Around line 180-202: Move the shared install_spam_gate().await call and
AppContext construction out of the Cashu-specific branch and place them before
the Settings::is_cashu_enabled() check. Preserve the existing AppContext inputs
and retain branch-specific setup only where required, then remove the duplicated
initialization from the Cashu and Lightning paths while keeping each branch’s
scheduler and event-loop behavior unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 49b7eccb-9154-4e4a-b27a-d8ec72797905
📒 Files selected for processing (5)
src/app.rssrc/app/add_cashu_escrow.rssrc/app/context.rssrc/main.rssrc/scheduler.rs
…ashu mode Two review findings from PR #828: - Bond-expiry paths in `job_expire_pending_older_orders` open `LndConnector::new()` via the bond-release helpers. Cashu mode has no LND, so gate all three release calls (WaitingMakerBond, taker-bond, range maker bond) behind `!Settings::is_cashu_enabled()`. Bonds are mutually exclusive with Cashu (CF-1), so a cashu node carries no bond rows by construction; this only matters defensively for a reused/mode-switched DB, where the order still expires but no doomed LND connection is attempted. - The Cashu boot branch early-returns before the RPC startup block, silently skipping the admin gRPC server. Warn when `[rpc].enabled = true` in Cashu mode so the missing API is visible instead of a surprise (the LN-independent RPC subset is a follow-up — the server currently requires an LND client). Skipped (with reasons posted on the PR): the `check_trade_index`-before-dispatch note (pre-existing shared-prologue behaviour, identical to the Lightning loop; the actions become valid in TA-2 where the check belongs) and the boot-init DRY hoist (it reorders the Lightning boot, violating the byte-identical guarantee). cargo fmt + clippy -D warnings + full suite (1023) green.
Review addressed —
|
What & why
The integration PR of the Cashu escrow foundation (
docs/cashu/01-fundamentals.md§6, CF-5) — the one sequential join point that makes a[cashu] enabled = truenode boot and run with no Lightning node, connect its configured mint, and route events through a Cashu event loop where every trade action is rejected withCantDo(InvalidAction)(nothing is implemented yet). This is what proves the foundation works end-to-end while keeping it inert for real trades. The four feature tracks (Track A onward) fill in the blocked arms one at a time in their own files.CF-0…CF-4 are already on
main; this is the last foundation PR.The prime directive (merge gate)
Verified: the full suite passes unmodified (1023 tests), and the Lightning boot branch is left byte-for-byte unchanged.
Changes
main.rs— branch onSettings::is_cashu_enabled(). Cashu mode skipsLndConnector::new()and the LN status probe entirely, connects the mint viaCashuClient::connect(fail fast withexit(1)if unreachable, mirroring the LND-refusal behaviour), attaches the client to the context, andreturnsrun_cashu(ctx). The spam-gate warm-up is extracted into a sharedinstall_spam_gate()helper used by both paths; the Lightning branch is otherwise untouched.app.rs— factor the transport/validation prologue and the post-dispatch error tail out ofrun()intoaccept_event/finalize_dispatch, shared VERBATIM byrun(Lightning) and the newrun_cashu(Cashu) so the two loops cannot drift.run_cashudiffers fromrunin exactly one line: the dispatch call. Adddispatch_cashuwith the closed allow-list from §6.app/add_cashu_escrow.rs(new) — stubadd_cashu_escrow_actionreturningInvalidAction; Track A (TA-1) fills the body in this same file, so it touches no shared file (the G-1 fix).app/context.rs—AppContextcarriesOption<Arc<CashuClient>>with awith_cashu_clientsetter and acashu_client()accessor (Nonein Lightning mode). See design note below.scheduler.rs— gate the LN-only jobs (cancel-orders, retry-payments, dev-fee, bond payouts, stranded-bond reconcile) behind!is_cashu_enabled()so they never callLndConnector::new()on a node with no LND; makejob_info_event_sendself-skip whenLN_STATUSis absent instead of panicking.dispatch_cashurouting (§6 closed decision)Orders,LastTradeIndex,RestoreSession,TradePubkeyhandle_message_action_no_ln(read-only / session; never touch escrow, LND, or order lifecycle)AddCashuEscrowadd_cashu_escrow_action(TA-1 stub →InvalidActionfor now)CantDo(InvalidAction)Design note —
escrowfield deferredThe spec sketches an
escrow: Arc<dyn EscrowBackend>onAppContext. It is deferred:run()still threads&mut LndConnector, and anArc<dyn EscrowBackend>cannot yield the&mutthose hold-invoice calls need, so adding it now would be unused dead weight. Onlycashu_client(which Track A actually consumes viactx.cashu_client()) is added. The escrow field lands when Track B's cashu release path needs it. This keeps CF-5 additive and behaviour-preserving.Tests
dispatch_cashublocks every order-lifecycle action (NewOrder, takes,FiatSent,AddInvoice,Release,Cancel,Dispute,RateUser,AddCashuEscrow, admin actions,AddBondInvoice) withInvalidAction.dispatch_cashuroutesRestoreSessionthrough the no-LN handler (returnsOk), proving the allow-list is not short-circuited.add_cashu_escrow_actionreturnsInvalidActionwithout panicking.Checklist
cargo fmt --checkcargo clippy --all-targets --all-features -- -D warningscargo test— 1023 passed, existing suite unmodifiedmain)Refs:
docs/cashu/01-fundamentals.md(CF-5). Foundation is complete once this merges; Track A follows.Summary by CodeRabbit
New Features
Bug Fixes
Tests
🧪 Manual testing — step by step
Prereqs: a
mostroddev setup + the throwaway Cashu test mint. Start the mint:docker compose -f docker-compose.cashu.yml up -d # nutshell on http://127.0.0.1:3338Enable Cashu mode in
settings.toml(LND is not required in this mode):Run with
RUST_LOG=mostro=info(or=debug) to see the log lines below.1. Boots in Cashu mode with NO Lightning node
mostrodwith the config above (no LND running).LndConnector::new(), and logs:Scheduler Startedand the event loop runs. ✔️docker compose -f docker-compose.cashu.yml down) and restartmostrod. Expected — refuses to boot (mirrors LND-refusal):2. Every trade action is rejected with
InvalidActionmostrodin Cashu mode, from any client send a trade action —NewOrder,TakeSell,TakeBuy,AddInvoice,FiatSent,Release,Cancel,Dispute,AddCashuEscrow,AdminCancel,AdminSettle.CantDo(InvalidAction)— no panic, no order created/advanced. ✔️Orders,RestoreSession,TradePubkey,LastTradeIndexrespond normally (the node still serves the book and restores sessions). ✔️3. Scheduler skips Lightning-only jobs
4. RPC warns instead of silently skipping
[rpc] enabled = truealongside[cashu] enabled = trueand startmostrod.5. Bonds ⊕ Cashu are mutually exclusive
[cashu] enabled = trueand[anti_abuse_bond] enabled = true.mostrodrefuses to start with a config-validation error (CF-1). ✔️6. Lightning regression — nothing changes when Cashu is off
[cashu] enabled = false(or remove the block) and run a normal LND-backed node.