1use 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
51pub const KIND_ZAP_REQUEST: Kind = Kind::ZAP_REQUEST;
54pub const KIND_ZAP_RECEIPT: Kind = Kind::ZAP_RECEIPT;
56
57pub mod tag_names {
60 pub const RELAYS: &str = "relays";
62 pub const AMOUNT: &str = "amount";
64 pub const LNURL: &str = "lnurl";
66 pub const BOLT11: &str = "bolt11";
68 pub const DESCRIPTION: &str = "description";
70 pub const PREIMAGE: &str = "preimage";
72 pub const ZAP: &str = "zap";
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ZapRequest {
79 pub recipient: PublicKey,
81 pub relays: Vec<RelayUrl>,
84 pub amount_msats: Option<u64>,
86 pub lnurl: Option<String>,
88 pub event_target: Option<EventId>,
90 pub address_target: Option<Coordinate>,
92 pub kind_target: Option<Kind>,
94 pub message: String,
96}
97
98impl ZapRequest {
99 #[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 #[must_use]
117 pub const fn amount_msats(mut self, amount: u64) -> Self {
118 self.amount_msats = Some(amount);
119 self
120 }
121
122 #[must_use]
124 pub fn lnurl(mut self, lnurl: impl Into<String>) -> Self {
125 self.lnurl = Some(lnurl.into());
126 self
127 }
128
129 #[must_use]
131 pub const fn event_target(mut self, id: EventId) -> Self {
132 self.event_target = Some(id);
133 self
134 }
135
136 #[must_use]
138 pub fn address_target(mut self, coord: Coordinate) -> Self {
139 self.address_target = Some(coord);
140 self
141 }
142
143 #[must_use]
145 pub const fn kind_target(mut self, kind: Kind) -> Self {
146 self.kind_target = Some(kind);
147 self
148 }
149
150 #[must_use]
152 pub fn message(mut self, message: impl Into<String>) -> Self {
153 self.message = message.into();
154 self
155 }
156
157 #[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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct ZapReceipt {
300 pub recipient: PublicKey,
302 pub sender: Option<PublicKey>,
304 pub event_target: Option<EventId>,
306 pub address_target: Option<Coordinate>,
308 pub kind_target: Option<Kind>,
310 pub bolt11: String,
313 pub description: String,
316 pub preimage: Option<String>,
319}
320
321impl ZapReceipt {
322 #[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 #[must_use]
345 pub const fn sender(mut self, sender: PublicKey) -> Self {
346 self.sender = Some(sender);
347 self
348 }
349
350 #[must_use]
352 pub const fn event_target(mut self, id: EventId) -> Self {
353 self.event_target = Some(id);
354 self
355 }
356
357 #[must_use]
359 pub fn address_target(mut self, coord: Coordinate) -> Self {
360 self.address_target = Some(coord);
361 self
362 }
363
364 #[must_use]
366 pub const fn kind_target(mut self, kind: Kind) -> Self {
367 self.kind_target = Some(kind);
368 self
369 }
370
371 #[must_use]
373 pub fn preimage(mut self, preimage: impl Into<String>) -> Self {
374 self.preimage = Some(preimage.into());
375 self
376 }
377
378 #[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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct ZapSplitTarget {
506 pub pubkey: PublicKey,
508 pub relay: Option<RelayUrl>,
511 pub weight: Option<u64>,
513}
514
515impl ZapSplitTarget {
516 #[must_use]
518 pub const fn new(pubkey: PublicKey) -> Self {
519 Self {
520 pubkey,
521 relay: None,
522 weight: None,
523 }
524 }
525
526 #[must_use]
528 pub fn relay(mut self, relay: RelayUrl) -> Self {
529 self.relay = Some(relay);
530 self
531 }
532
533 #[must_use]
535 pub const fn weight(mut self, weight: u64) -> Self {
536 self.weight = Some(weight);
537 self
538 }
539
540 #[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()); values.push(weight.to_string());
556 }
557 (None, None) => {}
558 }
559 custom_tag(tag_names::ZAP, values)
560 }
561
562 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
594pub 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#[derive(Debug, Error)]
672#[non_exhaustive]
673pub enum ZapError {
674 #[error("unexpected kind {}", .0.as_u16())]
676 WrongKind(Kind),
677 #[error("missing recipient `p` tag")]
679 MissingRecipient,
680 #[error("missing `bolt11` tag")]
682 MissingBolt11,
683 #[error("missing `description` tag")]
685 MissingDescription,
686 #[error("malformed `p` recipient tag")]
688 MalformedRecipient,
689 #[error("malformed `P` sender tag")]
691 MalformedSender,
692 #[error("malformed `e` event-target tag")]
694 MalformedEventTarget,
695 #[error("malformed `a` address-target tag")]
697 MalformedAddressTarget,
698 #[error("invalid `k` kind tag")]
700 InvalidKindTag,
701 #[error("invalid `amount`: {0}")]
703 InvalidAmount(String),
704 #[error("invalid `zap` weight: {0}")]
706 InvalidWeight(String),
707 #[error("invalid public key: {0}")]
709 InvalidPublicKey(#[source] PublicKeyError),
710 #[error("invalid event id: {0}")]
712 InvalidEventId(#[source] EventIdError),
713 #[error("invalid coordinate: {0}")]
715 InvalidCoordinate(#[source] CoordinateError),
716 #[error("invalid relay URL: {0}")]
718 InvalidRelayUrl(#[source] RelayUrlError),
719 #[error("invalid description JSON: {0}")]
721 DescriptionParse(#[source] serde_json::Error),
722 #[error("not a `zap` split tag")]
725 NotZapSplitTag,
726}
727
728#[derive(Debug, Error)]
730#[non_exhaustive]
731pub enum ZapValidationError {
732 #[error("zap request has no tags")]
734 MissingTags,
735 #[error("zap request must have exactly one `p` tag, found {0}")]
737 WrongPCount(usize),
738 #[error("zap request must have at most one `e` tag, found {0}")]
740 TooManyECount(usize),
741 #[error("`amount` tag mismatch: expected {expected}, got {actual}")]
744 AmountMismatch {
745 expected: u64,
747 actual: u64,
749 },
750}
751
752impl EventBuilder {
753 #[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 #[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 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}