use core::fmt;
use itertools::Itertools;
use nostr::key::XOnlyPublicKey;
use nostr::nips::nip47::{Error, Method};
use nostr::prelude::form_urlencoded::byte_serialize;
use nostr::Url;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::str::FromStr;
fn url_encode<T>(data: T) -> String
where
T: AsRef<[u8]>,
{
byte_serialize(data.as_ref()).collect()
}
pub const ALL_NIP49_BUDGET_PERIODS: [NIP49BudgetPeriod; 4] = [
NIP49BudgetPeriod::Daily,
NIP49BudgetPeriod::Weekly,
NIP49BudgetPeriod::Monthly,
NIP49BudgetPeriod::Yearly,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NIP49BudgetPeriod {
Daily,
Weekly,
Monthly,
Yearly,
}
impl Serialize for NIP49BudgetPeriod {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'a> Deserialize<'a> for NIP49BudgetPeriod {
fn deserialize<D: serde::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
NIP49BudgetPeriod::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl fmt::Display for NIP49BudgetPeriod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NIP49BudgetPeriod::Daily => write!(f, "daily"),
NIP49BudgetPeriod::Weekly => write!(f, "weekly"),
NIP49BudgetPeriod::Monthly => write!(f, "monthly"),
NIP49BudgetPeriod::Yearly => write!(f, "yearly"),
}
}
}
impl FromStr for NIP49BudgetPeriod {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"day" => Ok(NIP49BudgetPeriod::Daily),
"daily" => Ok(NIP49BudgetPeriod::Daily),
"week" => Ok(NIP49BudgetPeriod::Weekly),
"weekly" => Ok(NIP49BudgetPeriod::Weekly),
"month" => Ok(NIP49BudgetPeriod::Monthly),
"monthly" => Ok(NIP49BudgetPeriod::Monthly),
"year" => Ok(NIP49BudgetPeriod::Yearly),
"yearly" => Ok(NIP49BudgetPeriod::Yearly),
_ => Err(anyhow::anyhow!("Invalid NIP49BudgetPeriod")),
}
}
}
pub const NIP49_URI_SCHEME: &str = "nostr+walletauth";
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct NIP49Budget {
pub time_period: NIP49BudgetPeriod,
pub amount: u64,
}
impl fmt::Display for NIP49Budget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.amount, self.time_period)
}
}
impl FromStr for NIP49Budget {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut split = s.split('/');
let amount = split
.next()
.ok_or(Error::InvalidURI)?
.parse()
.map_err(|_| Error::InvalidURI)?;
let time_period = split
.next()
.ok_or(Error::InvalidURI)?
.parse()
.map_err(|_| Error::InvalidURI)?;
Ok(Self {
time_period,
amount,
})
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NIP49URI {
pub public_key: XOnlyPublicKey,
pub relay_url: Url,
pub secret: String,
pub required_commands: Vec<Method>,
pub optional_commands: Vec<Method>,
pub budget: Option<NIP49Budget>,
pub identity: Option<XOnlyPublicKey>,
}
fn method_from_str(s: &str) -> Result<Method, Error> {
match s {
"pay_invoice" => Ok(Method::PayInvoice),
"make_invoice" => Ok(Method::MakeInvoice),
"lookup_invoice" => Ok(Method::LookupInvoice),
"get_balance" => Ok(Method::GetBalance),
_ => Err(Error::InvalidURI),
}
}
fn method_to_string(method: &Method) -> String {
match method {
Method::PayInvoice => "pay_invoice",
Method::MakeInvoice => "make_invoice",
Method::LookupInvoice => "lookup_invoice",
Method::GetBalance => "get_balance",
}
.to_string()
}
impl FromStr for NIP49URI {
type Err = Error;
fn from_str(uri: &str) -> Result<Self, Self::Err> {
let url = Url::parse(uri)?;
if url.scheme() != NIP49_URI_SCHEME {
return Err(Error::InvalidURIScheme);
}
if let Some(pubkey) = url.domain() {
let public_key = XOnlyPublicKey::from_str(pubkey)?;
let mut relay_url: Option<Url> = None;
let mut required_commands: Vec<Method> = vec![];
let mut optional_commands: Vec<Method> = vec![];
let mut budget: Option<NIP49Budget> = None;
let mut secret: Option<String> = None;
let mut identity: Option<XOnlyPublicKey> = None;
for (key, value) in url.query_pairs() {
match key {
Cow::Borrowed("relay") => {
relay_url = Some(Url::parse(value.as_ref())?);
}
Cow::Borrowed("secret") => {
secret = Some(value.to_string());
}
Cow::Borrowed("required_commands") => {
required_commands = value
.split(' ')
.map(method_from_str)
.collect::<Result<Vec<Method>, Error>>()?;
}
Cow::Borrowed("optional_commands") => {
optional_commands = value
.split(' ')
.map(method_from_str)
.collect::<Result<Vec<Method>, Error>>()?;
}
Cow::Borrowed("budget") => {
budget = Some(NIP49Budget::from_str(value.as_ref())?);
}
Cow::Borrowed("identity") => {
identity = Some(XOnlyPublicKey::from_str(value.as_ref())?);
}
_ => (),
}
}
if required_commands.is_empty() {
return Err(Error::InvalidURI);
}
if let Some((relay_url, secret)) = relay_url.zip(secret) {
return Ok(Self {
public_key,
relay_url,
secret,
required_commands,
optional_commands,
budget,
identity,
});
}
}
Err(Error::InvalidURI)
}
}
impl fmt::Display for NIP49URI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{NIP49_URI_SCHEME}://{}?relay={}&secret={}&required_commands={}",
self.public_key,
url_encode(self.relay_url.to_string()),
self.secret,
url_encode(
self.required_commands
.iter()
.map(method_to_string)
.join(" ")
),
)?;
if !self.optional_commands.is_empty() {
write!(
f,
"&optional_commands={}",
url_encode(
self.optional_commands
.iter()
.map(method_to_string)
.join(" ")
)
)?;
}
if let Some(budget) = &self.budget {
write!(f, "&budget={}", url_encode(budget.to_string()))?;
}
if let Some(identity) = &self.identity {
write!(f, "&identity={identity}")?;
}
Ok(())
}
}
impl Serialize for NIP49URI {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'a> Deserialize<'a> for NIP49URI {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
let uri = String::deserialize(deserializer)?;
NIP49URI::from_str(&uri).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NIP49Confirmation {
pub secret: String,
pub commands: Vec<Method>,
pub relay: Option<String>,
}