use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::Split;
use serde_json::Value;
use crate::types::url::{ParseError, Url};
use crate::{PublicKey, RelayUrl};
#[derive(Debug)]
pub enum Error {
Json(serde_json::Error),
Url(ParseError),
InvalidFormat,
ImpossibleToVerify,
}
#[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::Json(e) => e.fmt(f),
Self::Url(e) => e.fmt(f),
Self::InvalidFormat => f.write_str("invalid format"),
Self::ImpossibleToVerify => f.write_str("impossible to verify"),
}
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::Json(e)
}
}
impl From<ParseError> for Error {
fn from(e: ParseError) -> Self {
Self::Url(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Nip05Address {
name: String,
domain: String,
url: Url,
}
impl fmt::Display for Nip05Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}", self.name, self.domain)
}
}
impl Nip05Address {
pub fn parse(address: &str) -> Result<Self, Error> {
let mut split: Split<char> = address.split('@');
let (name, domain): (&str, &str) = match (split.next(), split.next()) {
(Some(name), Some(domain)) => (name, domain),
(Some(address), None) => ("_", address),
_ => return Err(Error::InvalidFormat),
};
let url: String = format!("https://{domain}/.well-known/nostr.json?name={name}");
let url: Url = Url::parse(&url)?;
Ok(Self {
name: name.to_string(),
domain: domain.to_string(),
url,
})
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn domain(&self) -> &str {
&self.domain
}
#[inline]
pub fn url(&self) -> &Url {
&self.url
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Nip05Profile {
pub public_key: PublicKey,
pub relays: Vec<RelayUrl>,
pub nip46: Vec<RelayUrl>,
}
impl Nip05Profile {
pub fn from_json(address: &Nip05Address, json: &Value) -> Result<Self, Error> {
let public_key: PublicKey =
get_key_from_json(json, address).ok_or(Error::ImpossibleToVerify)?;
let relays: Vec<RelayUrl> = get_relays_from_json(json, &public_key);
let nip46: Vec<RelayUrl> = get_nip46_relays_from_json(json, &public_key);
Ok(Self {
public_key,
relays,
nip46,
})
}
#[inline]
pub fn from_raw_json(address: &Nip05Address, raw_json: &str) -> Result<Self, Error> {
let json: Value = serde_json::from_str(raw_json)?;
Self::from_json(address, &json)
}
}
#[inline]
fn get_key_from_json(json: &Value, address: &Nip05Address) -> Option<PublicKey> {
json.get("names")
.and_then(|names| names.get(&address.name))
.and_then(|value| value.as_str())
.and_then(|pubkey| PublicKey::from_hex(pubkey).ok())
}
#[inline]
fn get_relays_from_json(json: &Value, pk: &PublicKey) -> Vec<RelayUrl> {
json.get("relays")
.and_then(|relays| relays.get(pk.to_hex()))
.and_then(|value| serde_json::from_value(value.clone()).ok())
.unwrap_or_default()
}
#[inline]
fn get_nip46_relays_from_json(json: &Value, pk: &PublicKey) -> Vec<RelayUrl> {
json.get("nip46")
.and_then(|relays| relays.get(pk.to_hex()))
.and_then(|value| serde_json::from_value(value.clone()).ok())
.unwrap_or_default()
}
pub fn verify_from_json(public_key: &PublicKey, address: &Nip05Address, json: &Value) -> bool {
if let Some(pubkey) = get_key_from_json(json, address) {
if &pubkey == public_key {
return true;
}
}
false
}
#[inline]
pub fn verify_from_raw_json(
public_key: &PublicKey,
address: &Nip05Address,
raw_json: &str,
) -> Result<bool, Error> {
let json: Value = serde_json::from_str(raw_json)?;
Ok(verify_from_json(public_key, address, &json))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_nip05() {
let json: &str = r#"{
"names": {
"yuki": "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272",
"_": "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272"
}
}"#;
let json: Value = serde_json::from_str(json).unwrap();
let address = Nip05Address::parse("_@yukikishimoto.com").unwrap();
assert_eq!(
address.url().to_string(),
"https://yukikishimoto.com/.well-known/nostr.json?name=_"
);
assert_eq!(address.name(), "_");
let public_key =
PublicKey::from_hex("68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272")
.unwrap();
assert!(verify_from_json(&public_key, &address, &json));
let public_key =
PublicKey::from_hex("b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a")
.unwrap();
assert!(!verify_from_json(&public_key, &address, &json));
}
#[test]
fn test_verify_nip05_without_local() {
let json: &str = r#"{
"names": {
"yuki": "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272",
"_": "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272"
}
}"#;
let json: Value = serde_json::from_str(json).unwrap();
let address = Nip05Address::parse("yukikishimoto.com").unwrap();
assert_eq!(
address.url().to_string(),
"https://yukikishimoto.com/.well-known/nostr.json?name=_"
);
assert_eq!(address.name(), "_");
let public_key =
PublicKey::from_hex("68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272")
.unwrap();
assert!(verify_from_json(&public_key, &address, &json));
let public_key =
PublicKey::from_hex("b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a")
.unwrap();
assert!(!verify_from_json(&public_key, &address, &json));
}
#[test]
fn test_nip05_profile_from_json() {
let json = r#"{
"names": {
"nostr-tool-test-user": "94a9eb13c37b3c1519169a426d383c51530f4fe8f693c62f32b321adfdd4ec7f",
"robosatsob": "3b57518d02e6acfd5eb7198530b2e351e5a52278fb2499d14b66db2b5791c512",
"0xtr": "b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a",
"_": "b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a"
},
"relays": {
"b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a": [ "wss://nostr.oxtr.dev", "wss://relay.damus.io", "wss://relay.nostr.band" ]
}
}"#;
let json: Value = serde_json::from_str(json).unwrap();
let pubkey =
PublicKey::from_hex("b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a")
.unwrap();
let address = Nip05Address::parse("0xtr@oxtr.dev").unwrap();
let profile = Nip05Profile::from_json(&address, &json).unwrap();
assert_eq!(profile.public_key, pubkey);
assert_eq!(
profile.relays,
vec![
RelayUrl::parse("wss://nostr.oxtr.dev").unwrap(),
RelayUrl::parse("wss://relay.damus.io").unwrap(),
RelayUrl::parse("wss://relay.nostr.band").unwrap()
]
);
assert!(profile.nip46.is_empty());
}
}