feat(nut18): add PaymentRequestBuilder - #780
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #780 +/- ##
===========================================
+ Coverage 39.13% 95.52% +56.38%
===========================================
Files 55 55
Lines 5358 5428 +70
Branches 1328 1345 +17
===========================================
+ Hits 2097 5185 +3088
+ Misses 2794 107 -2687
+ Partials 467 136 -331
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:
|
robwoodgate
force-pushed
the
feat/payment-request-builder
branch
from
July 14, 2026 13:17
221ca2a to
e5dbfdf
Compare
robwoodgate
marked this pull request as ready for review
July 14, 2026 19:11
42 tasks
Fluent authoring API for NUT-18 payment requests: transport helpers (nostr NIP tags, HTTP POST), mint URL normalization with dedupe, and cross-field validation at build() (mp without mints, duplicate sm methods, amount without unit). lock() serializes a P2PKOptions into the request's nut10 option via the new p2pkOptionsToPRNut10(); the NUT-11 tag serialization moves out of OutputData into a shared buildP2PKTags() so authored requests and sender-side secrets cannot drift. RESERVED_P2PK_TAGS is consolidated into P2PK_KNOWN_TAG_KEYS.
…lidation Renames the feesFor/amountToSend mintMethods param to meltMethods: the sm check is against the mint's NUT-05 melt methods, and the old name invited the mint-vs-melt mixup the spec wording now rules out. PaymentRequestBuilder.build() rejects supported methods without a unit and unit() rejects an empty string, so a request the encoder would refuse cannot be authored via the builder.
robwoodgate
force-pushed
the
feat/payment-request-builder
branch
from
July 21, 2026 20:31
c864ea5 to
9cb626a
Compare
robwoodgate
added a commit
that referenced
this pull request
Jul 21, 2026
> 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.
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.
Summary
Adds a fluent builder for authoring NUT-18 payment requests, complementing the
PaymentRequestOptionsconstructor 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.Design notes
build()is the single validation point (mpwithout mints, duplicatesmmethods). ThePaymentRequestclass itself stays lenient since it is also the decode type for foreign requests, whose round-trip must be preserved. Same split asP2PKOptions(data) vsP2PKBuilder.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 newp2pkOptionsToPRNut10()serializes aP2PKOptionsinto the request'snut10option, and the NUT-11 tag serialization moves out ofOutputData.createSingleP2PKDatainto a sharedbuildP2PKTags()so authored requests and sender-side secrets cannot drift. A round-trip test pinslock()againsttoP2PKOptions().blindKeysthrows (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:uMUST be set whenais set).unit()remains for amountless requests.addMintrunsnormalizeUrl(the same canonicalizationMintapplies) somentries byte-match what senders compare against.PaymentRequestBuilderlives inPaymentRequest.ts: a separate file would makestatic PaymentRequest.builder()a circular import.RESERVED_P2PK_TAGSfolded intoP2PK_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
OutputDatarefactor is behaviour-preserving:buildP2PKTagsis the same logic extracted verbatim (threshold gating, post-blinding key layout, tag order), covered by the untouched OutputData tests.docs-src/usage/payment_requests.md); API report regenerated.