Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions 29.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This spec describes how a wallet can mint multiple quotes in one batched operati

## 1. Batch Checking Mint Quotes

Before minting, the wallet SHOULD verify each mint quote's current accounting state. It does this by sending:
Before minting, the wallet SHOULD verify each mint quote's current accounting data. It does this by sending:

```http
POST https://mint.host:3338/v1/mint/quote/{method}/check
Expand All @@ -26,15 +26,23 @@ The wallet includes the following body in its request:
}
```

where `quotes` is an array of _unique_ mint quote IDs.
where `quotes` is an array of _unique_ mint quote IDs. The array MAY be empty, in which case the mint MUST return an empty JSON array.

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.
If `quotes` contains duplicate IDs, the mint MUST reject the entire request with error `11016`.

#### Example
The mint returns a JSON array with exactly one entry per requested quote ID, in request order. Entry `i` corresponds to `quotes[i]`, and its `quote` field **MUST** equal the requested ID.

Known quotes are returned as mint quote objects as defined by the payment method's NUT specification. A requested ID is known only if it identifies a mint quote for the `{method}` in the endpoint path. All other IDs, including melt quote IDs and mint quote IDs for another payment method, MUST be returned as [unknown entries](#unknown-or-malformed-quote-ids).

Mints **MUST** return every mint quote they hold for `{method}`, including quotes whose `expiry` has passed. An unknown entry indicates that the ID does not identify a mint quote within this endpoint's `{method}` scope.

Mints **MUST NOT** delete a quote for which ecash is still owed (`amount_paid - amount_issued > 0`).

### Example

Below is an example for checking two bolt11 mint quotes.

##### Request
#### Request

```http
POST https://mint.host:3338/v1/mint/quote/bolt11/check
Expand All @@ -45,7 +53,7 @@ Content-Type: application/json
}
```

##### Response
#### Response

```json
[
Expand All @@ -55,7 +63,6 @@ Content-Type: application/json
"amount_paid": 100,
"amount_issued": 0,
"updated_at": 1234567800,
"state": "PAID",
"unit": "sat",
"amount": 100,
"expiry": 1234567890
Expand All @@ -66,20 +73,27 @@ Content-Type: application/json
"amount_paid": 0,
"amount_issued": 0,
"updated_at": 1234567800,
"state": "UNPAID",
"unit": "sat",
"amount": 50,
"expiry": 1234567890
}
]
```

#### Error Handling
### Unknown or Malformed Quote IDs

This is a query endpoint that uses all-or-nothing error handling, matching the behavior of the batch mint endpoint:
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs.

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.

I wonder if we should name the return type, as we do for other response shapes.

Suggested change
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs.
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `UnknownQuote` for it as below and continue processing the remaining IDs.


- If any `quote_id` is not known by the mint, the mint MUST reject the entire request and return an appropriate error
- If any `quote_id` cannot be parsed (invalid format), the mint MUST reject the entire request and return an appropriate error
```json
{
"quote": <str>,
"unknown": true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggest stating that unknown entries contain exactly quote and unknown: true, so wallets have a clean discriminator and an entry can't be both a full quote object and unknown.

```

An unknown entry MUST contain exactly the `quote` and `unknown` fields shown above. A known quote entry MUST NOT contain the `unknown` field.

