From 4f4f6970cfc546321148553514fea7e75d8a93ba Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 24 May 2026 08:08:03 +0200 Subject: [PATCH] feat: align nut04 with amount_paid/issued + updated_at --- 04.md | 47 ++++++++++++++++++++++++++++++++--------------- 20.md | 8 +++++++- 23.md | 16 +++++++++++++--- 25.md | 9 ++++++--- 29.md | 12 +++++++++--- 30.md | 11 +++++++---- 6 files changed, 74 insertions(+), 29 deletions(-) diff --git a/04.md b/04.md index 18f1d468..4905bcbe 100644 --- a/04.md +++ b/04.md @@ -52,19 +52,47 @@ The mint `Bob` responds with a quote that includes some common fields for all me "quote": , // UUID v7 "request": , "unit": , + "amount_paid": , + "amount_issued": , + "updated_at": , + "state": , // Deprecated, optional // Additional method-specific fields will be included } ``` -Where `quote` is the quote ID string in UUID v7 format, `request` is the payment request for the quote, and `unit` corresponds to the value provided in the request. +Where: + +- `quote` is the quote ID in UUID v7 format +- `request` is the payment request for the quote +- `unit` corresponds to the value provided in the request +- `amount_paid` is the total amount that has been paid to the mint for this quote and is eligible for minting, denominated in `unit` +- `amount_issued` is the total amount of ecash that has been issued for this quote, denominated in `unit` +- `updated_at` is a Unix timestamp integer indicating when the quote was last updated +- `state` is a deprecated compatibility field + +Mints **MUST** include `amount_paid`, `amount_issued`, and `updated_at` in all mint quote responses. `amount_paid` and `amount_issued` **MUST** be non-negative integers, and `amount_issued` **MUST NOT** exceed `amount_paid`. + +The amount currently mintable for a quote is `amount_paid - amount_issued`. Mints **MUST NOT** issue ecash whose total output amount exceeds `amount_paid - amount_issued`. If a wallet mints less than the currently mintable amount, `amount_issued` only increases by the amount that was issued. + +`amount_paid` and `amount_issued` are the canonical way to track the state of a mint quote. The deprecated `state` field **SHOULD** be included for single-use quotes for compatibility with older clients. Wallets **SHOULD** use `amount_paid` and `amount_issued` instead of `state` whenever these fields are present. + +For single-use quotes, the deprecated finite `state` can be derived from `amount_paid` and `amount_issued`: + +- `"UNPAID"` if `amount_paid == 0` and `amount_issued == 0` +- `"PAID"` if `amount_paid > amount_issued` +- `"ISSUED"` if `amount_paid == amount_issued` and `amount_issued > 0` + +For reusable quotes, no finite `state` can fully represent whether the quote may receive future payments. Wallets **MUST** use `amount_paid` and `amount_issued` to determine the currently mintable amount. + +Mints **MUST** update `updated_at` whenever `amount_paid` or `amount_issued` changes. Mints **SHOULD** ensure that `updated_at` monotonically increases for each quote, even if multiple updates occur within the same timestamp resolution. Wallets that receive multiple responses for the same quote **MUST NOT** replace locally stored quote data with a response whose `updated_at` is lower than the latest processed value for that quote. Wallets **MUST NOT** decrease locally stored `amount_paid` or `amount_issued` values based on stale responses. > [!CAUTION] > > `quote` is a **unique and random** id generated by the mint to internally look up the payment state. `quote` **SHOULD** be UUID v7 with all 74 variable bits generated by a CSPRNG and **MUST** remain a secret between user and mint and **MUST NOT** be derivable from the payment request. A third party who knows the `quote` ID can front-run and steal the tokens that this operation mints. To prevent this, use [NUT-20][20] locks to enforce public key authentication during minting. -### Check Mint Quote State +### Check Mint Quote -To check whether a mint quote has been paid, the wallet makes a `GET /v1/mint/quote/{method}/{quote_id}`. +To check the current accounting state of a mint quote, the wallet makes a `GET /v1/mint/quote/{method}/{quote_id}`. ```http GET https://mint.host:3338/v1/mint/quote/{method}/{quote_id} @@ -89,7 +117,7 @@ The wallet includes the following common data in its request: } ``` -with the `quote` being the quote ID from the previous step and `outputs` being `BlindedMessages` (see [NUT-00][00]) that the wallet requests signatures on, whose sum is `amount` as requested in the quote. +with the `quote` being the quote ID from the previous step and `outputs` being `BlindedMessages` (see [NUT-00][00]) that the wallet requests signatures on. The total output amount **MUST NOT** exceed the quote's currently mintable amount, `amount_paid - amount_issued`. The mint then responds with: @@ -146,18 +174,7 @@ The settings for this NUT indicate the supported method-unit pairs for minting. Upon receiving the `BlindSignatures` from the mint, the wallet unblinds them to generate `Proofs` (using the blinding factor `r` and the mint's public key `K`, see BDHKE [NUT-00][00]). The wallet then stores these `Proofs` in its database. [00]: 00.md -[01]: 01.md -[02]: 02.md -[03]: 03.md -[04]: 04.md -[05]: 05.md [06]: 06.md -[07]: 07.md -[08]: 08.md -[09]: 09.md -[10]: 10.md -[11]: 11.md -[12]: 12.md [20]: 20.md [23]: 23.md [25]: 25.md diff --git a/20.md b/20.md index 4bf1b598..26e77eba 100644 --- a/20.md +++ b/20.md @@ -45,7 +45,10 @@ The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: { "quote": , "request": , - "state": , + "amount_paid": , + "amount_issued": , + "updated_at": , + "state": , // Deprecated, optional "expiry": , "pubkey": // Optional <-- New } @@ -67,6 +70,9 @@ Response of `Bob`: { "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", "request": "lnbc100n1pj4apw9...", + "amount_paid": 0, + "amount_issued": 0, + "updated_at": 1701704657, "state": "UNPAID", "expiry": 1701704757, "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac" diff --git a/23.md b/23.md index fcde997e..f7f28a5f 100644 --- a/23.md +++ b/23.md @@ -28,17 +28,24 @@ The mint responds with a `PostMintQuoteBolt11Response`: "request": , // The bolt11 invoice to pay "amount": , "unit": , - "state": , + "amount_paid": , + "amount_issued": , + "updated_at": , + "state": , // Deprecated, optional "expiry": } ``` -`state` is an enum string field with possible values `"UNPAID"`, `"PAID"`, `"ISSUED"`: +`amount_paid`, `amount_issued`, and `updated_at` are defined in [NUT-04][04]. + +`state` is a deprecated enum string field with possible values `"UNPAID"`, `"PAID"`, `"ISSUED"`: - `"UNPAID"` means that the quote's request has not been paid yet. - `"PAID"` means that the quote's request has been paid but the ecash is not issued yet. - `"ISSUED"` means that the quote has been paid and the ecash has been issued. +Wallets **SHOULD** use `amount_paid` and `amount_issued` instead of `state` whenever these fields are present. + `expiry` is the Unix timestamp until which the `request` can be paid (i.e. the bolt11 invoice expiry). ### Example @@ -57,12 +64,15 @@ Response: "request": "lnbc100n1pj4apw9...", "amount": 10, "unit": "sat", + "amount_paid": 0, + "amount_issued": 0, + "updated_at": 1701704657, "state": "UNPAID", "expiry": 1701704757 } ``` -Check quote state: +Check mint quote: ```bash curl -X GET http://localhost:3338/v1/mint/quote/bolt11/019e6d5a-2347-7000-8322-05d51d498303 diff --git a/25.md b/25.md index 1a5b56ea..34a3f70e 100644 --- a/25.md +++ b/25.md @@ -36,7 +36,8 @@ The mint responds with a `PostMintQuoteBolt12Response`: "expiry": , "pubkey": , "amount_paid": , - "amount_issued": + "amount_issued": , + "updated_at": } ``` @@ -47,6 +48,7 @@ Where: - `expiry` is the Unix timestamp until which the mint quote is valid - `amount_paid` is the amount that has been paid to the mint via the bolt12 offer - `amount_issued` is the amount of ecash that has been issued for the given mint quote +- `updated_at` is defined in [NUT-04][04] ### Example @@ -69,11 +71,12 @@ curl -X POST http://localhost:3338/v1/mint/quote/bolt12 -d \ "expiry": 1701704757, "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", "amount_paid": 0, - "amount_issued": 0 + "amount_issued": 0, + "updated_at": 1701704657 } ``` -Check quote state: +Check mint quote: ```bash curl -X GET http://localhost:3338/v1/mint/quote/bolt12/019e6d5a-2347-7000-8449-370fefb42fed diff --git a/29.md b/29.md index 144a659d..6a020611 100644 --- a/29.md +++ b/29.md @@ -10,9 +10,9 @@ This spec describes how a wallet can mint multiple quotes in one batched operati --- -## 1. Batch Checking Quote State +## 1. Batch Checking Mint Quotes -Before minting, the wallet SHOULD verify that each mint quote has been paid. It does this by sending: +Before minting, the wallet SHOULD verify each mint quote's current accounting state. It does this by sending: ```http POST https://mint.host:3338/v1/mint/quote/{method}/check @@ -28,7 +28,7 @@ The wallet includes the following body in its request: where `quotes` is an array of _unique_ mint quote IDs. -The mint returns a JSON array of mint quotes objects as defined by the payment method's NUT specification. The quotes in this array MUST be in the same order as in the request. +The mint returns a JSON array of mint quote objects as defined by the payment method's NUT specification. The quotes in this array MUST be in the same order as in the request. #### Example @@ -52,6 +52,9 @@ Content-Type: application/json { "quote": "019e6d5a-2347-7000-8037-b42dae20f1fe", "request": "lnbc...", + "amount_paid": 100, + "amount_issued": 0, + "updated_at": 1234567800, "state": "PAID", "unit": "sat", "amount": 100, @@ -60,6 +63,9 @@ Content-Type: application/json { "quote": "019e6d5a-2347-7000-868a-08e59a1c6716", "request": "lnbc...", + "amount_paid": 0, + "amount_issued": 0, + "updated_at": 1234567800, "state": "UNPAID", "unit": "sat", "amount": 50, diff --git a/30.md b/30.md index 8edf1865..0a29c131 100644 --- a/30.md +++ b/30.md @@ -31,7 +31,8 @@ The mint responds with a `PostMintQuoteOnchainResponse`: "expiry": , "pubkey": , "amount_paid": , - "amount_issued": + "amount_issued": , + "updated_at": } ``` @@ -43,6 +44,7 @@ Where: - `pubkey` is the public key from the request - `amount_paid` is the total confirmed amount paid to the request in UTXOs that are eligible for minting - `amount_issued` is the amount of ecash that has been issued for the given mint quote +- `updated_at` is defined in [NUT-04][04] If `expiry` is not `null`, the wallet **SHOULD NOT** send payments to the request after `expiry`. Mints **MUST** keep monitoring transactions they detected before `expiry` until the transaction reaches the required number of confirmations or is evicted or replaced. Payments first detected by the mint after `expiry` **MUST NOT** increase `amount_paid`. @@ -66,11 +68,12 @@ curl -X POST http://localhost:3338/v1/mint/quote/onchain -d \ "expiry": 1701704757, "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", "amount_paid": 0, - "amount_issued": 0 + "amount_issued": 0, + "updated_at": 1701704657 } ``` -Check quote state: +Check mint quote: ```bash curl -X GET http://localhost:3338/v1/mint/quote/onchain/019e6d5a-2347-7000-8850-39c85ed1b5d3 @@ -78,7 +81,7 @@ curl -X GET http://localhost:3338/v1/mint/quote/onchain/019e6d5a-2347-7000-8850- ### Minting Tokens -The quote state will only update to show `amount_paid` once a Bitcoin transaction has reached the minimum number of confirmations specified in the mint's settings. +The quote accounting will only update to show `amount_paid` once a Bitcoin transaction has reached the minimum number of confirmations specified in the mint's settings. If the `onchain` mint method has a `min_amount` setting, each UTXO paid to the quote address is evaluated independently against `min_amount`. UTXOs with an amount less than `min_amount` **MUST NOT** increase `amount_paid` and **MUST NOT** count towards the mintable balance for the quote. Multiple UTXOs below `min_amount` **MUST NOT** be aggregated to reach `min_amount`.