diff --git a/69.md b/69.md new file mode 100644 index 0000000000..a96ce9c057 --- /dev/null +++ b/69.md @@ -0,0 +1,296 @@ +NIP-69 +====== + +Nostr Offer STRings +------------------- + +`rfc` `optional` + +This NIP proposes a format for static payment codes in Nostr as a successor to LNURL-Pay, enabling users to initiate Lightning Network payments by scanning or clicking a string. + +## Motivation + +Reliance on LNURL has led to centralization via custodial solutions due to the legacy baggage of IP4-NAT/Domain/SSL requirements. Nostr's use-cases are already centered around Lightning, whether as a wallet connector, zap receipts, or its overlapping network effects, rendering Nostr as a de facto "3rd layer" for Lightning and a natural successor to LNURL. + +When layered over kind 21000, NWC, or other potential Lightning RPCs, this specification enables a seamless user experience similar to legacy LNURL-Pay, without the domain and SSL requirements. Additionally, the signed nature of Nostr communications minimizes the trust requirements inherent in LNURL when a receiving node must outsource the serving of web requests. + +## Specification + +### Static Payment Code Format + +The static payment code is a bech32 (per [NIP-19](19.md)) encoded string prefixed with `noffer`. The encoded string will include the following TLV (Type-Length-Value) items: + +- `0`: The 32 bytes of the receiving service's public key, encoded in hex. +- `1`: The relay URL where the receiving service subscribes to payment requests. +- `2`: The offer identifying string. +- `3`: A flag indicating the pricing type: + - `0`: Fixed price (Price stated in 4 will not change) + - `1`: Variable price (Price must be calculated and will be reflected in the invoice, useful for products priced in fiat) + - `2`: Spontaneous payment (Payer specifies the amount in request payload) +- `4`: The price in sats (optional for display purposes). + +If neither the price nor the pricing type flag is present, the sender may assume it is a spontaneous payment offer. + +Example static payment code structure: + +``` +noffer1... + 0: + 1: + 2: + 3: (optional, 0 for fixed, 1 for variable, 2 for spontaneous) + 4: (optional) +``` + +## Integration with other NIPs + +### NIP-01 User Metadata + +Example user metadata content with `nip69` field: +```json +{ + "pubkey": "hex_pub", + "kind": 0, + "content": "{\"name\": \"bob\", \"nip05\": \"bob@example.com\", \"nip69\": \"noffer1...\"}" + // ... +} +``` + +### NIP-05 "Lightning Addresses" + +To support trust-minimized Lightning Addresses, services can add a `nip69` field in the NIP-05 content to contain an offer for spontaneous payments. This field will provide the offer on name-based lookups. + +Example NIP-05 Service Response with Offers + + +```json +{ + "names": { + "bob": "hex_pub" + }, + "nip69": { + "bob": "noffer1..." + } +} +``` + +### NIP-57 Zaps + +Instead of using the LNURL-pay callback, the "zap" flow can be initiated as content payload to an Offer: + +1. The client creates a kind 9734 zap request event as specified in NIP-57. +2. Instead of sending this to an LNURL-pay endpoint, the client sends it as the `zap` part of the encrypted content in a NIP-69 payment request. +3. The receiver processes the NIP-69 payment request, decrypts the content, extracts the kind 9734 event, and uses it to generate the invoice. +4. The receiver responds with the invoice as specified in NIP-69. +5. Once paid, the receiver generates and publishes the kind 9735 zap receipt as specified in NIP-57. + +Example NIP-57 zap request: + +```json +{ + "id": "", + "pubkey": "", + "created_at": 1234567890, + "kind": 21001, + "tags": [ + ["p", ""] + ], + "content": "\",\"zap\":}>", + "sig": "" +} +``` + +There is no inherent requirement on the service to acknowledge zap requests, since spontaneous payments are default behavior. However, both Clients and Service implementations SHOULD consider offer identifiers, where `startsWith("zap")`, indication that the service will honor zap requests for that offer. A service should not error if no zap request was included by the sender on a zap indicated offer, the service should treat it as any other spontaneous payment. + +## General Process Flow + +1. **Payer Scans or Clicks the Static Payment Code** +2. **Payer's Wallet Decodes the Payment Code** +3. **Payer's Wallet Sends a Nostr Event to the Specified Relay** + - Addressed to the receiver's public key, containing the offer identifying string. + - Event Type: Ephemeral Kind 21001 | NIP-44 Encrypted + - **Conditional**: Include the amount in sats the sender wishes to pay if the payment type is spontaneous. + - Optional: Include additional payer data. +4. **Receiver Responds with Lightning Invoice** + - Generates a Lightning invoice and responds with a Nostr event containing the invoice details. + - Event Type: Ephemeral Kind 21001 | NIP-44 Encrypted + - **Content**: `{"bolt11":""}` + - Optional: Include additional purchase data. +5. **Payer Pays the Invoice** + - Completes the payment by settling the Lightning invoice. +6. **Optional: Receiver Emits Payment Receipt** + - This step is out of scope for this NIP, but potential receipt considerations include: + - Nostr Event as Receipt: Emitting a public Nostr event as a "zap" receipt for verifiable record-keeping. + - Lightning Pre-Image: The Lightning pre-image is proof of payment. + - External / WebHook: The receiver may finalize the flow out-of-band. + +### Nostr Events + +This NIP specifies the use of event kind `21001` with the following structure: + +- `content`: NIP-44 encrypted payment details. +- `tags`: + - `p`: Receiver's public key (hex). + - `e`: Used in response to the requesting event. + +Example request event: + +```json +{ + "id": "", + "pubkey": "", + "created_at": 1234567890, + "kind": 21001, + "tags": [ + ["p", ""] + ], + "content": "", + "sig": "" +} +``` + +The `content` field, after NIP-44 decryption, should contain stringified JSON with the following structure: + +```json +{ + "offer": "", + "amount": "", + "payer_data": "" +} +``` + +Example response event with `bolt11` invoice: + +```json +{ + "id": "", + "pubkey": "", + "created_at": 1234567891, + "kind": 21001, + "tags": [ + ["p", ""], + ["e", ""] // original payment request event ID + ], + "content": "\"}>", + "sig": "" +} +``` + +## Client Behavior + +### Mandatory + +Clients implementing this NIP must: + +1. Decode and validate the structure of the `noffer` bech32-encoded static payment code. +2. Use NIP-44 encryption for all communication between payer and receiver. +3. Generate and send kind 21001 events as specified in this NIP. +4. Parse the `content` field of received events by decrypting and JSON-parsing the stringified content. +5. Handle potential errors gracefully, providing clear feedback to users. +6. Respect the relay URL specified in the static payment code for sending payment request events. + +### Optional + +Clients implementing this NIP may: + +1. Support both this protocol and LNURL for backward compatibility during the transition period. +2. Implement additional features such as multi-recipient payments or integration with other Lightning-related NIPs. + - Ex: If an offer lists multiple recipient keys, a client might "for each" over them to pay multiple recipients. +3. Use Addressable Events to nest offers in marketplace products etc where the offer variables might change more frequently than the product or service associated with it. +4. **Attempt Payments by Name**: Wallets might first attempt the combination of NIP-05 and NIP-69 before falling back to LNURL on name-based lookups. +5. Prefix and parse `noffer` strings with existing `lightning:` link handlers, just as they would invoices or legacy LNURL strings. + +### Prohibited + +Clients implementing this NIP MUST NOT: + +1. Send unencrypted sensitive information in event content or tags. +2. Ignore or modify any fields from the encoded static payment code. + +## Service Behavior + +### Optional + +Services implementing this NIP may: + +1. Use an account identifier as a default offer string for spontaneous payments without explicit user setup. +2. **NIP-05 Integration**: Add a `nip69` field in the NIP-05 content to provide the offer on name-based lookups for trust-minimized Lightning Addresses. + +## Error Handling + +To ensure consistent error handling across implementations, this NIP defines the following error responses, similar to other NIPs: + +1. **Invalid Offer**: When the offer ID is invalid or no longer available. +2. **Temporary Failure**: When the receiver is temporarily unable to process the request. +3. **Expired Offer**: When the offer has expired and is no longer valid. +4. **Unsupported Feature**: When the receiver doesn't support a feature requested by the payer. +5. **Invalid Amount**: When the amount specified is too big or too small, providing the acceptable range. + +Error codes: + +- 1: Invalid Offer +- 2: Temporary Failure +- 3: Expired Offer +- 4: Unsupported Feature +- 5: Invalid Amount + +Clients MUST handle these error responses gracefully and display appropriate messages to users. + +### Error Response Payloads + +Error responses should be sent as kind 21001 events with the following structure in the encrypted content: + +```json +{ + "error": "", + "code": "" + // Additional fields optionally included depending on the error type +} +``` +### Expected Payloads + +3. **Expired Offer** +- **Code**: 3 +- **Payload**: +```json +{ + "error": "Expired Offer", + "code": 3, + "latest": "" +} +``` + +5. **Invalid Amount** +- **Code**: 5 +- **Payload**: +```json +{ + "error": "Invalid Amount", + "code": 5, + "range": { //sats + "min": 10, + "max": 10000000 + } +} +``` + +## Transition from LNURL + +LNURL services wishing to signal the upgrade should add `nip69: ` to the inital GET response described in [LUD-06](https://github.com/lnurl/luds/blob/luds/06.md). Clients seeing this new value on a LNURL server should automatically upgrade the payment flow that was instantiated by the user for that address. + +## Future Considerations + +Future versions of this NIP may consider standardizing additional features such as: + +1. Multi-recipient payments with weighting. +2. Integration with other Lightning-related NIPs. +3. Extended metadata for more complex payment scenarios. +4. Best practices for use with Addressable Events. + +## Reference Implementations + +- Wallet Service: [Lightning.Pub](https://github.com/shocknet/Lightning.Pub/pull/727) +- Wallet Client: [ShockWallet](https://github.com/shocknet/wallet2/pull/268) +- Demo Client: [demo.nip69.dev](https://demo.nip69.dev) +- Lightning Address Bridge: [Bridgelet](https://github.com/shocknet/bridgelet) +- Nostr-Tools (temp fork): [nostr-tools](https://github.com/shocknet/nostr-tools) \ No newline at end of file diff --git a/README.md b/README.md index df1cc938f4..f2ec96e2a2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos - [NIP-59: Gift Wrap](59.md) - [NIP-64: Chess (PGN)](64.md) - [NIP-65: Relay List Metadata](65.md) +- [NIP-69: Nostr Static Payment Codes](69.md) - [NIP-70: Protected Events](70.md) - [NIP-71: Video Events](71.md) - [NIP-72: Moderated Communities](72.md) @@ -162,6 +163,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos | `10096` | File storage server list | [96](96.md) | | `13194` | Wallet Info | [47](47.md) | | `21000` | Lightning Pub RPC | [Lightning.Pub][lnpub] | +| `21001` | Static Payment Code | [69](69.md) | | `22242` | Client Authentication | [42](42.md) | | `23194` | Wallet Request | [47](47.md) | | `23195` | Wallet Response | [47](47.md) |