Skip to main content

nula_core/nips/
nip61.rs

1//! [NIP-61] Nutzaps — typed event bundles.
2//!
3//! A Nutzap is a P2PK-locked Cashu token in which the payment itself
4//! is the receipt: Alice mints (or swaps) ecash at a mint Bob has
5//! whitelisted, P2PK-locks it to the pubkey Bob advertises, and
6//! publishes a `kind:9321` event to the relays Bob lists in his
7//! `kind:10019` informational event.
8//!
9//! | Kind   | Role                                      | Replaceable |
10//! |--------|-------------------------------------------|-------------|
11//! | 10019  | Nutzap informational event                | ✓           |
12//! | 9321   | Nutzap (P2PK-locked Cashu proofs)         | —           |
13//!
14//! # Why a typed module
15//!
16//! Upstream `rust-nostr` ships nothing for NIP-61. We model:
17//!
18//! 1. [`NutzapInfo`] — the kind-10019 advert: read relays, accepted
19//!    [`NutzapMint`]s (mint URL plus optional supported base units),
20//!    and the dedicated P2PK pubkey clients MUST lock proofs to
21//!    (which corresponds to the `privkey` slot of the author's
22//!    NIP-60 [`crate::nips::nip60::WalletInfo`] — **NOT** the
23//!    user's main Nostr identity key).
24//! 2. [`Nutzap`] — the kind-9321 event: P2PK-locked
25//!    [`crate::nips::nip60::CashuProof`]s, the `unit`, the `u` mint
26//!    URL (which MUST match the recipient's whitelist exactly), the
27//!    optional target event reference, the optional target kind, and
28//!    the recipient `p` tag.
29//!
30//! Each bundle ships an [`EventBuilder`] integration plus a
31//! `from_event` parser.
32//!
33//! [NIP-61]: https://github.com/nostr-protocol/nips/blob/master/61.md
34
35use thiserror::Error;
36
37use crate::event::{
38    Alphabet, Event, EventBuilder, EventBuilderError, EventId, EventIdError, Kind, SingleLetterTag,
39    Tag, TagError, TagKind,
40};
41use crate::key::{PublicKey, PublicKeyError};
42use crate::nips::nip60::CashuProof;
43use crate::types::{RelayUrl, RelayUrlError, Url, UrlError};
44
45/// `kind: 9321` — nutzap event.
46pub const KIND_NUTZAP: Kind = Kind::NUTZAP;
47/// `kind: 10019` — nutzap informational event.
48pub const KIND_NUTZAP_INFO: Kind = Kind::NUTZAP_INFO;
49
50mod tag_names {
51    pub(super) const RELAY: &str = "relay";
52    pub(super) const MINT: &str = "mint";
53    pub(super) const PUBKEY: &str = "pubkey";
54    pub(super) const PROOF: &str = "proof";
55    pub(super) const UNIT: &str = "unit";
56    pub(super) const U: &str = "u";
57    pub(super) const K: &str = "k";
58}
59
60/// Errors raised by the NIP-61 typed bundles.
61#[derive(Debug, Error)]
62#[non_exhaustive]
63pub enum Nip61Error {
64    /// Event kind did not match the expected NIP-61 kind.
65    #[error("expected kind {expected}, got {got}")]
66    WrongKind {
67        /// Kind the caller asked for.
68        expected: Kind,
69        /// Kind the event actually carried.
70        got: Kind,
71    },
72    /// `kind:10019` advert had no `mint` rows (recipient cannot
73    /// receive a nutzap without listing at least one mint).
74    #[error("NIP-61 informational event must list at least one mint")]
75    NoMints,
76    /// `kind:10019` advert had no `pubkey` row (recipient cannot
77    /// receive a nutzap without a P2PK lock target).
78    #[error("NIP-61 informational event missing `pubkey` row")]
79    MissingPubkey,
80    /// `kind:9321` event carried no `proof` tags.
81    #[error("NIP-61 nutzap event must carry at least one `proof` tag")]
82    NoProofs,
83    /// `kind:9321` event carried no `u` mint tag.
84    #[error("NIP-61 nutzap event missing `u` mint tag")]
85    MissingMintUrl,
86    /// `kind:9321` event carried no recipient `p` tag.
87    #[error("NIP-61 nutzap event missing recipient `p` tag")]
88    MissingRecipient,
89    /// JSON serialisation / deserialisation failed inside a `proof`
90    /// tag.
91    #[error(transparent)]
92    Json(#[from] serde_json::Error),
93    /// A relay URL was malformed.
94    #[error(transparent)]
95    RelayUrl(#[from] RelayUrlError),
96    /// A mint URL was malformed.
97    #[error(transparent)]
98    Url(#[from] UrlError),
99    /// A `pubkey`, `p`, or sender pubkey was malformed.
100    #[error(transparent)]
101    PublicKey(#[from] PublicKeyError),
102    /// A target event id was malformed.
103    #[error(transparent)]
104    EventId(#[from] EventIdError),
105    /// A target kind value was not a valid `u16`.
106    #[error("NIP-61 nutzap `k` tag is not a valid kind integer")]
107    MalformedKind,
108    /// A typed [`Tag`] could not be constructed.
109    #[error(transparent)]
110    Tag(#[from] TagError),
111    /// [`EventBuilder`] signing failed.
112    #[error(transparent)]
113    Builder(#[from] EventBuilderError),
114}
115
116/// One mint entry on a [`NutzapInfo`] advert.
117///
118/// The `mint` tag's wire form is `["mint", <url>, <unit-1>?, <unit-2>?, ...]`.
119/// Both [`NutzapInfo::from_event`] and [`NutzapInfo::to_tags`] preserve the
120/// optional unit list verbatim.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct NutzapMint {
123    /// Mint URL the recipient agrees to receive at.
124    pub url: Url,
125    /// Optional list of base units the mint supports (`sat`, `usd`,
126    /// `eur`, …).
127    pub units: Vec<String>,
128}
129
130impl NutzapMint {
131    /// Construct a mint entry with no unit markers.
132    #[must_use]
133    pub const fn new(url: Url) -> Self {
134        Self {
135            url,
136            units: Vec::new(),
137        }
138    }
139
140    /// Append a supported base-unit marker.
141    #[must_use]
142    pub fn unit(mut self, unit: impl Into<String>) -> Self {
143        self.units.push(unit.into());
144        self
145    }
146}
147
148/// Typed bundle for the replaceable `kind: 10019` informational
149/// event.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct NutzapInfo {
152    /// Relays the recipient reads nutzap events from. Senders SHOULD
153    /// publish their `kind:9321` events to these relays.
154    pub relays: Vec<RelayUrl>,
155    /// Mints the recipient agrees to receive at (≥ 1 per spec).
156    pub mints: Vec<NutzapMint>,
157    /// P2PK lock target. **MUST** be the dedicated NIP-60 wallet
158    /// pubkey, **NOT** the recipient's main Nostr identity key.
159    pub pubkey: PublicKey,
160}
161
162impl NutzapInfo {
163    /// Build an informational advert.
164    #[must_use]
165    pub const fn new(pubkey: PublicKey, mints: Vec<NutzapMint>, relays: Vec<RelayUrl>) -> Self {
166        Self {
167            relays,
168            mints,
169            pubkey,
170        }
171    }
172
173    /// Render the typed bundle to the public tag list a kind-10019
174    /// event MUST carry.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`Nip61Error::NoMints`] when [`Self::mints`] is empty.
179    pub fn to_tags(&self) -> Result<Vec<Tag>, Nip61Error> {
180        if self.mints.is_empty() {
181            return Err(Nip61Error::NoMints);
182        }
183        let mut tags: Vec<Tag> = Vec::with_capacity(self.relays.len() + self.mints.len() + 1);
184        for relay in &self.relays {
185            tags.push(Tag::with(
186                &TagKind::custom(tag_names::RELAY),
187                [relay.as_str().to_owned()],
188            ));
189        }
190        for mint in &self.mints {
191            let mut row: Vec<String> = Vec::with_capacity(1 + mint.units.len());
192            row.push(mint.url.as_str().to_owned());
193            for unit in &mint.units {
194                row.push(unit.clone());
195            }
196            tags.push(Tag::with(&TagKind::custom(tag_names::MINT), row));
197        }
198        tags.push(Tag::with(
199            &TagKind::custom(tag_names::PUBKEY),
200            [self.pubkey.to_hex()],
201        ));
202        Ok(tags)
203    }
204
205    /// Parse a signed kind-10019 event back into a typed bundle.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`Nip61Error::WrongKind`] when the event's kind is
210    /// not `10019`, [`Nip61Error::MissingPubkey`] when no `pubkey`
211    /// tag is present, and [`Nip61Error::NoMints`] when no `mint`
212    /// tag is present; otherwise forwards every URL / pubkey parse
213    /// error.
214    pub fn from_event(event: &Event) -> Result<Self, Nip61Error> {
215        if event.kind != KIND_NUTZAP_INFO {
216            return Err(Nip61Error::WrongKind {
217                expected: KIND_NUTZAP_INFO,
218                got: event.kind,
219            });
220        }
221        let mut relays: Vec<RelayUrl> = Vec::new();
222        let mut mints: Vec<NutzapMint> = Vec::new();
223        let mut pubkey: Option<PublicKey> = None;
224        for tag in &event.tags {
225            let values = tag.values();
226            // Skip empty rows defensively (the head is values[0]).
227            let Some(first) = values.get(1) else {
228                continue;
229            };
230            match tag.name() {
231                tag_names::RELAY => relays.push(RelayUrl::parse(first)?),
232                tag_names::MINT => {
233                    let url = Url::parse(first)?;
234                    let units: Vec<String> = values.iter().skip(2).cloned().collect();
235                    mints.push(NutzapMint { url, units });
236                }
237                tag_names::PUBKEY => pubkey = Some(PublicKey::parse(first)?),
238                _ => {}
239            }
240        }
241        let pubkey = pubkey.ok_or(Nip61Error::MissingPubkey)?;
242        if mints.is_empty() {
243            return Err(Nip61Error::NoMints);
244        }
245        Ok(Self {
246            relays,
247            mints,
248            pubkey,
249        })
250    }
251}
252
253/// Typed bundle for the `kind: 9321` nutzap event.
254///
255/// `proofs` MUST be non-empty per spec; the proofs are P2PK-locked
256/// to the recipient's [`NutzapInfo::pubkey`] (with the `02` Cashu
257/// prefix applied at the mint level — `nula-core` does not perform
258/// the actual minting). `mint_url` MUST exactly match one of the
259/// URLs the recipient listed in their `kind:10019`.
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct Nutzap {
262    /// Optional `.content` comment.
263    pub comment: String,
264    /// One or more P2PK-locked Cashu proofs.
265    pub proofs: Vec<CashuProof>,
266    /// Base unit (`sat`, `usd`, `eur`, …); spec default is `sat`
267    /// when omitted.
268    pub unit: Option<String>,
269    /// Mint URL the proofs were minted at — MUST match one of the
270    /// recipient's `kind:10019` `mint` tags exactly.
271    pub mint_url: Url,
272    /// Optional event being nutzapped.
273    pub target_event: Option<EventId>,
274    /// Optional kind of [`Self::target_event`].
275    pub target_kind: Option<Kind>,
276    /// Recipient's main Nostr identity pubkey (the `p` tag).
277    pub recipient: PublicKey,
278}
279
280impl Nutzap {
281    /// Construct a nutzap with the spec-required columns.
282    #[must_use]
283    pub const fn new(recipient: PublicKey, mint_url: Url, proofs: Vec<CashuProof>) -> Self {
284        Self {
285            comment: String::new(),
286            proofs,
287            unit: None,
288            mint_url,
289            target_event: None,
290            target_kind: None,
291            recipient,
292        }
293    }
294
295    /// Set the optional `.content` comment.
296    #[must_use]
297    pub fn comment(mut self, comment: impl Into<String>) -> Self {
298        self.comment = comment.into();
299        self
300    }
301
302    /// Set the optional `unit` tag.
303    #[must_use]
304    pub fn unit(mut self, unit: impl Into<String>) -> Self {
305        self.unit = Some(unit.into());
306        self
307    }
308
309    /// Set the optional target event (`e` tag).
310    #[must_use]
311    pub const fn target_event(mut self, event: EventId) -> Self {
312        self.target_event = Some(event);
313        self
314    }
315
316    /// Set the optional target kind (`k` tag).
317    #[must_use]
318    pub const fn target_kind(mut self, kind: Kind) -> Self {
319        self.target_kind = Some(kind);
320        self
321    }
322
323    /// Sum of the proof amounts.
324    #[must_use]
325    pub fn amount(&self) -> u64 {
326        self.proofs.iter().map(|p| p.amount).sum()
327    }
328
329    /// Render the typed bundle to the public tag list.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`Nip61Error::NoProofs`] when [`Self::proofs`] is
334    /// empty, or [`Nip61Error::Json`] when serialising any proof
335    /// fails (which should not happen for the well-formed
336    /// [`CashuProof`] type but is surfaced for completeness).
337    pub fn to_tags(&self) -> Result<Vec<Tag>, Nip61Error> {
338        if self.proofs.is_empty() {
339            return Err(Nip61Error::NoProofs);
340        }
341        let mut tags: Vec<Tag> = Vec::with_capacity(self.proofs.len() + 5);
342        for proof in &self.proofs {
343            let json = serde_json::to_string(proof)?;
344            tags.push(Tag::with(&TagKind::custom(tag_names::PROOF), [json]));
345        }
346        if let Some(unit) = &self.unit {
347            tags.push(Tag::with(&TagKind::custom(tag_names::UNIT), [unit.clone()]));
348        }
349        tags.push(Tag::with(
350            &TagKind::custom(tag_names::U),
351            [self.mint_url.as_str().to_owned()],
352        ));
353        if let Some(event) = self.target_event {
354            tags.push(Tag::with(
355                &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E)),
356                [event.to_hex(), String::new()],
357            ));
358        }
359        if let Some(kind) = self.target_kind {
360            tags.push(Tag::with(
361                &TagKind::custom(tag_names::K),
362                [kind.as_u16().to_string()],
363            ));
364        }
365        tags.push(Tag::with(
366            &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P)),
367            [self.recipient.to_hex()],
368        ));
369        Ok(tags)
370    }
371
372    /// Parse a signed kind-9321 event back into a typed bundle.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`Nip61Error::WrongKind`] when the event's kind is
377    /// not `9321`, [`Nip61Error::NoProofs`] /
378    /// [`Nip61Error::MissingMintUrl`] / [`Nip61Error::MissingRecipient`]
379    /// when the spec-required columns are absent, and forwards
380    /// every parse / JSON error.
381    pub fn from_event(event: &Event) -> Result<Self, Nip61Error> {
382        if event.kind != KIND_NUTZAP {
383            return Err(Nip61Error::WrongKind {
384                expected: KIND_NUTZAP,
385                got: event.kind,
386            });
387        }
388        let mut proofs: Vec<CashuProof> = Vec::new();
389        let mut unit: Option<String> = None;
390        let mut mint_url: Option<Url> = None;
391        let mut target_event: Option<EventId> = None;
392        let mut target_kind: Option<Kind> = None;
393        let mut recipient: Option<PublicKey> = None;
394        for tag in &event.tags {
395            let values = tag.values();
396            let Some(first) = values.get(1) else {
397                continue;
398            };
399            match tag.name() {
400                tag_names::PROOF => {
401                    let proof: CashuProof = serde_json::from_str(first)?;
402                    proofs.push(proof);
403                }
404                tag_names::UNIT => unit = Some(first.clone()),
405                tag_names::U => mint_url = Some(Url::parse(first)?),
406                "e" => target_event = Some(EventId::parse(first)?),
407                tag_names::K => {
408                    let raw: u16 = first.parse().map_err(|_| Nip61Error::MalformedKind)?;
409                    target_kind = Some(Kind::new(raw));
410                }
411                "p" => recipient = Some(PublicKey::parse(first)?),
412                _ => {}
413            }
414        }
415        if proofs.is_empty() {
416            return Err(Nip61Error::NoProofs);
417        }
418        let mint_url = mint_url.ok_or(Nip61Error::MissingMintUrl)?;
419        let recipient = recipient.ok_or(Nip61Error::MissingRecipient)?;
420        Ok(Self {
421            comment: event.content.clone(),
422            proofs,
423            unit,
424            mint_url,
425            target_event,
426            target_kind,
427            recipient,
428        })
429    }
430}
431
432impl EventBuilder {
433    /// Author a NIP-61 informational event (`kind: 10019`) from a
434    /// typed [`NutzapInfo`].
435    ///
436    /// # Errors
437    ///
438    /// Forwards every error from [`NutzapInfo::to_tags`].
439    pub fn nutzap_info(info: &NutzapInfo) -> Result<Self, Nip61Error> {
440        let mut builder = Self::new(KIND_NUTZAP_INFO, "");
441        for tag in info.to_tags()? {
442            builder = builder.tag(tag);
443        }
444        Ok(builder)
445    }
446
447    /// Author a NIP-61 nutzap event (`kind: 9321`) from a typed
448    /// [`Nutzap`] bundle.
449    ///
450    /// # Errors
451    ///
452    /// Forwards every error from [`Nutzap::to_tags`].
453    pub fn nutzap(zap: &Nutzap) -> Result<Self, Nip61Error> {
454        let mut builder = Self::new(KIND_NUTZAP, zap.comment.clone());
455        for tag in zap.to_tags()? {
456            builder = builder.tag(tag);
457        }
458        Ok(builder)
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use crate::Keys;
466
467    fn keys() -> Keys {
468        Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
469    }
470
471    fn other_keys() -> Keys {
472        Keys::parse("0000000000000000000000000000000000000000000000000000000000000005").unwrap()
473    }
474
475    fn mint_url() -> Url {
476        Url::parse("https://stablenut.umint.cash").unwrap()
477    }
478
479    fn relay_url() -> RelayUrl {
480        RelayUrl::parse("wss://relay.example/").unwrap()
481    }
482
483    fn proof(amount: u64) -> CashuProof {
484        CashuProof {
485            id: "000a93d6f8a1d2c4".to_owned(),
486            amount,
487            secret:
488                "[\"P2PK\",{\"nonce\":\"deadbeef\",\"data\":\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\"}]"
489                    .to_owned(),
490            c: "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f"
491                .to_owned(),
492        }
493    }
494
495    #[test]
496    fn nutzap_info_round_trips_through_event() {
497        let info = NutzapInfo::new(
498            *other_keys().public_key(),
499            vec![NutzapMint::new(mint_url()).unit("sat").unit("usd")],
500            vec![relay_url()],
501        );
502        let event = EventBuilder::nutzap_info(&info)
503            .unwrap()
504            .sign_with_keys(&keys())
505            .unwrap();
506        assert_eq!(event.kind, KIND_NUTZAP_INFO);
507        let recovered = NutzapInfo::from_event(&event).unwrap();
508        assert_eq!(recovered, info);
509    }
510
511    #[test]
512    fn nutzap_info_to_tags_rejects_empty_mints() {
513        let info = NutzapInfo::new(*other_keys().public_key(), Vec::new(), vec![relay_url()]);
514        assert!(matches!(info.to_tags(), Err(Nip61Error::NoMints)));
515    }
516
517    #[test]
518    fn nutzap_info_from_event_requires_pubkey() {
519        let event = EventBuilder::new(KIND_NUTZAP_INFO, "")
520            .tag(Tag::with(
521                &TagKind::custom(tag_names::MINT),
522                [mint_url().as_str().to_owned()],
523            ))
524            .sign_with_keys(&keys())
525            .unwrap();
526        assert!(matches!(
527            NutzapInfo::from_event(&event),
528            Err(Nip61Error::MissingPubkey),
529        ));
530    }
531
532    #[test]
533    fn nutzap_info_from_event_requires_mint() {
534        let event = EventBuilder::new(KIND_NUTZAP_INFO, "")
535            .tag(Tag::with(
536                &TagKind::custom(tag_names::PUBKEY),
537                [other_keys().public_key().to_hex()],
538            ))
539            .sign_with_keys(&keys())
540            .unwrap();
541        assert!(matches!(
542            NutzapInfo::from_event(&event),
543            Err(Nip61Error::NoMints),
544        ));
545    }
546
547    #[test]
548    fn nutzap_info_from_event_rejects_wrong_kind() {
549        let event = EventBuilder::text_note("not info")
550            .sign_with_keys(&keys())
551            .unwrap();
552        assert!(matches!(
553            NutzapInfo::from_event(&event),
554            Err(Nip61Error::WrongKind { .. }),
555        ));
556    }
557
558    #[test]
559    fn nutzap_round_trips_through_event() {
560        let zap = Nutzap::new(
561            *other_keys().public_key(),
562            mint_url(),
563            vec![proof(1), proof(2)],
564        )
565        .comment("Thanks for this great idea.")
566        .unit("sat")
567        .target_event(EventId::from_byte_array([0xab; 32]))
568        .target_kind(Kind::TEXT_NOTE);
569        let event = EventBuilder::nutzap(&zap)
570            .unwrap()
571            .sign_with_keys(&keys())
572            .unwrap();
573        assert_eq!(event.kind, KIND_NUTZAP);
574        assert_eq!(event.content, "Thanks for this great idea.");
575        let recovered = Nutzap::from_event(&event).unwrap();
576        assert_eq!(recovered, zap);
577        assert_eq!(recovered.amount(), 3);
578    }
579
580    #[test]
581    fn nutzap_to_tags_rejects_empty_proofs() {
582        let zap = Nutzap::new(*other_keys().public_key(), mint_url(), Vec::new());
583        assert!(matches!(zap.to_tags(), Err(Nip61Error::NoProofs)));
584    }
585
586    #[test]
587    fn nutzap_from_event_requires_proofs() {
588        let event = EventBuilder::new(KIND_NUTZAP, "")
589            .tag(Tag::with(
590                &TagKind::custom(tag_names::U),
591                [mint_url().as_str().to_owned()],
592            ))
593            .tag(Tag::with(
594                &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P)),
595                [other_keys().public_key().to_hex()],
596            ))
597            .sign_with_keys(&keys())
598            .unwrap();
599        assert!(matches!(
600            Nutzap::from_event(&event),
601            Err(Nip61Error::NoProofs),
602        ));
603    }
604
605    #[test]
606    fn nutzap_from_event_requires_recipient() {
607        let event = EventBuilder::new(KIND_NUTZAP, "")
608            .tag(Tag::with(
609                &TagKind::custom(tag_names::PROOF),
610                [serde_json::to_string(&proof(1)).unwrap()],
611            ))
612            .tag(Tag::with(
613                &TagKind::custom(tag_names::U),
614                [mint_url().as_str().to_owned()],
615            ))
616            .sign_with_keys(&keys())
617            .unwrap();
618        assert!(matches!(
619            Nutzap::from_event(&event),
620            Err(Nip61Error::MissingRecipient),
621        ));
622    }
623
624    #[test]
625    fn nutzap_from_event_requires_mint_url() {
626        let event = EventBuilder::new(KIND_NUTZAP, "")
627            .tag(Tag::with(
628                &TagKind::custom(tag_names::PROOF),
629                [serde_json::to_string(&proof(1)).unwrap()],
630            ))
631            .tag(Tag::with(
632                &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P)),
633                [other_keys().public_key().to_hex()],
634            ))
635            .sign_with_keys(&keys())
636            .unwrap();
637        assert!(matches!(
638            Nutzap::from_event(&event),
639            Err(Nip61Error::MissingMintUrl),
640        ));
641    }
642
643    #[test]
644    fn nutzap_from_event_rejects_wrong_kind() {
645        let event = EventBuilder::text_note("not a nutzap")
646            .sign_with_keys(&keys())
647            .unwrap();
648        assert!(matches!(
649            Nutzap::from_event(&event),
650            Err(Nip61Error::WrongKind { .. }),
651        ));
652    }
653}