Skip to content

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

Open
robwoodgate wants to merge 9 commits into
mainfrom
feat/nut04-05-widening
Open

feat(nut04/05)!: quote accounting and custom payment method base structs#698
robwoodgate wants to merge 9 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).

  • 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.

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.
@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
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.
@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 92.36%. Comparing base (f568ed3) to head (85b1b53).
⚠️ Report is 1 commits behind head on main.
✅ 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   92.40%   92.36%   -0.04%     
==========================================
  Files          54       54              
  Lines        4990     5018      +28     
  Branches     1224     1237      +13     
==========================================
+ Hits         4611     4635      +24     
- Misses        160      163       +3     
- Partials      219      220       +1     
Flag Coverage Δ
integration 38.87% <45.45%> (+0.20%) ⬆️

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

nut hold This PR is held pending the merge of a new or updated NUT

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant