diff --git a/bindings/nostr-ffi/src/error.rs b/bindings/nostr-ffi/src/error.rs index fdb344c4f..4fe6a9a0e 100644 --- a/bindings/nostr-ffi/src/error.rs +++ b/bindings/nostr-ffi/src/error.rs @@ -123,6 +123,12 @@ impl From for NostrError { } } +impl From for NostrError { + fn from(e: nostr::nips::nip90::Error) -> NostrError { + Self::Generic { err: e.to_string() } + } +} + impl From for NostrError { fn from(e: nostr::secp256k1::Error) -> NostrError { Self::Generic { err: e.to_string() } diff --git a/bindings/nostr-ffi/src/event/tag.rs b/bindings/nostr-ffi/src/event/tag.rs index 1eaa0fc05..bbcc9bfea 100644 --- a/bindings/nostr-ffi/src/event/tag.rs +++ b/bindings/nostr-ffi/src/event/tag.rs @@ -10,9 +10,10 @@ use nostr::event::tag::{ use nostr::hashes::sha256::Hash as Sha256Hash; use nostr::nips::nip26::Conditions; use nostr::nips::nip48::Protocol; +use nostr::nips::nip90::DataVendingMachineStatus; use nostr::secp256k1::schnorr::Signature; use nostr::secp256k1::XOnlyPublicKey; -use nostr::{EventId, Kind, RelayMetadata, Timestamp, UncheckedUrl, Url}; +use nostr::{Event, EventId, JsonUtil, Kind, RelayMetadata, Timestamp, UncheckedUrl, Url}; use crate::error::{NostrError, Result}; @@ -115,6 +116,7 @@ pub enum TagKindKnown { Anon, Proxy, Emoji, + Request, } impl From for TagKind { @@ -264,6 +266,9 @@ impl From for TagKind { tag::TagKind::Emoji => Self::Known { known: TagKindKnown::Emoji, }, + tag::TagKind::Request => Self::Known { + known: TagKindKnown::Request, + }, tag::TagKind::Custom(unknown) => Self::Unknown { unknown }, } } @@ -321,6 +326,7 @@ impl From for tag::TagKind { TagKindKnown::Anon => Self::Anon, TagKindKnown::Proxy => Self::Proxy, TagKindKnown::Emoji => Self::Emoji, + TagKindKnown::Request => Self::Request, }, TagKind::Unknown { unknown } => Self::Custom(unknown), } @@ -437,7 +443,8 @@ pub enum TagEnum { urls: Vec, }, Amount { - amount: u64, + millisats: u64, + bolt11: Option, }, Lnurl { lnurl: String, @@ -485,7 +492,7 @@ pub enum TagEnum { Ends { timestamp: u64, }, - Status { + LiveEventStatus { status: String, }, CurrentParticipants { @@ -514,6 +521,13 @@ pub enum TagEnum { shortcode: String, url: String, }, + Request { + event: String, + }, + DataVendingMachineStatus { + status: String, + extra_info: Option, + }, } impl From for TagEnum { @@ -624,7 +638,7 @@ impl From for TagEnum { tag::Tag::Relays(relays) => Self::Relays { urls: relays.into_iter().map(|r| r.to_string()).collect(), }, - tag::Tag::Amount(amount) => Self::Amount { amount }, + tag::Tag::Amount { millisats, bolt11 } => Self::Amount { millisats, bolt11 }, tag::Tag::Name(name) => Self::Name { name }, tag::Tag::Lnurl(lnurl) => Self::Lnurl { lnurl }, tag::Tag::Url(url) => Self::Url { @@ -653,7 +667,7 @@ impl From for TagEnum { tag::Tag::Ends(timestamp) => Self::Ends { timestamp: timestamp.as_u64(), }, - tag::Tag::Status(s) => Self::Status { + tag::Tag::LiveEventStatus(s) => Self::LiveEventStatus { status: s.to_string(), }, tag::Tag::CurrentParticipants(num) => Self::CurrentParticipants { num }, @@ -676,6 +690,15 @@ impl From for TagEnum { shortcode, url: url.to_string(), }, + tag::Tag::Request(event) => Self::Request { + event: event.as_json(), + }, + tag::Tag::DataVendingMachineStatus { status, extra_info } => { + Self::DataVendingMachineStatus { + status: status.to_string(), + extra_info, + } + } } } } @@ -797,7 +820,7 @@ impl TryFrom for tag::Tag { TagEnum::Relays { urls } => Ok(Self::Relays( urls.into_iter().map(UncheckedUrl::from).collect(), )), - TagEnum::Amount { amount } => Ok(Self::Amount(amount)), + TagEnum::Amount { millisats, bolt11 } => Ok(Self::Amount { millisats, bolt11 }), TagEnum::Lnurl { lnurl } => Ok(Self::Lnurl(lnurl)), TagEnum::Name { name } => Ok(Self::Name(name)), TagEnum::PublishedAt { timestamp } => Ok(Self::PublishedAt(Timestamp::from(timestamp))), @@ -813,7 +836,9 @@ impl TryFrom for tag::Tag { TagEnum::Recording { url } => Ok(Self::Recording(UncheckedUrl::from(url))), TagEnum::Starts { timestamp } => Ok(Self::Starts(Timestamp::from(timestamp))), TagEnum::Ends { timestamp } => Ok(Self::Ends(Timestamp::from(timestamp))), - TagEnum::Status { status } => Ok(Self::Status(LiveEventStatus::from(status))), + TagEnum::LiveEventStatus { status } => { + Ok(Self::LiveEventStatus(LiveEventStatus::from(status))) + } TagEnum::CurrentParticipants { num } => Ok(Self::CurrentParticipants(num)), TagEnum::TotalParticipants { num } => Ok(Self::CurrentParticipants(num)), TagEnum::AbsoluteURL { url } => Ok(Self::AbsoluteURL(UncheckedUrl::from(url))), @@ -828,6 +853,13 @@ impl TryFrom for tag::Tag { shortcode, url: UncheckedUrl::from(url), }), + TagEnum::Request { event } => Ok(Self::Request(Event::from_json(event)?)), + TagEnum::DataVendingMachineStatus { status, extra_info } => { + Ok(Self::DataVendingMachineStatus { + status: DataVendingMachineStatus::from_str(&status)?, + extra_info, + }) + } } } } diff --git a/bindings/nostr-ffi/src/nostr.udl b/bindings/nostr-ffi/src/nostr.udl index ad3de1f89..ae7549b85 100644 --- a/bindings/nostr-ffi/src/nostr.udl +++ b/bindings/nostr-ffi/src/nostr.udl @@ -442,6 +442,7 @@ enum TagKindKnown { "Anon", "Proxy", "Emoji", + "Request", }; [Enum] @@ -475,7 +476,7 @@ interface TagEnum { Bolt11(string bolt11); Preimage(string preimage); Relays(sequence urls); - Amount(u64 amount); + Amount(u64 millisats, string? bolt11); Lnurl(string lnurl); Name(string name); PublishedAt(u64 timestamp); @@ -491,7 +492,7 @@ interface TagEnum { Recording(string url); Starts(u64 timestamp); Ends(u64 timestamp); - Status(string status); + LiveEventStatus(string status); CurrentParticipants(u64 num); TotalParticipants(u64 num); AbsoluteURL(string url); @@ -500,6 +501,8 @@ interface TagEnum { Anon(string? msg); Proxy(string id, string protocol); Emoji(string shortcode, string url); + Request(string event); + DataVendingMachineStatus(string status, string? extra_info); }; interface Tag { diff --git a/bindings/nostr-sdk-ffi/src/nostr_sdk.udl b/bindings/nostr-sdk-ffi/src/nostr_sdk.udl index 9bf807f04..e95b1b12c 100644 --- a/bindings/nostr-sdk-ffi/src/nostr_sdk.udl +++ b/bindings/nostr-sdk-ffi/src/nostr_sdk.udl @@ -462,6 +462,7 @@ enum TagKindKnown { "Anon", "Proxy", "Emoji", + "Request", }; [Enum] @@ -495,7 +496,7 @@ interface TagEnum { Bolt11(string bolt11); Preimage(string preimage); Relays(sequence urls); - Amount(u64 amount); + Amount(u64 millisats, string? bolt11); Lnurl(string lnurl); Name(string name); PublishedAt(u64 timestamp); @@ -511,7 +512,7 @@ interface TagEnum { Recording(string url); Starts(u64 timestamp); Ends(u64 timestamp); - Status(string status); + LiveEventStatus(string status); CurrentParticipants(u64 num); TotalParticipants(u64 num); AbsoluteURL(string url); @@ -520,7 +521,9 @@ interface TagEnum { Anon(string? msg); Proxy(string id, string protocol); Emoji(string shortcode, string url); -}; + Request(string event); + DataVendingMachineStatus(string status, string? extra_info); +}; interface Tag { [Throws=NostrError, Name=parse] diff --git a/crates/nostr/README.md b/crates/nostr/README.md index 21615beb2..d3e69b348 100644 --- a/crates/nostr/README.md +++ b/crates/nostr/README.md @@ -153,6 +153,7 @@ The following crate feature flags are available: | ✅ | [65 - Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md) | | ✅ | [78 - Arbitrary custom app data](https://github.com/nostr-protocol/nips/blob/master/78.md) | | ❌ | [89 - Recommended Application Handlers](https://github.com/nostr-protocol/nips/blob/master/89.md) | +| ✅ | [90 - Data Vending Machine](https://github.com/nostr-protocol/nips/blob/master/90.md) | | ✅ | [94 - File Metadata](https://github.com/nostr-protocol/nips/blob/master/94.md) | | ✅ | [98 - HTTP Auth](https://github.com/nostr-protocol/nips/blob/master/98.md) | | ❌ | [99 - Classified Listings](https://github.com/nostr-protocol/nips/blob/master/99.md) | diff --git a/crates/nostr/src/event/builder.rs b/crates/nostr/src/event/builder.rs index a2d334bf3..a8c489e50 100644 --- a/crates/nostr/src/event/builder.rs +++ b/crates/nostr/src/event/builder.rs @@ -6,6 +6,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::fmt; +use core::ops::Range; #[cfg(feature = "std")] use bitcoin::secp256k1::rand; @@ -14,9 +15,9 @@ use bitcoin::secp256k1::{self, Secp256k1, Signing, XOnlyPublicKey}; use serde_json::{json, Value}; use url_fork::Url; -pub use super::kind::Kind; -pub use super::tag::{ImageDimensions, Marker, Tag, TagKind}; -use super::{Event, EventId, UnsignedEvent}; +use super::kind::{Kind, NIP90_JOB_REQUEST_RANGE, NIP90_JOB_RESULT_RANGE}; +use super::tag::ImageDimensions; +use super::{Event, EventId, Marker, Tag, TagKind, UnsignedEvent}; use crate::key::{self, Keys}; #[cfg(feature = "nip04")] use crate::nips::nip04; @@ -26,6 +27,7 @@ use crate::nips::nip46::Message as NostrConnectMessage; use crate::nips::nip53::LiveEvent; use crate::nips::nip57::ZapRequestData; use crate::nips::nip58::Error as Nip58Error; +use crate::nips::nip90::DataVendingMachineStatus; use crate::nips::nip94::FileMetadata; use crate::nips::nip98::HttpData; use crate::nips::{nip13, nip58}; @@ -37,6 +39,24 @@ use crate::types::{ChannelId, Contact, Metadata, Timestamp}; use crate::SECP256K1; use crate::{JsonUtil, RelayMetadata, UncheckedUrl}; +/// Wrong kind error +#[derive(Debug)] +pub enum WrongKindError { + /// Singe kind + Single(Kind), + /// Range + Range(Range), +} + +impl fmt::Display for WrongKindError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Single(k) => write!(f, "{k}"), + Self::Range(range) => write!(f, "'{} <= k <= {}'", range.start, range.end), + } + } +} + /// [`EventBuilder`] error #[derive(Debug)] pub enum Error { @@ -53,6 +73,13 @@ pub enum Error { NIP04(nip04::Error), /// NIP58 error NIP58(nip58::Error), + /// Wrong kind + WrongKind { + /// The received wrong kind + received: Kind, + /// The expected kind (single or range) + expected: WrongKindError, + }, } #[cfg(feature = "std")] @@ -68,6 +95,9 @@ impl fmt::Display for Error { #[cfg(feature = "nip04")] Self::NIP04(e) => write!(f, "NIP04: {e}"), Self::NIP58(e) => write!(f, "NIP58: {e}"), + Self::WrongKind { received, expected } => { + write!(f, "Wrong kind: received={received}, expected={expected}") + } } } } @@ -613,7 +643,10 @@ impl EventBuilder { } if let Some(amount) = amount { - tags.push(Tag::Amount(amount)); + tags.push(Tag::Amount { + millisats: amount, + bolt11: None, + }); } if let Some(lnurl) = lnurl { @@ -863,6 +896,82 @@ impl EventBuilder { Ok(event_builder) } + /// Data Vending Machine - Job Request + /// + /// + pub fn job_request(kind: Kind, tags: &[Tag]) -> Result { + if kind.is_job_request() { + Ok(Self::new(kind, "", tags)) + } else { + Err(Error::WrongKind { + received: kind, + expected: WrongKindError::Range(NIP90_JOB_REQUEST_RANGE), + }) + } + } + + /// Data Vending Machine - Job Result + /// + /// + pub fn job_result( + job_request: Event, + amount_millisats: u64, + bolt11: Option, + ) -> Result { + let kind: Kind = job_request.kind + 1000; + if kind.is_job_result() { + let mut tags: Vec = job_request + .tags + .iter() + .filter_map(|t| { + if t.kind() == TagKind::I { + Some(t.clone()) + } else { + None + } + }) + .collect(); + tags.extend_from_slice(&[ + Tag::Event(job_request.id, None, None), + Tag::PubKey(job_request.pubkey, None), + Tag::Request(job_request), + Tag::Amount { + millisats: amount_millisats, + bolt11, + }, + ]); + Ok(Self::new(kind, "", &tags)) + } else { + Err(Error::WrongKind { + received: kind, + expected: WrongKindError::Range(NIP90_JOB_RESULT_RANGE), + }) + } + } + + /// Data Vending Machine - Job Feedback + /// + /// + pub fn job_feedback( + job_request: &Event, + status: DataVendingMachineStatus, + extra_info: Option, + amount_millisats: u64, + bolt11: Option, + payload: Option, + ) -> Self { + let tags = &[ + Tag::DataVendingMachineStatus { status, extra_info }, + Tag::Event(job_request.id, None, None), + Tag::PubKey(job_request.pubkey, None), + Tag::Amount { + millisats: amount_millisats, + bolt11, + }, + ]; + Self::new(Kind::JobFeedback, payload.unwrap_or_default(), tags) + } + /// File metadata /// /// diff --git a/crates/nostr/src/event/kind.rs b/crates/nostr/src/event/kind.rs index 135541167..2c16bb1a3 100644 --- a/crates/nostr/src/event/kind.rs +++ b/crates/nostr/src/event/kind.rs @@ -6,12 +6,16 @@ use core::fmt; use core::hash::{Hash, Hasher}; use core::num::ParseIntError; -use core::ops::Range; +use core::ops::{Add, Range}; use core::str::FromStr; use serde::de::{Deserialize, Deserializer, Error, Visitor}; use serde::ser::{Serialize, Serializer}; +/// NIP90 - Job request range +pub const NIP90_JOB_REQUEST_RANGE: Range = 5_000..5_999; +/// NIP90 - Job result range +pub const NIP90_JOB_RESULT_RANGE: Range = 6_000..6_999; /// Regular range pub const REGULAR_RANGE: Range = 1_000..10_000; /// Replaceable range @@ -108,6 +112,12 @@ pub enum Kind { SetStall, /// Set product (NIP15) SetProduct, + /// Job Feedback (NIP90) + JobFeedback, + /// Regular Events (must be between 5000 and <=5999) + JobRequest(u16), + /// Regular Events (must be between 6000 and <=6999) + JobResult(u16), /// Regular Events (must be between 1000 and <=9999) Regular(u16), /// Replaceable event (must be between 10000 and <20000) @@ -131,6 +141,20 @@ impl Kind { (*self).into() } + /// Check if [`Kind`] is a NIP90 job request + /// + /// + pub fn is_job_request(&self) -> bool { + NIP90_JOB_REQUEST_RANGE.contains(&self.as_u64()) + } + + /// Check if [`Kind`] is a NIP90 job result + /// + /// + pub fn is_job_result(&self) -> bool { + NIP90_JOB_RESULT_RANGE.contains(&self.as_u64()) + } + /// Check if [`Kind`] is `Regular` pub fn is_regular(&self) -> bool { REGULAR_RANGE.contains(&self.as_u64()) @@ -206,6 +230,9 @@ impl From for Kind { 30078 => Self::ApplicationSpecificData, 1063 => Self::FileMetadata, 27235 => Self::HttpAuth, + 7000 => Self::JobFeedback, + x if (NIP90_JOB_REQUEST_RANGE).contains(&x) => Self::JobRequest(x as u16), + x if (NIP90_JOB_RESULT_RANGE).contains(&x) => Self::JobResult(x as u16), x if (REGULAR_RANGE).contains(&x) => Self::Regular(x as u16), x if (REPLACEABLE_RANGE).contains(&x) => Self::Replaceable(x as u16), x if (EPHEMERAL_RANGE).contains(&x) => Self::Ephemeral(x as u16), @@ -262,6 +289,9 @@ impl From for u64 { Kind::ApplicationSpecificData => 30078, Kind::FileMetadata => 1063, Kind::HttpAuth => 27235, + Kind::JobFeedback => 7000, + Kind::JobRequest(u) => u as u64, + Kind::JobResult(u) => u as u64, Kind::Regular(u) => u as u64, Kind::Replaceable(u) => u as u64, Kind::Ephemeral(u) => u as u64, @@ -294,6 +324,14 @@ impl Hash for Kind { } } +impl Add for Kind { + type Output = Self; + fn add(self, rhs: u64) -> Self::Output { + let kind = self.as_u64(); + Kind::from(kind + rhs) + } +} + impl Serialize for Kind { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/nostr/src/event/mod.rs b/crates/nostr/src/event/mod.rs index 1fe4baee1..41960d458 100644 --- a/crates/nostr/src/event/mod.rs +++ b/crates/nostr/src/event/mod.rs @@ -175,6 +175,8 @@ impl Event { /// Returns `true` if the event has an expiration tag that is expired. /// If an event has no `Expiration` tag, then it will return `false`. + /// + /// #[cfg(feature = "std")] pub fn is_expired(&self) -> bool { let now: Instant = Instant::now(); @@ -183,6 +185,8 @@ impl Event { /// Returns `true` if the event has an expiration tag that is expired. /// If an event has no `Expiration` tag, then it will return `false`. + /// + /// pub fn is_expired_with_supplier(&self, supplier: &T) -> bool where T: TimeSupplier, @@ -196,7 +200,9 @@ impl Event { false } - /// Timestamp this event with OpenTimestamps, according to NIP-03 + /// Timestamp this event with OpenTimestamps + /// + /// #[cfg(feature = "nip03")] pub fn timestamp(&mut self) -> Result<(), Error> { let ots = nostr_ots::timestamp_event(&self.id.to_hex())?; @@ -204,22 +210,44 @@ impl Event { Ok(()) } + /// Check if [`Kind`] is a NIP90 job request + /// + /// + pub fn is_job_request(&self) -> bool { + self.kind.is_job_request() + } + + /// Check if [`Kind`] is a NIP90 job result + /// + /// + pub fn is_job_result(&self) -> bool { + self.kind.is_job_result() + } + /// Check if event [`Kind`] is `Regular` + /// + /// pub fn is_regular(&self) -> bool { self.kind.is_regular() } /// Check if event [`Kind`] is `Replaceable` + /// + /// pub fn is_replaceable(&self) -> bool { self.kind.is_replaceable() } /// Check if event [`Kind`] is `Ephemeral` + /// + /// pub fn is_ephemeral(&self) -> bool { self.kind.is_ephemeral() } /// Check if event [`Kind`] is `Parameterized replaceable` + /// + /// pub fn is_parameterized_replaceable(&self) -> bool { self.kind.is_parameterized_replaceable() } diff --git a/crates/nostr/src/event/tag.rs b/crates/nostr/src/event/tag.rs index 9fd1c458c..747636741 100644 --- a/crates/nostr/src/event/tag.rs +++ b/crates/nostr/src/event/tag.rs @@ -21,7 +21,8 @@ use url_fork::{ParseError, Url}; use super::id::{self, EventId}; use crate::nips::nip26::{Conditions, Error as Nip26Error}; use crate::nips::nip48::Protocol; -use crate::{Kind, Timestamp, UncheckedUrl}; +use crate::nips::nip90::DataVendingMachineStatus; +use crate::{Event, JsonUtil, Kind, Timestamp, UncheckedUrl}; /// [`Tag`] error #[derive(Debug)] @@ -481,6 +482,8 @@ pub enum TagKind { Proxy, /// Emoji Emoji, + /// Request (NIP90) + Request, /// Custom tag kind Custom(String), } @@ -536,6 +539,7 @@ impl fmt::Display for TagKind { Self::Anon => write!(f, "anon"), Self::Proxy => write!(f, "proxy"), Self::Emoji => write!(f, "emoji"), + Self::Request => write!(f, "request"), Self::Custom(tag) => write!(f, "{tag}"), } } @@ -595,6 +599,7 @@ where "anon" => Self::Anon, "proxy" => Self::Proxy, "emoji" => Self::Emoji, + "request" => Self::Request, t => Self::Custom(t.to_owned()), } } @@ -655,7 +660,10 @@ pub enum Tag { Bolt11(String), Preimage(String), Relays(Vec), - Amount(u64), + Amount { + millisats: u64, + bolt11: Option, + }, Lnurl(String), Name(String), PublishedAt(Timestamp), @@ -674,7 +682,7 @@ pub enum Tag { Recording(UncheckedUrl), Starts(Timestamp), Ends(Timestamp), - Status(LiveEventStatus), + LiveEventStatus(LiveEventStatus), CurrentParticipants(u64), TotalParticipants(u64), AbsoluteURL(UncheckedUrl), @@ -693,6 +701,11 @@ pub enum Tag { /// URL to the corresponding image file of the emoji url: UncheckedUrl, }, + Request(Event), + DataVendingMachineStatus { + status: DataVendingMachineStatus, + extra_info: Option, + }, } impl Tag { @@ -742,7 +755,7 @@ impl Tag { Self::Bolt11(..) => TagKind::Bolt11, Self::Preimage(..) => TagKind::Preimage, Self::Relays(..) => TagKind::Relays, - Self::Amount(..) => TagKind::Amount, + Self::Amount { .. } => TagKind::Amount, Self::Name(..) => TagKind::Name, Self::Lnurl(..) => TagKind::Lnurl, Self::Url(..) => TagKind::Url, @@ -757,7 +770,7 @@ impl Tag { Self::Recording(..) => TagKind::Recording, Self::Starts(..) => TagKind::Starts, Self::Ends(..) => TagKind::Ends, - Self::Status(..) => TagKind::Status, + Self::LiveEventStatus(..) | Self::DataVendingMachineStatus { .. } => TagKind::Status, Self::CurrentParticipants(..) => TagKind::CurrentParticipants, Self::TotalParticipants(..) => TagKind::TotalParticipants, Self::AbsoluteURL(..) => TagKind::U, @@ -766,6 +779,7 @@ impl Tag { Self::Anon { .. } => TagKind::Anon, Self::Proxy { .. } => TagKind::Proxy, Self::Emoji { .. } => TagKind::Emoji, + Self::Request(..) => TagKind::Request, } } } @@ -843,7 +857,10 @@ where TagKind::Description => Ok(Self::Description(tag_1.to_owned())), TagKind::Bolt11 => Ok(Self::Bolt11(tag_1.to_owned())), TagKind::Preimage => Ok(Self::Preimage(tag_1.to_owned())), - TagKind::Amount => Ok(Self::Amount(tag_1.parse()?)), + TagKind::Amount => Ok(Self::Amount { + millisats: tag_1.parse()?, + bolt11: None, + }), TagKind::Lnurl => Ok(Self::Lnurl(tag_1.to_owned())), TagKind::Name => Ok(Self::Name(tag_1.to_owned())), TagKind::Url => Ok(Self::Url(Url::parse(tag_1)?)), @@ -855,7 +872,13 @@ where TagKind::Recording => Ok(Self::Recording(UncheckedUrl::from(tag_1))), TagKind::Starts => Ok(Self::Starts(Timestamp::from_str(tag_1)?)), TagKind::Ends => Ok(Self::Ends(Timestamp::from_str(tag_1)?)), - TagKind::Status => Ok(Self::Status(LiveEventStatus::from(tag_1))), + TagKind::Status => match DataVendingMachineStatus::from_str(tag_1) { + Ok(status) => Ok(Self::DataVendingMachineStatus { + status, + extra_info: None, + }), + Err(_) => Ok(Self::LiveEventStatus(LiveEventStatus::from(tag_1))), /* TODO: check if unknown status error? */ + }, TagKind::CurrentParticipants => Ok(Self::CurrentParticipants(tag_1.parse()?)), TagKind::TotalParticipants => Ok(Self::TotalParticipants(tag_1.parse()?)), TagKind::U => Ok(Self::AbsoluteURL(UncheckedUrl::from(tag_1))), @@ -864,6 +887,7 @@ where TagKind::Anon => Ok(Self::Anon { msg: (!tag_1.is_empty()).then_some(tag_1.to_owned()), }), + TagKind::Request => Ok(Self::Request(Event::from_json(tag_1)?)), _ => Ok(Self::Generic(tag_kind, vec![tag_1.to_owned()])), } } else if tag_len == 3 { @@ -945,6 +969,16 @@ where shortcode: tag_1.to_owned(), url: UncheckedUrl::from(tag_2), }), + TagKind::Status => match DataVendingMachineStatus::from_str(tag_1) { + Ok(status) => Ok(Self::DataVendingMachineStatus { + status, + extra_info: Some(tag_2.to_string()), + }), + Err(_) => Ok(Self::Generic( + tag_kind, + tag[1..].iter().map(|s| s.as_ref().to_owned()).collect(), + )), + }, _ => Ok(Self::Generic( tag_kind, tag[1..].iter().map(|s| s.as_ref().to_owned()).collect(), @@ -1160,8 +1194,12 @@ impl From for Vec { .into_iter() .chain(relays.iter().map(|relay| relay.to_string())) .collect::>(), - Tag::Amount(amount) => { - vec![TagKind::Amount.to_string(), amount.to_string()] + Tag::Amount { millisats, bolt11 } => { + let mut tag = vec![TagKind::Amount.to_string(), millisats.to_string()]; + if let Some(bolt11) = bolt11 { + tag.push(bolt11); + } + tag } Tag::Name(name) => { vec![TagKind::Name.to_string(), name] @@ -1185,7 +1223,7 @@ impl From for Vec { Tag::Ends(timestamp) => { vec![TagKind::Ends.to_string(), timestamp.to_string()] } - Tag::Status(s) => { + Tag::LiveEventStatus(s) => { vec![TagKind::Status.to_string(), s.to_string()] } Tag::CurrentParticipants(num) => { @@ -1214,6 +1252,14 @@ impl From for Vec { Tag::Emoji { shortcode, url } => { vec![TagKind::Emoji.to_string(), shortcode, url.to_string()] } + Tag::Request(event) => vec![TagKind::Request.to_string(), event.as_json()], + Tag::DataVendingMachineStatus { status, extra_info } => { + let mut tag = vec![TagKind::Status.to_string(), status.to_string()]; + if let Some(extra_info) = extra_info { + tag.push(extra_info); + } + tag + } } } } @@ -1974,7 +2020,10 @@ mod tests { assert_eq!( Tag::parse(vec!["amount", "10000"]).unwrap(), - Tag::Amount(10000) + Tag::Amount { + millisats: 10_000, + bolt11: None + } ); } } diff --git a/crates/nostr/src/nips/mod.rs b/crates/nostr/src/nips/mod.rs index d3ff2996d..c863132cc 100644 --- a/crates/nostr/src/nips/mod.rs +++ b/crates/nostr/src/nips/mod.rs @@ -30,5 +30,6 @@ pub mod nip53; pub mod nip57; pub mod nip58; pub mod nip65; +pub mod nip90; pub mod nip94; pub mod nip98; diff --git a/crates/nostr/src/nips/nip53.rs b/crates/nostr/src/nips/nip53.rs index 956a62e8c..47d690fe6 100644 --- a/crates/nostr/src/nips/nip53.rs +++ b/crates/nostr/src/nips/nip53.rs @@ -98,7 +98,7 @@ impl From for Vec { } if let Some(status) = status { - tags.push(Tag::Status(status)); + tags.push(Tag::LiveEventStatus(status)); } if let Some(LiveEventHost { diff --git a/crates/nostr/src/nips/nip90.rs b/crates/nostr/src/nips/nip90.rs new file mode 100644 index 000000000..42808ead5 --- /dev/null +++ b/crates/nostr/src/nips/nip90.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Distributed under the MIT software license + +//! NIP90 +//! +//! + +use core::fmt; +use core::str::FromStr; + +/// DVM Error +#[derive(Debug)] +pub enum Error { + /// Unknown status + UnknownStatus, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownStatus => write!(f, "Unknown status"), + } + } +} + +/// Data Vending Machine Status +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DataVendingMachineStatus { + /// Service Provider requires payment before continuing + PaymentRequired, + /// Service Provider is processing the job + Processing, + /// Service Provider was unable to process the job + Error, + /// Service Provider successfully processed the job + Success, + /// Service Provider partially processed the job + Partial, +} + +impl fmt::Display for DataVendingMachineStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PaymentRequired => write!(f, "payment-required"), + Self::Processing => write!(f, "processing"), + Self::Error => write!(f, "error"), + Self::Success => write!(f, "success"), + Self::Partial => write!(f, "partial"), + } + } +} + +impl FromStr for DataVendingMachineStatus { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "payment-required" => Ok(Self::PaymentRequired), + "processing" => Ok(Self::Processing), + "error" => Ok(Self::Error), + "success" => Ok(Self::Success), + "partial" => Ok(Self::Partial), + _ => Err(Error::UnknownStatus), + } + } +} diff --git a/crates/nostr/src/prelude.rs b/crates/nostr/src/prelude.rs index c76708880..c7e6b6bb4 100644 --- a/crates/nostr/src/prelude.rs +++ b/crates/nostr/src/prelude.rs @@ -57,5 +57,6 @@ pub use crate::nips::nip53::{self, *}; pub use crate::nips::nip57::{self, *}; pub use crate::nips::nip58::{self, *}; pub use crate::nips::nip65::{self, *}; +pub use crate::nips::nip90::{self, *}; pub use crate::nips::nip94::{self, *}; pub use crate::nips::nip98::{self, *};