Skip to content
Draft
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
2 changes: 1 addition & 1 deletion 00.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Implementations **SHOULD** evaluate `Π_k e( Σ_{i:K_i=K_k} w_i·Y_i , K_k )` wi
Typical batch scopes:

- A wallet receiving a token batches all v3 proofs across that token.
- A mint batches all v3 input proofs of a single `/v1/swap` or `/v1/melt` request.
- A mint batches all v3 input proofs of a single `/v1/swap`, `/v1/melt` or `/v1/transaction` request.

A batch of one proof reduces to the single-proof pairing check above.

Expand Down
126 changes: 126 additions & 0 deletions XX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# NUT-XX: Transactions

`optional`

`depends on: NUT-04 NUT-05 NUT-10`

---

This document describes a single endpoint that carries any [NUT-00](00.md#the-transaction-transcript) transaction: proofs and paid mint quotes in, blinded messages and a melt quote out, in one request. The fixed-shape endpoints of [NUT-03][03], [NUT-04][04] and [NUT-05][05] remain; this endpoint carries any combination, eg paying an invoice straight from a paid mint quote.

## Request

```http
POST https://mint.host:3338/v1/transaction
```

The request is JSON, one array per [transcript](00.md#the-transaction-transcript) container type:

```json
{
"proof_inputs": <Array[Proof]>,
"mint_quote_inputs": <Array[QuoteInput]>,
"blinded_outputs": <Array[BlindedMessage]>,
"melt_quote_outputs": <Array[MeltOutput]>,
"prefer_async": <bool> // optional: false if omitted
}
```

where `proof_inputs` and `blinded_outputs` are as in [NUT-00][00], and a `QuoteInput` is a paid mint quote ([NUT-04][04]), the amount this transaction issues against it, and its witness:

```json
{
"quote": <str>,
"amount": <int>,
"witness": <str>
}
```

`witness` is serialized as `Proof.witness` is ([NUT-00](00.md#witnesses)), over the quote input's [input digest](00.md#the-transaction-transcript).

A `MeltOutput` is a melt quote ([NUT-05][05]), the fee reserve this transaction commits to it, and for a quote offering `fee_options` ([NUT-30][30]) the selected fee option:

```json
{
"quote": <str>,
"fee_reserve": <int>,
"fee_index": <int> // only if the quote offers fee_options
}
```

Any array **MAY** be empty or omitted, and each is subject to the mint's [NUT-06][06] `max_array_length`. A blinded message with `amount` `0` is a [NUT-08][08] blank output, allowed only beside a melt quote; otherwise it **MUST** be rejected.

### Rules

- The transaction follows NUT-00's [transaction](00.md#the-transaction-transcript) and [signing](00.md#the-signing-rule) rules. A pre-v3 proof with `SIG_ALL` ([NUT-11][11]) **MUST** be rejected.
- Every mint quote **MUST** be locked and in the transaction's unit, and its `amount` **MUST** be positive.
- Every `blinded_outputs` keyset **MUST** be in the transaction's unit. Beside a melt quote, each one's `final_expiry` ([NUT-02][02]) **MUST NOT** be earlier than the melt quote's `expiry`.
- `melt_quote_outputs` **MUST NOT** hold more than one quote. The quote **MUST** be in the transaction's unit, and its `fee_reserve` **MUST** equal the quote's `fee_reserve`, or the `fee_reserve` of the entry its `fee_index` names.

### Balance

```
amount_in = sum(proof_inputs) + sum(mint quote input amounts)
amount_out = sum(blinded_outputs) + melt.amount + melt.fee_reserve + fee
change = melt.fee_reserve - melt_fee_paid
```

Each quote's `amount_issued` grows by its input `amount`, as in a partial mint ([NUT-04][04]). `fee` is the input fee of the proofs ([NUT-02][02]) plus the [quote input fee](#quote-input-fee). `melt_fee_paid` is what the payment actually cost, known only at settlement; it never exceeds `melt.fee_reserve`.

`amount_in` **MUST** equal `amount_out`. After settlement, `change` is returned in blank outputs ([NUT-08][08]): each, in request order, takes the largest amount of its keyset that does not exceed what is left of it.

### Settlement

A transaction with no melt quote settles at once, like a swap ([NUT-03][03]).

A transaction with a melt quote follows [NUT-05][05]'s pending and settlement handling, including `prefer_async`: the proofs and input quotes are pending while the payment is in flight; the blinded messages are signed and each quote's `amount_issued` raised only once it succeeds, and a failed payment leaves the proofs unspent and the quotes untouched. While an input quote is pending, it reports its settled `amount_issued`, and the mint **MUST** reject any other transaction or mint request ([NUT-04][04]) that issues from it, with error `20005`.

Before it initiates the payment, the mint **MUST** complete every check that signing the blinded messages depends on.

A transaction with a melt quote is recorded by that quote, as in a [NUT-05][05] melt: the quote reaches `PAID` only once the transaction has settled.

## Response

```json
{
"state": <str_enum[STATE]>,
"signatures": <Array[BlindSignature]>,
"melt_quotes": <Array[MeltQuoteResponse]>
}
```

- `state` is `PENDING` while a melt payment is in flight, `PAID` once the transaction has settled, or `FAILED` if the payment failed and the inputs were released.
- `signatures` has one blind signature per element of `blinded_outputs`, in request order, omitting a blank output left at zero ([NUT-08][08]). It is empty unless `state` is `PAID`.
- `melt_quotes` holds the [NUT-05][05] melt quote response for each entry of the request's `melt_quote_outputs`, in order, and is empty if there were none. Its `change` field carries the same blind signatures as `signatures`, once `state` is `PAID`.

## Quote input fee

A mint **MAY** charge for quote inputs. A quote input contributes `popcount(amount) * quote_input_fee_ppk` to the ppk sum, and [NUT-02][02]'s single rounding applies to the whole transaction:

```
fee = (sum(proof input_fee_ppk) + sum(popcount(quote amount) * quote_input_fee_ppk) + 999) // 1000
```

A mint that does not charge advertises `0`.

## Settings

```json
{
"XX": {
"supported": <bool>,
"quote_input_fee_ppk": <int>
}
}
```

[00]: 00.md
[02]: 02.md
[03]: 03.md
[04]: 04.md
[05]: 05.md
[06]: 06.md
[08]: 08.md
[10]: 10.md
[11]: 11.md
[30]: 30.md
11 changes: 11 additions & 0 deletions tests/00-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,14 @@ The melt shares the swap's `input_id` but not its `input_digest`, so neither wit
"input_digest": "c5692db35c58c47c70aa4dab49d2ede41adec95f00a231bf40cc8af5ddee030f"
}
```

**Mint quote to melt.** Paying melt quote `quote-melt-0001` straight from mint quote `quote-mint-0001` in one transaction ([NUT-XX](../XX.md)): no proofs and no change outputs, so the transcript is the mint vector's quote input container followed by the melt vector's melt quote container. The quote input's bytes are unchanged, so it keeps the mint vector's `input_id`; the new transcript gives it a new `input_digest`, which its lock key signs:

```json
{
"transcript": "12003d0100010802000f71756f74652d6d696e742d303030310300210292905560a6a511a13e383ee27e220aeffc85b7a7bc293e6935b1d3678d2098120400002200160100010802000f71756f74652d6d656c742d30303031",
"digest": "8c54caecbdd6fdc5ae77bea97ed63dbf94a6919d6bc5d71cd7145765b52e49f3",
"input_id": "d294012dd54976b05eb3e36eb218b9a6d66b19d79ec122944a35b99b8ddd8839",
"input_digest": "492728212791e2222a327b9c4fe6c17e2da0e9e7d1aebbe0062c3585909edb57"
}
```
Loading