use alloc::string::String;
use core::convert::Infallible;
use core::fmt;
use super::nip19::{self, FromBech32, Nip19, Nip19Coordinate, Nip19Event, Nip19Profile, ToBech32};
use crate::{EventId, PublicKey};
pub const SCHEME: &str = "nostr";
pub(crate) const SCHEME_WITH_COLON: &str = "nostr:";
#[derive(Debug, PartialEq, Eq)]
pub enum UnsupportedVariant {
SecretKey,
}
impl fmt::Display for UnsupportedVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SecretKey => f.write_str("secret key"),
}
}
}
#[derive(Debug, PartialEq)]
pub enum Error {
NIP19(nip19::Error),
UnsupportedVariant(UnsupportedVariant),
InvalidURI,
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NIP19(e) => e.fmt(f),
Self::UnsupportedVariant(t) => write!(f, "Unsupported variant: {t}"),
Self::InvalidURI => f.write_str("Invalid nostr URI"),
}
}
}
impl From<nip19::Error> for Error {
fn from(e: nip19::Error) -> Self {
Self::NIP19(e)
}
}
impl From<Infallible> for Error {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
fn split_uri(uri: &str) -> Result<&str, Error> {
let mut splitted = uri.split(':');
let prefix: &str = splitted.next().ok_or(Error::InvalidURI)?;
if prefix != SCHEME {
return Err(Error::InvalidURI);
}
splitted.next().ok_or(Error::InvalidURI)
}
pub trait ToNostrUri: ToBech32
where
Error: From<<Self as ToBech32>::Err>,
{
#[inline]
fn to_nostr_uri(&self) -> Result<String, Error> {
Ok(format!("{SCHEME}:{}", self.to_bech32()?))
}
}
pub trait FromNostrUri: FromBech32
where
Error: From<<Self as FromBech32>::Err>,
{
#[inline]
fn from_nostr_uri(uri: &str) -> Result<Self, Error> {
let data: &str = split_uri(uri)?;
Ok(Self::from_bech32(data)?)
}
}
impl ToNostrUri for PublicKey {}
impl FromNostrUri for PublicKey {}
impl ToNostrUri for EventId {}
impl FromNostrUri for EventId {}
impl ToNostrUri for Nip19Profile {}
impl FromNostrUri for Nip19Profile {}
impl ToNostrUri for Nip19Event {}
impl FromNostrUri for Nip19Event {}
impl ToNostrUri for Nip19Coordinate {}
impl FromNostrUri for Nip19Coordinate {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Nip21 {
Pubkey(PublicKey),
Profile(Nip19Profile),
EventId(EventId),
Event(Nip19Event),
Coordinate(Nip19Coordinate),
}
impl From<Nip21> for Nip19 {
fn from(value: Nip21) -> Self {
match value {
Nip21::Pubkey(val) => Self::Pubkey(val),
Nip21::Profile(val) => Self::Profile(val),
Nip21::EventId(val) => Self::EventId(val),
Nip21::Event(val) => Self::Event(val),
Nip21::Coordinate(val) => Self::Coordinate(val),
}
}
}
impl TryFrom<Nip19> for Nip21 {
type Error = Error;
fn try_from(value: Nip19) -> Result<Self, Self::Error> {
match value {
Nip19::Secret(..) => Err(Error::UnsupportedVariant(UnsupportedVariant::SecretKey)),
#[cfg(feature = "nip49")]
Nip19::EncryptedSecret(..) => {
Err(Error::UnsupportedVariant(UnsupportedVariant::SecretKey))
}
Nip19::Pubkey(val) => Ok(Self::Pubkey(val)),
Nip19::Profile(val) => Ok(Self::Profile(val)),
Nip19::EventId(val) => Ok(Self::EventId(val)),
Nip19::Event(val) => Ok(Self::Event(val)),
Nip19::Coordinate(val) => Ok(Self::Coordinate(val)),
}
}
}
impl Nip21 {
#[inline]
pub fn parse(uri: &str) -> Result<Self, Error> {
let data: &str = split_uri(uri)?;
let nip19: Nip19 = Nip19::from_bech32(data)?;
Self::try_from(nip19)
}
pub fn to_nostr_uri(&self) -> Result<String, Error> {
match self {
Self::Pubkey(val) => Ok(val.to_nostr_uri()?),
Self::Profile(val) => Ok(val.to_nostr_uri()?),
Self::EventId(val) => Ok(val.to_nostr_uri()?),
Self::Event(val) => Ok(val.to_nostr_uri()?),
Self::Coordinate(val) => Ok(val.to_nostr_uri()?),
}
}
pub fn event_id(&self) -> Option<EventId> {
match self {
Self::EventId(id) => Some(*id),
Self::Event(e) => Some(e.event_id),
_ => None,
}
}
}
impl ToBech32 for Nip21 {
type Err = Error;
fn to_bech32(&self) -> Result<String, Self::Err> {
match self {
Self::Pubkey(val) => Ok(val.to_bech32()?),
Self::Profile(val) => Ok(val.to_bech32()?),
Self::EventId(val) => Ok(val.to_bech32()?),
Self::Event(val) => Ok(val.to_bech32()?),
Self::Coordinate(val) => Ok(val.to_bech32()?),
}
}
}
#[cfg(test)]
mod tests {
use core::str::FromStr;
use super::*;
use crate::types::RelayUrl;
#[test]
fn test_to_nostr_uri() {
let pubkey =
PublicKey::from_str("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4")
.unwrap();
assert_eq!(
pubkey.to_nostr_uri().unwrap(),
String::from("nostr:npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy")
);
let generic = Nip21::Pubkey(pubkey);
assert_eq!(
generic.to_nostr_uri().unwrap(),
String::from("nostr:npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy")
);
}
#[test]
fn test_from_nostr_uri() {
let pubkey =
PublicKey::from_str("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4")
.unwrap();
assert_eq!(
PublicKey::from_nostr_uri(
"nostr:npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy"
)
.unwrap(),
pubkey
);
assert_eq!(
Nip21::parse("nostr:npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy")
.unwrap(),
Nip21::Pubkey(pubkey),
);
assert_eq!(
Nip21::parse("nostr:nprofile1qqsr9cvzwc652r4m83d86ykplrnm9dg5gwdvzzn8ameanlvut35wy3gpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsz4nxck").unwrap(),
Nip21::Profile(Nip19Profile::new(
PublicKey::from_str(
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
)
.unwrap(),
[RelayUrl::parse("wss://relay.damus.io/").unwrap()]
)),
);
}
#[test]
fn test_unsupported_from_nostr_uri() {
assert_eq!(
Nip21::parse("nostr:nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")
.unwrap_err(),
Error::UnsupportedVariant(UnsupportedVariant::SecretKey)
);
}
}