-
Notifications
You must be signed in to change notification settings - Fork 52
docs(cashu): specs for Tracks B/C/D (release, coop-cancel, dispute) #833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| # Cashu Escrow — Track B: Release (Happy Path) | ||
|
|
||
| **Status:** Draft for review · **Target:** `main` (**requires `mostro-core ≥ 0.14.0`**) · | ||
| **Depends on:** Fundamentals **CF-1, CF-2, CF-5** + **Track A** (the escrow must be | ||
| locked before it can be released) · **Feature flag:** `[cashu].enabled` | ||
|
|
||
| Track B is the **happy-path settlement** of a Cashu trade: the buyer confirms | ||
| fiat sent, the seller releases, and the buyer redeems the locked 2-of-3 token | ||
| with **two signatures** (its own + the seller's). It is "box 3" of the sequence | ||
| diagram in [`../CASHU_ESCROW_ARCHITECTURE.md`](../CASHU_ESCROW_ARCHITECTURE.md). | ||
|
|
||
| This document assumes Fundamentals and Track A are merged. It only adds behaviour | ||
| *inside the Cashu branch*; the Lightning path is never changed. | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Goal and scope | ||
|
|
||
| ### Goal | ||
| Complete a locked Cashu trade without Mostro ever touching the funds: | ||
| 1. The **buyer** signals `FiatSent` (gated by the remaining-locktime guard, §4B). | ||
| 2. The **seller** signals `Release`, and delivers its **Cashu release signature** | ||
| directly to the buyer (P2P NIP-59 DM) so the buyer can build a valid 2-of-3 | ||
| `SwapRequest` and redeem the ecash **itself**. | ||
| 3. Mostro only **advances the order state** (`Active → FiatSent → Success`), | ||
| makes the trade **rateable**, and — because the fee was collected at lock | ||
| (Track A §4A) — takes **no** settlement action on the funds. | ||
|
|
||
| ### In scope | ||
| - The Cashu branch of `fiat_sent_action` carrying the **§4B remaining-locktime | ||
| guard** (`escrow_settlement_margin_days`, default 3). | ||
| - The Cashu branch of `release_action`: the seller marks release, the daemon | ||
| advances state and relays/acknowledges the seller's release signature to the | ||
| buyer, and the order becomes rateable. | ||
| - Unblocking `FiatSent`, `Release`, and `RateUser` in `dispatch_cashu`. | ||
|
|
||
| ### Out of scope (other tracks) | ||
| - **Cooperative cancel** → Track C. **Dispute resolution** → Track D. | ||
| - The **live fee redeem** (Track A TA-1f follow-up) — orthogonal to release. | ||
| - Any change to the Lightning release/settle path. | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Where Track B sits — flow and state transitions | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant B as Buyer | ||
| participant M as Mostro (cashu mode) | ||
| participant S as Seller | ||
| participant Mint as Cashu Mint | ||
|
|
||
| Note over B,S: Escrow already locked (Track A) — order is Active | ||
| B->>M: FiatSent | ||
| M->>M: guard: remaining locktime >= escrow_settlement_margin_days | ||
| M->>S: FiatSentOk (buyer paid — release when ready) | ||
| S->>B: Release signature (NIP-59 DM, seller signs SIG_INPUTS) | ||
| S->>M: Release | ||
| M->>M: advance Active/FiatSent -> Success; make rateable | ||
| B->>Mint: SwapRequest {2-of-3 proofs, buyer_sig + seller_sig, buyer outputs} | ||
| Mint-->>B: fresh ecash (buyer holds the funds) | ||
| ``` | ||
|
|
||
| **State transitions Track B performs:** | ||
| `Active → FiatSent` (buyer), then `FiatSent → Success` (seller release). Both are | ||
| Cashu analogues of the Lightning flow, but **no hold invoice is settled** — the | ||
| buyer redeems the token itself, off Mostro's servers. | ||
|
|
||
| > **Why the seller's signature goes P2P, not through Mostro.** On the happy path | ||
| > Mostro is neither a signer nor a required relay: the seller signs the release | ||
| > and hands it to the buyer over a NIP-59 DM using the trade keys, exactly as the | ||
| > `release`/`cancel` messages already travel. Mostro only needs the **state** | ||
| > update. (Mostro MAY still relay a copy for reliability, but the trust model | ||
| > does not depend on it.) | ||
|
|
||
| --- | ||
|
|
||
| ## 3. What Track B consumes from Fundamentals + Track A | ||
|
|
||
| | Needs | From | Exact item | | ||
| |-------|------|------------| | ||
| | Mode gate | CF-1 | `Settings::is_cashu_enabled()`, `escrow_mode()` | | ||
| | Locktime floor / margin | CF-1 | `get_cashu().escrow_locktime_days`; **new** `escrow_settlement_margin_days` (default 3, §4B) | | ||
| | Locked escrow row | Track A | `Order.{cashu_escrow_token, cashu_escrow_locked_at}` populated, status `Active` | | ||
| | Token locktime read | CF-2 | parse the stored token's `locktime` (the guard compares it to now) | | ||
| | Dispatch seam | CF-5 | `FiatSent`/`Release`/`RateUser` arms in `dispatch_cashu` | | ||
| | Notifications | existing | `enqueue_order_msg`, `update_order_event` | | ||
|
|
||
| Protocol (already on `main`, `mostro-core ≥ 0.14.0`): `Action::{FiatSent, | ||
| FiatSentOk, Release, Released, RateUser}` and the existing rating payloads — | ||
| **no new protocol variant is required**. The seller's Cashu release signature is | ||
| carried in the existing P2P release message shape (trade-key-signed NIP-59 DM); | ||
| Mostro validates the *state transition*, not the swap. | ||
|
|
||
| --- | ||
|
|
||
| ## 4. The `escrow_settlement_margin_days` guard (§4B — Track B executes it) | ||
|
|
||
| Track A §4B defines the attack: a seller stalls the fiat phase until little | ||
| locktime remains, lets the buyer send fiat on day 13 of 15, goes silent, and | ||
| reclaims via the refund path on day 15 — keeping both fiat and sats without | ||
| failing a single protocol check. **Track B closes it at the `FiatSent` gate.** | ||
|
|
||
| - New `#[serde(default)]` key on `CashuSettings`: **`escrow_settlement_margin_days`, | ||
| default 3**. Added by Track B (not needed during foundation). | ||
| - In the Cashu branch of `fiat_sent_action`: read the stored escrow token's | ||
| `locktime`; reject `FiatSent` with a clear `CantDo` (e.g. | ||
| `CashuEscrowNotLocked` if the token/locktime is missing, else a | ||
| settlement-window reason) when | ||
| `locktime - now < escrow_settlement_margin_days`. Fiat can never be sent inside | ||
| the danger window, so the seller cannot weaponise the locktime. | ||
|
|
||
| The margin must be comfortably below the locktime floor (`escrow_locktime_days`, | ||
| default 15) so a normal trade has ample settlement room; the difference (≈12 | ||
| days) is the usable fiat-settlement window. | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Handlers — the Cashu branches | ||
|
|
||
| ### 5A · `fiat_sent_action` (Cashu branch) | ||
| Same identity/status checks as today (only the **buyer** may send fiat; order | ||
| must be `Active`). Then, in Cashu mode only: apply the §4B guard, advance | ||
| `Active → FiatSent`, publish the order event, and notify both parties | ||
| (`FiatSentOk`) — no LND, no hold-invoice interaction. | ||
|
|
||
| ### 5B · `release_action` (Cashu branch) | ||
| The submitter must be the **seller** (same identity check as the Lightning | ||
| release). In Cashu mode: **do not** settle a hold invoice (there is none); | ||
| instead advance `FiatSent → Success`, publish the order event, acknowledge the | ||
| seller's release signature to the buyer (relay/ack the P2P DM), and make the | ||
| trade rateable (`RateUser` becomes valid). The fee was already collected at lock | ||
|
Comment on lines
+130
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In this flow the seller's Cashu signature is only described as a P2P DM, while the daemon advances Useful? React with 👍 / 👎. |
||
| (Track A §4A), so release moves **no** funds. | ||
|
|
||
| ### 5C · `dispatch_cashu` unblocks | ||
| Replace the `InvalidAction` arms for `FiatSent`, `Release`, and `RateUser` | ||
| (route through the existing `handle_message_action_no_ln`, whose branches now | ||
| carry the Cashu logic). No other action changes. | ||
|
|
||
| --- | ||
|
|
||
| ## 6. PR breakdown (atomic, backwards-compatible) | ||
|
|
||
| ### TB-1 · `escrow_settlement_margin_days` + `FiatSent` guard | ||
| Add the `CashuSettings` key and the Cashu branch of `fiat_sent_action` with the | ||
| §4B remaining-locktime guard. Unblock `FiatSent` in `dispatch_cashu`. | ||
| *Depends on CF-1, Track A. Conflict surface: `config/*`, `fiat_sent.rs`, | ||
| `app.rs` (one dispatch arm).* | ||
|
|
||
| ### TB-2 · `release_action` Cashu branch + rating | ||
| Add the Cashu branch of `release_action` (advance state, ack the seller | ||
| signature, make rateable) and unblock `Release` + `RateUser`. Completes the | ||
| happy path end-to-end with TB-1 and Track A. | ||
| *Depends on CF-5, Track A, TB-1 (for a full e2e test). Conflict surface: | ||
| `release.rs`, `rate_user.rs` (if touched), `app.rs` (two dispatch arms).* | ||
|
|
||
| --- | ||
|
|
||
| ## 7. Issues table — sequential vs parallel | ||
|
|
||
| | ID | Title | Depends on | Parallel with | Conflict surface | Risk | | ||
| |----|-------|-----------|---------------|------------------|------| | ||
| | **TB-1** | `escrow_settlement_margin_days` + `FiatSent` §4B guard | CF-1, Track A | Tracks C/D | `config/*`, `fiat_sent.rs` | Low-Medium | | ||
| | **TB-2** | `release_action` Cashu branch + unblock `Release`/`RateUser` | CF-5, Track A, TB-1 | Tracks C/D | `release.rs`, `app.rs` | Medium | | ||
|
|
||
| All of Track B is parallel with Tracks C/D (disjoint handler files); the shared | ||
| touch point is the `dispatch_cashu` allow-list, edited one arm at a time. | ||
|
Comment on lines
+163
to
+167
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Make the documented track ordering consistent.
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
|
|
||
| --- | ||
|
|
||
| ## 8. Definition of Done | ||
|
|
||
| 1. A locked Cashu order can be driven `Active → FiatSent → Success` end-to-end | ||
| against the CF-3 mint, with the buyer redeeming the 2-of-3 token itself. | ||
| 2. `FiatSent` is rejected inside the settlement-margin window (§4B), and accepted | ||
| outside it; the exact `CantDoReason` is asserted. | ||
| 3. Every identity/status rejection path returns the correct reason and leaves the | ||
| order unchanged (wrong sender, wrong status, guard tripped). | ||
| 4. The trade becomes rateable only after release; `RateUser` works. | ||
| 5. With Cashu disabled, behaviour is identical to `main`; existing tests pass | ||
| unmodified. `fmt`/`clippy -D warnings`/`test` green. | ||
|
|
||
| --- | ||
|
|
||
| ## 9. Cross-track obligations satisfied / raised | ||
|
|
||
| | Obligation | Defined in | Track B does | | ||
| |------------|-----------|--------------| | ||
| | `FiatSent` rejected when remaining locktime < `escrow_settlement_margin_days` | Track A §4B | **Executed** (TB-1) | | ||
| | `RateUser` unblocked once terminal state reachable | CF-5 §6 | **Executed** (TB-2) | | ||
| | Buyer locktime warnings as expiry approaches | Track A §4B | Surfaced by TA-3 monitor; TB may add a nudge on `FiatSent` | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| # Cashu Escrow — Track C: Cooperative Cancel | ||
|
|
||
| **Status:** Draft for review · **Target:** `main` (**requires `mostro-core ≥ 0.14.0`**) · | ||
| **Depends on:** Fundamentals **CF-1, CF-2, CF-5** + **Track A** (the escrow must be | ||
| locked before it can be cancelled) · **Feature flag:** `[cashu].enabled` | ||
|
|
||
| Track C is the **cooperative unwind**: both parties agree to abandon a locked | ||
| trade, the buyer hands the seller the signature needed to reclaim the escrow, and | ||
| Mostro records the cancellation and **refunds the seller the fee** it collected at | ||
| lock. No dispute, no arbitrator signature. | ||
|
|
||
| This document assumes Fundamentals and Track A are merged. It only adds behaviour | ||
| *inside the Cashu branch*; the Lightning path is never changed. | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Goal and scope | ||
|
|
||
| ### Goal | ||
| Let a locked Cashu trade be cancelled by mutual consent without Mostro moving the | ||
| escrow: | ||
| 1. Either party requests `Cancel`; when **both** have requested it (the existing | ||
| cooperative-cancel handshake), the trade is cancelled. | ||
| 2. The **buyer** delivers its **Cashu signature** directly to the **seller** (P2P | ||
| NIP-59 DM) so the seller can build a 2-of-3 `SwapRequest` | ||
| (`buyer_sig + seller_sig`) and **reclaim** the locked ecash itself. | ||
| 3. Mostro advances the order to `Canceled` and — because the fee was collected at | ||
| lock (Track A §4A) — **refunds the seller** the whole Mostro fee | ||
| (`2 * order.fee`). | ||
|
|
||
| ### In scope | ||
| - The Cashu branch of `cancel_action` (cooperative handshake → `Canceled`). | ||
| - The **fee-refund obligation** (Track A §4A): mint/send a fresh token of | ||
| `2 * order.fee` to the seller's **trade** pubkey on a cancelled-after-lock | ||
| order. (This is the first track to execute the refund obligation.) | ||
| - Unblocking `Cancel` in `dispatch_cashu`. | ||
|
|
||
| ### Out of scope (other tracks) | ||
| - **Unilateral / dispute-driven** cancellation → Track D (`admin_cancel`). | ||
| - **Release** happy path → Track B. | ||
| - The **live fee redeem** mechanics (Track A TA-1f follow-up); the refund shares | ||
| the same "Mostro sends ecash" capability and should land with it. | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Where Track C sits — flow and state transitions | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant B as Buyer | ||
| participant M as Mostro (cashu mode) | ||
| participant S as Seller | ||
| participant Mint as Cashu Mint | ||
|
|
||
| Note over B,S: Escrow already locked (Track A) | ||
| B->>M: Cancel | ||
| S->>M: Cancel | ||
| M->>M: both requested -> cooperative cancel | ||
| B->>S: Buyer signature (NIP-59 DM) | ||
| M->>M: advance -> Canceled | ||
| M->>S: Fee refund (fresh token 2*order.fee to P_S) | ||
| S->>Mint: SwapRequest {2-of-3 proofs, buyer_sig + seller_sig, seller outputs} | ||
| Mint-->>S: reclaimed ecash | ||
| ``` | ||
|
|
||
| **State transition Track C performs:** the existing cooperative-cancel handshake | ||
| drives the order to `Canceled`. No hold invoice is cancelled — the seller | ||
| reclaims the token itself with the buyer's signature. | ||
|
|
||
| > **Why the buyer signs for the seller here.** On the happy path the *seller* | ||
| > signs so the *buyer* redeems; on a cancel the roles invert — the funds return to | ||
| > the seller, so the *buyer* provides the second signature that lets the *seller* | ||
| > reclaim. Same 2-of-3 token, opposite redeemer. | ||
|
|
||
| --- | ||
|
|
||
| ## 3. What Track C consumes from Fundamentals + Track A | ||
|
|
||
| | Needs | From | Exact item | | ||
| |-------|------|------------| | ||
| | Mode gate | CF-1 | `Settings::is_cashu_enabled()`, `escrow_mode()` | | ||
| | Locked escrow row | Track A | `Order.{cashu_escrow_token, cashu_escrow_locked_at}` populated | | ||
| | Fee to refund | Track A §4A | `order.fee` → refund value `2 * order.fee`; `cashu_fee_token`/`cashu_fee_redeemed_at` state | | ||
| | Seller trade pubkey | existing | `order.get_seller_pubkey()` (refund recipient) | | ||
| | Mostro-sends-ecash | CF-2 (TA-1f follow-up) | mint/send a fresh token to `P_S` | | ||
| | Dispatch seam | CF-5 | `Cancel` arm in `dispatch_cashu` | | ||
|
|
||
| Protocol (already on `main`, `mostro-core ≥ 0.14.0`): the existing | ||
| cooperative-cancel messages (`Action::Cancel` and its | ||
| `CooperativeCancel*`/`Canceled` acknowledgements). The buyer's Cashu signature | ||
| travels in the existing P2P cancel message shape; **no new protocol variant is | ||
| required**. | ||
|
|
||
| --- | ||
|
|
||
| ## 4. The fee refund (Track A §4A obligation — Track C executes it) | ||
|
|
||
| Because the fee is realised at **lock**, not at success, the daemon **owes the | ||
| seller a refund** on every non-success path. A cooperative cancel is one such | ||
| path. | ||
|
|
||
| - Refund value = **`2 * order.fee`** (the whole Mostro fee), sent as a fresh | ||
| token to the seller's **trade** pubkey. | ||
| - Idempotency: gate the refund on the fee state so a replayed/duplicate cancel | ||
| never double-refunds — e.g. only refund when `cashu_fee_token IS NOT NULL` and | ||
| the order has not already been refunded (stamp a `cashu_fee_refunded_at`, or | ||
| reuse `cashu_fee_redeemed_at` semantics: a redeemed fee is refunded by minting | ||
| fresh ecash back, a not-yet-redeemed fee is simply not collected). The exact | ||
|
Comment on lines
+107
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the Track A fee model, the fee token is locked 1-of-1 to Useful? React with 👍 / 👎. |
||
| bookkeeping is a TC decision, but it **must** be single-shot. | ||
|
Comment on lines
+102
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Define one crash-safe, idempotent seller-refund contract.
🧰 Tools🪛 LanguageTool[style] ~104-~104: The adverb ‘never’ is usually put before the verb ‘cancel’. (ADVERB_WORD_ORDER) 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| - If the fee was **never charged** (`mostro.fee == 0`), there is nothing to | ||
| refund — the branch is a no-op. | ||
|
|
||
| > **Interaction with the self-service-refund refinement (Track A §4A).** If a | ||
| > future revision locks the fee token with `locktime` + `refund = [P_S]`, the | ||
| > seller can reclaim the fee unilaterally after locktime and Track C's proactive | ||
| > refund becomes a fast-path optimisation rather than the sole recovery route. | ||
| > Track C ships the proactive refund against the simple 1-of-1 fee token TA-1f | ||
| > defines. | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Handler — the Cashu branch of `cancel_action` | ||
|
|
||
| Keep the existing cooperative-cancel handshake (both parties must request | ||
| `Cancel`). In Cashu mode, at the point the Lightning path would cancel the hold | ||
| invoice: | ||
| - **Do not** touch LND (there is no hold invoice). | ||
| - Advance the order to `Canceled`, publish the order event. | ||
|
Comment on lines
+127
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The existing active-order cooperative cancel path persists Useful? React with 👍 / 👎. |
||
| - Acknowledge/relay the buyer's Cashu signature to the seller so the seller can | ||
| reclaim. | ||
| - Execute the **fee refund** (§4) once, if a fee was collected. | ||
|
|
||
| `dispatch_cashu` replaces the `InvalidAction` arm for `Cancel` (routing through | ||
| the cashu-aware `cancel` handler). A *unilateral* cancel of a locked escrow (no | ||
| peer consent) is **not** a Track C concern — that path is a dispute (Track D). | ||
|
|
||
| --- | ||
|
|
||
| ## 6. PR breakdown (atomic, backwards-compatible) | ||
|
|
||
| ### TC-1 · `cancel_action` Cashu branch + fee refund | ||
| Add the Cashu branch to `cancel_action` (cooperative handshake → `Canceled`, no | ||
| LND), the single-shot fee refund to `P_S`, and unblock `Cancel` in | ||
| `dispatch_cashu`. Unit-tested against the CF-3 mint (both-parties cancel → | ||
| `Canceled` + seller can reclaim + fee refunded once; single-party cancel does not | ||
| finalise; replay does not double-refund). | ||
| *Depends on CF-5, Track A (and TA-1f for the fee state + the Mostro-sends-ecash | ||
| capability). Conflict surface: `cancel.rs`, possibly `db.rs` (refund bookkeeping, | ||
| additive) + `migrations/` if a `cashu_fee_refunded_at` column is chosen, | ||
| `app.rs` (one dispatch arm).* | ||
|
|
||
| --- | ||
|
|
||
| ## 7. Issues table — sequential vs parallel | ||
|
|
||
| | ID | Title | Depends on | Parallel with | Conflict surface | Risk | | ||
| |----|-------|-----------|---------------|------------------|------| | ||
| | **TC-1** | `cancel_action` Cashu branch + fee refund + unblock `Cancel` | CF-5, Track A, TA-1f | Tracks B/D | `cancel.rs`, `app.rs`, (opt.) `migrations/` | Medium (funds return + refund) | | ||
|
|
||
| Track C is parallel with Tracks B/D; the only shared touch point is the | ||
| `dispatch_cashu` `Cancel` arm. | ||
|
|
||
| --- | ||
|
|
||
| ## 8. Definition of Done | ||
|
|
||
| 1. A locked Cashu order, cancelled cooperatively by both parties, reaches | ||
| `Canceled`, the seller can reclaim the escrow with the buyer's signature, and | ||
| the seller is refunded `2 * order.fee` — verified end-to-end against the CF-3 | ||
| mint. | ||
| 2. A single-party `Cancel` does **not** finalise the cancellation (the handshake | ||
| still requires both). | ||
| 3. The fee refund is **single-shot**: a replayed/duplicate cancel never | ||
| double-refunds; a fee-free order refunds nothing. | ||
| 4. With Cashu disabled, behaviour is identical to `main`; existing tests pass | ||
| unmodified. `fmt`/`clippy -D warnings`/`test` green. | ||
|
|
||
| --- | ||
|
|
||
| ## 9. Cross-track obligations satisfied / raised | ||
|
|
||
| | Obligation | Defined in | Track C does | | ||
| |------------|-----------|--------------| | ||
| | Fee refund on non-success (coop cancel after lock), `2 * order.fee` to `P_S` | Track A §4A / §10 | **Executed** (TC-1) | | ||
| | Single-shot refund bookkeeping | Track A §4A | **Executed** (idempotent refund) | | ||
| | Mostro-sends-ecash capability | Track A §4A (TA-1f) | **Shared** with the fee redeem; lands together | | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Specify margin validation and expired-locktime handling.
The supplied
CashuSettingscurrently has noescrow_settlement_margin_days; the implementation plan should require startup validation that the margin is belowescrow_locktime_days. Also define expired-locktime behavior explicitly and require checked/saturating subtraction, otherwise a configurable margin can disable everyFiatSentand unsignedlocktime - nowarithmetic can underflow.🤖 Prompt for AI Agents