nips/57.md
2024-06-07 18:44:33 -03:00

13 KiB

NIP-57

Lightning Zaps

draft optional

This NIP defines two new event types for recording lightning payments between users. 9734 is a zap request, representing a payer's request to a recipient's lightning wallet for an invoice. 9735 is a zap receipt, representing the confirmation by the recipient's lightning wallet that the invoice issued in response to a zap request has been paid.

Having lightning receipts on nostr allows clients to display lightning payments from entities on the network. These can be used for fun or for spam deterrence.

Protocol flow

  1. Client calculates a recipient's lnurl pay request url from the zap tag on the event being zapped (see Appendix G), or by decoding their lud06 or lud16 field on their profile according to the lnurl specifications. The client MUST send a GET request to this url and parse the response. If allowsNostr exists and it is true, and if nostrPubkey exists and is a valid BIP 340 public key in hex, the client should associate this information with the user, along with the response's callback, minSendable, and maxSendable values.
  2. Clients may choose to display a lightning zap button on each post or on a user's profile. If the user's lnurl pay request endpoint supports nostr, the client SHOULD use this NIP to request a zap receipt rather than a normal lnurl invoice.
  3. When a user (the "sender") indicates they want to send a zap to another user (the "recipient"), the client should create a zap request event as described in Appendix A of this NIP and sign it.
  4. Instead of publishing the zap request, the 9734 event should instead be sent to the callback url received from the lnurl pay endpoint for the recipient using a GET request. See Appendix B for details and an example.
  5. The recipient's lnurl server will receive this zap request and validate it. See Appendix C for details on how to properly configure an lnurl server to support zaps, and Appendix D for details on how to validate the nostr query parameter.
  6. If the zap request is valid, the server should fetch a description hash invoice where the description is this zap request note and this note only. No additional lnurl metadata is included in the description. This will be returned in the response according to LUD06.
  7. On receiving the invoice, the client MAY pay it or pass it to an app that can pay the invoice.
  8. Once the invoice is paid, the recipient's lnurl server MUST generate a zap receipt as described in Appendix E, and publish it to the relays specified in the zap request.
  9. Clients MAY fetch zap receipts on posts and profiles, but MUST authorize their validity as described in Appendix F. If the zap request note contains a non-empty content, it may display a zap comment. Generally clients should show users the zap request note, and use the zap receipt to show "zap authorized by ..." but this is optional.

Reference and examples

Appendix A: Zap Request Event

A zap request is an event of kind 9734 that is not published to relays, but is instead sent to a recipient's lnurl pay callback url. This event's content MAY be an optional message to send along with the payment. The event MUST include the following tags:

  • relays is a list of relays the recipient's wallet should publish its zap receipt to. Note that relays should not be nested in an additional list, but should be included as shown in the example below.
  • amount is the amount in millisats the sender intends to pay, formatted as a string. This is recommended, but optional.
  • lnurl is the lnurl pay url of the recipient, encoded using bech32 with the prefix lnurl. This is recommended, but optional.
  • p is the hex-encoded pubkey of the recipient.

In addition, the event MAY include the following tags:

  • e is an optional hex-encoded event id. Clients MUST include this if zapping an event rather than a person.
  • a is an optional event coordinate that allows tipping parameterized replaceable events such as NIP-23 long-form notes.

Example:

{
  "kind": 9734,
  "content": "Zap!",
  "tags": [
    ["relays", "wss://nostr-pub.wellorder.com", "wss://anotherrelay.example.com"],
    ["amount", "21000"],
    ["lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"],
    ["p", "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"],
    ["e", "9ae37aa68f48645127299e9453eb5d908a0cbb6058ff340d528ed4d37c8994fb"]
  ],
  "pubkey": "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322",
  "created_at": 1679673265,
  "id": "30efed56a035b2549fcaeec0bf2c1595f9a9b3bb4b1a38abaf8ee9041c4b7d93",
  "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d"
}

Appendix B: Zap Request HTTP Request

A signed zap request event is not published, but is instead sent using a HTTP GET request to the recipient's callback url, which was provided by the recipient's lnurl pay endpoint. This request should have the following query parameters defined:

  • amount is the amount in millisats the sender intends to pay
  • nostr is the 9734 zap request event, JSON encoded then URI encoded
  • lnurl is the lnurl pay url of the recipient, encoded using bech32 with the prefix lnurl

This request should return a JSON response with a pr key, which is the invoice the sender must pay to finalize his zap. Here is an example flow in javascript:

const senderPubkey // The sender's pubkey
const recipientPubkey = // The recipient's pubkey
const callback = // The callback received from the recipients lnurl pay endpoint
const lnurl = // The recipient's lightning address, encoded as a lnurl
const sats = 21

const amount = sats * 1000
const relays = ['wss://nostr-pub.wellorder.net']
const event = encodeURI(JSON.stringify(await signEvent({
  kind: 9734,
  content: "",
  pubkey: senderPubkey,
  created_at: Math.round(Date.now() / 1000),
  tags: [
    ["relays", ...relays],
    ["amount", amount.toString()],
    ["lnurl", lnurl],
    ["p", recipientPubkey],
  ],
})))

const {pr: invoice} = await fetchJson(`${callback}?amount=${amount}&nostr=${event}&lnurl=${lnurl}`)

Appendix C: LNURL Server Configuration

The lnurl server will need some additional pieces of information so that clients can know that zap invoices are supported:

  1. Add a nostrPubkey to the lnurl-pay static endpoint /.well-known/lnurlp/<user>, where nostrPubkey is the nostr pubkey your server will use to sign zap receipt events. Clients will use this to validate zap receipts.
  2. Add an allowsNostr field and set it to true.

Appendix D: LNURL Server Zap Request Validation

When a client sends a zap request event to a server's lnurl-pay callback URL, there will be a nostr query parameter whose value is that event which is URI- and JSON-encoded. If present, the zap request event must be validated in the following ways:

  1. It MUST have a valid nostr signature
  2. It MUST have tags
  3. It MUST have only one p tag
  4. It MUST have 0 or 1 e tags
  5. There should be a relays tag with the relays to send the zap receipt to.
  6. If there is an amount tag, it MUST be equal to the amount query parameter.
  7. If there is an a tag, it MUST be a valid event coordinate
  8. There MUST be 0 or 1 P tags. If there is one, it MUST be equal to the zap receipt's pubkey.

The event MUST then be stored for use later, when the invoice is paid.

Appendix E: Zap Receipt Event

A zap receipt is created by a lightning node when an invoice generated by a zap request is paid. Zap receipts are only created when the invoice description (committed to the description hash) contains a zap request note.

When receiving a payment, the following steps are executed:

  1. Get the description for the invoice. This needs to be saved somewhere during the generation of the description hash invoice. It is saved automatically for you with CLN, which is the reference implementation used here.
  2. Parse the bolt11 description as a JSON nostr event. This SHOULD be validated based on the requirements in Appendix D, either when it is received, or before the invoice is paid.
  3. Create a nostr event of kind 9735 as described below, and publish it to the relays declared in the zap request.

The following should be true of the zap receipt event:

  • The content SHOULD be empty.
  • The created_at date SHOULD be set to the invoice paid_at date for idempotency.
  • tags MUST include the p tag (zap recipient) AND optional e tag from the zap request AND optional a tag from the zap request AND optional P tag from the pubkey of the zap request (zap sender).
  • The zap receipt MUST have a bolt11 tag containing the description hash bolt11 invoice.
  • The zap receipt MUST contain a description tag which is the JSON-encoded zap request.
  • SHA256(description) MUST match the description hash in the bolt11 invoice.
  • The zap receipt MAY contain a preimage tag to match against the payment hash of the bolt11 invoice. This isn't really a payment proof, there is no real way to prove that the invoice is real or has been paid. You are trusting the author of the zap receipt for the legitimacy of the payment.

The zap receipt is not a proof of payment, all it proves is that some nostr user fetched an invoice. The existence of the zap receipt implies the invoice as paid, but it could be a lie given a rogue implementation.

A reference implementation for a zap-enabled lnurl server can be found here.

Example zap receipt:

{
    "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446",
    "pubkey": "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31",
    "created_at": 1674164545,
    "kind": 9735,
    "tags": [
      ["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
      ["P", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"],
      ["e", "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"],
      ["bolt11", "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"],
      ["description", "{\"pubkey\":\"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\",\"wss://nostr-relay.wlvs.space\",\"wss://nostr.fmt.wiz.biz\",\"wss://relay.nostr.bg\",\"wss://nostr.oxtr.dev\",\"wss://nostr.v0l.io\",\"wss://brb.io\",\"wss://nostr.bitcoiner.social\",\"ws://monad.jb55.com:8080\",\"wss://relay.snort.social\"]]}"],
      ["preimage", "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f"]
    ],
    "content": "",
  }

Appendix F: Validating Zap Receipts

A client can retrieve zap receipts on events and pubkeys using a NIP-01 filter, for example {"kinds": [9735], "#e": [...]}. Zaps MUST be validated using the following steps:

  • The zap receipt event's pubkey MUST be the same as the recipient's lnurl provider's nostrPubkey (retrieved in step 1 of the protocol flow).
  • The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present).
  • The lnurl tag of the zap request (if present) SHOULD equal the recipient's lnurl.

Appendix G: zap tag on other events

When an event includes one or more zap tags, clients wishing to zap it SHOULD calculate the lnurl pay request based on the tags value instead of the event author's profile field. The tag's second argument is the hex string of the receiver's pub key and the third argument is the relay to download the receiver's metadata (Kind-0). An optional fourth parameter specifies the weight (a generalization of a percentage) assigned to the respective receiver. Clients should parse all weights, calculate a sum, and then a percentage to each receiver. If weights are not present, CLIENTS should equally divide the zap amount to all receivers. If weights are only partially present, receivers without a weight should not be zapped (weight = 0).

{
    "tags": [
        [ "zap", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "wss://nostr.oxtr.dev", "1" ],  // 25%
        [ "zap", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "wss://nostr.wine/",    "1" ],  // 25%
        [ "zap", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "wss://nos.lol/",       "2" ]   // 50%
    ]
}

Clients MAY display the zap split configuration in the note.

Future Work

Zaps can be extended to be more private by encrypting zap request notes to the target user, but for simplicity it has been left out of this initial draft.