feat!: nut-18 mint preferences (mp, sm) - #683
Merged
robwoodgate merged 17 commits intoJul 21, 2026
Merged
Conversation
3 tasks
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #683 +/- ##
==========================================
+ Coverage 95.44% 95.48% +0.04%
==========================================
Files 55 55
Lines 5292 5363 +71
Branches 1309 1329 +20
==========================================
+ Hits 5051 5121 +70
Misses 107 107
- Partials 134 135 +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:
|
Collaborator
|
Thanks @d4rp4t - Please can you run |
robwoodgate
marked this pull request as ready for review
May 28, 2026 15:42
Merged
4 tasks
Spec moved from cashubtc/nuts#380 to cashubtc/nuts#381, adding fee_reserve (fr) and supported_methods (sm) alongside the existing mint-strict flag. Adds an isMintListStrict resolver for the spec default-to-true semantic.
…onfusion An untyped falsy CBOR value (ms: 0 or ms: null) read as strict via the isMintListStrict getter's `!== false` check while serializing to false over TLV, diverging across creqA/creqB. Coerce to a real boolean at construction so every representation agrees.
robwoodgate
force-pushed
the
feat/nut18-nut26-mint-strict-flag
branch
from
June 25, 2026 10:31
5fda60d to
e51c5a9
Compare
…_use tri-state Spec NUT-18/NUT-26 replaced `ms` (mint_strict) with `mp` (mint_preferred), inverting the default: a strict mint list is the omitted default, an advisory list is the explicit flag. Rename the field, TLV tag 0x09, and the `isMintListStrict` getter to match; strict now resolves from `mp` absent or false. Also make `single_use` tri-state optional (absent/false/true) like `mp`, defensively coerce both flags to real booleans so an untyped CBOR value can't leak a non-boolean, and pin the creqA/creqB spec vectors byte-for-byte.
Collaborator
|
closing / reopening to force refresh |
Track the NUT-18/NUT-26 update: sm entries become {mn, mf?} objects
carrying an optional per-method fee that stacks additively with the
top-level fr (non-preferred-mint fee).
- RawSupportedMethod {mn, mf} wire type; model SupportedMethod {method,
fee?: Amount} mirroring feeReserve's Amount treatment
- NUT-26 tag 0x0b promoted to sub-TLV (0x01 method, 0x02 fee u64) with
encode/parseSupportedMethod; duplicate sub-tags rejected
- PaymentRequest.amountToSend(mint, method?) sums the applicable fr + mf
fees; does not validate admissibility (caller's concern)
- update pinned creqA/creqB spec vectors; both byte-match the NUT vectors
robwoodgate
force-pushed
the
feat/nut18-nut26-mint-strict-flag
branch
from
July 1, 2026 20:53
32ef298 to
7ecf99d
Compare
Split the fr + mf surcharge calc out of amountToSend into feesFor(mint, method?), which never throws and returns 0 when nothing applies. Lets callers price the fees on an amountless request (where amountToSend has no base to add to) and add them to the payer-chosen amount. amountToSend now delegates to feesFor and still throws on a missing amount, pointing callers at feesFor.
Cover decoding, mint-list admissibility (isMintListStrict), fee-aware amountToSend / feesFor (fr + mf stacking, amountless case), locked requests via toP2PKOptions, creating/encoding, and the delivery payload. Register in typedoc projectDocuments and link from the usage index.
Resolve migration-5.0.0.md add-add conflict by keeping both new sections (main's P2PKOptions kind/data refactor and this branch's singleUse tri-state note). Code files auto-merged: main's P2PKOptions/toP2PKOptions rewrite is orthogonal to this branch's feesFor/sm additions. Typecheck and full unit suite (1658 tests) green post-merge.
- qualify {@link PaymentRequest.feesFor} so api-extractor resolves it as a
class member, not a missing package export (ae-unresolved-link warning)
- regenerate etc/cashu-ts.api.md for the new PaymentRequest surface
(amountToSend, feesFor, SupportedMethod, RawSupportedMethod, sm retype)
Add malformed-payload tests for the new tag 0x0b codec: duplicate method sub-tag, duplicate fee sub-tag, and a supported_method missing its method field. Covers the reject-not-last-wins paths introduced with per-method fees (tlv.ts:393/399/407), which had no coverage.
Track the latest NUT-18/NUT-26 candidate: the top-level fee (fr) is dropped in favor of a net-of-input-fees flag (nf, TLV tag 0x0a as u8), and the per-method fee (mf) now applies only when paying from a mint outside the request's list, with the payer owing the lowest fee among the listed methods their mint supports. feesFor/amountToSend now take the mint's supported methods; spec vectors repinned.
…-strict-flag # Conflicts: # test/wallet/paymentRequests.test.ts
Replace the 11-slot positional constructor with a single PaymentRequestOptions object whose keys mirror the class properties. Adjacent same-typed slots (three booleans) and undefined padding made positional calls unreadable and swap-prone, and every spec revision appended another slot. Decode paths are unaffected; a v4-style call fails to type-check. Documented in migration-5.0.0.md.
robwoodgate
added a commit
that referenced
this pull request
Jul 6, 2026
Fork PRs run with a read-only `GITHUB_TOKEN` regardless of the requested permissions, so the sticky comment upsert always 403s and fails an otherwise green run (seen on #683). Skip the comment step when the PR head is not this repo; the job summary already carries the same report, and the score gate itself is unaffected.
…f input fees NUT-18 dropped the nf flag: the requested amount is always net of input fees, so payers should select proofs with includeFees. The supported_method TLV moves to tag 0x0a and the pinned spec vectors are updated to match.
…-strict-flag # Conflicts: # migration-5.0.0.md
…a or sm) Per the amended spec, a request that sets an amount or supported methods must carry a unit: mf and the melt-method check are denominated in it. Encoding (creqA and creqB) and fee pricing now throw on such requests; decoding stays lenient so foreign requests can still be inspected. Docs now state the sm check is against the mint's NUT-05 melt methods for the request unit.
robwoodgate
force-pushed
the
feat/nut18-nut26-mint-strict-flag
branch
from
July 14, 2026 13:16
d761336 to
4144898
Compare
42 tasks
robwoodgate
added a commit
that referenced
this pull request
Jul 21, 2026
> Stacked on #683 (based on its head branch); the diff shows that PR's commits until it merges. Last #683 commit is 4144898 ## Summary Adds a fluent builder for authoring NUT-18 payment requests, complementing the `PaymentRequestOptions` constructor from #683. The builder handles the fiddly parts an options object cannot: transport tag formats, NUT-10 lock serialization, mint URL normalization, and cross-field validation. ```ts const pr = PaymentRequest.builder() .amount(100, 'sat') // unit required with amount (NUT-18 MUST) .description('Coffee') .addMint('https://my.mint') // normalized + deduped .mintsPreferred() .addNostrTransport(nprofile) // NIP-17 tags applied .addHttpPostTransport('https://pay.example.com') .addSupportedMethod('bolt12', 5) .lock(new P2PKBuilder().addLockPubkey(pk).toOptions()) .build(); ``` ## Design notes - **Authoring-strict, wire-lenient.** Setters collect state in any order; `build()` is the single validation point (`mp` without mints, duplicate `sm` methods). The `PaymentRequest` class itself stays lenient since it is also the decode type for foreign requests, whose round-trip must be preserved. Same split as `P2PKOptions` (data) vs `P2PKBuilder.toOptions()` (validated authoring). - **`lock()` closes the receiver-side NUT-10 gap.** There was a parse path (`toP2PKOptions()`) but no build path; receivers hand-serialized NUT-11 tags. The new `p2pkOptionsToPRNut10()` serializes a `P2PKOptions` into the request's `nut10` option, and the NUT-11 tag serialization moves out of `OutputData.createSingleP2PKData` into a shared `buildP2PKTags()` so authored requests and sender-side secrets cannot drift. A round-trip test pins `lock()` against `toP2PKOptions()`. `blindKeys` throws (P2BK blinding is per output at send time). For NUT-10 kinds beyond P2PK/HTLC, `nut10()` passes a raw option through. - **`amount(value, unit)`** takes the unit as a required parameter, making "amount without unit" unrepresentable (NUT-18: `u` MUST be set when `a` is set). `unit()` remains for amountless requests. - **`addMint`** runs `normalizeUrl` (the same canonicalization `Mint` applies) so `m` entries byte-match what senders compare against. - **Colocation.** `PaymentRequestBuilder` lives in `PaymentRequest.ts`: a separate file would make `static PaymentRequest.builder()` a circular import. - `RESERVED_P2PK_TAGS` folded into `P2PK_KNOWN_TAG_KEYS` (identical contents, one set with both meanings documented); tag helpers moved to a new "Lock Tag Serialization" section in NUT11.ts. ## Reviewer notes - The `OutputData` refactor is behaviour-preserving: `buildP2PKTags` is the same logic extracted verbatim (threshold gating, post-blinding key layout, tag order), covered by the untouched OutputData tests. - Usage guide updated (`docs-src/usage/payment_requests.md`); API report regenerated.
69 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NUT: cashubtc/nuts#381
Summary
Aligns the NUT-18 / NUT-26 mint-preferences fields with the latest payment-request consolidation candidate.
mp(mint_preferred, TLV 0x09) — whether the mint list is advisory. Supersedes the earlierms(mint_strict) flag with inverted polarity: a strict mint list is now the omitted default, and an advisory/preferred list is the explicitmp = truesm(supported_method, TLV 0x0a, repeatable) — payment methods the payee accepts (e.g.bolt11,bolt12,onchain), each carrying an optional per-method fee (mf)The per-method fee compensates the receiver for melting out: it applies only to payments from a mint outside
m(or from any mint ifmis not set), and the payer owes the lowestmfamong the listed methods their mint supports. Payments from a listed mint carry no fee.The requested amount is always net of input fees (NUT-18): the payer selects proofs such that the receiver can swap/melt them without dipping below the amount. This maps directly onto the existing
includeFees(true)proof-selection machinery; the usage guide shows the wiring.Motivation
The original branch implemented
ms(mint_strict) from an earlier draft. The consolidation candidate renames the strictness flag tomp(mint_preferred) and inverts the default so the flag only needs to be set for the non-default (advisory) case — strict is implied by omission — removing the redundant "omitted == explicit default" encoding themsform carried.Later revisions promoted
smfrom a bare list of method-name strings to a list of{mn, mf?}objects, replaced the top-level non-preferred-mint fee (fr) with a net-of-input-fees flag (nf), folding all melt-out pricing into the per-methodmf, and finally droppednfentirely: the spec now makes the requested amount unconditionally net of input fees, so the flag carried no information. Withnfgone,supported_methodmoved down to TLV tag 0x0a.Changes
RawPaymentRequest: renamems→mp; addsm?: RawSupportedMethod[]({mn, mf?})PaymentRequest: renamemintsStrict→mintsPreferred; addsupportedMethods(SupportedMethod[],{method, fee?: Amount}); plumb throughtoRawRequest,toEncodedCreqA,toEncodedCreqB,fromRawRequest,fromEncodedRequestDecodedTLVPaymentRequest: renamemintsStrict→mintsPreferred; addsupportedMethods?: Array<{method, fee?: bigint}>mint_strict→mint_preferred(default-if-absent flips1→0); tag 0x0a (supported_method, repeatable) is a sub-TLV — sub-tag 0x01 method (string), 0x02 fee (u64, optional). Duplicate method/fee sub-tags are rejected, matching the nut10 sub-TLVget isMintListStrict(): resolves the spec's default-to-strict semantic —undefinedwhen no mint list is set,mintsPreferred !== trueotherwisefeesFor(mint, mintMethods?)/amountToSend(mint, mintMethods?): price the applicable per-method fee —0for a mint inm, otherwise the lowestmfamong thesmentries the mint supports (mintMethods). Pure fee calculators; they do not validate admissibility (strict-list /sm-method gating is the caller's concern).amountToSendthrows on an amountless requestsingle_useis now tri-state optional (absent/false/true) instead of abooleandefaulting tofalse, so it round-trips the absent-vs-explicit-falsedistinction rather than always serializingsingle_use = 0. Breaking (v5): a decoded request that omits the flag now exposessingleUse: undefined(wasfalse) — documented inmigration-5.0.0.mdmp,single_use) are defensively coerced to real booleans on decode, so an untyped CBOR value can't leak a non-boolean into the getter or be re-serialized verbatimPaymentRequestconstructor now takes a singlePaymentRequestOptionsobject whose keys mirror the class properties, replacing the positional form. The old signature had grown to 11 optional slots (three of them adjacent booleans), making call sitesundefined-padded and swap-prone, and every spec revision appended another slot. A positional call fails to type-check; documented inmigration-5.0.0.mdPaymentRequestOptions,RawSupportedMethod,SupportedMethoddocs-src/usage/payment_requests.md) covers decoding, strictness, fee pricing and selecting proofs withincludeFees(true)so the receiver nets the requested amountReviewer Notes
mpandsmare only serialized when explicitly set, andsingle_useis omitted when absent — encoded requests round-trip canonically (minimal CBOR, no redundant default tags). The NUT-18 and NUT-26 spec vectors for the combinedmp/smpayload (with a per-method fee onbolt12) are pinned byte-for-byte (creqA + creqB) and match the upstream test vectorsmintsPreferredreflects raw wire state (undefined when absent). The spec's "absent means strict whenmis set" semantic is exposed via theisMintListStrictgetter rather than mutated into the field on decode — keeps roundtrip clean and avoids gaining a tag the original didn't carryfeeusesAmountin the class, mirroring howamountis handled; raw/TLV layers stay numeric/bigintcanPayFrom-style admissibility helper was considered and deliberately skipped: the mint-list case is already a correct one-liner viaisMintListStrict, and a usefulsm-method check needs the sending mint'sMintInfo(not self-contained onPaymentRequest), so it is left for when a caller actually needs to gate