1use std::fmt;
59use std::str::FromStr;
60
61use serde::{Deserialize, Deserializer, Serialize, Serializer};
62use thiserror::Error;
63use url::Url;
64
65use crate::event::{Event, EventError, Kind, UnsignedEvent, UnsignedEventError};
66use crate::key::{PublicKey, PublicKeyError};
67use crate::types::{RelayUrl, RelayUrlError};
68use crate::util::JsonUtil;
69
70pub const URI_SCHEME_CLIENT: &str = "nostrconnect";
72pub const URI_SCHEME_BUNKER: &str = "bunker";
74
75pub const KIND: u16 = 24_133;
80
81#[derive(Debug, Error)]
83#[non_exhaustive]
84pub enum Nip46Error {
85 #[error(transparent)]
87 PublicKey(#[from] PublicKeyError),
88 #[error(transparent)]
90 RelayUrl(#[from] RelayUrlError),
91 #[error("invalid JSON payload: {0}")]
93 Json(#[from] serde_json::Error),
94 #[error(transparent)]
96 UnsignedEvent(#[from] UnsignedEventError),
97 #[error(transparent)]
99 Event(#[from] EventError),
100 #[error("method `{method}` expects {expected} param(s), got {actual}")]
102 InvalidParamLength {
103 method: Method,
105 expected: usize,
107 actual: usize,
109 },
110 #[error("unsupported NIP-46 method: {0}")]
112 UnsupportedMethod(String),
113 #[error("invalid switch_relays response payload")]
116 InvalidSwitchRelaysPayload,
117 #[error("{0}")]
120 WrongMessageKind(&'static str),
121 #[error("unknown URI scheme `{0}` (expected `bunker` or `nostrconnect`)")]
124 UnknownUriScheme(String),
125 #[error("malformed connection URI: {0}")]
127 MalformedUri(&'static str),
128 #[error(transparent)]
130 Url(#[from] url::ParseError),
131 #[error("unexpected response for method `{method}` (expected {expected}, got `{received}`)")]
134 UnexpectedResponse {
135 method: Method,
137 expected: &'static str,
139 received: String,
141 },
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
146#[non_exhaustive]
147pub enum Method {
148 Connect,
150 GetPublicKey,
152 SignEvent,
154 Nip04Encrypt,
156 Nip04Decrypt,
158 Nip44Encrypt,
160 Nip44Decrypt,
162 Ping,
164 SwitchRelays,
167}
168
169impl Method {
170 #[must_use]
172 pub const fn as_str(self) -> &'static str {
173 match self {
174 Self::Connect => "connect",
175 Self::GetPublicKey => "get_public_key",
176 Self::SignEvent => "sign_event",
177 Self::Nip04Encrypt => "nip04_encrypt",
178 Self::Nip04Decrypt => "nip04_decrypt",
179 Self::Nip44Encrypt => "nip44_encrypt",
180 Self::Nip44Decrypt => "nip44_decrypt",
181 Self::Ping => "ping",
182 Self::SwitchRelays => "switch_relays",
183 }
184 }
185}
186
187impl fmt::Display for Method {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.write_str(self.as_str())
190 }
191}
192
193impl FromStr for Method {
194 type Err = Nip46Error;
195
196 fn from_str(s: &str) -> Result<Self, Self::Err> {
197 Ok(match s {
198 "connect" => Self::Connect,
199 "get_public_key" => Self::GetPublicKey,
200 "sign_event" => Self::SignEvent,
201 "nip04_encrypt" => Self::Nip04Encrypt,
202 "nip04_decrypt" => Self::Nip04Decrypt,
203 "nip44_encrypt" => Self::Nip44Encrypt,
204 "nip44_decrypt" => Self::Nip44Decrypt,
205 "ping" => Self::Ping,
206 "switch_relays" => Self::SwitchRelays,
207 other => return Err(Nip46Error::UnsupportedMethod(other.to_owned())),
208 })
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237#[non_exhaustive]
238pub enum Permission {
239 Method(Method),
242 SignEventKind(Kind),
245 Other(String),
248}
249
250impl Permission {
251 #[must_use]
253 pub fn to_wire(&self) -> String {
254 match self {
255 Self::Method(method) => method.to_string(),
256 Self::SignEventKind(kind) => {
257 format!("{}:{}", Method::SignEvent.as_str(), kind.as_u16())
258 }
259 Self::Other(raw) => raw.clone(),
260 }
261 }
262
263 #[must_use]
267 pub fn join(perms: &[Self]) -> String {
268 let mut out = String::new();
269 for (i, perm) in perms.iter().enumerate() {
270 if i > 0 {
271 out.push(',');
272 }
273 out.push_str(&perm.to_wire());
274 }
275 out
276 }
277
278 #[must_use]
282 pub fn split(wire: &str) -> Vec<Self> {
283 if wire.is_empty() {
284 return Vec::new();
285 }
286 wire.split(',')
287 .map(str::trim)
288 .filter(|tok| !tok.is_empty())
289 .map(|tok| tok.parse().unwrap_or_else(|_| Self::Other(tok.to_owned())))
290 .collect()
291 }
292}
293
294impl fmt::Display for Permission {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 f.write_str(&self.to_wire())
297 }
298}
299
300impl FromStr for Permission {
301 type Err = Nip46Error;
302
303 fn from_str(s: &str) -> Result<Self, Self::Err> {
304 if let Some((head, tail)) = s.split_once(':') {
306 if head == Method::SignEvent.as_str()
307 && let Ok(raw) = tail.parse::<u16>()
308 {
309 return Ok(Self::SignEventKind(Kind::new(raw)));
310 }
311 return Ok(Self::Other(s.to_owned()));
314 }
315 Ok(s.parse::<Method>()
318 .map_or_else(|_| Self::Other(s.to_owned()), Self::Method))
319 }
320}
321
322impl Serialize for Method {
323 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
324 serializer.serialize_str(self.as_str())
325 }
326}
327
328impl<'de> Deserialize<'de> for Method {
329 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
330 let raw = <&str>::deserialize(deserializer)?;
331 Self::from_str(raw).map_err(serde::de::Error::custom)
332 }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
340#[non_exhaustive]
341pub enum Request {
342 Connect {
350 remote_signer_public_key: PublicKey,
352 secret: Option<String>,
355 perms: Option<Vec<Permission>>,
357 },
358 GetPublicKey,
360 SignEvent(UnsignedEvent),
362 Nip04Encrypt {
364 peer: PublicKey,
366 text: String,
368 },
369 Nip04Decrypt {
371 peer: PublicKey,
373 ciphertext: String,
375 },
376 Nip44Encrypt {
378 peer: PublicKey,
380 text: String,
382 },
383 Nip44Decrypt {
385 peer: PublicKey,
387 ciphertext: String,
389 },
390 Ping,
392 SwitchRelays,
394}
395
396impl Request {
397 #[must_use]
399 pub const fn method(&self) -> Method {
400 match self {
401 Self::Connect { .. } => Method::Connect,
402 Self::GetPublicKey => Method::GetPublicKey,
403 Self::SignEvent(_) => Method::SignEvent,
404 Self::Nip04Encrypt { .. } => Method::Nip04Encrypt,
405 Self::Nip04Decrypt { .. } => Method::Nip04Decrypt,
406 Self::Nip44Encrypt { .. } => Method::Nip44Encrypt,
407 Self::Nip44Decrypt { .. } => Method::Nip44Decrypt,
408 Self::Ping => Method::Ping,
409 Self::SwitchRelays => Method::SwitchRelays,
410 }
411 }
412
413 #[must_use]
421 pub fn params(&self) -> Vec<String> {
422 match self {
423 Self::Connect {
424 remote_signer_public_key,
425 secret,
426 perms,
427 } => {
428 let mut out = Vec::with_capacity(
429 1 + usize::from(secret.is_some()) + usize::from(perms.is_some()),
430 );
431 out.push(remote_signer_public_key.to_hex());
432 if perms.is_some() {
433 out.push(secret.clone().unwrap_or_default());
436 } else if let Some(s) = secret {
437 out.push(s.clone());
438 }
439 if let Some(perms) = perms {
440 out.push(Permission::join(perms));
441 }
442 out
443 }
444 Self::GetPublicKey | Self::Ping | Self::SwitchRelays => Vec::new(),
445 Self::SignEvent(unsigned) => vec![unsigned.try_to_json().unwrap_or_default()],
446 Self::Nip04Encrypt { peer, text } | Self::Nip44Encrypt { peer, text } => {
447 vec![peer.to_hex(), text.clone()]
448 }
449 Self::Nip04Decrypt { peer, ciphertext } | Self::Nip44Decrypt { peer, ciphertext } => {
450 vec![peer.to_hex(), ciphertext.clone()]
451 }
452 }
453 }
454
455 pub fn from_wire(method: Method, params: &[String]) -> Result<Self, Nip46Error> {
468 match (method, params) {
469 (Method::Connect, [pk_hex]) => Ok(Self::Connect {
473 remote_signer_public_key: PublicKey::parse(pk_hex)?,
474 secret: None,
475 perms: None,
476 }),
477 (Method::Connect, [pk_hex, secret]) => Ok(Self::Connect {
478 remote_signer_public_key: PublicKey::parse(pk_hex)?,
479 secret: Some(secret.clone()),
480 perms: None,
481 }),
482 (Method::Connect, [pk_hex, secret, perms]) => Ok(Self::Connect {
483 remote_signer_public_key: PublicKey::parse(pk_hex)?,
484 secret: if secret.is_empty() {
488 None
489 } else {
490 Some(secret.clone())
491 },
492 perms: Some(Permission::split(perms)),
493 }),
494 (Method::GetPublicKey, []) => Ok(Self::GetPublicKey),
495 (Method::SignEvent, [json]) => Ok(Self::SignEvent(UnsignedEvent::from_json(json)?)),
496 (Method::Nip04Encrypt, [pk_hex, text]) => Ok(Self::Nip04Encrypt {
497 peer: PublicKey::parse(pk_hex)?,
498 text: text.clone(),
499 }),
500 (Method::Nip44Encrypt, [pk_hex, text]) => Ok(Self::Nip44Encrypt {
501 peer: PublicKey::parse(pk_hex)?,
502 text: text.clone(),
503 }),
504 (Method::Nip04Decrypt, [pk_hex, ciphertext]) => Ok(Self::Nip04Decrypt {
505 peer: PublicKey::parse(pk_hex)?,
506 ciphertext: ciphertext.clone(),
507 }),
508 (Method::Nip44Decrypt, [pk_hex, ciphertext]) => Ok(Self::Nip44Decrypt {
509 peer: PublicKey::parse(pk_hex)?,
510 ciphertext: ciphertext.clone(),
511 }),
512 (Method::Ping, []) => Ok(Self::Ping),
513 (Method::SwitchRelays, []) => Ok(Self::SwitchRelays),
514 (Method::GetPublicKey | Method::Ping | Method::SwitchRelays, _) => {
518 Err(invalid_param_length(method, 0, params.len()))
519 }
520 (Method::SignEvent | Method::Connect, _) => {
525 Err(invalid_param_length(method, 1, params.len()))
526 }
527 (
528 Method::Nip04Encrypt
529 | Method::Nip04Decrypt
530 | Method::Nip44Encrypt
531 | Method::Nip44Decrypt,
532 _,
533 ) => Err(invalid_param_length(method, 2, params.len())),
534 }
535 }
536}
537
538const fn invalid_param_length(method: Method, expected: usize, actual: usize) -> Nip46Error {
539 Nip46Error::InvalidParamLength {
540 method,
541 expected,
542 actual,
543 }
544}
545
546#[derive(Debug, Clone, PartialEq, Eq)]
552#[non_exhaustive]
553pub enum ResponseResult {
554 Ack,
556 ConnectSecret(String),
560 GetPublicKey(PublicKey),
562 SignEvent(Box<Event>),
564 Nip04Encrypt(String),
566 Nip04Decrypt(String),
568 Nip44Encrypt(String),
570 Nip44Decrypt(String),
572 Pong,
574 SwitchRelays(Option<Vec<RelayUrl>>),
577 AuthUrl,
581 Error,
583}
584
585impl ResponseResult {
586 pub fn from_wire(method: Method, result: &str) -> Result<Self, Nip46Error> {
596 match result {
599 "auth_url" => return Ok(Self::AuthUrl),
600 "error" => return Ok(Self::Error),
601 _ => {}
602 }
603 match method {
604 Method::Connect => {
605 if result == "ack" {
606 Ok(Self::Ack)
607 } else {
608 Ok(Self::ConnectSecret(result.to_owned()))
609 }
610 }
611 Method::GetPublicKey => Ok(Self::GetPublicKey(PublicKey::parse(result)?)),
612 Method::SignEvent => Ok(Self::SignEvent(Box::new(Event::from_json(result)?))),
613 Method::Nip04Encrypt => Ok(Self::Nip04Encrypt(result.to_owned())),
614 Method::Nip04Decrypt => Ok(Self::Nip04Decrypt(result.to_owned())),
615 Method::Nip44Encrypt => Ok(Self::Nip44Encrypt(result.to_owned())),
616 Method::Nip44Decrypt => Ok(Self::Nip44Decrypt(result.to_owned())),
617 Method::Ping => {
618 if result == "pong" {
619 Ok(Self::Pong)
620 } else {
621 Err(Nip46Error::UnexpectedResponse {
622 method,
623 expected: "pong",
624 received: result.to_owned(),
625 })
626 }
627 }
628 Method::SwitchRelays => {
629 let trimmed = result.trim();
630 if trimmed == "null" {
631 return Ok(Self::SwitchRelays(None));
632 }
633 let raw: Vec<String> = serde_json::from_str(trimmed)
634 .map_err(|_| Nip46Error::InvalidSwitchRelaysPayload)?;
635 let mut relays = Vec::with_capacity(raw.len());
636 for url in raw {
637 relays.push(RelayUrl::parse(&url)?);
638 }
639 Ok(Self::SwitchRelays(Some(relays)))
640 }
641 }
642 }
643
644 #[must_use]
651 pub fn to_wire(&self) -> String {
652 match self {
653 Self::Ack => "ack".to_owned(),
654 Self::ConnectSecret(s)
655 | Self::Nip04Encrypt(s)
656 | Self::Nip04Decrypt(s)
657 | Self::Nip44Encrypt(s)
658 | Self::Nip44Decrypt(s) => s.clone(),
659 Self::GetPublicKey(pk) => pk.to_hex(),
660 Self::SignEvent(ev) => ev.try_to_json().unwrap_or_default(),
661 Self::Pong => "pong".to_owned(),
662 Self::SwitchRelays(None) => "null".to_owned(),
663 Self::SwitchRelays(Some(relays)) => {
664 let urls: Vec<&str> = relays.iter().map(RelayUrl::as_str).collect();
665 serde_json::to_string(&urls).unwrap_or_else(|_| "null".to_owned())
666 }
667 Self::AuthUrl => "auth_url".to_owned(),
668 Self::Error => "error".to_owned(),
669 }
670 }
671
672 #[must_use]
674 pub const fn is_auth_url(&self) -> bool {
675 matches!(self, Self::AuthUrl)
676 }
677
678 #[must_use]
680 pub const fn is_error(&self) -> bool {
681 matches!(self, Self::Error)
682 }
683}
684
685#[derive(Debug, Clone, PartialEq, Eq)]
691#[non_exhaustive]
692pub struct Response {
693 pub result: Option<ResponseResult>,
695 pub error: Option<String>,
698}
699
700impl Response {
701 #[must_use]
703 pub const fn with_result(result: ResponseResult) -> Self {
704 Self {
705 result: Some(result),
706 error: None,
707 }
708 }
709
710 #[must_use]
712 pub fn with_error(error: impl Into<String>) -> Self {
713 Self {
714 result: None,
715 error: Some(error.into()),
716 }
717 }
718
719 pub fn from_wire(
725 method: Method,
726 result: Option<&str>,
727 error: Option<String>,
728 ) -> Result<Self, Nip46Error> {
729 let decoded = match result {
730 Some(s) => Some(ResponseResult::from_wire(method, s)?),
731 None => None,
732 };
733 Ok(Self {
734 result: decoded,
735 error,
736 })
737 }
738}
739
740#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
742#[serde(untagged)]
743#[non_exhaustive]
744pub enum Message {
745 Request {
747 id: String,
750 method: Method,
752 params: Vec<String>,
754 },
755 Response {
757 id: String,
759 result: Option<String>,
762 error: Option<String>,
765 },
766}
767
768impl Message {
769 #[must_use]
772 pub fn request(id: impl Into<String>, request: &Request) -> Self {
773 Self::Request {
774 id: id.into(),
775 method: request.method(),
776 params: request.params(),
777 }
778 }
779
780 #[must_use]
783 pub fn response(id: impl Into<String>, response: Response) -> Self {
784 Self::Response {
785 id: id.into(),
786 result: response.result.as_ref().map(ResponseResult::to_wire),
787 error: response.error,
788 }
789 }
790
791 #[must_use]
793 pub fn id(&self) -> &str {
794 match self {
795 Self::Request { id, .. } | Self::Response { id, .. } => id,
796 }
797 }
798
799 pub fn into_request(self) -> Result<Request, Nip46Error> {
807 match self {
808 Self::Request { method, params, .. } => Request::from_wire(method, ¶ms),
809 Self::Response { .. } => Err(Nip46Error::WrongMessageKind(
810 "expected Request, got Response",
811 )),
812 }
813 }
814
815 pub fn into_response(self, method: Method) -> Result<Response, Nip46Error> {
823 match self {
824 Self::Response { result, error, .. } => {
825 Response::from_wire(method, result.as_deref(), error)
826 }
827 Self::Request { .. } => Err(Nip46Error::WrongMessageKind(
828 "expected Response, got Request",
829 )),
830 }
831 }
832}
833
834#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
844#[non_exhaustive]
845pub struct Metadata {
846 pub name: String,
848 #[serde(skip_serializing_if = "Option::is_none", default)]
850 pub url: Option<String>,
851 #[serde(skip_serializing_if = "Option::is_none", default)]
853 pub description: Option<String>,
854 #[serde(skip_serializing_if = "Option::is_none", default)]
856 pub icons: Option<Vec<String>>,
857}
858
859impl Metadata {
860 #[must_use]
862 pub fn new(name: impl Into<String>) -> Self {
863 Self {
864 name: name.into(),
865 url: None,
866 description: None,
867 icons: None,
868 }
869 }
870}
871
872#[derive(Debug, Clone, PartialEq, Eq)]
877#[non_exhaustive]
878pub enum Uri {
879 Bunker {
884 remote_signer_public_key: PublicKey,
886 relays: Vec<RelayUrl>,
888 secret: Option<String>,
891 },
892 Client {
900 public_key: PublicKey,
902 relays: Vec<RelayUrl>,
904 metadata: Metadata,
906 secret: String,
908 perms: Vec<Permission>,
911 },
912}
913
914impl Uri {
915 pub fn parse(uri: &str) -> Result<Self, Nip46Error> {
921 let parsed = Url::parse(uri)?;
922 let host = parsed
923 .host_str()
924 .ok_or(Nip46Error::MalformedUri("missing pubkey host"))?;
925 let public_key = PublicKey::parse(host)?;
926
927 let mut relays: Vec<RelayUrl> = Vec::new();
928 let mut secret: Option<String> = None;
929 let mut metadata: Option<Metadata> = None;
930 let mut perms: Vec<Permission> = Vec::new();
931 for (key, value) in parsed.query_pairs() {
932 match key.as_ref() {
933 "relay" => relays.push(RelayUrl::parse(value.as_ref())?),
934 "secret" => secret = Some(value.into_owned()),
935 "metadata" => metadata = Some(Metadata::from_json(value.as_ref())?),
936 "perms" => perms = Permission::split(value.as_ref()),
937 _ => {}
940 }
941 }
942
943 match parsed.scheme() {
944 URI_SCHEME_BUNKER => Ok(Self::Bunker {
945 remote_signer_public_key: public_key,
946 relays,
947 secret,
948 }),
949 URI_SCHEME_CLIENT => {
950 let secret = secret.ok_or(Nip46Error::MalformedUri(
951 "`nostrconnect://` URIs require the `secret` query parameter",
952 ))?;
953 let metadata = metadata.ok_or(Nip46Error::MalformedUri(
954 "`nostrconnect://` URIs require the `metadata` query parameter",
955 ))?;
956 Ok(Self::Client {
957 public_key,
958 relays,
959 metadata,
960 secret,
961 perms,
962 })
963 }
964 other => Err(Nip46Error::UnknownUriScheme(other.to_owned())),
965 }
966 }
967
968 #[must_use]
970 pub const fn is_bunker(&self) -> bool {
971 matches!(self, Self::Bunker { .. })
972 }
973
974 #[must_use]
976 pub fn relays(&self) -> &[RelayUrl] {
977 match self {
978 Self::Bunker { relays, .. } | Self::Client { relays, .. } => relays,
979 }
980 }
981
982 #[must_use]
985 pub fn secret(&self) -> Option<&str> {
986 match self {
987 Self::Bunker { secret, .. } => secret.as_deref(),
988 Self::Client { secret, .. } => Some(secret),
989 }
990 }
991}
992
993impl FromStr for Uri {
994 type Err = Nip46Error;
995
996 fn from_str(s: &str) -> Result<Self, Self::Err> {
997 Self::parse(s)
998 }
999}
1000
1001impl fmt::Display for Uri {
1002 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1003 match self {
1004 Self::Bunker {
1005 remote_signer_public_key,
1006 relays,
1007 secret,
1008 } => {
1009 write!(f, "{URI_SCHEME_BUNKER}://{remote_signer_public_key}")?;
1010 write_query(f, relays, secret.as_deref(), None, &[])
1011 }
1012 Self::Client {
1013 public_key,
1014 relays,
1015 metadata,
1016 secret,
1017 perms,
1018 } => {
1019 write!(f, "{URI_SCHEME_CLIENT}://{public_key}")?;
1020 let metadata_json = metadata.try_to_json().unwrap_or_default();
1021 write_query(f, relays, Some(secret), Some(&metadata_json), perms)
1022 }
1023 }
1024 }
1025}
1026
1027fn write_query(
1028 out: &mut fmt::Formatter<'_>,
1029 relays: &[RelayUrl],
1030 secret: Option<&str>,
1031 metadata_json: Option<&str>,
1032 perms: &[Permission],
1033) -> fmt::Result {
1034 let mut first = true;
1035 let mut emit = |sink: &mut fmt::Formatter<'_>, key: &str, value: &str| -> fmt::Result {
1036 sink.write_str(if first { "?" } else { "&" })?;
1037 first = false;
1038 write!(sink, "{key}={}", url_encode(value))
1039 };
1040 for relay in relays {
1041 emit(out, "relay", relay.as_str())?;
1042 }
1043 if let Some(meta) = metadata_json {
1044 emit(out, "metadata", meta)?;
1045 }
1046 if let Some(s) = secret {
1047 emit(out, "secret", s)?;
1048 }
1049 if !perms.is_empty() {
1050 emit(out, "perms", &Permission::join(perms))?;
1051 }
1052 Ok(())
1053}
1054
1055fn url_encode(input: &str) -> String {
1061 let mut out = String::with_capacity(input.len());
1062 for byte in input.bytes() {
1063 let preserve =
1064 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~' | b'/' | b':');
1065 if preserve {
1066 out.push(byte as char);
1067 } else {
1068 out.push('%');
1069 out.push(hex_nibble(byte >> 4));
1070 out.push(hex_nibble(byte & 0x0f));
1071 }
1072 }
1073 out
1074}
1075
1076const fn hex_nibble(n: u8) -> char {
1077 match n {
1078 0..=9 => (b'0' + n) as char,
1079 10..=15 => (b'A' + (n - 10)) as char,
1080 _ => '0',
1081 }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::*;
1087 use crate::Keys;
1088
1089 fn fixture_pk() -> PublicKey {
1090 *Keys::parse("0000000000000000000000000000000000000000000000000000000000000003")
1093 .unwrap()
1094 .public_key()
1095 }
1096
1097 #[test]
1098 fn method_round_trips_through_str() {
1099 for method in [
1100 Method::Connect,
1101 Method::GetPublicKey,
1102 Method::SignEvent,
1103 Method::Nip04Encrypt,
1104 Method::Nip04Decrypt,
1105 Method::Nip44Encrypt,
1106 Method::Nip44Decrypt,
1107 Method::Ping,
1108 Method::SwitchRelays,
1109 ] {
1110 let s = method.as_str();
1111 let parsed: Method = s.parse().unwrap();
1112 assert_eq!(parsed, method);
1113 }
1114 }
1115
1116 #[test]
1117 fn unknown_method_is_rejected() {
1118 let err: Nip46Error = "open_my_drone".parse::<Method>().unwrap_err();
1119 assert!(matches!(err, Nip46Error::UnsupportedMethod(s) if s == "open_my_drone"));
1120 }
1121
1122 #[test]
1123 fn request_round_trip_through_wire_params() {
1124 let pk = fixture_pk();
1125 let cases: Vec<Request> = vec![
1126 Request::Connect {
1127 remote_signer_public_key: pk,
1128 secret: Some("hunter2".to_owned()),
1129 perms: None,
1130 },
1131 Request::Connect {
1132 remote_signer_public_key: pk,
1133 secret: None,
1134 perms: None,
1135 },
1136 Request::Connect {
1137 remote_signer_public_key: pk,
1138 secret: Some("hunter2".to_owned()),
1139 perms: Some(vec![
1140 Permission::Method(Method::Nip44Encrypt),
1141 Permission::SignEventKind(Kind::TEXT_NOTE),
1142 ]),
1143 },
1144 Request::Connect {
1145 remote_signer_public_key: pk,
1146 secret: None,
1147 perms: Some(vec![Permission::Method(Method::GetPublicKey)]),
1148 },
1149 Request::Connect {
1150 remote_signer_public_key: pk,
1151 secret: None,
1152 perms: Some(Vec::new()),
1153 },
1154 Request::GetPublicKey,
1155 Request::Nip04Encrypt {
1156 peer: pk,
1157 text: "hi".to_owned(),
1158 },
1159 Request::Nip04Decrypt {
1160 peer: pk,
1161 ciphertext: "AAAA?iv=AAAA".to_owned(),
1162 },
1163 Request::Nip44Encrypt {
1164 peer: pk,
1165 text: "hello".to_owned(),
1166 },
1167 Request::Nip44Decrypt {
1168 peer: pk,
1169 ciphertext: "AgAB...".to_owned(),
1170 },
1171 Request::Ping,
1172 Request::SwitchRelays,
1173 ];
1174
1175 for req in cases {
1176 let method = req.method();
1177 let params = req.params();
1178 let recovered = Request::from_wire(method, ¶ms).unwrap();
1179 assert_eq!(recovered, req);
1180 }
1181 }
1182
1183 #[test]
1184 fn request_param_count_validation() {
1185 let pk = fixture_pk();
1186 let bad = Request::from_wire(Method::Nip04Encrypt, &[pk.to_hex()]).unwrap_err();
1187 assert!(matches!(
1188 bad,
1189 Nip46Error::InvalidParamLength {
1190 method: Method::Nip04Encrypt,
1191 expected: 2,
1192 actual: 1,
1193 }
1194 ));
1195 }
1196
1197 #[test]
1198 fn response_decode_handles_universal_sentinels() {
1199 let auth = ResponseResult::from_wire(Method::SignEvent, "auth_url").unwrap();
1200 assert!(auth.is_auth_url());
1201 let err = ResponseResult::from_wire(Method::Connect, "error").unwrap();
1202 assert!(err.is_error());
1203 }
1204
1205 #[test]
1206 fn response_decode_for_each_method() {
1207 let pk = fixture_pk();
1208 match ResponseResult::from_wire(Method::GetPublicKey, &pk.to_hex()).unwrap() {
1210 ResponseResult::GetPublicKey(decoded) => assert_eq!(decoded, pk),
1211 other => panic!("unexpected variant: {other:?}"),
1212 }
1213 let ack = ResponseResult::from_wire(Method::Connect, "ack").unwrap();
1215 assert!(matches!(ack, ResponseResult::Ack));
1216 let secret = ResponseResult::from_wire(Method::Connect, "abcdef0123").unwrap();
1218 assert!(matches!(secret, ResponseResult::ConnectSecret(s) if s == "abcdef0123"));
1219 let pong = ResponseResult::from_wire(Method::Ping, "pong").unwrap();
1221 assert!(matches!(pong, ResponseResult::Pong));
1222 let err = ResponseResult::from_wire(Method::Ping, "ping").unwrap_err();
1224 assert!(matches!(err, Nip46Error::UnexpectedResponse { .. }));
1225 }
1226
1227 #[test]
1228 fn message_request_round_trips_through_json() {
1229 let pk = fixture_pk();
1230 let request = Request::Nip44Encrypt {
1231 peer: pk,
1232 text: "hello".to_owned(),
1233 };
1234 let msg = Message::request("req-1", &request);
1235 let json = msg.try_to_json().unwrap();
1236 let recovered = Message::from_json(&json).unwrap();
1237 assert_eq!(recovered.id(), "req-1");
1238 let recovered_req = recovered.into_request().unwrap();
1239 assert_eq!(recovered_req, request);
1240 }
1241
1242 #[test]
1243 fn message_response_round_trips_through_json() {
1244 let response = Response::with_result(ResponseResult::Pong);
1245 let msg = Message::response("ping-42", response);
1246 let json = msg.try_to_json().unwrap();
1247 let recovered = Message::from_json(&json).unwrap();
1248 assert_eq!(recovered.id(), "ping-42");
1249 let recovered_resp = recovered.into_response(Method::Ping).unwrap();
1250 assert!(matches!(recovered_resp.result, Some(ResponseResult::Pong)));
1251 assert!(recovered_resp.error.is_none());
1252 }
1253
1254 #[test]
1255 fn into_request_rejects_response_envelopes() {
1256 let msg = Message::Response {
1257 id: "x".into(),
1258 result: Some("ack".into()),
1259 error: None,
1260 };
1261 let err = msg.into_request().unwrap_err();
1262 assert!(matches!(err, Nip46Error::WrongMessageKind(_)));
1263 }
1264
1265 #[test]
1266 fn bunker_uri_round_trip() {
1267 let pk = fixture_pk();
1268 let original = format!(
1269 "bunker://{}?relay=wss%3A%2F%2Frelay.example%2F&secret=hunter2",
1270 pk.to_hex(),
1271 );
1272 let parsed = Uri::parse(&original).unwrap();
1273 match &parsed {
1274 Uri::Bunker {
1275 remote_signer_public_key,
1276 relays,
1277 secret,
1278 } => {
1279 assert_eq!(*remote_signer_public_key, pk);
1280 assert_eq!(relays.len(), 1);
1281 assert_eq!(relays[0].as_str(), "wss://relay.example/");
1282 assert_eq!(secret.as_deref(), Some("hunter2"));
1283 }
1284 other => panic!("unexpected variant: {other:?}"),
1285 }
1286 let rendered = parsed.to_string();
1291 let reparsed = Uri::parse(&rendered).unwrap();
1292 assert_eq!(reparsed, parsed);
1293 }
1294
1295 #[test]
1296 fn nostrconnect_uri_requires_secret() {
1297 let pk = fixture_pk();
1298 let bad = format!(
1299 "nostrconnect://{}?relay=wss%3A%2F%2Frelay.example%2F&metadata=%7B%22name%22%3A%22demo%22%7D",
1300 pk.to_hex(),
1301 );
1302 let err = Uri::parse(&bad).unwrap_err();
1303 assert!(matches!(err, Nip46Error::MalformedUri(_)));
1304 }
1305
1306 #[test]
1307 fn nostrconnect_uri_round_trip() {
1308 let pk = fixture_pk();
1309 let metadata = Metadata::new("demo");
1310 let original = Uri::Client {
1311 public_key: pk,
1312 relays: vec![RelayUrl::parse("wss://relay.example/").unwrap()],
1313 metadata: metadata.clone(),
1314 secret: "anti-mitm".into(),
1315 perms: Vec::new(),
1316 };
1317 let rendered = original.to_string();
1318 let reparsed = Uri::parse(&rendered).unwrap();
1319 assert_eq!(reparsed, original);
1320 assert_eq!(reparsed.secret(), Some("anti-mitm"));
1321 match reparsed {
1322 Uri::Client {
1323 metadata: parsed_meta,
1324 ..
1325 } => assert_eq!(parsed_meta, metadata),
1326 other => panic!("unexpected variant: {other:?}"),
1327 }
1328 }
1329
1330 #[test]
1331 fn unknown_scheme_is_rejected() {
1332 let pk = fixture_pk();
1333 let err = Uri::parse(&format!("nip46://{}", pk.to_hex())).unwrap_err();
1334 assert!(matches!(err, Nip46Error::UnknownUriScheme(s) if s == "nip46"));
1335 }
1336
1337 #[test]
1338 fn permission_token_round_trips() {
1339 let bare: Permission = "get_public_key".parse().unwrap();
1341 assert_eq!(bare, Permission::Method(Method::GetPublicKey));
1342 assert_eq!(bare.to_wire(), "get_public_key");
1343 let kinded: Permission = "sign_event:4".parse().unwrap();
1345 assert_eq!(kinded, Permission::SignEventKind(Kind::new(4)));
1346 assert_eq!(kinded.to_wire(), "sign_event:4");
1347 let vendor: Permission = "weird_vendor:opt=1".parse().unwrap();
1349 assert_eq!(vendor, Permission::Other("weird_vendor:opt=1".to_owned()));
1350 assert_eq!(vendor.to_wire(), "weird_vendor:opt=1");
1351 let extensible: Permission = "sign_event:any".parse().unwrap();
1354 assert_eq!(extensible, Permission::Other("sign_event:any".to_owned()));
1355 }
1356
1357 #[test]
1358 fn permission_list_round_trips_via_join_split() {
1359 let perms = vec![
1361 Permission::Method(Method::Nip44Encrypt),
1362 Permission::SignEventKind(Kind::new(4)),
1363 ];
1364 let joined = Permission::join(&perms);
1365 assert_eq!(joined, "nip44_encrypt,sign_event:4");
1366 let parsed = Permission::split(&joined);
1367 assert_eq!(parsed, perms);
1368 assert!(Permission::split("").is_empty());
1370 assert_eq!(
1371 Permission::split(" ping , sign_event:1 "),
1372 vec![
1373 Permission::Method(Method::Ping),
1374 Permission::SignEventKind(Kind::TEXT_NOTE),
1375 ],
1376 );
1377 }
1378
1379 #[test]
1380 fn connect_request_with_perms_emits_positional_layout() {
1381 let pk = fixture_pk();
1382 let req = Request::Connect {
1385 remote_signer_public_key: pk,
1386 secret: None,
1387 perms: Some(vec![Permission::Method(Method::GetPublicKey)]),
1388 };
1389 let params = req.params();
1390 assert_eq!(params.len(), 3);
1391 assert_eq!(params[0], pk.to_hex());
1392 assert_eq!(params[1], "");
1393 assert_eq!(params[2], "get_public_key");
1394 let recovered = Request::from_wire(Method::Connect, ¶ms).unwrap();
1395 assert_eq!(recovered, req);
1396 }
1397
1398 #[test]
1399 fn switch_relays_response_round_trips_through_wire() {
1400 let null_value = ResponseResult::SwitchRelays(None);
1402 assert_eq!(null_value.to_wire(), "null");
1403 let null_recovered = ResponseResult::from_wire(Method::SwitchRelays, "null").unwrap();
1404 assert_eq!(null_recovered, null_value);
1405
1406 let empty_value = ResponseResult::SwitchRelays(Some(Vec::new()));
1408 let empty_wire = empty_value.to_wire();
1409 assert_eq!(empty_wire, "[]");
1410 let empty_recovered = ResponseResult::from_wire(Method::SwitchRelays, &empty_wire).unwrap();
1411 assert_eq!(empty_recovered, empty_value);
1412
1413 let relays = vec![
1415 RelayUrl::parse("wss://relay.one/").unwrap(),
1416 RelayUrl::parse("wss://relay.two/").unwrap(),
1417 ];
1418 let populated = ResponseResult::SwitchRelays(Some(relays));
1419 let populated_wire = populated.to_wire();
1420 let populated_recovered =
1421 ResponseResult::from_wire(Method::SwitchRelays, &populated_wire).unwrap();
1422 assert_eq!(populated_recovered, populated);
1423
1424 let err =
1426 ResponseResult::from_wire(Method::SwitchRelays, "not-json").expect_err("must reject");
1427 assert!(matches!(err, Nip46Error::InvalidSwitchRelaysPayload));
1428 }
1429
1430 #[test]
1431 fn switch_relays_request_envelope_round_trips_through_json() {
1432 let msg = Message::request("sw-1", &Request::SwitchRelays);
1433 let json = msg.try_to_json().unwrap();
1434 let recovered = Message::from_json(&json).unwrap();
1435 assert_eq!(recovered.id(), "sw-1");
1436 let req = recovered.into_request().unwrap();
1437 assert_eq!(req, Request::SwitchRelays);
1438 }
1439
1440 #[test]
1441 fn nostrconnect_uri_carries_perms_round_trip() {
1442 let pk = fixture_pk();
1443 let metadata = Metadata::new("demo");
1444 let original = Uri::Client {
1445 public_key: pk,
1446 relays: vec![RelayUrl::parse("wss://relay.example/").unwrap()],
1447 metadata,
1448 secret: "anti-mitm".into(),
1449 perms: vec![
1450 Permission::Method(Method::Nip44Encrypt),
1451 Permission::Method(Method::Nip44Decrypt),
1452 Permission::SignEventKind(Kind::new(13)),
1453 Permission::SignEventKind(Kind::new(14)),
1454 Permission::SignEventKind(Kind::new(1059)),
1455 ],
1456 };
1457 let rendered = original.to_string();
1458 assert!(rendered.contains("perms="));
1459 let reparsed = Uri::parse(&rendered).unwrap();
1460 assert_eq!(reparsed, original);
1461 }
1462}