From 740e6ea00a3a8d47aa6a2cb23dd41652b748d2bc Mon Sep 17 00:00:00 2001 From: Egge Date: Sat, 4 Jul 2026 12:30:50 +0000 Subject: [PATCH] docs: record NUT-04 quote accounting design --- CONTEXT.md | 24 ++++++-- ...-cashu-ts-owns-wire-quote-normalization.md | 59 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 docs/adr/0006-cashu-ts-owns-wire-quote-normalization.md diff --git a/CONTEXT.md b/CONTEXT.md index 78a7c7d9..70419549 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -47,9 +47,15 @@ _Avoid_: Method flow, payment workflow **Quote Observation**: A mint response that reports the current remote state of a quote and is recorded into Coco's -canonical quote row before any Quote-backed Operation is advanced from it. +canonical quote row before any Quote-backed Operation is advanced from it. Coco evaluates Quote +Observations against the existing canonical quote row and may accept, merge, or ignore them. _Avoid_: Quote refresh, subscription update +**Remote Quote Update Time**: +The mint-reported Unix timestamp, in seconds, at which a quote last changed remotely. Coco exposes +this separately from the local canonical quote row update time. +_Avoid_: Updated at, row timestamp + **Quote Identity**: A methodless reference to a mint or melt quote by mint URL and quote ID. Mint quote identities and melt quote identities are separate namespaces. @@ -73,16 +79,26 @@ older `PENDING` observation. _Avoid_: Payment status, melt lifecycle **Mint Quote Claimability**: -Whether a mint quote currently has paid value that coco can claim into proofs. BOLT11 mint quotes -are claimable when their state is `PAID`; reusable mint quotes are claimable when their paid amount -exceeds their issued amount. +Whether a mint quote currently has paid value that coco can claim into proofs. A mint quote is +claimable when its paid amount exceeds its issued amount; legacy method-specific state is +compatibility metadata, not the canonical claimability model. _Avoid_: Mint quote paid state, payment status +**Mint Quote Accounting**: +The mint-reported paid and issued totals for a mint quote. Coco treats the difference between paid +amount and issued amount as the quote's currently claimable value. +_Avoid_: Quote state, payment status + **Mint Quote Payment Observation**: A newly observed increase in paid value for a mint quote. It is distinct from Mint Quote Claimability because reusable mint quotes can already be claimable before another payment arrives. _Avoid_: Mint quote paid state, payment status +**Mint Quote Reservation**: +Locally in-flight claimable value from a reusable mint quote that Coco has assigned to a +Quote-backed Operation but the mint has not yet reflected as issued accounting. +_Avoid_: Remote issued amount, proof reservation + **Quote Expiry**: The time after which a quote can no longer receive a new payment. Expiry does not prevent claiming value that was already paid before expiry. diff --git a/docs/adr/0006-cashu-ts-owns-wire-quote-normalization.md b/docs/adr/0006-cashu-ts-owns-wire-quote-normalization.md new file mode 100644 index 00000000..4df4a2d9 --- /dev/null +++ b/docs/adr/0006-cashu-ts-owns-wire-quote-normalization.md @@ -0,0 +1,59 @@ +# cashu-ts owns wire quote normalization + +Status: proposed + +Coco relies on cashu-ts v5 `Mint` and `Wallet` APIs to normalize raw mint quote responses from +mints, including legacy quote accounting derivation. Coco does not duplicate that wire-level +normalization in `MintAdapter`; instead, Coco owns canonical quote persistence, merge rules, stale +observation handling, and operation advancement from normalized quote snapshots. + +When an incoming Quote Observation has a lower remote update timestamp than the canonical quote row, +Coco treats it as stale, ignores it for canonical quote fields, and emits no quote-updated event. +When the incoming and canonical remote update timestamps are equal but the accounting differs, Coco +treats the observation as invalid or stale, keeps the canonical row unchanged, and logs a warning. +When either side lacks a remote update timestamp, Coco falls back to comparing +`amountPaid + amountIssued` as a freshness marker. Coco accepts the observation only when that sum +increases without decreasing either accounting component. Out-of-spec accounting observations are +ignored non-destructively: Coco keeps the canonical row unchanged, emits no quote-updated event, and +logs a warning rather than breaking watcher or refresh paths. +Coco may persist a newer remote update timestamp when quote accounting and other meaningful quote +fields are unchanged, but timestamp-only freshness changes do not emit quote-updated events. +Coco may keep legacy method-specific mint quote state for compatibility, especially for BOLT11, but +mint quote behavior is driven by canonical Mint Quote Accounting rather than that state. +BOLT11 remains atomic in Coco's built-in behavior: minting from a BOLT11 quote is all-or-nothing for +the quote amount, even though readiness is derived from accounting rather than legacy state. +For reusable quotes, Coco preserves the existing local reservation model: effective local +availability subtracts in-flight Mint Quote Reservations, inferred from executing operations, and +also treats finalized local operations as issued if the mint's remote accounting has not caught up. +BOLT11 uses Mint Quote Accounting for readiness and stale protection, but it does not participate in +reusable quote reservation math because Coco treats BOLT11 mint quotes as atomic. +Coco exposes Remote Quote Update Time on canonical quote objects. Storage migrations backfill +accounting from legacy BOLT11 state when possible, preserve reusable quote accounting, and leave +Remote Quote Update Time unset because local row timestamps are not mint-reported timestamps. +Coco keeps the public BOLT11 mint quote `state` field for compatibility, marks it deprecated in +JSDoc, and avoids using it to drive mint quote behavior. +Canonical mint quote accounting fields live on the shared mint quote shape rather than inside +method-specific `quoteData`; `quoteData` is reserved for method-specific facts. +Repositories store `amountPaid`, `amountIssued`, and `remoteUpdatedAt` as first-class canonical +quote columns rather than inside method-specific JSON. +Mint quote persistence migrates away from `lastObservedRemoteState` and +`lastObservedRemoteStateAt`; those concepts belong to the old mint state model and do not apply to +melt quote state semantics. +This change does not add change-reason metadata to quote update events; event payloads continue to +carry the canonical quote, and consumers can inspect that quote if they need details. +The refactor is a public type-level breaking change because canonical mint quotes gain required +accounting fields and nullable Remote Quote Update Time. Runtime create, check, and import paths +remain compatible by deriving or normalizing legacy BOLT11 data where possible. +For explicit quote imports, Coco accepts legacy BOLT11 snapshots with `state` and `amount`, derives +canonical accounting from them, and persists the normalized canonical quote shape. +Coco also derives the deprecated public BOLT11 `state` field from accounting when a BOLT11 snapshot +has accounting but omits state, so compatibility consumers continue to receive a populated state. +Coco rejects explicitly imported mint quote accounting where issued amount exceeds paid amount. For +background Quote Observations, Coco treats that accounting as invalid, keeps the canonical row +unchanged, emits no quote-updated event, and logs a warning. +Coco persists the canonical quote method from its own route or handler context. Remote or imported +snapshots that report a conflicting method are invalid and must not be persisted under the requested +method. +Coco keeps separate local and remote update times: `updatedAt` is the local canonical row update +time in milliseconds, while `remoteUpdatedAt` is the mint-reported Remote Quote Update Time in +seconds. `remoteUpdatedAt` is nullable because legacy mints cannot provide or derive it.