NUT-18: Payment request consolidation (and extra fees for non-preferred mints) - #381
Conversation
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.
|
@callebtc , @thesimplekid - we have three implementations, can we consider final review / merge please. cc @ye0man |
|
4 implementations: |
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.
|
The only thing I still don't love about this is the It opens up all kinds of edge case issues as I'd rather default to What are the thoughts on this? |
I agree, I think default true is an anti pattern and we should avoid it. |
Agreed. Would you prefer to keep |
I would flip the semantics. |
yeah, that's my leaning too. @d4rp4t @a1denvalu3, do you agree? |
i agree with this ... |
|
This gives the same semantic, but flipped so an omitted value means |
|
Cashu-TS updated to use new |
|
CTS updated to use new fee per method: |
…f (method_fee) is for non-preferred mints only
|
Following discussion in the TS Devs meeting, I've updated the spec as follows: Commit: 7ff7864
Note: Change of spec, implementations will need to be updated if this new direction is agreeableRelated discussion: #381 (comment) cc @callebtc , @thesimplekid , @d4rp4t , @KvngMikey |
Payers must select proofs so the amount received after input fees covers the request. Removes the nf flag and its TLV tag; the supported_method sub-TLV moves to tag 0x0a. Test vectors regenerated.
thesimplekid
left a comment
There was a problem hiding this comment.
Updated cdk to current spec in cashubtc/cdk@b5a5eb4
Drop `fr` and replace `sm: List[str]` with `sm: List[SupportedMethod]` (`mn`, optional `mf`) per cashubtc/nuts#381. NUT-26 tag 0x0a is now a repeatable supported_method sub-TLV (0x01=method, 0x02=fee u64 BE); 0x0b removed. Also fixes a mis-scoped guard error message (tag 0x01 is id, not transport). Byte-verified against frozen spec vectors.
There was a problem hiding this comment.
Nutshell updated to current spec cashubtc/nutshell@91195fd
|
Updated CDK PR here |
NUT-05 melt settings are (method, unit) pairs, so the sm check cannot be evaluated without a unit, and mf is denominated in it. Require u whenever a or sm is set, and state the melt check explicitly in terms of the request unit.
# 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 earlier `ms` (`mint_strict`) flag with **inverted polarity**: a strict mint list is now the omitted default, and an advisory/preferred list is the explicit `mp = true` - `sm` (`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 if `m` is not set), and the payer owes the **lowest** `mf` among 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 to `mp` (`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 the `ms` form carried. Later revisions promoted `sm` from 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-method `mf`, and finally dropped `nf` entirely: the spec now makes the requested amount unconditionally net of input fees, so the flag carried no information. With `nf` gone, `supported_method` moved down to TLV tag 0x0a. ## Changes - `RawPaymentRequest`: rename `ms` → `mp`; add `sm?: RawSupportedMethod[]` (`{mn, mf?}`) - `PaymentRequest`: rename `mintsStrict` → `mintsPreferred`; add `supportedMethods` (`SupportedMethod[]`, `{method, fee?: Amount}`); plumb through `toRawRequest`, `toEncodedCreqA`, `toEncodedCreqB`, `fromRawRequest`, `fromEncodedRequest` - `DecodedTLVPaymentRequest`: rename `mintsStrict` → `mintsPreferred`; add `supportedMethods?: Array<{method, fee?: bigint}>` - TLV codec: TLV 0x09 renamed `mint_strict` → `mint_preferred` (default-if-absent flips `1` → `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-TLV - `get isMintListStrict()`: resolves the spec's default-to-strict semantic — `undefined` when no mint list is set, `mintsPreferred !== true` otherwise - `feesFor(mint, mintMethods?)` / `amountToSend(mint, mintMethods?)`: price the applicable per-method fee — `0` for a mint in `m`, otherwise the lowest `mf` among the `sm` entries the mint supports (`mintMethods`). Pure fee calculators; they do not validate admissibility (strict-list / `sm`-method gating is the caller's concern). `amountToSend` throws on an amountless request - `single_use` is now tri-state optional (`absent` / `false` / `true`) instead of a `boolean` defaulting to `false`, so it round-trips the absent-vs-explicit-`false` distinction rather than always serializing `single_use = 0`. **Breaking (v5):** a decoded request that omits the flag now exposes `singleUse: undefined` (was `false`) — documented in `migration-5.0.0.md` - Both flags (`mp`, `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 verbatim - **Breaking (v5):** the `PaymentRequest` constructor now takes a single `PaymentRequestOptions` object 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 sites `undefined`-padded and swap-prone, and every spec revision appended another slot. A positional call fails to type-check; documented in `migration-5.0.0.md` - Public exports: `PaymentRequestOptions`, `RawSupportedMethod`, `SupportedMethod` - Usage guide (`docs-src/usage/payment_requests.md`) covers decoding, strictness, fee pricing and selecting proofs with `includeFees(true)` so the receiver nets the requested amount ## Reviewer Notes - Wire identity preserved: `mp` and `sm` are only serialized when explicitly set, and `single_use` is omitted when absent — encoded requests round-trip canonically (minimal CBOR, no redundant default tags). The NUT-18 and NUT-26 spec vectors for the combined `mp`/`sm` payload (with a per-method fee on `bolt12`) are pinned byte-for-byte (creqA + creqB) and match the upstream test vectors - `mintsPreferred` reflects raw wire state (undefined when absent). The spec's "absent means strict when `m` is set" semantic is exposed via the `isMintListStrict` getter rather than mutated into the field on decode — keeps roundtrip clean and avoids gaining a tag the original didn't carry - Per-method `fee` uses `Amount` in the class, mirroring how `amount` is handled; raw/TLV layers stay numeric/bigint - `canPayFrom`-style admissibility helper was considered and deliberately skipped: the mint-list case is already a correct one-liner via `isMintListStrict`, and a useful `sm`-method check needs the sending mint's `MintInfo` (not self-contained on `PaymentRequest`), so it is left for when a caller actually needs to gate --------- Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
cashubtc/nuts#381 consolidated the mint preference proposals: - `ms`/MintsStrict becomes `mp`/MintPreferred, with flipped semantics (true = advisory list, absent/false = strict) - `fr`/FeeReserve is dropped; the per-method `mf` fee replaces it - `sm` is now a list of {mn, mf?} objects instead of bare method names - NUT-26 TLV: 0x09 mint_preferred, 0x0a supported_method sub-TLV (0x01 method, 0x02 fee); 0x0b is gone Also stops emitting an empty CBOR `t` array, which the new test vector (no transports) tripped over on decode/re-encode.
Drop `fr` and replace `sm: List[str]` with `sm: List[SupportedMethod]` (`mn`, optional `mf`) per cashubtc/nuts#381. NUT-26 tag 0x0a is now a repeatable supported_method sub-TLV (0x01=method, 0x02=fee u64 BE); 0x0b removed. Also fixes a mis-scoped guard error message (tag 0x01 is id, not transport). Byte-verified against frozen spec vectors.
* feat: mint strict flag * Update DotNut/Encoding/PaymentRequestBech32Encoder.cs * add new pr fields * add tests * Update DotNut.Tests/Unit/Nut18Tests.cs * feat: align payment request fields with merged NUT-18/26 spec cashubtc/nuts#381 consolidated the mint preference proposals: - `ms`/MintsStrict becomes `mp`/MintPreferred, with flipped semantics (true = advisory list, absent/false = strict) - `fr`/FeeReserve is dropped; the per-method `mf` fee replaces it - `sm` is now a list of {mn, mf?} objects instead of bare method names - NUT-26 TLV: 0x09 mint_preferred, 0x0a supported_method sub-TLV (0x01 method, 0x02 fee); 0x0b is gone Also stops emitting an empty CBOR `t` array, which the new test vector (no transports) tripped over on decode/re-encode.
Consolidates and Supersedes: #355, #380
Summary
Consolidates the two open NUT-18 payment request proposals around mint preferences, strict mint lists, fee signalling, and required mint payment methods.
This PR keeps the existing
mmint list as the single mint-list field, and adds small companion fields to clarify how that list should be interpreted and how the payer compensates the receiver for fees.Changes
sm(supported_method) from feat(nut-18, nut-26): add preferred_mints to payment requests #355 to require that the payer's mint supports at least one listed payment method (e.g.bolt11,bolt12,onchain), now as a list of{mn, mf?}objects. The optional per-method feemfcompensates the receiver for melting out via that method:m, or from any mint ifmis not set; payments from a listed mint carry no per-method feemfamong the listed methods their mint supportsmp(mint_preferred) to signal whether themmint list is strict or preferred (this is a flip frommsin feat: preferred mints #380):false: receiver only accepts proofs from listed mintstrue: listed mints are preferred, but other mints may be acceptedsum(proofs) - input_fee(proofs) >= amount owed, with the input fee computed from the sending mint'sinput_fee_ppk(NUT-02). This protects the receiver from pathological proof sets (e.g. 1000 x 1 sat proofs at 250 ppk costs the receiver 250 sats to swap).Note: earlier revisions had a flat
fr(fee_required) surcharge for non-preferred mints, then annf(net_fees) opt-in flag. Review discussion concludedfrwas redundant oncemfis scoped to non-preferred mints, andnfhad no sensiblefalsecase (no receiver wants to be underpaid by dust), so net-of-input-fees is now the unconditional rule. Both flags have been dropped and thesupported_methodsub-TLV uses tag 0x0a.Rationale
There were overlapping proposals for preferred mints and strict mint lists. Instead of introducing a separate preferred mint list, this keeps the request format minimal:
mremains the mint listmpdefines whether that list is strict or advisorysmlets receivers express the mint capabilities required for the payment flow, with per-method fees that compensate for melting out of a non-preferred mintThis avoids two competing mint-list fields while covering the desired behaviours.