diff --git a/28.md b/28.md new file mode 100644 index 000000000..43c598abc --- /dev/null +++ b/28.md @@ -0,0 +1,93 @@ +# NUT-28: Mint Quote Lookup by Public Key + +`optional` + +`depends on: NUT-04, NUT-20` + +--- + +This NUT adds an endpoint for users to query mint quotes associated with their public key. This enables third-party mint quotes where one user creates and pays a quote for another user to redeem. + +> [!NOTE] +> +> Quote creation with a public key and minting with a signature are defined in [NUT-20][20]. This NUT only adds the ability to discover quotes by 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 + +## Querying quotes by public key + +To query quotes assigned to a public key, the wallet makes a `POST /v1/mint/quote/{method}/pubkey` request. + +```http +POST https://mint.host:3338/v1/mint/quote/bolt11/pubkey +``` + +The wallet includes the following `PostMintQuotesByPubkeyRequest` data: + +```json +{ + "pubkey": , + "timestamp": , + "signature": , + "unit": , + "state": +} +``` + +| Field | Type | Description | +| --- | --- | --- | +| `pubkey` | `str` | Compressed secp256k1 public key (33 bytes, hex-encoded) | +| `timestamp` | `int` | Unix timestamp (provides entropy for signature) | +| `signature` | `str` | BIP340 Schnorr signature (see below) | +| `unit` | `str\|null` | Optional filter by currency unit | +| `state` | `str\|null` | Optional filter by quote state | + +### Signature scheme + +The message to sign is: + +``` +msg_to_sign = "quote_lookup" || pubkey || timestamp || unit || state +``` + +Where `||` denotes concatenation. All fields are UTF-8 strings. If `unit` or `state` is null, use an empty string. + +The signature is a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) Schnorr signature on `SHA256(msg_to_sign)`. + +### Response + +The mint responds with a `PostMintQuotesByPubkeyResponse`: + +```json +{ + "quotes": +} +``` + +Where `MintQuoteResponse` is the quote response type defined in [NUT-04][04]. + +## Errors + +See [Error Codes][errors]: + +- `20010`: Signature for quote lookup invalid + +## Settings + +The settings for this NUT are part of the mint info response ([NUT-06][06]): + +```json +{ + "28": { + "supported": + } +} +``` + +[04]: 04.md +[06]: 06.md +[20]: 20.md +[errors]: error_codes.md diff --git a/README.md b/README.md index 60d2f47ab..18aa6e6ab 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [25][25] | Payment Method: BOLT12 | [cdk], [cashu-ts][ts] | [cdk-mintd] | | [26][26] | Payment Request Bech32m Encoding | [cdk] | - | | [27][27] | Nostr Mint Backup | [Cashu.me][cashume], [cdk] | - | +| [28][28] | Third-Party Mint Quotes | - | - | #### Wallets: @@ -102,3 +103,4 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio [25]: 25.md [26]: 26.md [27]: 27.md +[28]: 28.md diff --git a/error_codes.md b/error_codes.md index e3fdfb9f3..989d93613 100644 --- a/error_codes.md +++ b/error_codes.md @@ -27,6 +27,7 @@ | 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | | 20008 | Signature for mint request invalid | [NUT-20][20] | | 20009 | Pubkey required for mint quote | [NUT-20][20] | +| 20010 | Signature for quote lookup invalid | [NUT-28][28] | | 30001 | Endpoint requires clear auth | [NUT-21][21] | | 30002 | Clear authentication failed | [NUT-21][21] | | 31001 | Endpoint requires blind auth | [NUT-22][22] | @@ -50,3 +51,4 @@ [20]: 20.md [21]: 21.md [22]: 22.md +[28]: 28.md diff --git a/tests/28-test.md b/tests/28-test.md new file mode 100644 index 000000000..a921cada8 --- /dev/null +++ b/tests/28-test.md @@ -0,0 +1,33 @@ +# NUT-28 Test Vectors + +## Quote Lookup Signature + +### Message Construction + +For a request with: +- `pubkey` = `03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac` +- `timestamp` = `1701704800` +- `unit` = `sat` +- `state` = `PAID` + +``` +msg_to_sign = "quote_lookup" || pubkey || timestamp || unit || state + = "quote_lookup03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac1701704800satPAID" +``` + +For a request with null filters: +- `pubkey` = `03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac` +- `timestamp` = `1701704800` +- `unit` = null +- `state` = null + +``` +msg_to_sign = "quote_lookup" || pubkey || timestamp || "" || "" + = "quote_lookup03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac1701704800" +``` + +The signature is BIP340 Schnorr on `SHA256(msg_to_sign)`. + +### Invalid Signature + +A request with an invalid signature should return error code `20010`.