Skip to content

feat(nut04/05)!: quote accounting and custom payment method base structs - #698

Merged
robwoodgate merged 12 commits into
mainfrom
feat/nut04-05-widening
Jul 8, 2026
Merged

feat(nut04/05)!: quote accounting and custom payment method base structs#698
robwoodgate merged 12 commits into
mainfrom
feat/nut04-05-widening

Conversation

@robwoodgate

@robwoodgate robwoodgate commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

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_issued and updated_at become base mint quote response fields for every payment method; state is 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 / MeltQuoteGenericResponse passthrough 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 method field: 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 generic mint_quote/melt_quote notifications become routable by method.

Follows up on #672 (nuts#374 method_name), which already merged.

Design notes

  • Required-but-derived accounting. For mints that predate quote accounting, amount_paid/amount_issued are derived from the legacy single-use state + amount (UNPAID → 0/0, PAID → amount/0, ISSUED → amount/amount); conversely a missing bolt11 state is derived from the accounting fields. Underivable responses throw, matching the existing melt-side strictness. updated_at is number | null (null when the mint does not report it — it cannot be derived).
  • Base shapes per the nuts#382 review suggestions. Mint quote request base gains optional amount/description; expiry moves into the mint quote response base (normalized to null when unset); melt quote request base gains optional amount; melt quote response base gains required request and optional fee_reserve (bolt methods keep it required, onchain keeps fee_options).
  • Mintable amount enforced for all methods. validateMintQuoteAvailableAmount loses its bolt12/onchain gate: mintable = amount_paid − amount_issued is checked whenever the quote object carries accounting fields. Stored quotes without them (pre-accounting) skip the check, as before.
  • Method-id validation (^[a-z0-9_-]+$) and the generic /v1/{mint,melt}/quote/{method} plumbing already existed and are unchanged.

Breaking changes

  • MintQuoteBaseResponse requires amount_paid, amount_issued, updated_at, expiry; MeltQuoteBaseResponse requires request. Type-breaking for code constructing quote literals; runtime consumers are unaffected because normalization guarantees the fields.
  • Non-conformant quote responses that previously passed through unvalidated (mint quotes missing quote/request/unit or underivable accounting; melt quotes missing request) now throw Invalid response from mint.

Sequencing

Suggest merging in order:#675 → this.

Test plan

  • New unit tests: state↔accounting derivation matrix, updated_at/expiry normalization, custom-method base validation (mint + melt), unknown-field passthrough, generalized available-amount enforcement.
  • 1605 node tests pass; tsc clean; api report regenerated.

@github-project-automation github-project-automation Bot moved this to Backlog in cashu-ts Jun 18, 2026
robwoodgate added a commit that referenced this pull request Jun 18, 2026
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
robwoodgate added a commit that referenced this pull request Jun 18, 2026
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
robwoodgate added a commit that referenced this pull request Jun 18, 2026
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.
robwoodgate added a commit that referenced this pull request Jun 18, 2026
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.
@robwoodgate robwoodgate added stacked hold PR is based on top of another PR, so should be merged in sequence. See comments for details. nut hold This PR is held pending the merge of a new or updated NUT and removed stacked hold PR is based on top of another PR, so should be merged in sequence. See comments for details. labels Jun 18, 2026
@robwoodgate robwoodgate added this to the v5.0 milestone Jun 20, 2026
@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.15%. Comparing base (80a55b9) to head (75a302f).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/mint/Mint.ts 95.23% 1 Missing and 1 partial ⚠️
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     
Flag Coverage Δ
integration 38.56% <45.45%> (+0.22%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@KvngMikey KvngMikey left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solid work here, LGTM!

@robwoodgate robwoodgate added the experimental release PR is in current @experimental release. Install: npm i @cashu/cashu-ts@experimental label Jul 1, 2026
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.
@robwoodgate
robwoodgate force-pushed the feat/nut04-05-widening branch from e8e17a6 to 6a06679 Compare July 3, 2026 13:36
@robwoodgate

Copy link
Copy Markdown
Collaborator Author

Only cashubtc/nuts#382 pending merge now.

That PR makes request a first class NUT-05 (melt) base field (it has to be for custom methods to share their request instructions).

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.

@robwoodgate robwoodgate added the meeting-discussion Put this on the weekly meeting agenda label Jul 6, 2026
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.
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧬 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):

  • mint/Mint.ts:1237 BlockStatement: {}
  • mint/Mint.ts:1339 BlockStatement: {}
  • mint/Mint.ts:1340 StringLiteral: ""
  • mint/Mint.ts:1340 ObjectLiteral: {}
  • mint/Mint.ts:1341 StringLiteral: ""
  • mint/Mint.ts:216 OptionalChaining: data.signatures
  • mint/Mint.ts:473 OptionalChaining: data.signatures
  • mint/Mint.ts:548 OptionalChaining: data.signatures
  • mint/Mint.ts:859 OptionalChaining: data.states
  • mint/Mint.ts:954 OptionalChaining: data.outputs
  • mint/Mint.ts:954 OptionalChaining: data.signatures
  • mint/Mint.ts:990 OptionalChaining: this.ws.close
  • mint/Mint.ts:1030 LogicalOperator: mintInfo && (await this.getLazyMintInfo())
  • mint/Mint.ts:1052 LogicalOperator: mintInfo && (await this.getLazyMintInfo())
  • mint/Mint.ts:1076 LogicalOperator: init.headers && {}
  • mint/Mint.ts:1160 StringLiteral: ‘‘
  • mint/Mint.ts:1165 ConditionalExpression: true
  • mint/Mint.ts:1180 ConditionalExpression: true
  • mint/Mint.ts:1180 LogicalOperator: data.amount_paid != null || data.amount_issued != null
  • mint/Mint.ts:1180 ConditionalExpression: true
  • mint/Mint.ts:1186 StringLiteral: ""
  • mint/Mint.ts:1186 ObjectLiteral: {}
  • mint/Mint.ts:1193 StringLiteral: ""
  • mint/Mint.ts:1198 LogicalOperator: (typeof data.quote !== 'string' || typeof data.request !== 'string') && typeof d
  • mint/Mint.ts:1198 ConditionalExpression: false
  • mint/Mint.ts:1198 LogicalOperator: typeof data.quote !== 'string' && typeof data.request !== 'string'
  • mint/Mint.ts:1198 ConditionalExpression: false
  • mint/Mint.ts:1202 StringLiteral: ""
  • mint/Mint.ts:1202 ObjectLiteral: {}
  • mint/Mint.ts:1199 ConditionalExpression: false

Full report (all files): download the mutation-report-pr artifact from this run. Codecov shows lines this PR left uncovered; this shows changed lines that run but aren’t asserted.

@robwoodgate
robwoodgate merged commit 0e373f9 into main Jul 8, 2026
19 checks passed
@robwoodgate
robwoodgate deleted the feat/nut04-05-widening branch July 8, 2026 11:37
@github-project-automation github-project-automation Bot moved this from Backlog to Done in cashu-ts Jul 8, 2026
@robwoodgate robwoodgate removed nut hold This PR is held pending the merge of a new or updated NUT experimental release PR is in current @experimental release. Install: npm i @cashu/cashu-ts@experimental meeting-discussion Put this on the weekly meeting agenda labels Jul 8, 2026
robwoodgate added a commit that referenced this pull request Jul 9, 2026
🤖 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants