Skip to main content

nula_core/nips/
nip57.rs

1//! [NIP-57] Lightning Zaps.
2//!
3//! Two events stitch Lightning payments to Nostr identities:
4//!
5//! - **Zap request** (`kind: 9734`) — built and signed by the
6//!   payer, *never published to relays*; instead URL-encoded into
7//!   the `nostr=…` query parameter of the recipient's LNURL-pay
8//!   callback (Appendix B).
9//! - **Zap receipt** (`kind: 9735`) — emitted by the recipient's
10//!   LNURL provider once the BOLT-11 invoice it minted in response
11//!   has been settled (Appendix E). Receipts carry the BOLT-11
12//!   string, the JSON-encoded zap request, and the optional
13//!   payment preimage.
14//!
15//! NIP-57 also defines a `zap` *tag* (Appendix G) that lets a
16//! regular event split incoming zaps across multiple recipients,
17//! each with an optional weight.
18//!
19//! # Why a typed module
20//!
21//! Upstream `rust-nostr` ships only a free-form
22//! `EventBuilder::zap_request` taking a [`Vec<Tag>`]. We instead
23//! model:
24//!
25//! - [`ZapRequest`] / [`ZapReceipt`] — typed bundles with the full
26//!   set of MUST / MAY tags from spec §"Appendix A" / §"Appendix
27//!   E";
28//! - [`ZapRequest::validate`] — the intra-event MUST/SHOULD checks
29//!   from §"Appendix D" (single `p`, ≤ 1 `e`, ≤ 1 `P`, optional
30//!   `amount` consistency);
31//! - [`ZapReceipt::description_request`] — parse the
32//!   `description` tag (which spec §"Appendix E" mandates be the
33//!   JSON-encoded zap request) back into a [`ZapRequest`] so
34//!   clients can cross-check Appendix F invariants;
35//! - [`ZapSplitTarget`] / [`ZapSplitTarget::to_tag`] /
36//!   [`parse_zap_split_targets`] — the `zap` tag (Appendix G)
37//!   surfaced as a typed value with weight handling.
38//!
39//! [NIP-57]: https://github.com/nostr-protocol/nips/blob/master/57.md
40
41use thiserror::Error;
42
43use crate::event::{
44    Alphabet, Coordinate, CoordinateError, Event, EventBuilder, EventId, EventIdError, Kind,
45    SingleLetterTag, Tag, TagKind, Tags,
46};
47use crate::key::{PublicKey, PublicKeyError};
48use crate::types::{RelayUrl, RelayUrlError};
49use crate::util::JsonUtil;
50
51/// `kind: 9734` — zap request (sent to LNURL callback, never
52/// published to relays).
53pub const KIND_ZAP_REQUEST: Kind = Kind::ZAP_REQUEST;
54/// `kind: 9735` — zap receipt (emitted by the LNURL provider).
55pub const KIND_ZAP_RECEIPT: Kind = Kind::ZAP_RECEIPT;
56
57/// Tag head names used by NIP-57 (string constants kept here so a
58/// caller can reuse them without re-typing the literal).
59pub mod tag_names {
60    /// `relays` tag — multi-value list of relay URLs.
61    pub const RELAYS: &str = "relays";
62    /// `amount` tag — millisats as a decimal string.
63    pub const AMOUNT: &str = "amount";
64    /// `lnurl` tag — bech32-encoded LNURL.
65    pub const LNURL: &str = "lnurl";
66    /// `bolt11` tag — settled BOLT-11 invoice on a receipt.
67    pub const BOLT11: &str = "bolt11";
68    /// `description` tag — JSON-encoded zap request on a receipt.
69    pub const DESCRIPTION: &str = "description";
70    /// `preimage` tag — optional payment preimage on a receipt.
71    pub const PREIMAGE: &str = "preimage";
72    /// `zap` tag — split-zap target on a regular event.
73    pub const ZAP: &str = "zap";
74}
75
76/// Typed bundle for a `kind: 9734` zap request.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ZapRequest {
79    /// `p` — recipient pubkey (MUST).
80    pub recipient: PublicKey,
81    /// `relays` — relays the LNURL provider SHOULD publish the
82    /// receipt to (MUST).
83    pub relays: Vec<RelayUrl>,
84    /// `amount` — millisats the payer intends to pay (MAY).
85    pub amount_msats: Option<u64>,
86    /// `lnurl` — bech32-encoded LNURL of the recipient (MAY).
87    pub lnurl: Option<String>,
88    /// `e` — event being zapped (MAY).
89    pub event_target: Option<EventId>,
90    /// `a` — addressable event coordinate being zapped (MAY).
91    pub address_target: Option<Coordinate>,
92    /// `k` — kind of the zapped event (MAY).
93    pub kind_target: Option<Kind>,
94    /// Free-form payer message — surfaces as `event.content`.
95    pub message: String,
96}
97
98impl ZapRequest {
99    /// Construct a minimal zap-request bundle. The `relays` list
100    /// is required by spec §"Appendix A"; pass at least one URL.
101    #[must_use]
102    pub const fn new(recipient: PublicKey, relays: Vec<RelayUrl>) -> Self {
103        Self {
104            recipient,
105            relays,
106            amount_msats: None,
107            lnurl: None,
108            event_target: None,
109            address_target: None,
110            kind_target: None,
111            message: String::new(),
112        }
113    }
114
115    /// Set [`Self::amount_msats`].
116    #[must_use]
117    pub const fn amount_msats(mut self, amount: u64) -> Self {
118        self.amount_msats = Some(amount);
119        self
120    }
121
122    /// Set [`Self::lnurl`].
123    #[must_use]
124    pub fn lnurl(mut self, lnurl: impl Into<String>) -> Self {
125        self.lnurl = Some(lnurl.into());
126        self
127    }
128
129    /// Set [`Self::event_target`].
130    #[must_use]
131    pub const fn event_target(mut self, id: EventId) -> Self {
132        self.event_target = Some(id);
133        self
134    }
135
136    /// Set [`Self::address_target`].
137    #[must_use]
138    pub fn address_target(mut self, coord: Coordinate) -> Self {
139        self.address_target = Some(coord);
140        self
141    }
142
143    /// Set [`Self::kind_target`].
144    #[must_use]
145    pub const fn kind_target(mut self, kind: Kind) -> Self {
146        self.kind_target = Some(kind);
147        self
148    }
149
150    /// Set [`Self::message`].
151    #[must_use]
152    pub fn message(mut self, message: impl Into<String>) -> Self {
153        self.message = message.into();
154        self
155    }
156
157    /// Render to the tag list of a `kind: 9734` event.
158    #[must_use]
159    pub fn to_tags(&self) -> Vec<Tag> {
160        let mut tags: Vec<Tag> = Vec::with_capacity(7);
161        if !self.relays.is_empty() {
162            let mut values: Vec<String> = Vec::with_capacity(self.relays.len());
163            for r in &self.relays {
164                values.push(r.as_str().to_owned());
165            }
166            tags.push(custom_tag(tag_names::RELAYS, values));
167        }
168        if let Some(amount) = self.amount_msats {
169            tags.push(custom_tag(tag_names::AMOUNT, [amount.to_string()]));
170        }
171        if let Some(lnurl) = &self.lnurl {
172            tags.push(custom_tag(tag_names::LNURL, [lnurl.clone()]));
173        }
174        tags.push(letter_tag(Alphabet::P, [self.recipient.to_hex()]));
175        if let Some(id) = self.event_target {
176            tags.push(letter_tag(Alphabet::E, [id.to_hex()]));
177        }
178        if let Some(coord) = &self.address_target {
179            tags.push(letter_tag(Alphabet::A, [coord.to_wire()]));
180        }
181        if let Some(k) = self.kind_target {
182            tags.push(letter_tag(Alphabet::K, [k.as_u16().to_string()]));
183        }
184        tags
185    }
186
187    /// Parse a `kind: 9734` event back into a typed bundle.
188    ///
189    /// # Errors
190    ///
191    /// - [`ZapError::WrongKind`] for unrelated kinds.
192    /// - [`ZapError::MissingRecipient`] when no `p` tag is present.
193    /// - Forwarded parse errors for malformed values.
194    pub fn from_event(event: &Event) -> Result<Self, ZapError> {
195        if event.kind != KIND_ZAP_REQUEST {
196            return Err(ZapError::WrongKind(event.kind));
197        }
198        Self::from_tags_and_content(&event.tags, &event.content)
199    }
200
201    fn from_tags_and_content(tags: &Tags, content: &str) -> Result<Self, ZapError> {
202        let mut recipient: Option<PublicKey> = None;
203        let mut relays: Vec<RelayUrl> = Vec::new();
204        let mut amount_msats: Option<u64> = None;
205        let mut lnurl: Option<String> = None;
206        let mut event_target: Option<EventId> = None;
207        let mut address_target: Option<Coordinate> = None;
208        let mut kind_target: Option<Kind> = None;
209
210        for tag in tags {
211            match tag.kind() {
212                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
213                    let pk_hex = tag.get(1).ok_or(ZapError::MalformedRecipient)?;
214                    recipient = Some(PublicKey::parse(pk_hex).map_err(ZapError::InvalidPublicKey)?);
215                }
216                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
217                    let id_hex = tag.get(1).ok_or(ZapError::MalformedEventTarget)?;
218                    event_target = Some(EventId::parse(id_hex).map_err(ZapError::InvalidEventId)?);
219                }
220                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::A => {
221                    let coord_str = tag.get(1).ok_or(ZapError::MalformedAddressTarget)?;
222                    address_target =
223                        Some(Coordinate::parse(coord_str).map_err(ZapError::InvalidCoordinate)?);
224                }
225                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::K => {
226                    parse_kind_tag(tag, &mut kind_target)?;
227                }
228                _ if tag.name() == tag_names::RELAYS => {
229                    parse_relays_tag(tag, &mut relays)?;
230                }
231                _ if tag.name() == tag_names::AMOUNT => {
232                    parse_amount_tag(tag, &mut amount_msats)?;
233                }
234                _ if tag.name() == tag_names::LNURL => {
235                    lnurl = tag.get(1).map(str::to_owned);
236                }
237                _ => {}
238            }
239        }
240
241        let recipient = recipient.ok_or(ZapError::MissingRecipient)?;
242        Ok(Self {
243            recipient,
244            relays,
245            amount_msats,
246            lnurl,
247            event_target,
248            address_target,
249            kind_target,
250            message: content.to_owned(),
251        })
252    }
253
254    /// Validate the intra-event invariants from spec §"Appendix D":
255    ///
256    /// 2. event has tags;
257    /// 3. exactly one `p` tag (already enforced by [`Self::recipient`]);
258    /// 4. zero or one `e` tags (the bundle stores `Option<EventId>`,
259    ///    so duplicate `e` tags would have surfaced as the *last*
260    ///    one when parsing — this method recounts to make sure the
261    ///    raw event was conformant).
262    /// 8. zero or one `P` tags (NB: this is the *uppercase* tag
263    ///    used on the receipt; not validated here because the
264    ///    request must not carry it).
265    ///
266    /// `expected_amount_msats` lets the LNURL server enforce check
267    /// 6 (`amount` query parameter equality) when one was sent.
268    ///
269    /// # Errors
270    ///
271    /// One of the [`ZapValidationError`] variants.
272    pub fn validate(
273        &self,
274        raw_tags: &Tags,
275        expected_amount_msats: Option<u64>,
276    ) -> Result<(), ZapValidationError> {
277        if raw_tags.iter().count() == 0 {
278            return Err(ZapValidationError::MissingTags);
279        }
280        let p_count = count_lowercase_letter(raw_tags, Alphabet::P);
281        if p_count != 1 {
282            return Err(ZapValidationError::WrongPCount(p_count));
283        }
284        let e_count = count_lowercase_letter(raw_tags, Alphabet::E);
285        if e_count > 1 {
286            return Err(ZapValidationError::TooManyECount(e_count));
287        }
288        if let (Some(expected), Some(actual)) = (expected_amount_msats, self.amount_msats)
289            && expected != actual
290        {
291            return Err(ZapValidationError::AmountMismatch { expected, actual });
292        }
293        Ok(())
294    }
295}
296
297/// Typed bundle for a `kind: 9735` zap receipt.
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct ZapReceipt {
300    /// `p` — zap recipient (MUST).
301    pub recipient: PublicKey,
302    /// `P` — zap sender (MAY).
303    pub sender: Option<PublicKey>,
304    /// `e` — event being zapped (MAY, copied from request).
305    pub event_target: Option<EventId>,
306    /// `a` — addressable event coordinate (MAY, copied).
307    pub address_target: Option<Coordinate>,
308    /// `k` — kind of the zapped event (MAY).
309    pub kind_target: Option<Kind>,
310    /// `bolt11` — the description-hash invoice that was paid
311    /// (MUST).
312    pub bolt11: String,
313    /// `description` — JSON-encoded zap request that committed to
314    /// the BOLT-11 description hash (MUST).
315    pub description: String,
316    /// `preimage` — payment preimage (MAY). Not a proof of
317    /// payment; spec §"Appendix E" calls it out explicitly.
318    pub preimage: Option<String>,
319}
320
321impl ZapReceipt {
322    /// Construct from the minimum required fields. `bolt11` and
323    /// `description` are MUST per spec; build them via the LNURL
324    /// server's response and the original zap-request JSON.
325    #[must_use]
326    pub fn new(
327        recipient: PublicKey,
328        bolt11: impl Into<String>,
329        description: impl Into<String>,
330    ) -> Self {
331        Self {
332            recipient,
333            sender: None,
334            event_target: None,
335            address_target: None,
336            kind_target: None,
337            bolt11: bolt11.into(),
338            description: description.into(),
339            preimage: None,
340        }
341    }
342
343    /// Set [`Self::sender`].
344    #[must_use]
345    pub const fn sender(mut self, sender: PublicKey) -> Self {
346        self.sender = Some(sender);
347        self
348    }
349
350    /// Set [`Self::event_target`].
351    #[must_use]
352    pub const fn event_target(mut self, id: EventId) -> Self {
353        self.event_target = Some(id);
354        self
355    }
356
357    /// Set [`Self::address_target`].
358    #[must_use]
359    pub fn address_target(mut self, coord: Coordinate) -> Self {
360        self.address_target = Some(coord);
361        self
362    }
363
364    /// Set [`Self::kind_target`].
365    #[must_use]
366    pub const fn kind_target(mut self, kind: Kind) -> Self {
367        self.kind_target = Some(kind);
368        self
369    }
370
371    /// Set [`Self::preimage`].
372    #[must_use]
373    pub fn preimage(mut self, preimage: impl Into<String>) -> Self {
374        self.preimage = Some(preimage.into());
375        self
376    }
377
378    /// Render to the tag list of a `kind: 9735` event.
379    #[must_use]
380    pub fn to_tags(&self) -> Vec<Tag> {
381        let mut tags: Vec<Tag> = Vec::with_capacity(8);
382        tags.push(letter_tag(Alphabet::P, [self.recipient.to_hex()]));
383        if let Some(sender) = self.sender {
384            tags.push(letter_tag_uppercase(Alphabet::P, [sender.to_hex()]));
385        }
386        if let Some(id) = self.event_target {
387            tags.push(letter_tag(Alphabet::E, [id.to_hex()]));
388        }
389        if let Some(coord) = &self.address_target {
390            tags.push(letter_tag(Alphabet::A, [coord.to_wire()]));
391        }
392        if let Some(k) = self.kind_target {
393            tags.push(letter_tag(Alphabet::K, [k.as_u16().to_string()]));
394        }
395        tags.push(custom_tag(tag_names::BOLT11, [self.bolt11.clone()]));
396        tags.push(custom_tag(
397            tag_names::DESCRIPTION,
398            [self.description.clone()],
399        ));
400        if let Some(preimage) = &self.preimage {
401            tags.push(custom_tag(tag_names::PREIMAGE, [preimage.clone()]));
402        }
403        tags
404    }
405
406    /// Parse a `kind: 9735` event back into a typed bundle.
407    ///
408    /// # Errors
409    ///
410    /// - [`ZapError::WrongKind`] for unrelated kinds.
411    /// - [`ZapError::MissingRecipient`] / `MissingBolt11` /
412    ///   `MissingDescription` per spec §"Appendix E".
413    /// - Forwarded parse errors.
414    pub fn from_event(event: &Event) -> Result<Self, ZapError> {
415        if event.kind != KIND_ZAP_RECEIPT {
416            return Err(ZapError::WrongKind(event.kind));
417        }
418        let mut recipient: Option<PublicKey> = None;
419        let mut sender: Option<PublicKey> = None;
420        let mut event_target: Option<EventId> = None;
421        let mut address_target: Option<Coordinate> = None;
422        let mut kind_target: Option<Kind> = None;
423        let mut bolt11: Option<String> = None;
424        let mut description: Option<String> = None;
425        let mut preimage: Option<String> = None;
426
427        for tag in &event.tags {
428            match tag.kind() {
429                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
430                    let pk_hex = tag.get(1).ok_or(ZapError::MalformedRecipient)?;
431                    recipient = Some(PublicKey::parse(pk_hex).map_err(ZapError::InvalidPublicKey)?);
432                }
433                TagKind::SingleLetter(s) if s.uppercase && s.character == Alphabet::P => {
434                    let pk_hex = tag.get(1).ok_or(ZapError::MalformedSender)?;
435                    sender = Some(PublicKey::parse(pk_hex).map_err(ZapError::InvalidPublicKey)?);
436                }
437                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
438                    let id_hex = tag.get(1).ok_or(ZapError::MalformedEventTarget)?;
439                    event_target = Some(EventId::parse(id_hex).map_err(ZapError::InvalidEventId)?);
440                }
441                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::A => {
442                    let coord_str = tag.get(1).ok_or(ZapError::MalformedAddressTarget)?;
443                    address_target =
444                        Some(Coordinate::parse(coord_str).map_err(ZapError::InvalidCoordinate)?);
445                }
446                TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::K => {
447                    parse_kind_tag(tag, &mut kind_target)?;
448                }
449                _ if tag.name() == tag_names::BOLT11 => {
450                    bolt11 = tag.get(1).map(str::to_owned);
451                }
452                _ if tag.name() == tag_names::DESCRIPTION => {
453                    description = tag.get(1).map(str::to_owned);
454                }
455                _ if tag.name() == tag_names::PREIMAGE => {
456                    preimage = tag.get(1).map(str::to_owned);
457                }
458                _ => {}
459            }
460        }
461
462        Ok(Self {
463            recipient: recipient.ok_or(ZapError::MissingRecipient)?,
464            sender,
465            event_target,
466            address_target,
467            kind_target,
468            bolt11: bolt11.ok_or(ZapError::MissingBolt11)?,
469            description: description.ok_or(ZapError::MissingDescription)?,
470            preimage,
471        })
472    }
473
474    /// Parse [`Self::description`] as the JSON-encoded zap-request
475    /// event that committed to this receipt's BOLT-11 description
476    /// hash (spec §"Appendix E" step 1).
477    ///
478    /// This does *not* verify the embedded event's signature —
479    /// callers chasing Appendix F invariants SHOULD run
480    /// [`Event::verify`] on the returned event before trusting it.
481    ///
482    /// # Errors
483    ///
484    /// - [`ZapError::DescriptionParse`] when the JSON is malformed.
485    /// - Forwarded errors from [`ZapRequest::from_event`].
486    pub fn description_request(&self) -> Result<(Event, ZapRequest), ZapError> {
487        let event: Event =
488            Event::from_json(&self.description).map_err(ZapError::DescriptionParse)?;
489        let request = ZapRequest::from_event(&event)?;
490        Ok((event, request))
491    }
492}
493
494/// One target inside a split-zap `zap` tag (Appendix G).
495///
496/// Weights are *generalised percentages*: clients SHOULD sum the
497/// weights of every `zap` tag and divide each receiver's share
498/// proportionally. If a tag omits the weight, the spec's
499/// behaviour depends on whether *any* sibling tag carries one:
500///
501/// - **None of them carry weights** → split equally.
502/// - **Some of them carry weights** → those without one MUST be
503///   skipped (`weight = 0`).
504#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct ZapSplitTarget {
506    /// Recipient pubkey.
507    pub pubkey: PublicKey,
508    /// Optional relay hint where the recipient's metadata can be
509    /// fetched.
510    pub relay: Option<RelayUrl>,
511    /// Optional split weight.
512    pub weight: Option<u64>,
513}
514
515impl ZapSplitTarget {
516    /// Construct without a relay hint or weight.
517    #[must_use]
518    pub const fn new(pubkey: PublicKey) -> Self {
519        Self {
520            pubkey,
521            relay: None,
522            weight: None,
523        }
524    }
525
526    /// Set the relay hint.
527    #[must_use]
528    pub fn relay(mut self, relay: RelayUrl) -> Self {
529        self.relay = Some(relay);
530        self
531    }
532
533    /// Set the weight.
534    #[must_use]
535    pub const fn weight(mut self, weight: u64) -> Self {
536        self.weight = Some(weight);
537        self
538    }
539
540    /// Render to a wire `zap` tag.
541    #[must_use]
542    pub fn to_tag(&self) -> Tag {
543        let mut values: Vec<String> = Vec::with_capacity(4);
544        values.push(self.pubkey.to_hex());
545        match (&self.relay, self.weight) {
546            (Some(relay), Some(weight)) => {
547                values.push(relay.as_str().to_owned());
548                values.push(weight.to_string());
549            }
550            (Some(relay), None) => {
551                values.push(relay.as_str().to_owned());
552            }
553            (None, Some(weight)) => {
554                values.push(String::new()); // empty relay slot
555                values.push(weight.to_string());
556            }
557            (None, None) => {}
558        }
559        custom_tag(tag_names::ZAP, values)
560    }
561
562    /// Parse a wire `zap` tag.
563    ///
564    /// # Errors
565    ///
566    /// Forwarded from [`PublicKey::parse`] / [`RelayUrl::parse`].
567    pub fn from_tag(tag: &Tag) -> Result<Self, ZapError> {
568        if tag.name() != tag_names::ZAP {
569            return Err(ZapError::NotZapSplitTag);
570        }
571        let pk_hex = tag.get(1).ok_or(ZapError::MalformedRecipient)?;
572        let pubkey = PublicKey::parse(pk_hex).map_err(ZapError::InvalidPublicKey)?;
573        let relay = match tag.get(2) {
574            Some(s) if !s.is_empty() => {
575                Some(RelayUrl::parse(s).map_err(ZapError::InvalidRelayUrl)?)
576            }
577            _ => None,
578        };
579        let weight = match tag.get(3) {
580            Some(s) if !s.is_empty() => Some(
581                s.parse::<u64>()
582                    .map_err(|_| ZapError::InvalidWeight(s.to_owned()))?,
583            ),
584            _ => None,
585        };
586        Ok(Self {
587            pubkey,
588            relay,
589            weight,
590        })
591    }
592}
593
594/// Collect every split-zap target announced by an event's `zap`
595/// tags (Appendix G).
596///
597/// # Errors
598///
599/// Forwarded from [`ZapSplitTarget::from_tag`] for any malformed
600/// row.
601pub fn parse_zap_split_targets(event: &Event) -> Result<Vec<ZapSplitTarget>, ZapError> {
602    let mut out: Vec<ZapSplitTarget> = Vec::new();
603    for tag in &event.tags {
604        if tag.name() == tag_names::ZAP {
605            out.push(ZapSplitTarget::from_tag(tag)?);
606        }
607    }
608    Ok(out)
609}
610
611fn parse_kind_tag(tag: &Tag, target: &mut Option<Kind>) -> Result<(), ZapError> {
612    let Some(k) = tag.get(1) else { return Ok(()) };
613    let parsed = k.parse::<u16>().map_err(|_| ZapError::InvalidKindTag)?;
614    *target = Some(Kind::new(parsed));
615    Ok(())
616}
617
618fn parse_relays_tag(tag: &Tag, relays: &mut Vec<RelayUrl>) -> Result<(), ZapError> {
619    for v in tag.values().iter().skip(1) {
620        let url = RelayUrl::parse(v).map_err(ZapError::InvalidRelayUrl)?;
621        relays.push(url);
622    }
623    Ok(())
624}
625
626fn parse_amount_tag(tag: &Tag, target: &mut Option<u64>) -> Result<(), ZapError> {
627    let Some(a) = tag.get(1) else { return Ok(()) };
628    let parsed = a
629        .parse::<u64>()
630        .map_err(|_| ZapError::InvalidAmount(a.to_owned()))?;
631    *target = Some(parsed);
632    Ok(())
633}
634
635fn count_lowercase_letter(tags: &Tags, letter: Alphabet) -> usize {
636    tags.iter()
637        .filter(|t| {
638            matches!(t.kind(), TagKind::SingleLetter(s)
639                if !s.uppercase && s.character == letter)
640        })
641        .count()
642}
643
644fn custom_tag<I, S>(name: &str, args: I) -> Tag
645where
646    I: IntoIterator<Item = S>,
647    S: Into<String>,
648{
649    Tag::with(&TagKind::from_wire(name), args)
650}
651
652fn letter_tag<I, S>(alphabet: Alphabet, args: I) -> Tag
653where
654    I: IntoIterator<Item = S>,
655    S: Into<String>,
656{
657    let head = TagKind::single_letter(SingleLetterTag::lowercase(alphabet));
658    Tag::with(&head, args)
659}
660
661fn letter_tag_uppercase<I, S>(alphabet: Alphabet, args: I) -> Tag
662where
663    I: IntoIterator<Item = S>,
664    S: Into<String>,
665{
666    let head = TagKind::single_letter(SingleLetterTag::uppercase(alphabet));
667    Tag::with(&head, args)
668}
669
670/// Errors raised while building or parsing a zap-related event.
671#[derive(Debug, Error)]
672#[non_exhaustive]
673pub enum ZapError {
674    /// Wrapping event was not the expected kind.
675    #[error("unexpected kind {}", .0.as_u16())]
676    WrongKind(Kind),
677    /// `p` tag absent on a request or receipt.
678    #[error("missing recipient `p` tag")]
679    MissingRecipient,
680    /// `bolt11` tag absent on a receipt.
681    #[error("missing `bolt11` tag")]
682    MissingBolt11,
683    /// `description` tag absent on a receipt.
684    #[error("missing `description` tag")]
685    MissingDescription,
686    /// `p` tag column missing.
687    #[error("malformed `p` recipient tag")]
688    MalformedRecipient,
689    /// `P` tag column missing.
690    #[error("malformed `P` sender tag")]
691    MalformedSender,
692    /// `e` tag column missing.
693    #[error("malformed `e` event-target tag")]
694    MalformedEventTarget,
695    /// `a` tag column missing.
696    #[error("malformed `a` address-target tag")]
697    MalformedAddressTarget,
698    /// `k` value did not parse as `u16`.
699    #[error("invalid `k` kind tag")]
700    InvalidKindTag,
701    /// `amount` value did not parse as `u64`.
702    #[error("invalid `amount`: {0}")]
703    InvalidAmount(String),
704    /// `weight` value did not parse as `u64`.
705    #[error("invalid `zap` weight: {0}")]
706    InvalidWeight(String),
707    /// Pubkey hex did not parse.
708    #[error("invalid public key: {0}")]
709    InvalidPublicKey(#[source] PublicKeyError),
710    /// Event id hex did not parse.
711    #[error("invalid event id: {0}")]
712    InvalidEventId(#[source] EventIdError),
713    /// Coordinate string did not parse.
714    #[error("invalid coordinate: {0}")]
715    InvalidCoordinate(#[source] CoordinateError),
716    /// Relay URL did not parse.
717    #[error("invalid relay URL: {0}")]
718    InvalidRelayUrl(#[source] RelayUrlError),
719    /// `description` JSON did not deserialise.
720    #[error("invalid description JSON: {0}")]
721    DescriptionParse(#[source] serde_json::Error),
722    /// Tag passed to [`ZapSplitTarget::from_tag`] was not a
723    /// `zap` tag.
724    #[error("not a `zap` split tag")]
725    NotZapSplitTag,
726}
727
728/// Errors raised by [`ZapRequest::validate`] (NIP-57 Appendix D).
729#[derive(Debug, Error)]
730#[non_exhaustive]
731pub enum ZapValidationError {
732    /// The event had no tags at all (rule 2).
733    #[error("zap request has no tags")]
734    MissingTags,
735    /// `p` tag count was not exactly one (rule 3).
736    #[error("zap request must have exactly one `p` tag, found {0}")]
737    WrongPCount(usize),
738    /// `e` tag count exceeded one (rule 4).
739    #[error("zap request must have at most one `e` tag, found {0}")]
740    TooManyECount(usize),
741    /// `amount` tag did not match the expected query-string value
742    /// (rule 6).
743    #[error("`amount` tag mismatch: expected {expected}, got {actual}")]
744    AmountMismatch {
745        /// `amount` query-parameter value.
746        expected: u64,
747        /// `amount` tag value.
748        actual: u64,
749    },
750}
751
752impl EventBuilder {
753    /// Author a NIP-57 zap-request event from a typed bundle.
754    #[must_use]
755    pub fn zap_request(request: &ZapRequest) -> Self {
756        let mut builder = Self::new(KIND_ZAP_REQUEST, request.message.clone());
757        for tag in request.to_tags() {
758            builder = builder.tag(tag);
759        }
760        builder
761    }
762
763    /// Author a NIP-57 zap-receipt event from a typed bundle.
764    #[must_use]
765    pub fn zap_receipt(receipt: &ZapReceipt) -> Self {
766        let mut builder = Self::new(KIND_ZAP_RECEIPT, "");
767        for tag in receipt.to_tags() {
768            builder = builder.tag(tag);
769        }
770        builder
771    }
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777    use crate::Keys;
778
779    fn keys() -> Keys {
780        Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
781    }
782
783    fn other_keys() -> Keys {
784        Keys::parse("0000000000000000000000000000000000000000000000000000000000000005").unwrap()
785    }
786
787    fn relay() -> RelayUrl {
788        RelayUrl::parse("wss://relay.example/").unwrap()
789    }
790
791    #[test]
792    fn zap_request_round_trips_full_bundle() {
793        let req = ZapRequest::new(*other_keys().public_key(), vec![relay()])
794            .amount_msats(21_000)
795            .lnurl("lnurl1abcdef")
796            .event_target(EventId::from_byte_array([0xee; 32]))
797            .kind_target(Kind::TEXT_NOTE)
798            .message("Zap!");
799        let event = EventBuilder::zap_request(&req)
800            .sign_with_keys(&keys())
801            .unwrap();
802        assert_eq!(event.kind, KIND_ZAP_REQUEST);
803        assert_eq!(event.content, "Zap!");
804        let parsed = ZapRequest::from_event(&event).unwrap();
805        assert_eq!(parsed, req);
806    }
807
808    #[test]
809    fn zap_request_validate_rejects_missing_p() {
810        let event = EventBuilder::new(KIND_ZAP_REQUEST, "")
811            .tag(custom_tag(tag_names::RELAYS, [relay().as_str()]))
812            .sign_with_keys(&keys())
813            .unwrap();
814        assert!(matches!(
815            ZapRequest::from_event(&event),
816            Err(ZapError::MissingRecipient)
817        ));
818    }
819
820    #[test]
821    fn zap_request_validate_rejects_double_p() {
822        // Two `p` tags — must fail rule 3.
823        let req = ZapRequest::new(*other_keys().public_key(), vec![relay()]);
824        let event = EventBuilder::zap_request(&req)
825            .tag(letter_tag(Alphabet::P, [keys().public_key().to_hex()]))
826            .sign_with_keys(&keys())
827            .unwrap();
828        let parsed = ZapRequest::from_event(&event).unwrap();
829        let err = parsed.validate(&event.tags, None).unwrap_err();
830        assert!(matches!(err, ZapValidationError::WrongPCount(2)));
831    }
832
833    #[test]
834    fn zap_request_validate_rejects_double_e() {
835        let req = ZapRequest::new(*other_keys().public_key(), vec![relay()]);
836        let event = EventBuilder::zap_request(&req)
837            .tag(letter_tag(
838                Alphabet::E,
839                [EventId::from_byte_array([1; 32]).to_hex()],
840            ))
841            .tag(letter_tag(
842                Alphabet::E,
843                [EventId::from_byte_array([2; 32]).to_hex()],
844            ))
845            .sign_with_keys(&keys())
846            .unwrap();
847        let parsed = ZapRequest::from_event(&event).unwrap();
848        let err = parsed.validate(&event.tags, None).unwrap_err();
849        assert!(matches!(err, ZapValidationError::TooManyECount(2)));
850    }
851
852    #[test]
853    fn zap_request_validate_amount_mismatch() {
854        let req = ZapRequest::new(*other_keys().public_key(), vec![relay()]).amount_msats(21_000);
855        let event = EventBuilder::zap_request(&req)
856            .sign_with_keys(&keys())
857            .unwrap();
858        let parsed = ZapRequest::from_event(&event).unwrap();
859        let err = parsed.validate(&event.tags, Some(42_000)).unwrap_err();
860        assert!(matches!(err, ZapValidationError::AmountMismatch { .. }));
861    }
862
863    #[test]
864    fn zap_receipt_round_trips() {
865        let recipient = *other_keys().public_key();
866        let sender = *keys().public_key();
867        let receipt = ZapReceipt::new(recipient, "lnbc1...invoice", "{\"kind\":9734}")
868            .sender(sender)
869            .event_target(EventId::from_byte_array([0xab; 32]))
870            .kind_target(Kind::TEXT_NOTE)
871            .preimage("deadbeef".repeat(8));
872        let event = EventBuilder::zap_receipt(&receipt)
873            .sign_with_keys(&other_keys())
874            .unwrap();
875        assert_eq!(event.kind, KIND_ZAP_RECEIPT);
876        let parsed = ZapReceipt::from_event(&event).unwrap();
877        assert_eq!(parsed, receipt);
878    }
879
880    #[test]
881    fn zap_receipt_missing_bolt11_is_rejected() {
882        let event = EventBuilder::new(KIND_ZAP_RECEIPT, "")
883            .tag(letter_tag(Alphabet::P, [keys().public_key().to_hex()]))
884            .tag(custom_tag(tag_names::DESCRIPTION, ["{}"]))
885            .sign_with_keys(&keys())
886            .unwrap();
887        assert!(matches!(
888            ZapReceipt::from_event(&event),
889            Err(ZapError::MissingBolt11)
890        ));
891    }
892
893    #[test]
894    fn description_request_round_trips() {
895        let req = ZapRequest::new(*other_keys().public_key(), vec![relay()]).message("yo");
896        let req_event = EventBuilder::zap_request(&req)
897            .sign_with_keys(&keys())
898            .unwrap();
899        let description = req_event.try_to_json().unwrap();
900        let receipt = ZapReceipt::new(*other_keys().public_key(), "lnbc1...invoice", description);
901        let (_event, parsed_req) = receipt.description_request().unwrap();
902        assert_eq!(parsed_req, req);
903    }
904
905    #[test]
906    fn zap_split_tag_round_trips() {
907        let target = ZapSplitTarget::new(*other_keys().public_key())
908            .relay(relay())
909            .weight(2);
910        let tag = target.to_tag();
911        let parsed = ZapSplitTarget::from_tag(&tag).unwrap();
912        assert_eq!(parsed, target);
913    }
914
915    #[test]
916    fn zap_split_tag_round_trips_without_weight() {
917        let target = ZapSplitTarget::new(*other_keys().public_key()).relay(relay());
918        let tag = target.to_tag();
919        let parsed = ZapSplitTarget::from_tag(&tag).unwrap();
920        assert_eq!(parsed, target);
921    }
922
923    #[test]
924    fn zap_split_tag_round_trips_minimal() {
925        let target = ZapSplitTarget::new(*other_keys().public_key());
926        let tag = target.to_tag();
927        let parsed = ZapSplitTarget::from_tag(&tag).unwrap();
928        assert_eq!(parsed, target);
929    }
930
931    #[test]
932    fn parse_zap_split_targets_picks_only_zap_tags() {
933        let a = ZapSplitTarget::new(*keys().public_key()).weight(1);
934        let b = ZapSplitTarget::new(*other_keys().public_key()).weight(3);
935        let event = EventBuilder::text_note("split me")
936            .tag(a.to_tag())
937            .tag(b.to_tag())
938            .tag(letter_tag(Alphabet::P, [keys().public_key().to_hex()]))
939            .sign_with_keys(&keys())
940            .unwrap();
941        let parsed = parse_zap_split_targets(&event).unwrap();
942        assert_eq!(parsed.len(), 2);
943        assert_eq!(parsed[0], a);
944        assert_eq!(parsed[1], b);
945    }
946
947    #[test]
948    fn unknown_method_passes_round_trip_via_kind_target() {
949        let req = ZapRequest::new(*other_keys().public_key(), vec![relay()])
950            .kind_target(Kind::new(31_337));
951        let event = EventBuilder::zap_request(&req)
952            .sign_with_keys(&keys())
953            .unwrap();
954        let parsed = ZapRequest::from_event(&event).unwrap();
955        assert_eq!(parsed.kind_target, Some(Kind::new(31_337)));
956    }
957
958    #[test]
959    fn relays_tag_supports_multiple_values() {
960        let req = ZapRequest::new(
961            *other_keys().public_key(),
962            vec![
963                RelayUrl::parse("wss://relay.one/").unwrap(),
964                RelayUrl::parse("wss://relay.two/").unwrap(),
965            ],
966        );
967        let event = EventBuilder::zap_request(&req)
968            .sign_with_keys(&keys())
969            .unwrap();
970        let parsed = ZapRequest::from_event(&event).unwrap();
971        assert_eq!(parsed.relays.len(), 2);
972    }
973}