Skip to content

NUT-18: Payment request consolidation (and extra fees for non-preferred mints) - #381

Merged
callebtc merged 26 commits into
cashubtc:mainfrom
robwoodgate:payment-request-consolidation
Jul 21, 2026
Merged

NUT-18: Payment request consolidation (and extra fees for non-preferred mints)#381
callebtc merged 26 commits into
cashubtc:mainfrom
robwoodgate:payment-request-consolidation

Conversation

@robwoodgate

@robwoodgate robwoodgate commented May 27, 2026

Copy link
Copy Markdown
Collaborator

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 m mint 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

  • Retains 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 fee mf compensates the receiver for melting out via that method:
    • applies only to payments from a mint outside m, or from any mint if m is not set; payments from a listed mint carry no per-method fee
    • the payer owes the lowest mf among the listed methods their mint supports
  • Adds mp (mint_preferred) to signal whether the m mint list is strict or preferred (this is a flip from ms in feat: preferred mints #380):
    • absent / false: receiver only accepts proofs from listed mints
    • true: listed mints are preferred, but other mints may be accepted
  • Specifies that the requested amount is net of input fees: the payer MUST select proofs such that sum(proofs) - input_fee(proofs) >= amount owed, with the input fee computed from the sending mint's input_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).
  • Updates NUT-26 TLV tags for the new fields
  • Adds test vectors covering the combined behaviour

Note: earlier revisions had a flat fr (fee_required) surcharge for non-preferred mints, then an nf (net_fees) opt-in flag. Review discussion concluded fr was redundant once mf is scoped to non-preferred mints, and nf had no sensible false case (no receiver wants to be underpaid by dust), so net-of-input-fees is now the unconditional rule. Both flags have been dropped and the supported_method sub-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:

  • m remains the mint list
  • mp defines whether that list is strict or advisory
  • sm lets receivers express the mint capabilities required for the payment flow, with per-method fees that compensate for melting out of a non-preferred mint
  • the requested amount is net of input fees, so receivers get the amount they asked for regardless of the sender's proof selection

This avoids two competing mint-list fields while covering the desired behaviours.

@github-project-automation github-project-automation Bot moved this to Backlog in nuts May 27, 2026
@robwoodgate robwoodgate changed the title Payment request consolidation Payment request consolidation (and add extr fees for non-preferred mints) May 27, 2026
@robwoodgate robwoodgate changed the title Payment request consolidation (and add extr fees for non-preferred mints) Payment request consolidation (and add extra fees for non-preferred mints) May 27, 2026
@robwoodgate robwoodgate changed the title Payment request consolidation (and add extra fees for non-preferred mints) Payment request consolidation (and extra fees for non-preferred mints) May 27, 2026
robwoodgate added a commit to d4rp4t/cashu-ts that referenced this pull request May 28, 2026
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.
@robwoodgate

robwoodgate commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

@callebtc , @thesimplekid - we have three implementations, can we consider final review / merge please.

cc @ye0man

@KvngMikey

Copy link
Copy Markdown
Member

4 implementations:
cashubtc/nutshell#1059

robwoodgate added a commit to d4rp4t/cashu-ts that referenced this pull request Jun 25, 2026
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.
@robwoodgate

robwoodgate commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

The only thing I still don't love about this is the mint_strict boolean being true if omitted.

It opens up all kinds of edge case issues as undefined is true but other falsey values are false.

I'd rather default to false on omitted, or if strict is the desirable default, flip the boolean semantic to mint_prefer, where true means the list of mints is a preference and false or omitted means it is strict

What are the thoughts on this?

Comment thread 18.md Outdated
@thesimplekid

Copy link
Copy Markdown
Collaborator

The only thing I still don't love about this is the mint_strict boolean being true if omitted.

It opens up all kinds of edge case issues as undefined is true but other falsey values are false.

I'd rather default to false on omitted, or if strict is the desirable default, flip the boolean semantic to mint_prefer, where true means the list of mints is a preference and false or omitted means it is strict

What are the thoughts on this?

I agree, I think default true is an anti pattern and we should avoid it.

@robwoodgate

Copy link
Copy Markdown
Collaborator Author

I agree, I think default true is an anti pattern and we should avoid it.

Agreed. Would you prefer to keep mint_strict and make the default false, or flip the boolean semantics to mint_prefer / mint_suggest or similar?

@thesimplekid

Copy link
Copy Markdown
Collaborator

I agree, I think default true is an anti pattern and we should avoid it.

Agreed. Would you prefer to keep mint_strict and make the default false, or flip the boolean semantics to mint_prefer / mint_suggest or similar?

I would flip the semantics. mint_preferred / mp: absent or false means the existing strict m behavior, true means the m list is advisory/preferred.

@robwoodgate

Copy link
Copy Markdown
Collaborator Author

I would flip the semantics. mint_preferred / mp: absent or false means the existing strict m behavior, true means the m list is advisory/preferred.

yeah, that's my leaning too. @d4rp4t @a1denvalu3, do you agree?

@KvngMikey

Copy link
Copy Markdown
Member

I would flip the semantics. mint_preferred / mp: absent or false means the existing strict m behavior, true means the m list is advisory/preferred.

i agree with this ...

@robwoodgate

Copy link
Copy Markdown
Collaborator Author

ms (mint_strict) now replaced with mp (mint_preferred).

This gives the same semantic, but flipped so an omitted value means false (strict).

@robwoodgate
robwoodgate requested a review from thesimplekid June 26, 2026 22:12
@robwoodgate

Copy link
Copy Markdown
Collaborator Author

Cashu-TS updated to use new mp semantic (replacing ms)

@robwoodgate
robwoodgate requested a review from callebtc July 1, 2026 18:52
@robwoodgate

Copy link
Copy Markdown
Collaborator Author

CTS updated to use new fee per method:

Comment thread 18.md Outdated
@callebtc callebtc changed the title Payment request consolidation (and extra fees for non-preferred mints) NUT-18: Payment request consolidation (and extra fees for non-preferred mints) Jul 4, 2026
…f (method_fee) is for non-preferred mints only
@robwoodgate

robwoodgate commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Following discussion in the TS Devs meeting, I've updated the spec as follows:

Commit: 7ff7864

  • fr (fee required) dropped
  • mf (method fee) semantics clarified to ONLY apply for non-preferred mints (or when m is unset)
  • amount specified as being net of per-proof input fees

Note: Change of spec, implementations will need to be updated if this new direction is agreeable

Related 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 thesimplekid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Updated cdk to current spec in cashubtc/cdk@b5a5eb4

Comment thread 18.md Outdated
KvngMikey added a commit to KvngMikey/nutshell that referenced this pull request Jul 10, 2026
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.

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

Nutshell updated to current spec cashubtc/nutshell@91195fd

@a1denvalu3

Copy link
Copy Markdown
Contributor

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.
@callebtc
callebtc merged commit df1ca22 into cashubtc:main Jul 21, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in nuts Jul 21, 2026
robwoodgate added a commit to cashubtc/cashu-ts that referenced this pull request Jul 21, 2026
# 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>
d4rp4t added a commit to d4rp4t/DotNut that referenced this pull request Aug 2, 2026
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.
KvngMikey added a commit to KvngMikey/nutshell that referenced this pull request Aug 10, 2026
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.
d4rp4t added a commit to Kukks/DotNut that referenced this pull request Aug 30, 2026
* 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.
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.

6 participants