-
Notifications
You must be signed in to change notification settings - Fork 87
NUT-XX: get locked mint quotes by public keys #329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,320 @@ | ||
| # NUT-28: Third-Party Mint Quotes | ||
|
|
||
| `optional` | ||
|
|
||
| `depends on: NUT-04, NUT-20` | ||
|
|
||
| --- | ||
|
|
||
| This NUT enables third-party mint quotes where one user (`Alice`) creates and pays for a mint quote that only another user (`Bob`) can redeem. When creating a mint quote, `Alice` provides `Bob`'s public key. The mint will then allow only `Bob` to discover and redeem the quote by signing requests with the corresponding private key. | ||
|
|
||
| > [!NOTE] | ||
| > | ||
| > This NUT extends [NUT-20][20] to support payer-initiated, recipient-locked quotes. `Alice` uses `Bob`'s public key when creating the quote (instead of her own), and `Bob` can query the mint to discover quotes assigned to his public key. | ||
|
|
||
| ## Use cases | ||
|
|
||
| - **Mining pools**: A pool creates paid quotes for miners' public keys when shares are validated | ||
| - **Gift payments**: `Alice` pays for `Bob` to receive ecash without `Bob` initiating a request | ||
| - **Batch payouts**: Services create multiple paid quotes for different recipients | ||
|
|
||
| ## Quote creation | ||
|
|
||
| To create a third-party mint quote, `Alice` makes a `POST /v1/mint/quote/{method}` request with `Bob`'s public key. We present an example with the `method` being `bolt11` here. | ||
|
|
||
| ```http | ||
| POST https://mint.host:3338/v1/mint/quote/bolt11 | ||
| ``` | ||
|
|
||
| `Alice` includes the following `PostMintQuoteBolt11Request` data in her request: | ||
|
|
||
| ```json | ||
| { | ||
| "amount": <int>, | ||
| "unit": <str_enum[UNIT]>, | ||
| "description": <str>, // Optional | ||
| "pubkey": <str> // Bob's pubkey | ||
| } | ||
| ``` | ||
|
|
||
| with the requested `amount`, `unit`, and `description` according to [NUT-04][04] and [NUT-23][23]. | ||
|
|
||
| `pubkey` is `Bob`'s compressed secp256k1 public key (33 bytes, hex-encoded). The mint will require a valid signature from `Bob`'s corresponding private key to process the mint operation. | ||
|
|
||
| The mint responds with a `PostMintQuoteBolt11Response` as in [NUT-20][20]: | ||
|
|
||
| ```json | ||
| { | ||
| "quote": <str>, | ||
| "request": <str>, | ||
| "state": <str_enum[STATE]>, | ||
| "expiry": <int>, | ||
| "pubkey": <str> | ||
| } | ||
| ``` | ||
|
|
||
| `Alice` then pays the Lightning invoice in `request`. The quote state transitions to `PAID`. | ||
|
|
||
| ### Example | ||
|
|
||
| Request of `Alice` with curl: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3338/v1/mint/quote/bolt11 -d '{"amount": 100, "unit": "sat", "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"}' -H "Content-Type: application/json" | ||
| ``` | ||
|
|
||
| Response of the mint: | ||
|
|
||
| ```json | ||
| { | ||
| "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", | ||
| "request": "lnbc1000n1pj4apw9...", | ||
| "amount": 100, | ||
| "unit": "sat", | ||
| "state": "UNPAID", | ||
| "expiry": 1701704757, | ||
| "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac" | ||
| } | ||
| ``` | ||
|
|
||
| ## Querying quotes by public key | ||
|
|
||
| `Bob` can query the mint to discover quotes assigned to his public key. The wallet makes a `POST /v1/mint/quote/{method}/pubkey` request. | ||
|
|
||
| ```http | ||
| POST https://mint.host:3338/v1/mint/quote/bolt11/pubkey | ||
| ``` | ||
|
|
||
| `Bob` includes the following `PostMintQuotesByPubkeyRequest` data: | ||
|
|
||
| ```json | ||
| { | ||
| "pubkey": <str>, | ||
| "timestamp": <int>, | ||
| "signature": <str>, | ||
| "unit": <str|null>, | ||
| "state": <str|null> | ||
| } | ||
| ``` | ||
|
|
||
| | Field | Type | Description | | ||
| | --- | --- | --- | | ||
| | `pubkey` | `str` | `Bob`'s compressed secp256k1 public key (33 bytes, hex-encoded) | | ||
| | `timestamp` | `int` | Unix timestamp of the request (for replay protection) | | ||
| | `signature` | `str` | BIP340 Schnorr signature on the message (see below) | | ||
| | `unit` | `str\|null` | Optional: Filter by currency unit (e.g., `"sat"`) | | ||
| | `state` | `str\|null` | Optional: Filter by quote state (e.g., `"PAID"`) | | ||
|
|
||
| ### Signature scheme | ||
|
|
||
| To query quotes, `Bob` must sign a message proving ownership of the public key. The message to sign is: | ||
|
|
||
| ``` | ||
| msg_to_sign = "quote_lookup" || pubkey || timestamp | ||
| ``` | ||
|
|
||
| Where `||` denotes concatenation, `"quote_lookup"` is a literal UTF-8 domain separator string, `pubkey` is the hex-encoded public key string, and `timestamp` is the UTF-8 string representation of the Unix timestamp. | ||
|
|
||
| The signature is a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) Schnorr signature on the SHA-256 hash of `msg_to_sign`. | ||
|
|
||
| ### Replay protection | ||
|
|
||
| The mint **MUST** implement replay protection: | ||
|
|
||
| 1. Reject requests where `timestamp` differs from the mint's clock by more than the configured tolerance (default: 60 seconds) | ||
| 2. Track recent `(pubkey, timestamp)` pairs and reject duplicates within the validity window | ||
|
|
||
| > [!TIP] | ||
| > | ||
| > Wallets can use the `time` field in the mint info response ([NUT-06][06]) to adjust for clock skew. | ||
|
|
||
| ### Response | ||
|
|
||
| The mint responds with a `PostMintQuotesByPubkeyResponse`: | ||
|
|
||
| ```json | ||
| { | ||
| "quotes": [ | ||
| { | ||
| "quote": <str>, | ||
| "request": <str>, | ||
| "amount": <int>, | ||
| "unit": <str>, | ||
| "state": <str_enum[STATE]>, | ||
| "expiry": <int|null>, | ||
| "pubkey": <str> | ||
| }, | ||
| ... | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| The response contains an array of quote objects matching the query criteria. If no quotes are found, the array is empty. | ||
|
|
||
| ### Example | ||
|
|
||
| Request of `Bob` with curl: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3338/v1/mint/quote/bolt11/pubkey -H "Content-Type: application/json" -d \ | ||
| '{ | ||
| "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", | ||
| "timestamp": 1701704800, | ||
| "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acb6e9c2f0e3f9a1b5c8d7e6f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2", | ||
| "state": "PAID" | ||
| }' | ||
| ``` | ||
|
|
||
| Response: | ||
|
|
||
| ```json | ||
| { | ||
| "quotes": [ | ||
| { | ||
| "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", | ||
| "request": "lnbc1000n1pj4apw9...", | ||
| "amount": 100, | ||
| "unit": "sat", | ||
| "state": "PAID", | ||
| "expiry": 1701704757, | ||
| "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ## Minting tokens | ||
|
|
||
| After discovering a `PAID` quote, `Bob` mints tokens using the standard [NUT-20][20] flow. `Bob` signs the mint request with his private key. | ||
|
|
||
| ```http | ||
| POST https://mint.host:3338/v1/mint/bolt11 | ||
| ``` | ||
|
|
||
| `Bob` includes the following `PostMintBolt11Request` data: | ||
|
|
||
| ```json | ||
| { | ||
| "quote": <str>, | ||
| "outputs": <Array[BlindedMessage]>, | ||
| "signature": <str> | ||
| } | ||
| ``` | ||
|
|
||
| The `signature` follows the [NUT-20][20] message aggregation scheme: | ||
|
|
||
| ``` | ||
| msg_to_sign = quote || B_0 || ... || B_(n-1) | ||
| ``` | ||
|
|
||
| The mint verifies the signature against the `pubkey` stored with the quote and responds with blind signatures as in [NUT-04][04]. | ||
|
average-gary marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### Example | ||
|
|
||
| Request of `Bob` with curl: | ||
|
|
||
| ```bash | ||
| curl -X POST https://mint.host:3338/v1/mint/bolt11 -H "Content-Type: application/json" -d \ | ||
| '{ | ||
| "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", | ||
| "outputs": [ | ||
| { | ||
| "amount": 64, | ||
| "id": "009a1f293253e41e", | ||
| "B_": "035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d" | ||
| }, | ||
| { | ||
| "amount": 32, | ||
| "id": "009a1f293253e41e", | ||
| "B_": "0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6" | ||
| }, | ||
| { | ||
| "amount": 4, | ||
| "id": "009a1f293253e41e", | ||
| "B_": "02407b3c1d4a8e6f5b9c7d2e1f0a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b" | ||
| } | ||
| ], | ||
| "signature": "e5bc2a6f8c..." | ||
| }' | ||
| ``` | ||
|
|
||
| ## State transitions | ||
|
|
||
| ``` | ||
| UNPAID ──[Alice pays]──► PAID ──[Bob mints]──► ISSUED | ||
| │ │ | ||
| │ invoice expiry │ keyset final_expiry | ||
| ▼ ▼ | ||
| EXPIRED EXPIRED | ||
| ``` | ||
|
|
||
| Quote states follow [NUT-04][04]: | ||
|
|
||
| - `UNPAID`: Quote created, Lightning invoice not yet paid | ||
| - `PAID`: Invoice paid, waiting for recipient to mint | ||
| - `ISSUED`: Tokens have been minted | ||
| - `EXPIRED`: Quote expired before completion | ||
|
|
||
| ### Expiry policy | ||
|
|
||
| Quotes **SHOULD** respect the keyset's `final_expiry` ([NUT-01][01]). Mints define their own retention policy for `PAID` quotes that have not been redeemed. | ||
|
average-gary marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Errors | ||
|
|
||
| See [Error Codes][errors]: | ||
|
|
||
| - `20010`: Signature for quote lookup invalid | ||
| - `20011`: Timestamp for quote lookup outside valid window | ||
| - `20012`: Duplicate request (replay detected) | ||
|
|
||
| Errors from [NUT-20][20] also apply: | ||
|
|
||
| - `20008`: Mint quote with `pubkey` but no valid `signature` provided for mint request | ||
| - `20009`: Mint quote requires `pubkey` but none given or invalid `pubkey` | ||
|
|
||
| ## Settings | ||
|
|
||
| The settings for this NUT indicate support for third-party mint quote lookup. They are part of the info response of the mint ([NUT-06][06]) which in this case reads: | ||
|
|
||
| ```json | ||
| { | ||
| "28": { | ||
| "supported": <bool>, | ||
| "methods": <Array[str]>, | ||
| "timestamp_tolerance_secs": <int|null>, | ||
| "quote_retention_days": <int|null> | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| | Field | Type | Description | | ||
| | --- | --- | --- | | ||
| | `supported` | `bool` | Whether third-party quote lookup is supported | | ||
| | `methods` | `str[]` | Payment methods supporting third-party quotes (e.g., `["bolt11"]`) | | ||
| | `timestamp_tolerance_secs` | `int\|null` | Tolerance window for signature timestamps in seconds (default: 60) | | ||
| | `quote_retention_days` | `int\|null` | How long `PAID` quotes are retained in days (`null` = indefinite) | | ||
|
average-gary marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Optional: Nostr discovery | ||
|
|
||
| `Alice` can notify `Bob` of a paid quote via an encrypted Nostr DM ([NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) or [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md)): | ||
|
|
||
| ```json | ||
| { | ||
| "type": "cashu_third_party_quote", | ||
| "mint": "https://mint.host:3338", | ||
| "method": "bolt11", | ||
| "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", | ||
| "amount": 100, | ||
| "unit": "sat" | ||
| } | ||
| ``` | ||
|
average-gary marked this conversation as resolved.
Outdated
|
||
|
|
||
| This is optional. The core protocol works with direct mint queries. | ||
|
|
||
| [00]: 00.md | ||
| [01]: 01.md | ||
| [04]: 04.md | ||
| [06]: 06.md | ||
| [20]: 20.md | ||
| [23]: 23.md | ||
| [errors]: error_codes.md | ||
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
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.