Skip to content

feat(nut18): pay and validate payment requests end to end - #787

Merged
robwoodgate merged 2 commits into
mainfrom
feat/pay-payment-request
Jul 21, 2026
Merged

feat(nut18): pay and validate payment requests end to end#787
robwoodgate merged 2 commits into
mainfrom
feat/pay-payment-request

Conversation

@robwoodgate

@robwoodgate robwoodgate commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #780 (targets its head branch, so the diff shows only this PR's commits).
Retarget to main when #780 merges, last commit of 780 was c864ea5

Summary

Closes the gap between documenting the NUT-18 payer/receiver rules and enforcing them. Until now the docs choreographed four separate steps (mint-list check, melt-method check, amountToSend, includeFees(true)); a wallet that skipped one could underpay the receiver or violate a spec MUST silently.

Payer side: wallet.ops.sendToRequest(pr, proofs, amount?) builds a send that enforces the request in one step:

  • strict mint list (URL-normalized membership)
  • unit rule (request unit must match the wallet)
  • NUT-05 melt-method support for the request unit, resolved from the wallet's own MintInfo (callers never pass method strings, so the mint-vs-melt and unit-binding mistakes are unrepresentable)
  • lowest applicable per-method fee (mf), waived for listed mints
  • the request's nut10 lock via asP2PK, throwing on kinds it cannot express (an exotic lock would otherwise send unlocked)
  • net-of-input-fees selection (includeFees(true))

It returns the normal SendBuilder, so output types, keysets and offline modes chain as usual. Amountless requests take the payer-chosen amount as the third argument.

Receiver side: wallet.isPaymentRequestSatisfied(pr, proofs, expectedAmount?) checks sum(proofs) - inputFees >= amount + mf before a payment is treated as settled, with input fees from the wallet's keysets and mf priced from its melt methods. Scope is the amount only; DLEQ/lock validation stays separate.

PaymentRequest.includesMint(url) is the shared primitive: mint-list membership compared after URL normalization (unparsable foreign entries fall back to a raw comparison). The docs' previous pr.mints?.includes(myMint) example had the normalization footgun this removes.

Design notes

  • Both checks live on Wallet/WalletOps because that is where the state lives (mint URL, unit, keyset fees, melt methods). An earlier draft put the settlement check on PaymentRequest and needed three wallet-state options passed in; review moved it.
  • sendToRequest returns the builder rather than running it, so .includeFees(false) after the fact remains possible: deliberate opt-out, not a hole, and it keeps the builder contract uniform.
  • The low-level pieces (feesFor, amountToSend, isMintListStrict) are unchanged and remain the manual-control path; docs now lead with the one-step call and keep the manual recipe below it.

Tests

  • sendToRequest: fee quadrants (listed/unlisted, with/without list), URL-normalized strict-list matching, unit mismatch, unit-bound melt rejection, amountless handling, lock honour/reject.
  • isPaymentRequestSatisfied: net-of-fees formula against a 1000 ppk keyset (the spec's dust scenario), mf inside/outside the mint list, unit mismatch, amountless expectation.
  • includesMint: case/trailing-slash normalization, unparsable entries, absent list.

@github-project-automation github-project-automation Bot moved this to Backlog in cashu-ts Jul 14, 2026
@robwoodgate
robwoodgate changed the base branch from main to feat/payment-request-builder July 14, 2026 17:04
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.55556% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.52%. Comparing base (49f6034) to head (236de85).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/wallet/WalletOps.ts 92.30% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #787      +/-   ##
==========================================
- Coverage   95.54%   95.52%   -0.02%     
==========================================
  Files          55       55              
  Lines        5428     5473      +45     
  Branches     1345     1359      +14     
==========================================
+ Hits         5186     5228      +42     
- Misses        107      108       +1     
- Partials      135      137       +2     
Flag Coverage Δ
integration 38.36% <0.00%> (-0.34%) ⬇️

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.

@robwoodgate robwoodgate added the stacked hold PR is based on top of another PR, so should be merged in sequence. See comments for details. label Jul 14, 2026
@robwoodgate
robwoodgate marked this pull request as ready for review July 14, 2026 19:34
@robwoodgate
robwoodgate force-pushed the feat/payment-request-builder branch from c864ea5 to 9cb626a Compare July 21, 2026 20:31
Base automatically changed from feat/payment-request-builder to main July 21, 2026 20:37
wallet.ops.sendToRequest(pr, proofs, amount?) builds a send that
enforces the payer-side MUSTs in one step: strict mint list, unit
rule, NUT-05 melt-method support for the request unit, lowest
applicable mf, the request's nut10 lock (throws on kinds it cannot
honour), and net-of-input-fees selection via includeFees(true).
Returns the normal SendBuilder for chaining.

wallet.isPaymentRequestSatisfied(pr, proofs, expectedAmount?) is the
receiver-side check: sum(proofs) - input fees >= amount + mf, with
input fees and melt methods resolved from the wallet's own state.

PaymentRequest.includesMint(url) resolves mint-list membership with
URL normalization; both new paths and the docs use it.
…terms

Restructures the usage doc: payer story (decode, pay, deliver) and
payee story (create, validate) first, the rule-by-rule material moved
under a marked Manual control section. Sender/receiver wording was
ambiguous (request vs payment) and is now payer/payee throughout the
doc, TSDocs and comments.
@robwoodgate
robwoodgate force-pushed the feat/pay-payment-request branch from 693d4d1 to 236de85 Compare July 21, 2026 20:40
@robwoodgate robwoodgate removed the stacked hold PR is based on top of another PR, so should be merged in sequence. See comments for details. label Jul 21, 2026
@robwoodgate
robwoodgate merged commit 2c218f4 into main Jul 21, 2026
18 checks passed
@robwoodgate
robwoodgate deleted the feat/pay-payment-request branch July 21, 2026 20:43
@github-project-automation github-project-automation Bot moved this from Backlog to Done in cashu-ts Jul 21, 2026
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.

1 participant