feat(nut04/05)!: quote accounting and custom payment method base structs - #698
Conversation
Subscribe to mint/melt quote updates independent of payment method: prefer the generic mint_quote/melt_quote kinds when the mint advertises them, else fan out across the per-method kinds it advertises (bolt11, bolt12, custom) and merge them. Quote event helpers are now generic over the response type (default generic), and "paid" is accounting-based (amount_paid > amount_issued) with a legacy state fallback. Builds on #698. Claude-Session: https://claude.ai/code/session_01WF8XpkeC5uV1aYhpHxw79Q
Subscribe to mint/melt quote updates independent of payment method: prefer the generic mint_quote/melt_quote kinds when the mint advertises them, else fan out across the per-method kinds it advertises (bolt11, bolt12, custom) and merge them. Quote event helpers are now generic over the response type (default generic), and "paid" is accounting-based (amount_paid > amount_issued) with a legacy state fallback. Builds on #698. Claude-Session: https://claude.ai/code/session_01WF8XpkeC5uV1aYhpHxw79Q
Subscribe to mint/melt quote updates independent of payment method: prefer the generic mint_quote/melt_quote kinds when the mint advertises them, else fan out across the per-method kinds it advertises (bolt11, bolt12, custom) and merge them. Quote event helpers are now generic over the response type (default generic), and "paid" is accounting-based (amount_paid > amount_issued) with a legacy state fallback. Builds on #698.
Subscribe to mint/melt quote updates independent of payment method: prefer the generic mint_quote/melt_quote kinds when the mint advertises them, else fan out across the per-method kinds it advertises (bolt11, bolt12, custom) and merge them. Fanned-out kinds isolate errors, so one kind's subscribe failure no longer tears down the siblings that work. Quote event helpers are now generic over the response type (default generic), and "paid" is accounting-based (amount_paid > amount_issued) with a legacy state fallback. Builds on #698.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #698 +/- ##
==========================================
- Coverage 95.20% 95.15% -0.06%
==========================================
Files 54 54
Lines 5068 5098 +30
Branches 1253 1267 +14
==========================================
+ Hits 4825 4851 +26
- Misses 103 106 +3
- Partials 140 141 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Implements cashubtc/nuts#377: amount_paid, amount_issued and updated_at become base mint quote response fields for every payment method. For mints that predate quote accounting, the amounts are derived from the legacy single-use state; conversely, a missing bolt11 state is derived from the accounting fields. updated_at is null when not reported. Generic mint quote responses now get base normalization (quote, request, unit, accounting) like melt quotes already did, and the wallet enforces the mintable amount (paid minus issued) for all methods, not just bolt12/onchain. BREAKING CHANGE: MintQuoteBaseResponse requires amount_paid, amount_issued and updated_at; non-conformant mint quote responses that previously passed through unvalidated now throw.
Implements the base-shape harmonization proposed on cashubtc/nuts#382: - MintQuoteBaseRequest gains optional amount and description; method NUTs tighten as needed (bolt11 requires amount). - MintQuoteBaseResponse gains expiry (normalized to null when unset), subsuming the per-method expiry fields. - MeltQuoteBaseRequest gains optional amount (onchain requires it). - MeltQuoteBaseResponse gains request and optional fee_reserve; bolt methods keep fee_reserve required. - Melt base validation now requires the payment request for every method, and normalizes fee_reserve when present. - New MintQuoteGenericResponse/MeltQuoteGenericResponse passthrough types are the defaults on the generic quote methods, so custom-method fields are reachable without casting. BREAKING CHANGE: melt quote responses without a request field now throw; generic quote methods default to the Generic response types.
…ctivity The canonical bolt11 flow is create quote -> pay externally -> mint with the original quote object, so a 0/0 accounting snapshot is indistinguishable from a stale pre-payment quote. Skip the client-side mintable-amount check in that case and let the mint decide; once amount_paid is non-zero the snapshot proves a payment event and the check applies as before.
Implements cashubtc/nuts#387: every mint and melt quote response carries a method field. The wallet always knows the method from the request endpoint, so normalization populates it when the mint omits it (no mint ships the field yet) and throws when a reported method disagrees with the endpoint. Once mints implement it, NUT-17 generic mint_quote/melt_quote notifications become routable by method.
Custom mint example reads quote progress via the accounting fields instead of state, and both generic examples note the automatic base normalization and the Generic response type defaults.
normalizeMint/MeltBaseFields used strict !== undefined, so an off-spec null amount_paid/amount_issued/fee_reserve slipped past the guard into Amount.from() and threw AmountError instead of funnelling to the derive path. Loose != null treats null and absent identically, matching how the rest of the file (nullIfUndefined) handles mint inconsistency.
e8e17a6 to
6a06679
Compare
|
Only cashubtc/nuts#382 pending merge now. That PR makes If that PR changes, the fallback is to relax that one field to optional on MeltQuoteBaseResponse. No shipping mint offers custom methods yet, so nothing can break in the interim. |
The mutation tests from main predate the NUT-04/05 quote base widening: fixtures now need request and unit on every quote response, expiry range errors report the shared mintQuote.expiry field, and the mintable-amount check applies to bolt11 once accounting reports a payment event.
🧬 Mutation testing (incremental)✅ Mutation score: 88.9% (break threshold: 80%) — repo-wide 5232 killed / 565 survived / 87 no-coverage. Surviving mutants in files this PR changed (292):
Full report (all files): download the |
🤖 I have created a release *beep* *boop* --- ## [5.0.0-rc.3](v5.0.0-rc.2...v5.0.0-rc.3) (2026-07-08) ### ⚠ BREAKING CHANGES * **crypto:** align NUT-29 batch quote signatures with amended spec ([#675](#675)) * **nut04/05:** quote accounting and custom payment method base structs ([#698](#698)) ### Features * **crypto:** deterministic P2PK & NUT-20 quote-lock key derivation ([#697](#697)) ([042e56c](042e56c)) * **nut04/05:** quote accounting and custom payment method base structs ([#698](#698)) ([0e373f9](0e373f9)) * **nut29:** add batch quote checks ([#768](#768)) ([854d91f](854d91f)) ### Bug Fixes * **crypto:** align NUT-29 batch quote signatures with amended spec ([#675](#675)) ([f3a3841](f3a3841)) * **p2bk:** align HTLC key slots with the [data, ...pubkeys, ...refund] order (NUT-28) ([#763](#763)) ([bb0b766](bb0b766)) * **p2pk:** cap NUT-28 locking slots at 11 (P2PK 11 keys, HTLC 10) ([#753](#753)) ([80a55b9](80a55b9)) * **scripts:** probe docker tags with and without v prefix ([#726](#726)) ([8667aca](8667aca)) * **utils:** throw CTSError for malformed token templates ([#742](#742)) ([c60cd35](c60cd35)) ### Miscellaneous Chores * **deps:** bump actions/setup-node from 4 to 6 in /.github/actions/integration-against-mint in the github-actions group ([#729](#729)) ([b94af5f](b94af5f)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Matures the NUT-04/05 base payloads so wallets can handle generic and custom payment methods, implementing:
Align NUT-04 mint quote accounting with amount_paid / amount_issued nuts#377 — mint quote accounting:
amount_paid,amount_issuedandupdated_atbecome base mint quote response fields for every payment method;stateis deprecated (but always populated for bolt11).NUT-04/05: custom payment methods nuts#382 — custom payment methods: base request/response structs widened per the shapes suggested in review, generic quote responses get base normalization + validation, and new
MintQuoteGenericResponse/MeltQuoteGenericResponsepassthrough types are the defaults on the generic quote methods so custom-method fields are reachable without casting.add method field to quote responses, closes #378 nuts#387 — every quote response carries a
methodfield: populated from the request endpoint when the mint omits it (no mint ships it yet), throwing when a reported method disagrees with the endpoint. Once mints implement it, NUT-17 genericmint_quote/melt_quotenotifications become routable by method.Follows up on #672 (nuts#374
method_name), which already merged.Design notes
amount_paid/amount_issuedare derived from the legacy single-usestate+amount(UNPAID → 0/0, PAID → amount/0, ISSUED → amount/amount); conversely a missing bolt11stateis derived from the accounting fields. Underivable responses throw, matching the existing melt-side strictness.updated_atisnumber | null(null when the mint does not report it — it cannot be derived).amount/description;expirymoves into the mint quote response base (normalized tonullwhen unset); melt quote request base gains optionalamount; melt quote response base gains requiredrequestand optionalfee_reserve(bolt methods keep it required, onchain keepsfee_options).validateMintQuoteAvailableAmountloses its bolt12/onchain gate: mintable =amount_paid − amount_issuedis checked whenever the quote object carries accounting fields. Stored quotes without them (pre-accounting) skip the check, as before.^[a-z0-9_-]+$) and the generic/v1/{mint,melt}/quote/{method}plumbing already existed and are unchanged.Breaking changes
MintQuoteBaseResponserequiresamount_paid,amount_issued,updated_at,expiry;MeltQuoteBaseResponserequiresrequest. Type-breaking for code constructing quote literals; runtime consumers are unaffected because normalization guarantees the fields.quote/request/unitor underivable accounting; melt quotes missingrequest) now throwInvalid response from mint.Sequencing
Suggest merging in order:#675 → this.
Test plan
updated_at/expirynormalization, custom-method base validation (mint + melt), unknown-field passthrough, generalized available-amount enforcement.