1use std::collections::HashSet;
59use std::fmt;
60
61use serde::{Deserialize, Serialize};
62use thiserror::Error;
63use url::form_urlencoded;
64
65use crate::event::{Alphabet, Event, EventBuilder, Kind, SingleLetterTag, Tag, TagKind};
66use crate::key::{PublicKey, PublicKeyError, SecretKey, SecretKeyError};
67#[cfg(feature = "nip04")]
68use crate::nips::nip04;
69#[cfg(feature = "nip44")]
70use crate::nips::nip44;
71use crate::types::{RelayUrl, RelayUrlError, Timestamp};
72
73pub const KIND_INFO: Kind = Kind::WALLET_CONNECT_INFO;
75pub const KIND_REQUEST: Kind = Kind::WALLET_CONNECT_REQUEST;
77pub const KIND_RESPONSE: Kind = Kind::WALLET_CONNECT_RESPONSE;
79pub const KIND_NOTIFICATION: Kind = Kind::WALLET_CONNECT_NOTIFICATION;
81pub const KIND_NOTIFICATION_LEGACY: Kind = Kind::WALLET_CONNECT_NOTIFICATION_LEGACY;
83
84pub const URI_SCHEME: &str = "nostr+walletconnect://";
86pub const ENCRYPTION_TAG: &str = "encryption";
88pub const NOTIFICATIONS_TAG: &str = "notifications";
90
91pub mod encryption_tokens {
93 pub const NIP44_V2: &str = "nip44_v2";
95 pub const NIP04: &str = "nip04";
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub enum Encryption {
102 Nip44V2,
104 Nip04,
108}
109
110impl Encryption {
111 #[must_use]
113 pub const fn as_token(self) -> &'static str {
114 match self {
115 Self::Nip44V2 => encryption_tokens::NIP44_V2,
116 Self::Nip04 => encryption_tokens::NIP04,
117 }
118 }
119
120 pub fn parse(token: &str) -> Result<Self, NwcError> {
126 match token {
127 encryption_tokens::NIP44_V2 => Ok(Self::Nip44V2),
128 encryption_tokens::NIP04 => Ok(Self::Nip04),
129 other => Err(NwcError::UnknownEncryption(other.to_owned())),
130 }
131 }
132
133 pub fn negotiate(
145 wallet_supported: &[Self],
146 client_supported: &[Self],
147 ) -> Result<Self, NwcError> {
148 if wallet_supported.is_empty() {
149 return Ok(Self::Nip04);
150 }
151 let wallet: HashSet<Self> = wallet_supported.iter().copied().collect();
152 let client: HashSet<Self> = client_supported.iter().copied().collect();
153 if wallet.contains(&Self::Nip44V2) && client.contains(&Self::Nip44V2) {
154 Ok(Self::Nip44V2)
155 } else if wallet.contains(&Self::Nip04) && client.contains(&Self::Nip04) {
156 Ok(Self::Nip04)
157 } else {
158 Err(NwcError::EncryptionNotNegotiable)
159 }
160 }
161}
162
163impl fmt::Display for Encryption {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 f.write_str(self.as_token())
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct ConnectionUri {
172 pub wallet_pubkey: PublicKey,
174 pub relays: Vec<RelayUrl>,
176 pub secret: SecretKey,
179 pub lud16: Option<String>,
181}
182
183impl ConnectionUri {
184 pub fn parse(input: &str) -> Result<Self, NwcError> {
197 let rest = input
198 .strip_prefix(URI_SCHEME)
199 .ok_or(NwcError::UriBadScheme)?;
200 let (host, query) = match rest.split_once('?') {
201 Some((host, query)) => (host, query),
202 None => (rest, ""),
203 };
204 if host.is_empty() {
205 return Err(NwcError::UriMissingPubkey);
206 }
207 let wallet_pubkey = PublicKey::parse(host).map_err(NwcError::InvalidPublicKey)?;
208
209 let mut relays: Vec<RelayUrl> = Vec::new();
210 let mut secret: Option<SecretKey> = None;
211 let mut lud16: Option<String> = None;
212 for (key, value) in form_urlencoded::parse(query.as_bytes()) {
213 match key.as_ref() {
214 "relay" => {
215 let url = RelayUrl::parse(value.as_ref()).map_err(NwcError::InvalidRelayUrl)?;
216 relays.push(url);
217 }
218 "secret" => {
219 secret =
220 Some(SecretKey::parse(value.as_ref()).map_err(NwcError::InvalidSecretKey)?);
221 }
222 "lud16" => {
223 lud16 = Some(value.into_owned());
224 }
225 _ => { }
226 }
227 }
228 if relays.is_empty() {
229 return Err(NwcError::UriMissingRelay);
230 }
231 let secret = secret.ok_or(NwcError::UriMissingSecret)?;
232 Ok(Self {
233 wallet_pubkey,
234 relays,
235 secret,
236 lud16,
237 })
238 }
239
240 #[must_use]
242 pub fn to_uri(&self) -> String {
243 let mut serializer = form_urlencoded::Serializer::new(String::new());
244 for relay in &self.relays {
245 serializer.append_pair("relay", relay.as_str());
246 }
247 serializer.append_pair("secret", &self.secret.to_hex());
248 if let Some(lud16) = &self.lud16 {
249 serializer.append_pair("lud16", lud16);
250 }
251 let query = serializer.finish();
252 format!("{URI_SCHEME}{}?{query}", self.wallet_pubkey.to_hex())
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct InfoEvent {
259 pub methods: Vec<String>,
261 pub notifications: Vec<String>,
263 pub encryption_schemes: Vec<Encryption>,
267}
268
269impl InfoEvent {
270 #[must_use]
272 pub const fn new() -> Self {
273 Self {
274 methods: Vec::new(),
275 notifications: Vec::new(),
276 encryption_schemes: Vec::new(),
277 }
278 }
279
280 #[must_use]
282 pub fn method(mut self, method: impl Into<String>) -> Self {
283 self.methods.push(method.into());
284 self
285 }
286
287 #[must_use]
289 pub fn notification(mut self, notification: impl Into<String>) -> Self {
290 self.notifications.push(notification.into());
291 self
292 }
293
294 #[must_use]
296 pub fn encryption(mut self, scheme: Encryption) -> Self {
297 self.encryption_schemes.push(scheme);
298 self
299 }
300
301 #[must_use]
303 pub fn content(&self) -> String {
304 self.methods.join(" ")
305 }
306
307 #[must_use]
309 pub fn to_tags(&self) -> Vec<Tag> {
310 let mut tags: Vec<Tag> = Vec::with_capacity(2);
311 if !self.encryption_schemes.is_empty() {
312 let mut values: Vec<String> = Vec::with_capacity(self.encryption_schemes.len() + 1);
313 for scheme in &self.encryption_schemes {
314 values.push(scheme.as_token().to_owned());
315 }
316 tags.push(custom_tag(ENCRYPTION_TAG, [values.join(" ")]));
317 }
318 if !self.notifications.is_empty() {
319 tags.push(custom_tag(
320 NOTIFICATIONS_TAG,
321 [self.notifications.join(" ")],
322 ));
323 }
324 tags
325 }
326
327 pub fn from_event(event: &Event) -> Result<Self, NwcError> {
339 if event.kind != KIND_INFO {
340 return Err(NwcError::WrongKind(event.kind));
341 }
342 let methods = event
343 .content
344 .split_whitespace()
345 .map(str::to_owned)
346 .collect();
347 let mut notifications: Vec<String> = Vec::new();
348 let mut encryption_schemes: Vec<Encryption> = Vec::new();
349 for tag in &event.tags {
350 match tag.name() {
351 NOTIFICATIONS_TAG => parse_notifications_tag(tag, &mut notifications),
352 ENCRYPTION_TAG => parse_encryption_tag(tag, &mut encryption_schemes)?,
353 _ => {}
354 }
355 }
356 Ok(Self {
357 methods,
358 notifications,
359 encryption_schemes,
360 })
361 }
362}
363
364impl Default for InfoEvent {
365 fn default() -> Self {
366 Self::new()
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct Request {
374 pub method: String,
376 pub params: serde_json::Value,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383pub struct Response {
384 pub result_type: String,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
388 pub error: Option<ResponseError>,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub result: Option<serde_json::Value>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396pub struct ResponseError {
397 pub code: ErrorCode,
399 pub message: String,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406pub struct Notification {
407 pub notification_type: String,
409 pub notification: serde_json::Value,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Hash)]
415#[non_exhaustive]
416pub enum ErrorCode {
417 RateLimited,
419 NotImplemented,
421 InsufficientBalance,
423 QuotaExceeded,
425 Restricted,
427 Unauthorized,
429 Internal,
431 UnsupportedEncryption,
433 PaymentFailed,
436 NotFound,
438 Other,
440 Custom(String),
442}
443
444impl ErrorCode {
445 #[must_use]
450 #[expect(
451 clippy::missing_const_for_fn,
452 reason = "`Self::Custom` borrows from a heap `String`"
453 )]
454 pub fn as_str(&self) -> &str {
455 match self {
456 Self::RateLimited => "RATE_LIMITED",
457 Self::NotImplemented => "NOT_IMPLEMENTED",
458 Self::InsufficientBalance => "INSUFFICIENT_BALANCE",
459 Self::QuotaExceeded => "QUOTA_EXCEEDED",
460 Self::Restricted => "RESTRICTED",
461 Self::Unauthorized => "UNAUTHORIZED",
462 Self::Internal => "INTERNAL",
463 Self::UnsupportedEncryption => "UNSUPPORTED_ENCRYPTION",
464 Self::PaymentFailed => "PAYMENT_FAILED",
465 Self::NotFound => "NOT_FOUND",
466 Self::Other => "OTHER",
467 Self::Custom(s) => s.as_str(),
468 }
469 }
470
471 #[must_use]
473 pub fn parse(token: &str) -> Self {
474 match token {
475 "RATE_LIMITED" => Self::RateLimited,
476 "NOT_IMPLEMENTED" => Self::NotImplemented,
477 "INSUFFICIENT_BALANCE" => Self::InsufficientBalance,
478 "QUOTA_EXCEEDED" => Self::QuotaExceeded,
479 "RESTRICTED" => Self::Restricted,
480 "UNAUTHORIZED" => Self::Unauthorized,
481 "INTERNAL" => Self::Internal,
482 "UNSUPPORTED_ENCRYPTION" => Self::UnsupportedEncryption,
483 "PAYMENT_FAILED" => Self::PaymentFailed,
484 "NOT_FOUND" => Self::NotFound,
485 "OTHER" => Self::Other,
486 other => Self::Custom(other.to_owned()),
487 }
488 }
489}
490
491impl fmt::Display for ErrorCode {
492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493 f.write_str(self.as_str())
494 }
495}
496
497impl Serialize for ErrorCode {
498 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
499 s.serialize_str(self.as_str())
500 }
501}
502
503impl<'de> Deserialize<'de> for ErrorCode {
504 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
505 let s = String::deserialize(d)?;
506 Ok(Self::parse(&s))
507 }
508}
509
510fn parse_notifications_tag(tag: &Tag, out: &mut Vec<String>) {
511 if let Some(v) = tag.get(1) {
512 *out = v.split_whitespace().map(str::to_owned).collect();
513 }
514}
515
516fn parse_encryption_tag(tag: &Tag, out: &mut Vec<Encryption>) -> Result<(), NwcError> {
517 let Some(v) = tag.get(1) else { return Ok(()) };
518 for token in v.split_whitespace() {
519 out.push(Encryption::parse(token)?);
520 }
521 Ok(())
522}
523
524fn custom_tag<I, S>(name: &str, args: I) -> Tag
525where
526 I: IntoIterator<Item = S>,
527 S: Into<String>,
528{
529 Tag::with(&TagKind::from_wire(name), args)
530}
531
532fn p_tag(pubkey: PublicKey) -> Tag {
533 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
534 Tag::with(&head, [pubkey.to_hex()])
535}
536
537fn e_tag(id: crate::event::EventId) -> Tag {
538 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
539 Tag::with(&head, [id.to_hex()])
540}
541
542#[derive(Debug, Error)]
544#[non_exhaustive]
545pub enum NwcError {
546 #[error("unexpected kind {}", .0.as_u16())]
548 WrongKind(Kind),
549 #[error("URI must start with `{URI_SCHEME}`")]
551 UriBadScheme,
552 #[error("URI is missing the wallet pubkey host")]
554 UriMissingPubkey,
555 #[error("URI is missing the `relay` query parameter")]
557 UriMissingRelay,
558 #[error("URI is missing the `secret` query parameter")]
560 UriMissingSecret,
561 #[error("invalid public key: {0}")]
563 InvalidPublicKey(#[source] PublicKeyError),
564 #[error("invalid secret key: {0}")]
566 InvalidSecretKey(#[source] SecretKeyError),
567 #[error("invalid relay URL: {0}")]
569 InvalidRelayUrl(#[source] RelayUrlError),
570 #[error("unknown encryption scheme: {0}")]
572 UnknownEncryption(String),
573 #[error("client and wallet do not share a supported encryption scheme")]
575 EncryptionNotNegotiable,
576 #[error("invalid JSON-RPC payload: {0}")]
578 InvalidJson(#[source] serde_json::Error),
579 #[error("event missing required `p` tag")]
581 MissingPTag,
582 #[error("response event missing required `e` tag")]
584 MissingETag,
585 #[cfg(feature = "nip44")]
587 #[cfg_attr(docsrs, doc(cfg(feature = "nip44")))]
588 #[error("NIP-44 failure: {0}")]
589 Nip44(#[source] nip44::Nip44Error),
590 #[cfg(feature = "nip04")]
592 #[cfg_attr(docsrs, doc(cfg(feature = "nip04")))]
593 #[error("NIP-04 failure: {0}")]
594 Nip04(#[source] nip04::Nip04Error),
595 #[cfg(not(feature = "nip04"))]
597 #[error("NIP-04 fallback required but the `nip04` feature is disabled")]
598 Nip04Unavailable,
599}
600
601#[cfg(feature = "nip44")]
602fn encrypt_with(
603 encryption: Encryption,
604 secret: &SecretKey,
605 peer: &PublicKey,
606 plaintext: &str,
607) -> Result<String, NwcError> {
608 match encryption {
609 Encryption::Nip44V2 => nip44::encrypt(secret, peer, plaintext).map_err(NwcError::Nip44),
610 #[cfg(feature = "nip04")]
611 Encryption::Nip04 => nip04::encrypt(secret, peer, plaintext).map_err(NwcError::Nip04),
612 #[cfg(not(feature = "nip04"))]
613 Encryption::Nip04 => Err(NwcError::Nip04Unavailable),
614 }
615}
616
617#[cfg(feature = "nip44")]
618fn decrypt_with(
619 encryption: Encryption,
620 secret: &SecretKey,
621 peer: &PublicKey,
622 payload: &str,
623) -> Result<String, NwcError> {
624 match encryption {
625 Encryption::Nip44V2 => nip44::decrypt(secret, peer, payload).map_err(NwcError::Nip44),
626 #[cfg(feature = "nip04")]
627 Encryption::Nip04 => nip04::decrypt(secret, peer, payload).map_err(NwcError::Nip04),
628 #[cfg(not(feature = "nip04"))]
629 Encryption::Nip04 => Err(NwcError::Nip04Unavailable),
630 }
631}
632
633pub fn encryption_for_event(event: &Event) -> Result<Encryption, NwcError> {
641 for tag in &event.tags {
642 if tag.name() == ENCRYPTION_TAG
643 && let Some(token) = tag.get(1)
644 {
645 return Encryption::parse(token);
646 }
647 }
648 Ok(Encryption::Nip04)
649}
650
651#[cfg(feature = "nip44")]
652#[cfg_attr(docsrs, doc(cfg(feature = "nip44")))]
653impl EventBuilder {
654 #[must_use]
657 pub fn nwc_info(info: &InfoEvent) -> Self {
658 let mut builder = Self::new(KIND_INFO, info.content());
659 for tag in info.to_tags() {
660 builder = builder.tag(tag);
661 }
662 builder
663 }
664
665 pub fn nwc_request(
680 client_secret: &SecretKey,
681 wallet_public: &PublicKey,
682 request: &Request,
683 encryption: Encryption,
684 expiration: Option<Timestamp>,
685 ) -> Result<Self, NwcError> {
686 let plaintext = serde_json::to_string(request).map_err(NwcError::InvalidJson)?;
687 let ciphertext = encrypt_with(encryption, client_secret, wallet_public, &plaintext)?;
688 let mut builder = Self::new(KIND_REQUEST, ciphertext)
689 .tag(p_tag(*wallet_public))
690 .tag(custom_tag(ENCRYPTION_TAG, [encryption.as_token()]));
691 if let Some(ts) = expiration {
692 builder = builder.expiration(ts);
693 }
694 Ok(builder)
695 }
696
697 pub fn nwc_response(
703 wallet_secret: &SecretKey,
704 client_public: &PublicKey,
705 request_event_id: crate::event::EventId,
706 response: &Response,
707 encryption: Encryption,
708 ) -> Result<Self, NwcError> {
709 let plaintext = serde_json::to_string(response).map_err(NwcError::InvalidJson)?;
710 let ciphertext = encrypt_with(encryption, wallet_secret, client_public, &plaintext)?;
711 Ok(Self::new(KIND_RESPONSE, ciphertext)
712 .tag(p_tag(*client_public))
713 .tag(e_tag(request_event_id))
714 .tag(custom_tag(ENCRYPTION_TAG, [encryption.as_token()])))
715 }
716
717 pub fn nwc_notification(
726 wallet_secret: &SecretKey,
727 client_public: &PublicKey,
728 notification: &Notification,
729 encryption: Encryption,
730 ) -> Result<Self, NwcError> {
731 let kind = match encryption {
732 Encryption::Nip44V2 => KIND_NOTIFICATION,
733 Encryption::Nip04 => KIND_NOTIFICATION_LEGACY,
734 };
735 let plaintext = serde_json::to_string(notification).map_err(NwcError::InvalidJson)?;
736 let ciphertext = encrypt_with(encryption, wallet_secret, client_public, &plaintext)?;
737 Ok(Self::new(kind, ciphertext)
738 .tag(p_tag(*client_public))
739 .tag(custom_tag(ENCRYPTION_TAG, [encryption.as_token()])))
740 }
741}
742
743#[cfg(feature = "nip44")]
755#[cfg_attr(docsrs, doc(cfg(feature = "nip44")))]
756pub fn decrypt_request(event: &Event, wallet_secret: &SecretKey) -> Result<Request, NwcError> {
757 if event.kind != KIND_REQUEST {
758 return Err(NwcError::WrongKind(event.kind));
759 }
760 let encryption = encryption_for_event(event)?;
761 let plaintext = decrypt_with(encryption, wallet_secret, &event.pubkey, &event.content)?;
762 serde_json::from_str(&plaintext).map_err(NwcError::InvalidJson)
763}
764
765#[cfg(feature = "nip44")]
771#[cfg_attr(docsrs, doc(cfg(feature = "nip44")))]
772pub fn decrypt_response(event: &Event, client_secret: &SecretKey) -> Result<Response, NwcError> {
773 if event.kind != KIND_RESPONSE {
774 return Err(NwcError::WrongKind(event.kind));
775 }
776 let encryption = encryption_for_event(event)?;
777 let plaintext = decrypt_with(encryption, client_secret, &event.pubkey, &event.content)?;
778 serde_json::from_str(&plaintext).map_err(NwcError::InvalidJson)
779}
780
781#[cfg(feature = "nip44")]
788#[cfg_attr(docsrs, doc(cfg(feature = "nip44")))]
789pub fn decrypt_notification(
790 event: &Event,
791 client_secret: &SecretKey,
792) -> Result<Notification, NwcError> {
793 if event.kind != KIND_NOTIFICATION && event.kind != KIND_NOTIFICATION_LEGACY {
794 return Err(NwcError::WrongKind(event.kind));
795 }
796 let encryption = encryption_for_event(event)?;
797 let plaintext = decrypt_with(encryption, client_secret, &event.pubkey, &event.content)?;
798 serde_json::from_str(&plaintext).map_err(NwcError::InvalidJson)
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use crate::Keys;
805
806 fn wallet() -> Keys {
807 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
808 }
809
810 fn client() -> Keys {
811 Keys::parse("0000000000000000000000000000000000000000000000000000000000000005").unwrap()
812 }
813
814 #[test]
815 fn connection_uri_round_trips_with_lud16() {
816 let uri = ConnectionUri {
817 wallet_pubkey: *wallet().public_key(),
818 relays: vec![
819 RelayUrl::parse("wss://relay.one/").unwrap(),
820 RelayUrl::parse("wss://relay.two/").unwrap(),
821 ],
822 secret: client().secret_key().clone(),
823 lud16: Some("alice@example.com".to_owned()),
824 };
825 let wire = uri.to_uri();
826 let parsed = ConnectionUri::parse(&wire).unwrap();
827 assert_eq!(parsed, uri);
828 }
829
830 #[test]
831 fn connection_uri_rejects_bad_scheme() {
832 let err = ConnectionUri::parse("https://example.com/").unwrap_err();
833 assert!(matches!(err, NwcError::UriBadScheme));
834 }
835
836 #[test]
837 fn connection_uri_requires_relay_and_secret() {
838 let pk = wallet().public_key().to_hex();
839 let no_relay = format!(
840 "nostr+walletconnect://{pk}?secret={}",
841 client().secret_key().to_hex()
842 );
843 assert!(matches!(
844 ConnectionUri::parse(&no_relay),
845 Err(NwcError::UriMissingRelay)
846 ));
847 let no_secret = format!("nostr+walletconnect://{pk}?relay=wss%3A%2F%2Frelay/");
848 assert!(matches!(
849 ConnectionUri::parse(&no_secret),
850 Err(NwcError::UriMissingSecret)
851 ));
852 }
853
854 #[test]
855 fn info_event_round_trips() {
856 let info = InfoEvent::new()
857 .method("pay_invoice")
858 .method("get_balance")
859 .notification("payment_received")
860 .encryption(Encryption::Nip44V2)
861 .encryption(Encryption::Nip04);
862 let event = EventBuilder::nwc_info(&info)
863 .sign_with_keys(&wallet())
864 .unwrap();
865 assert_eq!(event.kind, KIND_INFO);
866 let parsed = InfoEvent::from_event(&event).unwrap();
867 assert_eq!(parsed.methods, vec!["pay_invoice", "get_balance"]);
868 assert_eq!(parsed.notifications, vec!["payment_received"]);
869 assert_eq!(
870 parsed.encryption_schemes,
871 vec![Encryption::Nip44V2, Encryption::Nip04]
872 );
873 }
874
875 #[test]
876 fn info_event_without_encryption_tag_is_nip04_only() {
877 let event = EventBuilder::new(KIND_INFO, "pay_invoice")
878 .sign_with_keys(&wallet())
879 .unwrap();
880 let info = InfoEvent::from_event(&event).unwrap();
881 assert!(info.encryption_schemes.is_empty());
882 let scheme = Encryption::negotiate(
883 &info.encryption_schemes,
884 &[Encryption::Nip44V2, Encryption::Nip04],
885 )
886 .unwrap();
887 assert_eq!(scheme, Encryption::Nip04);
888 }
889
890 #[test]
891 fn encryption_negotiation_prefers_nip44_v2() {
892 let scheme = Encryption::negotiate(
893 &[Encryption::Nip44V2, Encryption::Nip04],
894 &[Encryption::Nip44V2, Encryption::Nip04],
895 )
896 .unwrap();
897 assert_eq!(scheme, Encryption::Nip44V2);
898 }
899
900 #[test]
901 fn encryption_negotiation_falls_back_to_nip04_when_only_overlap() {
902 let scheme = Encryption::negotiate(
903 &[Encryption::Nip04],
904 &[Encryption::Nip44V2, Encryption::Nip04],
905 )
906 .unwrap();
907 assert_eq!(scheme, Encryption::Nip04);
908 }
909
910 #[test]
911 fn encryption_negotiation_fails_when_no_overlap() {
912 let err = Encryption::negotiate(&[Encryption::Nip04], &[Encryption::Nip44V2]).unwrap_err();
913 assert!(matches!(err, NwcError::EncryptionNotNegotiable));
914 }
915
916 #[test]
917 fn error_code_round_trips_through_serde() {
918 let code = ErrorCode::PaymentFailed;
919 let json = serde_json::to_string(&code).unwrap();
920 assert_eq!(json, "\"PAYMENT_FAILED\"");
921 let parsed: ErrorCode = serde_json::from_str(&json).unwrap();
922 assert_eq!(parsed, code);
923 }
924
925 #[test]
926 fn error_code_unknown_passes_through_as_custom() {
927 let code: ErrorCode = serde_json::from_str("\"FUTURE_CODE\"").unwrap();
928 assert_eq!(code, ErrorCode::Custom("FUTURE_CODE".to_owned()));
929 }
930
931 #[cfg(feature = "nip44")]
932 #[test]
933 fn request_response_round_trip_through_nip44() {
934 let request = Request {
935 method: "pay_invoice".to_owned(),
936 params: serde_json::json!({ "invoice": "lnbc1..." }),
937 };
938 let req_event = EventBuilder::nwc_request(
939 client().secret_key(),
940 wallet().public_key(),
941 &request,
942 Encryption::Nip44V2,
943 None,
944 )
945 .unwrap()
946 .sign_with_keys(&client())
947 .unwrap();
948
949 let parsed = decrypt_request(&req_event, wallet().secret_key()).unwrap();
950 assert_eq!(parsed, request);
951
952 let response = Response {
953 result_type: "pay_invoice".to_owned(),
954 error: None,
955 result: Some(serde_json::json!({ "preimage": "deadbeef" })),
956 };
957 let resp_event = EventBuilder::nwc_response(
958 wallet().secret_key(),
959 client().public_key(),
960 req_event.id,
961 &response,
962 Encryption::Nip44V2,
963 )
964 .unwrap()
965 .sign_with_keys(&wallet())
966 .unwrap();
967
968 let parsed_resp = decrypt_response(&resp_event, client().secret_key()).unwrap();
969 assert_eq!(parsed_resp, response);
970 }
971
972 #[cfg(all(feature = "nip44", feature = "nip04"))]
973 #[test]
974 fn legacy_notification_uses_nip04_kind_and_works_end_to_end() {
975 let notification = Notification {
976 notification_type: "payment_received".to_owned(),
977 notification: serde_json::json!({ "payment_hash": "abc" }),
978 };
979 let event = EventBuilder::nwc_notification(
980 wallet().secret_key(),
981 client().public_key(),
982 ¬ification,
983 Encryption::Nip04,
984 )
985 .unwrap()
986 .sign_with_keys(&wallet())
987 .unwrap();
988 assert_eq!(event.kind, KIND_NOTIFICATION_LEGACY);
989
990 let parsed = decrypt_notification(&event, client().secret_key()).unwrap();
991 assert_eq!(parsed, notification);
992 }
993
994 #[cfg(feature = "nip44")]
995 #[test]
996 fn request_with_expiration_attaches_nip40_tag() {
997 let request = Request {
998 method: "get_balance".to_owned(),
999 params: serde_json::json!({}),
1000 };
1001 let event = EventBuilder::nwc_request(
1002 client().secret_key(),
1003 wallet().public_key(),
1004 &request,
1005 Encryption::Nip44V2,
1006 Some(Timestamp::from_secs(2_000_000_000)),
1007 )
1008 .unwrap()
1009 .sign_with_keys(&client())
1010 .unwrap();
1011 let has_expiration = event.tags.iter().any(|t| t.name() == "expiration");
1012 assert!(has_expiration);
1013 }
1014}