diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ca599d2e..c26d6aebb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: -p nostr --no-default-features --features alloc, -p nostr --no-default-features --features "alloc all-nips", -p nostr --features blocking, - -p nostr-database + -p nostr-database, -p nostr-sdk, -p nostr-sdk --no-default-features, -p nostr-sdk --features blocking, diff --git a/crates/nostr-database/src/index.rs b/crates/nostr-database/src/index.rs index 350f190c4..3bc9a407c 100644 --- a/crates/nostr-database/src/index.rs +++ b/crates/nostr-database/src/index.rs @@ -165,7 +165,7 @@ impl DatabaseIndexes { if event.is_replaceable() { let filter: Filter = Filter::new().author(event.pubkey).kind(event.kind); - for ev in self.internal_query(&index, filter).await { + for ev in self.internal_query(&index, filter, false).await { if ev.created_at > event.created_at { should_insert = false; } else if ev.created_at <= event.created_at { @@ -179,7 +179,7 @@ impl DatabaseIndexes { .author(event.pubkey) .kind(event.kind) .identifier(identifier); - for ev in self.internal_query(&index, filter).await { + for ev in self.internal_query(&index, filter, false).await { if ev.created_at >= event.created_at { should_insert = false; } else if ev.created_at < event.created_at { @@ -191,16 +191,30 @@ impl DatabaseIndexes { } } else if event.kind == Kind::EventDeletion { let mut deleted = self.deleted.write().await; - let ids = event.event_ids().copied(); - let filter = Filter::new().ids(ids); let pubkey_prefix: PublicKeyPrefix = PublicKeyPrefix::from(event.pubkey); - for ev in self.internal_query(&index, filter).await { + + // Check `e` tags + let ids = event.event_ids().copied(); + let filter: Filter = Filter::new().ids(ids); + for ev in self.internal_query(&index, filter, false).await { if ev.pubkey == pubkey_prefix { to_discard.insert(ev.event_id); deleted.insert(ev.event_id); } } - // TODO: support event deletion by coordinate (`a` tag) + + // Check `a` tags + for coordinate in event.coordinates() { + let coordinate_pubkey_prefix: PublicKeyPrefix = + PublicKeyPrefix::from(coordinate.pubkey); + if coordinate_pubkey_prefix == pubkey_prefix { + let filter: Filter = coordinate.into(); + for ev in self.internal_query(&index, filter, false).await { + to_discard.insert(ev.event_id); + deleted.insert(ev.event_id); + } + } + } } // Remove events @@ -223,6 +237,7 @@ impl DatabaseIndexes { &self, index: &'a BTreeSet, filter: Filter, + allow_empty_filter: bool, ) -> impl Iterator { let authors: HashSet = filter .authors @@ -230,12 +245,16 @@ impl DatabaseIndexes { .map(|p| PublicKeyPrefix::from(*p)) .collect(); index.iter().filter(move |m| { - (filter.ids.is_empty() || filter.ids.contains(&m.event_id)) - && filter.since.map_or(true, |t| m.created_at >= t) - && filter.until.map_or(true, |t| m.created_at <= t) - && (filter.authors.is_empty() || authors.contains(&m.pubkey)) - && (filter.kinds.is_empty() || filter.kinds.contains(&m.kind)) - && m.filter_tags_match(&filter) + if (filter.is_empty() && allow_empty_filter) || !filter.is_empty() { + (filter.ids.is_empty() || filter.ids.contains(&m.event_id)) + && filter.since.map_or(true, |t| m.created_at >= t) + && filter.until.map_or(true, |t| m.created_at <= t) + && (filter.authors.is_empty() || authors.contains(&m.pubkey)) + && (filter.kinds.is_empty() || filter.kinds.contains(&m.kind)) + && m.filter_tags_match(&filter) + } else { + false + } }) } @@ -257,7 +276,7 @@ impl DatabaseIndexes { } let limit: Option = filter.limit; - let iter = self.internal_query(&index, filter).await; + let iter = self.internal_query(&index, filter, true).await; if let Some(limit) = limit { matching_ids.extend(iter.take(limit)) } else { @@ -280,3 +299,105 @@ impl DatabaseIndexes { index.clear(); } } + +#[cfg(test)] +mod tests { + use nostr::nips::nip01::Coordinate; + use nostr::secp256k1::SecretKey; + use nostr::{EventBuilder, FromBech32, Keys, Tag}; + + use super::*; + + const SECRET_KEY_A: &str = "nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85"; + const SECRET_KEY_B: &str = "nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"; + + #[tokio::test] + async fn test_event_deletion() { + let indexes = DatabaseIndexes::new(); + + // Keys + let keys_a = Keys::new(SecretKey::from_bech32(SECRET_KEY_A).unwrap()); + let keys_b = Keys::new(SecretKey::from_bech32(SECRET_KEY_B).unwrap()); + + // Build some events + let events = [ + EventBuilder::new_text_note("Text note", &[]) + .to_event(&keys_a) + .unwrap(), + EventBuilder::new( + Kind::ParameterizedReplaceable(32121), + "Empty 1", + &[Tag::Identifier(String::from("abdefgh:12345678"))], + ) + .to_event(&keys_a) + .unwrap(), + EventBuilder::new( + Kind::ParameterizedReplaceable(32122), + "Empty 2", + &[Tag::Identifier(String::from("abdefgh:12345678"))], + ) + .to_event(&keys_a) + .unwrap(), + EventBuilder::new( + Kind::ParameterizedReplaceable(32122), + "", + &[Tag::Identifier(String::from("ijklmnop:87654321"))], + ) + .to_event(&keys_a) + .unwrap(), + EventBuilder::new( + Kind::ParameterizedReplaceable(32122), + "", + &[Tag::Identifier(String::from("abdefgh:87654321"))], + ) + .to_event(&keys_b) + .unwrap(), + EventBuilder::new( + Kind::ParameterizedReplaceable(32122), + "", + &[Tag::Identifier(String::from("abdefgh:12345678"))], + ) + .to_event(&keys_b) + .unwrap(), + EventBuilder::new( + Kind::ParameterizedReplaceable(32122), + "Test", + &[Tag::Identifier(String::from("abdefgh:12345678"))], + ) + .to_event(&keys_a) + .unwrap(), + ]; + + for event in events.iter() { + indexes.index_event(event).await; + } + + // Total events: 6 + + // Invalid event deletion (wrong signing keys) + let coordinate = + Coordinate::new(Kind::ParameterizedReplaceable(32122), keys_a.public_key()); + let event = EventBuilder::delete([coordinate]) + .to_event(&keys_b) + .unwrap(); + indexes.index_event(&event).await; + + // Total events: 7 (6 + 1) + + // Delete valid event + let coordinate = + Coordinate::new(Kind::ParameterizedReplaceable(32122), keys_a.public_key()) + .identifier("ijklmnop:87654321"); + let event = EventBuilder::delete([coordinate]) + .to_event(&keys_a) + .unwrap(); + indexes.index_event(&event).await; + + // Total events: 7 (7 - 1 + 1) + + // Check total number of indexes + let filter = Filter::new(); + let res = indexes.query([filter]).await; + assert_eq!(res.len(), 7); + } +} diff --git a/crates/nostr-sdk/README.md b/crates/nostr-sdk/README.md index a08b1a1c5..0a912f5a0 100644 --- a/crates/nostr-sdk/README.md +++ b/crates/nostr-sdk/README.md @@ -124,6 +124,7 @@ The following crate feature flags are available: | Feature | Default | Description | | ------------------- | :-----: | ---------------------------------------------------------------------------------------- | | `blocking` | No | Needed to use `NIP-05` and `NIP-11` features in not async/await context | +| `sqlite` | No | Enable SQLite Storage backend | | `rocksdb` | No | Enable RocksDB Storage backend | | `indexeddb` | No | Enable Web's IndexedDb Storage backend | | `all-nips` | Yes | Enable all NIPs | diff --git a/crates/nostr/README.md b/crates/nostr/README.md index 5c9d55250..0d342e463 100644 --- a/crates/nostr/README.md +++ b/crates/nostr/README.md @@ -136,7 +136,6 @@ The following crate feature flags are available: | ✅ | [30 - Custom Emoji](https://github.com/nostr-protocol/nips/blob/master/30.md) | | ❌ | [31 - Dealing with Unknown Events](https://github.com/nostr-protocol/nips/blob/master/31.md) | | ❌ | [32 - Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md) | -| ✅ | [33 - Parameterized Replaceable Events](https://github.com/nostr-protocol/nips/blob/master/33.md) | | ✅ | [36 - Sensitive Content](https://github.com/nostr-protocol/nips/blob/master/36.md) | | ✅ | [39 - External Identities in Profiles](https://github.com/nostr-protocol/nips/blob/master/39.md) | | ✅ | [40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md) | diff --git a/crates/nostr/src/event/builder.rs b/crates/nostr/src/event/builder.rs index 38513ba81..29ba0da1b 100644 --- a/crates/nostr/src/event/builder.rs +++ b/crates/nostr/src/event/builder.rs @@ -35,6 +35,7 @@ use crate::nips::{nip13, nip58}; use crate::types::time::Instant; use crate::types::time::TimeSupplier; use crate::types::{ChannelId, Contact, Metadata, Timestamp}; +use crate::util::EventIdOrCoordinate; #[cfg(feature = "std")] use crate::SECP256K1; use crate::{JsonUtil, RelayMetadata, UncheckedUrl}; @@ -440,22 +441,27 @@ impl EventBuilder { } /// Create delete event - pub fn delete(ids: I) -> Self + pub fn delete(ids: I) -> Self where - I: IntoIterator, + I: IntoIterator, + T: Into, { Self::delete_with_reason(ids, "") } /// Create delete event with reason - pub fn delete_with_reason(ids: I, reason: S) -> Self + pub fn delete_with_reason(ids: I, reason: S) -> Self where - I: IntoIterator, + I: IntoIterator, + T: Into, S: Into, { let tags: Vec = ids .into_iter() - .map(|id| Tag::Event(id, None, None)) + .map(|t| { + let middle: EventIdOrCoordinate = t.into(); + middle.into() + }) .collect(); Self::new(Kind::EventDeletion, reason.into(), &tags) diff --git a/crates/nostr/src/event/id.rs b/crates/nostr/src/event/id.rs index 5d967207f..6a3c199db 100644 --- a/crates/nostr/src/event/id.rs +++ b/crates/nostr/src/event/id.rs @@ -150,3 +150,9 @@ impl From for String { event_id.to_string() } } + +impl From for Tag { + fn from(id: EventId) -> Self { + Tag::Event(id, None, None) + } +} diff --git a/crates/nostr/src/event/kind.rs b/crates/nostr/src/event/kind.rs index 2c16bb1a3..fb9095b6b 100644 --- a/crates/nostr/src/event/kind.rs +++ b/crates/nostr/src/event/kind.rs @@ -386,4 +386,10 @@ mod tests { assert_ne!(Kind::Authentication, Kind::EncryptedDirectMessage); assert_ne!(Kind::TextNote, Kind::Custom(2)); } + + #[test] + fn test_kind_is_parameterized_replaceable() { + assert!(Kind::ParameterizedReplaceable(32122).is_parameterized_replaceable()); + assert!(!Kind::ParameterizedReplaceable(1).is_parameterized_replaceable()); + } } diff --git a/crates/nostr/src/event/mod.rs b/crates/nostr/src/event/mod.rs index e3c97527d..9b08ad6c3 100644 --- a/crates/nostr/src/event/mod.rs +++ b/crates/nostr/src/event/mod.rs @@ -26,6 +26,7 @@ pub use self::kind::Kind; pub use self::partial::{MissingPartialEvent, PartialEvent}; pub use self::tag::{Marker, Tag, TagIndexValues, TagIndexes, TagKind}; pub use self::unsigned::UnsignedEvent; +use crate::nips::nip01::Coordinate; #[cfg(feature = "std")] use crate::types::time::Instant; use crate::types::time::TimeSupplier; @@ -292,6 +293,24 @@ impl Event { }) } + /// Extract coordinates from tags (`a` tag) + pub fn coordinates(&self) -> impl Iterator + '_ { + self.tags.iter().filter_map(|t| match t { + Tag::A { + kind, + public_key, + identifier, + .. + } => Some(Coordinate { + kind: *kind, + pubkey: *public_key, + identifier: identifier.clone(), + relays: Vec::new(), + }), + _ => None, + }) + } + /// Build tags index pub fn build_tags_index(&self) -> TagIndexes { TagIndexes::from(self.tags.iter().map(|t| t.as_vec())) diff --git a/crates/nostr/src/message/subscription.rs b/crates/nostr/src/message/subscription.rs index 7cc242ecb..6060daba6 100644 --- a/crates/nostr/src/message/subscription.rs +++ b/crates/nostr/src/message/subscription.rs @@ -683,6 +683,11 @@ impl Filter { && self.authors_match(event) && self.tag_match(event) } + + /// Check if [`Filter`] is empty + pub fn is_empty(&self) -> bool { + self == &Filter::default() + } } /// Filters match event trait @@ -980,4 +985,13 @@ mod test { assert!(!filter.match_event(&event)); assert!(!filter.match_event(&event_with_empty_tags)); } + + #[test] + fn test_filter_is_empty() { + let filter = Filter::new().identifier("test"); + assert!(!filter.is_empty()); + + let filter = Filter::new(); + assert!(filter.is_empty()); + } } diff --git a/crates/nostr/src/nips/mod.rs b/crates/nostr/src/nips/mod.rs index c863132cc..7bcc41409 100644 --- a/crates/nostr/src/nips/mod.rs +++ b/crates/nostr/src/nips/mod.rs @@ -5,6 +5,7 @@ //! //! See all at +pub mod nip01; #[cfg(feature = "nip04")] pub mod nip04; #[cfg(all(feature = "std", feature = "nip05"))] @@ -18,6 +19,7 @@ pub mod nip15; pub mod nip19; pub mod nip21; pub mod nip26; +#[deprecated(since = "0.26.0", note = "moved to `nip01`")] pub mod nip33; #[cfg(feature = "nip44")] pub mod nip44; diff --git a/crates/nostr/src/nips/nip01.rs b/crates/nostr/src/nips/nip01.rs new file mode 100644 index 000000000..11fb1b6b9 --- /dev/null +++ b/crates/nostr/src/nips/nip01.rs @@ -0,0 +1,177 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Distributed under the MIT software license + +//! NIP01 +//! +//! + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use bitcoin::bech32::{self, FromBase32, ToBase32, Variant}; +use bitcoin::secp256k1::XOnlyPublicKey; + +use crate::nips::nip19::{ + Error as Bech32Error, FromBech32, ToBech32, AUTHOR, KIND, + PREFIX_BECH32_PARAMETERIZED_REPLACEABLE_EVENT, RELAY, SPECIAL, +}; +use crate::{Filter, Kind, Tag, UncheckedUrl}; + +/// Coordinate for event (`a` tag) +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Coordinate { + /// Kind + pub kind: Kind, + /// Public Key + pub pubkey: XOnlyPublicKey, + /// `d` tag identifier + /// + /// Needed for a parametrized replaceable event. + /// Leave empty for a replaceable event. + pub identifier: String, + /// Relays + pub relays: Vec, +} + +impl Coordinate { + /// Create new event coordinate + pub fn new(kind: Kind, pubkey: XOnlyPublicKey) -> Self { + Self { + kind, + pubkey, + identifier: String::new(), + relays: Vec::new(), + } + } + + /// Set a `d` tag identifier + /// + /// Needed for a parametrized replaceable event. + pub fn identifier(mut self, identifier: S) -> Self + where + S: Into, + { + self.identifier = identifier.into(); + self + } +} + +impl From for Tag { + fn from(value: Coordinate) -> Self { + Self::A { + kind: value.kind, + public_key: value.pubkey, + identifier: value.identifier, + relay_url: value.relays.first().map(UncheckedUrl::from), + } + } +} + +impl From for Filter { + fn from(value: Coordinate) -> Self { + if value.identifier.is_empty() { + Filter::new().kind(value.kind).author(value.pubkey) + } else { + Filter::new() + .kind(value.kind) + .author(value.pubkey) + .identifier(value.identifier) + } + } +} + +impl FromBech32 for Coordinate { + type Err = Bech32Error; + fn from_bech32(s: S) -> Result + where + S: Into, + { + let (hrp, data, checksum) = bech32::decode(&s.into())?; + + if hrp != PREFIX_BECH32_PARAMETERIZED_REPLACEABLE_EVENT || checksum != Variant::Bech32 { + return Err(Bech32Error::WrongPrefixOrVariant); + } + + let mut data: Vec = Vec::from_base32(&data)?; + + let mut identifier: Option = None; + let mut pubkey: Option = None; + let mut kind: Option = None; + let mut relays: Vec = Vec::new(); + + while !data.is_empty() { + let t = data.first().ok_or(Bech32Error::TLV)?; + let l = data.get(1).ok_or(Bech32Error::TLV)?; + let l = *l as usize; + + let bytes = data.get(2..l + 2).ok_or(Bech32Error::TLV)?; + + match *t { + SPECIAL => { + if identifier.is_none() { + identifier = Some(String::from_utf8(bytes.to_vec())?); + } + } + RELAY => { + relays.push(String::from_utf8(bytes.to_vec())?); + } + AUTHOR => { + if pubkey.is_none() { + pubkey = Some(XOnlyPublicKey::from_slice(bytes)?); + } + } + KIND => { + if kind.is_none() { + let k: u64 = u32::from_be_bytes( + bytes.try_into().map_err(|_| Bech32Error::TryFromSlice)?, + ) as u64; + kind = Some(Kind::from(k)); + } + } + _ => (), + }; + + data.drain(..l + 2); + } + + Ok(Self { + kind: kind.ok_or_else(|| Bech32Error::FieldMissing("kind".to_string()))?, + pubkey: pubkey.ok_or_else(|| Bech32Error::FieldMissing("pubkey".to_string()))?, + identifier: identifier + .ok_or_else(|| Bech32Error::FieldMissing("identifier".to_string()))?, + relays, + }) + } +} + +impl ToBech32 for Coordinate { + type Err = Bech32Error; + + fn to_bech32(&self) -> Result { + let mut bytes: Vec = Vec::new(); + + // Identifier + bytes.extend([SPECIAL, self.identifier.len() as u8]); + bytes.extend(self.identifier.as_bytes()); + + for relay in self.relays.iter() { + bytes.extend([RELAY, relay.len() as u8]); + bytes.extend(relay.as_bytes()); + } + + // Author + bytes.extend([AUTHOR, 32]); + bytes.extend(self.pubkey.serialize()); + + // Kind + bytes.extend([KIND, 4]); + bytes.extend(self.kind.as_u32().to_be_bytes()); + + let data = bytes.to_base32(); + Ok(bech32::encode( + PREFIX_BECH32_PARAMETERIZED_REPLACEABLE_EVENT, + data, + Variant::Bech32, + )?) + } +} diff --git a/crates/nostr/src/nips/nip21.rs b/crates/nostr/src/nips/nip21.rs index 971ccc9e0..5b52fe3ed 100644 --- a/crates/nostr/src/nips/nip21.rs +++ b/crates/nostr/src/nips/nip21.rs @@ -11,8 +11,8 @@ use core::fmt; use bitcoin::secp256k1::XOnlyPublicKey; +use super::nip01::Coordinate; use super::nip19::{Error as NIP19Error, FromBech32, Nip19Event, ToBech32}; -use super::nip33::ParameterizedReplaceableEvent; use crate::event::id::EventId; use crate::types::profile::Profile; @@ -73,7 +73,7 @@ impl NostrURI for XOnlyPublicKey {} impl NostrURI for EventId {} impl NostrURI for Profile {} impl NostrURI for Nip19Event {} -impl NostrURI for ParameterizedReplaceableEvent {} +impl NostrURI for Coordinate {} #[cfg(test)] mod tests { diff --git a/crates/nostr/src/nips/nip33.rs b/crates/nostr/src/nips/nip33.rs index 79a5fa85d..bfa73e8e7 100644 --- a/crates/nostr/src/nips/nip33.rs +++ b/crates/nostr/src/nips/nip33.rs @@ -5,134 +5,21 @@ //! //! -use alloc::string::{String, ToString}; -use alloc::vec::Vec; +#![allow(missing_docs)] -use bitcoin::bech32::{self, FromBase32, ToBase32, Variant}; -use bitcoin::secp256k1::XOnlyPublicKey; +use core::ops::Deref; -use crate::nips::nip19::{ - Error as Bech32Error, FromBech32, ToBech32, AUTHOR, KIND, - PREFIX_BECH32_PARAMETERIZED_REPLACEABLE_EVENT, RELAY, SPECIAL, -}; -use crate::{Kind, Tag, UncheckedUrl}; +pub use super::nip01::Coordinate; -/// Parameterized Replaceable Event -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[deprecated(since = "0.26.0", note = "use `Coordinate` instead")] pub struct ParameterizedReplaceableEvent { - /// Kind - pub kind: Kind, - /// Public Key - pub pubkey: XOnlyPublicKey, - /// `d` tag identifier - pub identifier: String, - /// Relays - pub relays: Vec, + inner: Coordinate, } -impl From for Tag { - fn from(value: ParameterizedReplaceableEvent) -> Self { - Self::A { - kind: value.kind, - public_key: value.pubkey, - identifier: value.identifier, - relay_url: value.relays.first().map(UncheckedUrl::from), - } - } -} - -impl FromBech32 for ParameterizedReplaceableEvent { - type Err = Bech32Error; - fn from_bech32(s: S) -> Result - where - S: Into, - { - let (hrp, data, checksum) = bech32::decode(&s.into())?; - - if hrp != PREFIX_BECH32_PARAMETERIZED_REPLACEABLE_EVENT || checksum != Variant::Bech32 { - return Err(Bech32Error::WrongPrefixOrVariant); - } - - let mut data: Vec = Vec::from_base32(&data)?; - - let mut identifier: Option = None; - let mut pubkey: Option = None; - let mut kind: Option = None; - let mut relays: Vec = Vec::new(); - - while !data.is_empty() { - let t = data.first().ok_or(Bech32Error::TLV)?; - let l = data.get(1).ok_or(Bech32Error::TLV)?; - let l = *l as usize; - - let bytes = data.get(2..l + 2).ok_or(Bech32Error::TLV)?; - - match *t { - SPECIAL => { - if identifier.is_none() { - identifier = Some(String::from_utf8(bytes.to_vec())?); - } - } - RELAY => { - relays.push(String::from_utf8(bytes.to_vec())?); - } - AUTHOR => { - if pubkey.is_none() { - pubkey = Some(XOnlyPublicKey::from_slice(bytes)?); - } - } - KIND => { - if kind.is_none() { - let k: u64 = u32::from_be_bytes( - bytes.try_into().map_err(|_| Bech32Error::TryFromSlice)?, - ) as u64; - kind = Some(Kind::from(k)); - } - } - _ => (), - }; - - data.drain(..l + 2); - } - - Ok(Self { - kind: kind.ok_or_else(|| Bech32Error::FieldMissing("kind".to_string()))?, - pubkey: pubkey.ok_or_else(|| Bech32Error::FieldMissing("pubkey".to_string()))?, - identifier: identifier - .ok_or_else(|| Bech32Error::FieldMissing("identifier".to_string()))?, - relays, - }) - } -} - -impl ToBech32 for ParameterizedReplaceableEvent { - type Err = Bech32Error; - - fn to_bech32(&self) -> Result { - let mut bytes: Vec = Vec::new(); - - // Identifier - bytes.extend([SPECIAL, self.identifier.len() as u8]); - bytes.extend(self.identifier.as_bytes()); - - for relay in self.relays.iter() { - bytes.extend([RELAY, relay.len() as u8]); - bytes.extend(relay.as_bytes()); - } - - // Author - bytes.extend([AUTHOR, 32]); - bytes.extend(self.pubkey.serialize()); - - // Kind - bytes.extend([KIND, 4]); - bytes.extend(self.kind.as_u32().to_be_bytes()); - - let data = bytes.to_base32(); - Ok(bech32::encode( - PREFIX_BECH32_PARAMETERIZED_REPLACEABLE_EVENT, - data, - Variant::Bech32, - )?) +#[allow(deprecated)] +impl Deref for ParameterizedReplaceableEvent { + type Target = Coordinate; + fn deref(&self) -> &Self::Target { + &self.inner } } diff --git a/crates/nostr/src/nips/nip57.rs b/crates/nostr/src/nips/nip57.rs index c143b7500..4144dc405 100644 --- a/crates/nostr/src/nips/nip57.rs +++ b/crates/nostr/src/nips/nip57.rs @@ -10,7 +10,7 @@ use alloc::vec::Vec; use bitcoin::secp256k1::XOnlyPublicKey; -use super::nip33::ParameterizedReplaceableEvent; +use super::nip01::Coordinate; use crate::{EventId, UncheckedUrl}; /// Zap Request Data @@ -27,7 +27,7 @@ pub struct ZapRequestData { /// Event ID pub event_id: Option, /// NIP-33 event coordinate that allows tipping parameterized replaceable events such as NIP-23 long-form notes. - pub event_coordinate: Option, + pub event_coordinate: Option, } impl ZapRequestData { @@ -71,7 +71,7 @@ impl ZapRequestData { } /// NIP-33 event coordinate that allows tipping parameterized replaceable events such as NIP-23 long-form notes. - pub fn event_coordinate(self, event_coordinate: ParameterizedReplaceableEvent) -> Self { + pub fn event_coordinate(self, event_coordinate: Coordinate) -> Self { Self { event_coordinate: Some(event_coordinate), ..self diff --git a/crates/nostr/src/prelude.rs b/crates/nostr/src/prelude.rs index c7e6b6bb4..3348c9f73 100644 --- a/crates/nostr/src/prelude.rs +++ b/crates/nostr/src/prelude.rs @@ -32,6 +32,7 @@ pub use crate::util::*; pub use crate::{Result, SECP256K1}; // NIPs +pub use crate::nips::nip01::{self, *}; #[cfg(feature = "nip04")] pub use crate::nips::nip04::{self, *}; #[cfg(all(feature = "std", feature = "nip05"))] @@ -45,6 +46,7 @@ pub use crate::nips::nip15::{self, *}; pub use crate::nips::nip19::{self, *}; pub use crate::nips::nip21::{self, *}; pub use crate::nips::nip26::{self, *}; +#[allow(deprecated)] pub use crate::nips::nip33::{self, *}; #[cfg(feature = "nip44")] pub use crate::nips::nip44::{self, *}; diff --git a/crates/nostr/src/util.rs b/crates/nostr/src/util.rs index ce04deca7..00b061d5d 100644 --- a/crates/nostr/src/util.rs +++ b/crates/nostr/src/util.rs @@ -14,6 +14,9 @@ use once_cell::sync::Lazy; use serde::de::DeserializeOwned; use serde::Serialize; +use crate::nips::nip01::Coordinate; +use crate::{EventId, Tag}; + /// Generate shared key /// /// **Important: use of a strong cryptographic hash function may be critical to security! Do NOT use @@ -63,3 +66,32 @@ where serde_json::json!(self).to_string() } } + +/// Event ID or Coordinate +pub enum EventIdOrCoordinate { + /// Event ID + Id(EventId), + /// Event Coordinate (`a` tag) + Coordinate(Coordinate), +} + +impl From for Tag { + fn from(value: EventIdOrCoordinate) -> Self { + match value { + EventIdOrCoordinate::Id(id) => id.into(), + EventIdOrCoordinate::Coordinate(a) => a.into(), + } + } +} + +impl From for EventIdOrCoordinate { + fn from(id: EventId) -> Self { + Self::Id(id) + } +} + +impl From for EventIdOrCoordinate { + fn from(coordinate: Coordinate) -> Self { + Self::Coordinate(coordinate) + } +}