-
Notifications
You must be signed in to change notification settings - Fork 594
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
NIP-69 Nostr Offer STRings #1460
Open
shocknet-justin
wants to merge
28
commits into
nostr-protocol:master
Choose a base branch
from
shocknet-justin:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
4a913d6
Merge pull request #1 from nostr-protocol/master
shocknet-justin e60b8d0
Merge pull request #2 from nostr-protocol/master
shocknet-justin 858e73e
first pass
shocknet-justin aa57d5f
errors
shocknet-justin 81a6788
Merge pull request #3 from nostr-protocol/master
shocknet-justin d43c9e1
nip05
shocknet-justin 4f41a9a
format
shocknet-justin 39e8ea5
amounts
shocknet-justin f89b090
readme
shocknet-justin ecacbf4
range error
shocknet-justin 75069ab
better nip05
shocknet-justin 0cb9a86
name fix
shocknet-justin 4693f53
better tlv order
shocknet-justin 993b3a8
better other nips descriptions
shocknet-justin c6a3a29
zap signaling
shocknet-justin c8ca0b5
link handling
shocknet-justin 62c86a7
multiple payees
shocknet-justin 2190481
wordsmith
shocknet-justin d1f78e8
words
shocknet-justin cc4b665
demo
shocknet-justin 7b15e7d
error payloads
shocknet-justin 4522742
clarify types
shocknet-justin 05cce5f
stringified
shocknet-justin 8c5cf20
fiat reference
shocknet-justin 4c3879b
bridgelet
shocknet-justin 836c23f
Merge pull request #5 from nostr-protocol/master
shocknet-justin b977791
nostr-tools ref
shocknet-justin eca648d
typo
shocknet-justin 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 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,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... | ||
shocknet-justin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
0: <receiver_public_key_in_hex> | ||
1: <relay_url> | ||
2: <offer_id_string> | ||
3: <pricing_type_flag> (optional, 0 for fixed, 1 for variable, 2 for spontaneous) | ||
4: <price_in_sats> (optional) | ||
shocknet-justin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
## 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\": \"[email protected]\", \"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": "<event_id>", | ||
"pubkey": "<sender_pubkey>", | ||
"created_at": 1234567890, | ||
"kind": 21001, | ||
"tags": [ | ||
["p", "<receiver_pubkey>"] | ||
], | ||
"content": "<NIP-44 encrypted {\"offer\":\"<zap1234>\",\"zap\":<kind 9734 zap request event>}>", | ||
"sig": "<signature>" | ||
} | ||
``` | ||
|
||
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":"<BOLT11_invoice_string>"}` | ||
- 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. | ||
shocknet-justin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- `tags`: | ||
- `p`: Receiver's public key (hex). | ||
- `e`: Used in response to the requesting event. | ||
|
||
Example request event: | ||
|
||
```json | ||
{ | ||
"id": "<event_id>", | ||
"pubkey": "<sender_pubkey>", | ||
"created_at": 1234567890, | ||
"kind": 21001, | ||
"tags": [ | ||
["p", "<receiver_pubkey>"] | ||
], | ||
"content": "<NIP-44 encrypted offer identifier and conditional payer data>", | ||
"sig": "<signature>" | ||
} | ||
``` | ||
|
||
The `content` field, after NIP-44 decryption, should contain stringified JSON with the following structure: | ||
|
||
```json | ||
{ | ||
"offer": "<offer_string>", | ||
"amount": "<conditional_amount_in_sats>", | ||
"payer_data": "<optional_payer_data>" | ||
shocknet-justin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
``` | ||
|
||
Example response event with `bolt11` invoice: | ||
shocknet-justin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```json | ||
{ | ||
"id": "<response_event_id>", | ||
"pubkey": "<receiver_pubkey>", | ||
"created_at": 1234567891, | ||
"kind": 21001, | ||
"tags": [ | ||
["p", "<sender_pubkey>"], | ||
["e", "<event_id>"] // original payment request event ID | ||
], | ||
"content": "<NIP-44 encrypted {\"bolt11\":\"<BOLT11_invoice_string>\"}>", | ||
"sig": "<signature>" | ||
} | ||
``` | ||
|
||
## 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": "<error_message>", | ||
"code": "<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": "<noffer1..>" | ||
} | ||
``` | ||
|
||
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: <noffer>` 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) |
This file contains 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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afaict, this means that one can only specify invoices in sats. It would be nice if you could consider to use msats here. Explanation:
LUD-06, NIP-47 and NIP-57 also specify the amount in msats. It would also be good for us at Stacker News if this spec supports msats out of the box because we forward payments between users and take 10% as a fee for sybil resistance and rewards. This means that if someone zaps 25 sats, 22.5 sats will be received which requires being able to specify invoices in msats.
Ideal would be amount + unit like BOLT11 though but msats is probably good enough and would be consistent with other specs as mentioned.
I have to mention though that most LNURL providers seem to simply round to nearest sat so don't really support msats; contrary to the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I considered this but steered away from assuming any baggage from LNURL... the issue with millisats is that they can't actually be settled in payments (technically they are shitcoins and thus rounding happens regardless of whether we use them or not). The scope operates on the assumption that offers will be used in sales reports and things of that nature which do not go to decimal places of a penny etc and so I didn't re-introduce that here.