`max_batch_size` (see [Batch Size Limits](#batch-size-limits)) applies to this endpoint. A request that exceeds it **MUST** be rejected in full with error `11017` rather than answered partially, so a successful response always has exactly one entry per requested quote ID.

---
Comment thread
Egge21M marked this conversation as resolved.

Expand Down Expand Up @@ -107,8 +121,10 @@ The wallet includes the following body in its request:
```

- `quotes`: array of _unique_ quote IDs.
- `quote_amounts`: array of expected amounts to mint per quote, in the same order as `quotes`.
- `quote_amounts`: array of amounts to issue from each quote in this batch, in the same order as `quotes`.
- Required for payment methods that demand an amount like bolt12; Optional for other methods like bolt11.
- If it is an array, its length MUST equal the length of `quotes`, and `quote_amounts[i]` corresponds to `quotes[i]`.
- For payment methods where `quote_amounts` is optional, if it is omitted or `null`, the amount to issue from each quote is its full currently mintable amount, `amount_paid - amount_issued`.
- `outputs`: array of blinded messages (see [NUT-00][00]).
- `signatures`: array of signatures for NUT-20 locked quotes. See [NUT-20 Support][nut-20-support]

Expand Down Expand Up @@ -192,12 +208,15 @@ The mint MUST validate the following before processing a batch mint request:
3. **Valid quote IDs**: All quote IDs MUST exist in the mint's database
4. **Payment method consistency**: All quotes MUST have the same payment method, matching `{method}` in the URL path
5. **Currency unit consistency**: All quotes MUST use the same currency unit
6. **Quote state**: All quotes MUST be in PAID state (or have a mintable amount for payment methods that allow multiple mint operations like bolt12)
7. **Amount balance**: The sum of amounts contained in the `outputs` MUST equal the sum of `quote_amounts` (bolt11) or MUST NOT exceed it (bolt12)
8. **Signature validation (NUT-20)**: The `signatures` array length MUST match the `quotes` array length; locked quotes MUST include a valid signature; unlocked quotes MUST NOT include one
6. **Per-quote amount**: All quotes MUST have a positive currently mintable amount (`amount_paid - amount_issued`). If `quote_amounts` is an array, each `quote_amounts[i]` MUST be positive and MUST NOT exceed the currently mintable amount of `quotes[i]`.
7. **Expiry**: A quote with a positive currently mintable amount remains mintable after its `expiry` has passed. The mint MUST NOT reject issuance solely because the quote has expired.
8. **Amount balance**: The sum of amounts contained in the `outputs` MUST equal the sum of the amounts to issue from the quotes. For payment methods where `quote_amounts` is optional, when it is omitted or `null`, this is the sum of each quote's currently mintable amount.
9. **Signature validation (NUT-20)**: The `signatures` array length MUST match the `quotes` array length; locked quotes MUST include a valid signature; unlocked quotes MUST NOT include one

Implementations MAY impose additional constraints such as maximum batch size based on their resource limitations. If any validation fails, the mint MUST reject the entire batch and return an appropriate error without minting any quotes.

If all validations succeed, the mint MUST atomically increase each quote's `amount_issued` by the corresponding amount issued from that quote. For payment methods where `quote_amounts` is optional, if it was omitted or `null`, the mint increases each quote's `amount_issued` by its full currently mintable amount before the batch.

### NUT-20 support

Per [NUT-20][20], quotes can require authentication via signatures. When using batch minting with NUT-20 locked quotes:
Expand Down
136 changes: 129 additions & 7 deletions tests/29-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,145 @@ The following is the corresponding response with a blind signature.
}
```

## Check endpoint with unknown quotes
After the batch succeeds, the mint increases `quote_id_a.amount_issued` by 5 and `quote_id_b.amount_issued` by 3.

The following is an invalid check request containing an unknown quote ID.
## Successful batch mint with omitted quote amounts

For bolt11, `quote_amounts` can be omitted. The mint then uses the full currently mintable amount of each quote. Given `quote_id_a` with `amount_paid = 5` and `amount_issued = 2`, and `quote_id_b` with `amount_paid = 3` and `amount_issued = 0`, the following is a valid request for 6 sats:

```json
{ "quotes": ["known-1", "bogus", "unknown-2"] }
{
"quotes": ["quote_id_a", "quote_id_b"],
"outputs": [{ "amount": 6, "id": "keyset_1", "B_": "<blinded_message>" }]
}
```

Per NUT-29, quote check uses all-or-nothing error handling. If any quote is unknown, the entire request must be rejected.
After the batch succeeds, the mint atomically sets `quote_id_a.amount_issued` to 5 and `quote_id_b.amount_issued` to 3.

## Successful batch mint with an expired quote

Given an expired quote with `amount_paid = 5` and `amount_issued = 2`, the following request is valid because the quote still has a currently mintable amount of 3 sats:

```json
{
"code": "UNKNOWN_QUOTE",
"error": "one or more quote IDs are unknown"
"quotes": ["expired_quote_id"],
"quote_amounts": [3],
"outputs": [{ "amount": 3, "id": "keyset_1", "B_": "<blinded_message>" }]
}
```

The mint MUST NOT reject issuance solely because the quote's `expiry` has passed. After the batch succeeds, the mint sets `expired_quote_id.amount_issued` to 5.

## Batch mint rejects an amount exceeding a quote's balance

Given `quote_id_a` with `amount_paid = 5` and `amount_issued = 2`, the following request is invalid because its allocation of 4 sats exceeds the quote's currently mintable amount of 3 sats:

```json
{
"quotes": ["quote_id_a"],
"quote_amounts": [4],
"outputs": [{ "amount": 4, "id": "keyset_1", "B_": "<blinded_message>" }]
}
```

Expected behavior:

- The mint rejects the whole request with an error.
- No outputs are signed.
- `quote_id_a.amount_issued` remains 2.

## Check endpoint marks unknown and malformed quotes

The following check request contains two known quote IDs, one malformed quote ID, and one unknown quote ID.

```json
{ "quotes": ["known-1", "not-a-valid-quote-id", "unknown-2", "known-2"] }
```

The mint returns one entry per requested quote ID, in request order. The malformed and unknown quote IDs are returned as `unknown` entries.

```json
[
{
"quote": "known-1",
"amount_paid": 5,
"amount_issued": 0,
"updated_at": 1234567800
},
{ "quote": "not-a-valid-quote-id", "unknown": true },
{ "quote": "unknown-2", "unknown": true },
{
"quote": "known-2",
"amount_paid": 0,
"amount_issued": 0,
"updated_at": 1234567800
}
]
Comment thread
Egge21M marked this conversation as resolved.
```

Each unknown entry contains exactly `quote` and `unknown: true`. Known quote entries do not contain the `unknown` field.

If the mint cannot handle any of the requested quote IDs, every entry in the response is an `unknown` entry. The response is never shorter than the request.

For example:

```json
[
{ "quote": "not-a-valid-quote-id", "unknown": true },
{ "quote": "unknown-2", "unknown": true }
]
```

## Check endpoint is scoped to mint quotes for the requested method

The following request to `/v1/mint/quote/bolt11/check` contains a bolt11 mint quote ID, a bolt12 mint quote ID, and a melt quote ID.

```json
{ "quotes": ["bolt11-mint-quote", "bolt12-mint-quote", "bolt11-melt-quote"] }
```

Only the bolt11 mint quote is within the endpoint's scope. The other IDs are returned as `unknown` entries, even if the mint holds records for them elsewhere.

```json
[
{
"quote": "bolt11-mint-quote",
"amount_paid": 5,
"amount_issued": 0,
"updated_at": 1234567800
},
{ "quote": "bolt12-mint-quote", "unknown": true },
{ "quote": "bolt11-melt-quote", "unknown": true }
]
```

## Check endpoint accepts an empty quotes array

The following empty request is valid:

```json
{ "quotes": [] }
```

The mint returns an empty array:

```json
[]
```

## Check endpoint rejects duplicate quote IDs

The following request is invalid because the quote ID occurs twice:

```json
{ "quotes": ["quote_id_dup", "quote_id_dup"] }
```

Expected behavior:

- The mint rejects the entire request with error code `11016`.
- The mint does not return a partial or positional response.

## Batch mint atomic failure

The following is an invalid batch mint request containing one unknown quote ID, causing the entire batch to fail atomically with no partial minting.
Expand All @@ -52,7 +174,7 @@ Expected behavior:

- The mint rejects the whole request with an error.
- No outputs are signed.
- No quote state is consumed/changed by partial processing.
- No quote's `amount_issued` is increased by partial processing.

## Batch mint rejects empty quotes array

Expand Down
Loading