lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit c9c069d726ab62b39ae1c15e2a8c17a2120ec8c3
parent 0d77b3cc6925f76f51da83ce18c04846ee966ef7
Author: triesap <tyson@radroots.org>
Date:   Wed, 24 Dec 2025 14:50:06 +0000

nostr: refactor `radroots-identity` integration and client `radroots-types`



- Replace identity spec system with RadrootsIdentity loader/saver (json/text/binary) and no_std support
- Rename nostr feature sdk->client, add RadrootsNostrClient wrapper and unified type aliases/prelude exports
- Update `radroots-net-core` nostr client and key management to use radroots_nostr prelude types and helpers
- Extend runtime with backoff + cli init helpers and regenerate TS bindings for new listing/trade types

Diffstat:
MCargo.lock | 7+++----
Mevents/bindings/ts/package.json | 2+-
Mevents/bindings/ts/src/types.ts | 10++++++++--
Midentity/Cargo.toml | 9++++++---
Midentity/src/error.rs | 33++++++++++++++++++++++++++-------
Aidentity/src/identity.rs | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Midentity/src/lib.rs | 15+++++++++++----
Didentity/src/spec.rs | 96-------------------------------------------------------------------------------
Aidentity/tests/identity.rs | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnet-core/Cargo.toml | 6+-----
Mnet-core/src/keys.rs | 34+++++++++++++++++++---------------
Mnet-core/src/nostr_client/events/post.rs | 16++++++++++------
Mnet-core/src/nostr_client/events/profile.rs | 21+++++++++++++--------
Mnet-core/src/nostr_client/inner.rs | 18++++++++++++------
Mnet-core/src/nostr_client/manager.rs | 23++++++++++++++++-------
Mnet-core/src/nostr_client/status.rs | 11+++++++----
Mnostr/Cargo.toml | 5+++--
Mnostr/src/client.rs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mnostr/src/codec_adapters.rs | 28++++++++++++++--------------
Mnostr/src/error.rs | 8++++----
Mnostr/src/event_adapters.rs | 8++++----
Mnostr/src/event_convert.rs | 6+++---
Mnostr/src/events/jobs.rs | 31+++++++++++++++----------------
Mnostr/src/events/metadata.rs | 52+++++++++++++++++++++++++++++++++-------------------
Mnostr/src/events/mod.rs | 25+++++++++++++++++--------
Mnostr/src/events/post.rs | 58++++++++++++++++++++++++++++++++--------------------------
Mnostr/src/filter.rs | 14+++++++-------
Mnostr/src/job_adapter.rs | 18+++++++++---------
Mnostr/src/lib.rs | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mnostr/src/parse.rs | 14++++++++------
Mnostr/src/relays.rs | 16+++++++++++-----
Mnostr/src/tags.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Anostr/src/types.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Mnostr/src/util.rs | 13+++++++++----
Aruntime/src/backoff.rs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mruntime/src/cli.rs | 36++++++++++++++++++++++++++++++++++++
Mruntime/src/lib.rs | 5+++++
Mtrade/Cargo.toml | 2+-
Mtrade/bindings/ts/package.json | 5+++--
Mtrade/bindings/ts/src/types.ts | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtrade/src/listing/validation.rs | 70++++++++++++++++++++++++++++++++++++++++++++++------------------------
41 files changed, 1220 insertions(+), 380 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1737,9 +1737,10 @@ dependencies = [ "nostr", "radroots-runtime", "serde", + "serde_json", + "tempfile", "thiserror 1.0.69", "tracing", - "uuid", ] [[package]] @@ -1766,8 +1767,6 @@ dependencies = [ "directories", "futures", "hex", - "nostr", - "nostr-sdk", "radroots-events", "radroots-log", "radroots-nostr", @@ -1790,6 +1789,7 @@ dependencies = [ "nostr-sdk", "radroots-events", "radroots-events-codec", + "radroots-identity", "reqwest", "serde", "serde_json", @@ -2964,7 +2964,6 @@ dependencies = [ "getrandom 0.3.3", "js-sys", "rand 0.9.2", - "serde", "wasm-bindgen", ] diff --git a/events/bindings/ts/package.json b/events/bindings/ts/package.json @@ -23,7 +23,7 @@ "build:cjs": "tsc -p tsconfig.cjs.json", "build": "npm run build:esm && npm run build:cjs", "prebuild": "npm run clean && npm run prepend-imports", - "prepend-imports": "bash -c 'f=./src/types.ts; line=\"import type { RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice } from \\\"@radroots/core-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")'", + "prepend-imports": "bash -c 'f=./src/types.ts; line=\"import type { RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice } from \\\"@radroots/core-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")'", "clean": "rimraf dist", "dev": "npm run watch", "watch": "tsc -w" diff --git a/events/bindings/ts/src/types.ts b/events/bindings/ts/src/types.ts @@ -1,4 +1,4 @@ -import type { RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice } from "@radroots/core-bindings"; +import type { RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice } from "@radroots/core-bindings"; // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. @@ -44,7 +44,11 @@ export type RadrootsJobResultEventIndex = { event: RadrootsNostrEvent, metadata: export type RadrootsJobResultEventMetadata = { id: string, author: string, published_at: number, kind: number, job_result: RadrootsJobResult, }; -export type RadrootsListing = { d_tag: string, product: RadrootsListingProduct, quantities: Array<RadrootsListingQuantity>, prices: RadrootsCoreQuantityPrice[], discounts?: RadrootsListingDiscount[] | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, }; +export type RadrootsListing = { d_tag: string, product: RadrootsListingProduct, quantities: Array<RadrootsListingQuantity>, prices: RadrootsCoreQuantityPrice[], discounts?: RadrootsListingDiscount[] | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, }; + +export type RadrootsListingAvailability = { "kind": "window", "amount": { start?: number | null, end?: number | null, } } | { "kind": "status", "amount": { status: RadrootsListingStatus, } }; + +export type RadrootsListingDeliveryMethod = { "kind": "pickup" } | { "kind": "local_delivery" } | { "kind": "shipping" } | { "kind": "other", "amount": { method: string, } }; export type RadrootsListingDiscount = { "kind": "quantity", "amount": { ref_quantity: string, threshold: RadrootsCoreQuantity, value: RadrootsCoreMoney, } } | { "kind": "mass", "amount": { threshold: RadrootsCoreQuantity, value: RadrootsCoreMoney, } } | { "kind": "subtotal", "amount": { threshold: RadrootsCoreMoney, value: RadrootsCoreDiscountValue, } } | { "kind": "total", "amount": { total_min: RadrootsCoreMoney, value: RadrootsCorePercent, } }; @@ -62,6 +66,8 @@ export type RadrootsListingProduct = { key: string, title: string, category: str export type RadrootsListingQuantity = { value: RadrootsCoreQuantity, label?: string | null, count?: number | null, }; +export type RadrootsListingStatus = { "kind": "active" } | { "kind": "sold" } | { "kind": "other", "amount": { value: string, } }; + export type RadrootsNostrEvent = { id: string, author: string, created_at: number, kind: number, tags: Array<Array<string>>, content: string, sig: string, }; export type RadrootsNostrEventPtr = { id: string, relays?: string | null, }; diff --git a/identity/Cargo.toml b/identity/Cargo.toml @@ -8,12 +8,15 @@ license.workspace = true [features] default = ["std"] -std = [] +std = ["dep:radroots-runtime"] [dependencies] -radroots-runtime = { workspace = true } +radroots-runtime = { workspace = true, optional = true } nostr = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } thiserror = { workspace = true } -uuid = { workspace = true, features = ["v4", "serde"] } tracing = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/identity/src/error.rs b/identity/src/error.rs @@ -1,18 +1,37 @@ -use radroots_runtime::RuntimeJsonError; -use std::path::PathBuf; use thiserror::Error; +#[cfg(feature = "std")] +use radroots_runtime::RuntimeJsonError; +#[cfg(feature = "std")] +use std::{io, path::PathBuf}; + #[derive(Debug, Error)] pub enum IdentityError { - #[error(transparent)] - Store(#[from] RuntimeJsonError), - - #[error("invalid identity: {0}")] - Invalid(#[source] Box<dyn std::error::Error + Send + Sync>), + #[cfg(feature = "std")] + #[error("identity file missing at {0}")] + NotFound(PathBuf), + #[cfg(feature = "std")] #[error( "identity file missing at {0} and generation is not permitted \ (pass --allow-generate-identity)" )] GenerationNotAllowed(PathBuf), + + #[cfg(feature = "std")] + #[error("failed to read identity file at {0}: {1}")] + Read(PathBuf, #[source] io::Error), + + #[error("invalid identity JSON: {0}")] + InvalidJson(#[from] serde_json::Error), + + #[error("invalid secret key: {0}")] + InvalidSecretKey(#[from] nostr::key::Error), + + #[error("unsupported identity file format")] + InvalidIdentityFormat, + + #[cfg(feature = "std")] + #[error(transparent)] + Store(#[from] RuntimeJsonError), } diff --git a/identity/src/identity.rs b/identity/src/identity.rs @@ -0,0 +1,280 @@ +use crate::error::IdentityError; +use core::convert::Infallible; +use nostr::{Keys, SecretKey}; +use serde::{Deserialize, Serialize}; + +#[cfg(not(feature = "std"))] +use alloc::string::String; +#[cfg(feature = "std")] +use radroots_runtime::JsonFile; +#[cfg(feature = "std")] +use std::{ + fs, + path::{Path, PathBuf}, +}; + +pub const DEFAULT_IDENTITY_PATH: &str = "identity.json"; + +#[derive(Debug, Clone)] +pub struct RadrootsIdentity { + keys: Keys, + profile: Option<RadrootsIdentityProfile>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RadrootsIdentityProfile { + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option<nostr::Event>, + #[serde(skip_serializing_if = "Option::is_none")] + pub application_handler: Option<nostr::Event>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RadrootsIdentityFile { + pub secret_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option<nostr::Event>, + #[serde(skip_serializing_if = "Option::is_none")] + pub application_handler: Option<nostr::Event>, +} + +#[derive(Debug, Clone, Copy)] +pub enum RadrootsIdentitySecretKeyFormat { + Hex, + Nsec, +} + +impl RadrootsIdentityProfile { + pub fn is_empty(&self) -> bool { + self.identifier.is_none() && self.metadata.is_none() && self.application_handler.is_none() + } +} + +impl RadrootsIdentity { + pub fn new(keys: Keys) -> Self { + Self { + keys, + profile: None, + } + } + + pub fn with_profile(keys: Keys, profile: RadrootsIdentityProfile) -> Self { + let profile = if profile.is_empty() { None } else { Some(profile) }; + Self { keys, profile } + } + + #[cfg(feature = "std")] + pub fn generate() -> Self { + Self::new(Keys::generate()) + } + + #[cfg(feature = "std")] + pub fn generate_with_profile(profile: RadrootsIdentityProfile) -> Self { + Self::with_profile(Keys::generate(), profile) + } + + pub fn keys(&self) -> &Keys { + &self.keys + } + + pub fn into_keys(self) -> Keys { + self.keys + } + + pub fn public_key(&self) -> nostr::PublicKey { + self.keys.public_key() + } + + pub fn public_key_hex(&self) -> String { + self.keys.public_key().to_hex() + } + + pub fn public_key_npub(&self) -> String { + use nostr::nips::nip19::ToBech32; + infallible_to_string(self.keys.public_key().to_bech32()) + } + + pub fn npub(&self) -> String { + self.public_key_npub() + } + + pub fn secret_key_hex(&self) -> String { + self.keys.secret_key().to_secret_hex() + } + + pub fn secret_key_nsec(&self) -> String { + use nostr::nips::nip19::ToBech32; + infallible_to_string(self.keys.secret_key().to_bech32()) + } + + pub fn nsec(&self) -> String { + self.secret_key_nsec() + } + + pub fn secret_key_bytes(&self) -> [u8; SecretKey::LEN] { + self.keys.secret_key().to_secret_bytes() + } + + pub fn profile(&self) -> Option<&RadrootsIdentityProfile> { + self.profile.as_ref() + } + + pub fn profile_mut(&mut self) -> Option<&mut RadrootsIdentityProfile> { + self.profile.as_mut() + } + + pub fn set_profile(&mut self, profile: RadrootsIdentityProfile) { + self.profile = if profile.is_empty() { None } else { Some(profile) }; + } + + pub fn clear_profile(&mut self) { + self.profile = None; + } + + pub fn to_file(&self) -> RadrootsIdentityFile { + self.to_file_with_secret_format(RadrootsIdentitySecretKeyFormat::Hex) + } + + pub fn to_file_with_secret_format( + &self, + format: RadrootsIdentitySecretKeyFormat, + ) -> RadrootsIdentityFile { + let secret_key = match format { + RadrootsIdentitySecretKeyFormat::Hex => self.secret_key_hex(), + RadrootsIdentitySecretKeyFormat::Nsec => self.secret_key_nsec(), + }; + let (identifier, metadata, application_handler) = match &self.profile { + Some(profile) => ( + profile.identifier.clone(), + profile.metadata.clone(), + profile.application_handler.clone(), + ), + None => (None, None, None), + }; + RadrootsIdentityFile { + secret_key, + identifier, + metadata, + application_handler, + } + } + + #[cfg(feature = "std")] + pub fn from_file(file: RadrootsIdentityFile) -> Result<Self, IdentityError> { + Self::try_from(file) + } + + #[cfg(feature = "std")] + pub fn from_secret_key_str(secret_key: &str) -> Result<Self, IdentityError> { + Ok(Self::new(Keys::parse(secret_key)?)) + } + + #[cfg(feature = "std")] + pub fn from_secret_key_bytes(secret_key: &[u8]) -> Result<Self, IdentityError> { + if secret_key.len() != SecretKey::LEN { + return Err(IdentityError::InvalidIdentityFormat); + } + let secret_key = SecretKey::from_slice(secret_key)?; + Ok(Self::new(Keys::new(secret_key))) + } + + #[cfg(feature = "std")] + pub fn load_from_path_auto(path: impl AsRef<Path>) -> Result<Self, IdentityError> { + let path = path.as_ref(); + let bytes = read_identity_bytes(path)?; + parse_identity_bytes(&bytes) + } + + #[cfg(feature = "std")] + pub fn load_or_generate<P: AsRef<Path>>( + path: Option<P>, + allow_generate: bool, + ) -> Result<Self, IdentityError> { + let path = path + .map(|p| p.as_ref().to_path_buf()) + .unwrap_or_else(|| PathBuf::from(DEFAULT_IDENTITY_PATH)); + if path.exists() { + return Self::load_from_path_auto(&path); + } + if !allow_generate { + return Err(IdentityError::GenerationNotAllowed(path)); + } + let identity = Self::generate(); + identity.save_json(&path)?; + Ok(identity) + } + + #[cfg(feature = "std")] + pub fn save_json(&self, path: impl AsRef<Path>) -> Result<(), IdentityError> { + let payload = self.to_file(); + let mut store = JsonFile::load_or_create_with(path.as_ref(), || payload.clone())?; + store.value = payload; + store.save()?; + Ok(()) + } +} + +#[cfg(feature = "std")] +impl TryFrom<RadrootsIdentityFile> for RadrootsIdentity { + type Error = IdentityError; + + fn try_from(file: RadrootsIdentityFile) -> Result<Self, Self::Error> { + let keys = Keys::parse(&file.secret_key)?; + let profile = RadrootsIdentityProfile { + identifier: file.identifier, + metadata: file.metadata, + application_handler: file.application_handler, + }; + if profile.is_empty() { + Ok(Self::new(keys)) + } else { + Ok(Self::with_profile(keys, profile)) + } + } +} + +impl From<Keys> for RadrootsIdentity { + fn from(keys: Keys) -> Self { + Self::new(keys) + } +} + +#[cfg(feature = "std")] +fn read_identity_bytes(path: &Path) -> Result<Vec<u8>, IdentityError> { + match fs::read(path) { + Ok(bytes) => Ok(bytes), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + Err(IdentityError::NotFound(path.to_path_buf())) + } + Err(err) => Err(IdentityError::Read(path.to_path_buf(), err)), + } +} + +#[cfg(feature = "std")] +fn parse_identity_bytes(bytes: &[u8]) -> Result<RadrootsIdentity, IdentityError> { + if bytes.len() == SecretKey::LEN { + return RadrootsIdentity::from_secret_key_bytes(bytes); + } + + let text = std::str::from_utf8(bytes).map_err(|_| IdentityError::InvalidIdentityFormat)?; + let trimmed = text.trim(); + if trimmed.is_empty() { + return Err(IdentityError::InvalidIdentityFormat); + } + if trimmed.starts_with('{') { + let file: RadrootsIdentityFile = serde_json::from_str(trimmed)?; + return RadrootsIdentity::from_file(file); + } + RadrootsIdentity::from_secret_key_str(trimmed) +} + +fn infallible_to_string(value: Result<String, Infallible>) -> String { + match value { + Ok(value) => value, + Err(err) => match err {}, + } +} diff --git a/identity/src/lib.rs b/identity/src/lib.rs @@ -1,7 +1,14 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + pub mod error; -pub mod spec; +pub mod identity; pub use error::IdentityError; -pub use spec::{ExtendedIdentity, IdentitySpec, MinimalIdentity, load_or_generate, to_keys}; - -pub const DEFAULT_IDENTITY_PATH: &str = "identity.json"; +pub use identity::{ + RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityProfile, + RadrootsIdentitySecretKeyFormat, DEFAULT_IDENTITY_PATH, +}; diff --git a/identity/src/spec.rs b/identity/src/spec.rs @@ -1,96 +0,0 @@ -use crate::{DEFAULT_IDENTITY_PATH, error::IdentityError}; -use radroots_runtime::JsonFile; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; -use uuid::Uuid; - -pub trait IdentitySpec: Serialize + DeserializeOwned + Sized { - type Keys; - - type ParseError: std::error::Error + Send + Sync + 'static; - - fn generate_new() -> Self; - - fn to_keys(&self) -> Result<Self::Keys, Self::ParseError>; -} - -pub fn to_keys<I: IdentitySpec>(id: &I) -> Result<I::Keys, IdentityError> { - id.to_keys() - .map_err(|e| IdentityError::Invalid(Box::new(e))) -} - -pub fn load_or_generate<I, P>( - path: Option<P>, - allow_generate: bool, -) -> Result<JsonFile<I>, IdentityError> -where - I: IdentitySpec + Serialize + for<'de> Deserialize<'de>, - P: AsRef<Path>, -{ - let p = path - .map(|p| p.as_ref().to_path_buf()) - .unwrap_or_else(|| PathBuf::from(DEFAULT_IDENTITY_PATH)); - - if p.exists() { - let store = JsonFile::load(&p)?; - return Ok(store); - } - - if !allow_generate { - return Err(IdentityError::GenerationNotAllowed(p)); - } - - let store = JsonFile::load_or_create_with(&p, I::generate_new)?; - Ok(store) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MinimalIdentity { - pub key: String, -} - -impl IdentitySpec for MinimalIdentity { - type Keys = nostr::Keys; - type ParseError = nostr::key::Error; - - fn generate_new() -> Self { - let keys = nostr::Keys::generate(); - Self { - key: keys.secret_key().to_secret_hex(), - } - } - - fn to_keys(&self) -> Result<Self::Keys, Self::ParseError> { - nostr::Keys::from_str(&self.key) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ExtendedIdentity { - pub key: String, - pub identifier: String, - pub metadata: Option<nostr::Event>, - pub application_handler: Option<nostr::Event>, -} - -impl IdentitySpec for ExtendedIdentity { - type Keys = nostr::Keys; - type ParseError = nostr::key::Error; - - fn generate_new() -> Self { - let keys = nostr::Keys::generate(); - Self { - key: keys.secret_key().to_secret_hex(), - identifier: Uuid::new_v4().to_string(), - metadata: None, - application_handler: None, - } - } - - fn to_keys(&self) -> Result<Self::Keys, Self::ParseError> { - nostr::Keys::from_str(&self.key) - } -} diff --git a/identity/tests/identity.rs b/identity/tests/identity.rs @@ -0,0 +1,78 @@ +use radroots_identity::{IdentityError, RadrootsIdentity}; + +#[test] +fn load_from_json_file_hex() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + let json = serde_json::to_string(&identity.to_file()).unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + std::fs::write(&path, json).unwrap(); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + assert_eq!(loaded.public_key(), keys.public_key()); +} + +#[test] +fn load_from_text_file_hex() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + let secret = identity.secret_key_hex(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.txt"); + std::fs::write(&path, secret).unwrap(); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + assert_eq!(loaded.public_key(), keys.public_key()); +} + +#[test] +fn load_from_text_file_nsec() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + let secret = identity.secret_key_nsec(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.txt"); + std::fs::write(&path, secret).unwrap(); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + assert_eq!(loaded.public_key(), keys.public_key()); +} + +#[test] +fn load_from_binary_file() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + let secret = identity.secret_key_bytes(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.key"); + std::fs::write(&path, secret).unwrap(); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + assert_eq!(loaded.public_key(), keys.public_key()); +} + +#[test] +fn load_or_generate_missing_disallowed() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + + let err = RadrootsIdentity::load_or_generate(Some(&path), false).unwrap_err(); + assert!(matches!(err, IdentityError::GenerationNotAllowed(p) if p == path)); +} + +#[test] +fn load_or_generate_missing_allowed_creates_json() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + + let identity = RadrootsIdentity::load_or_generate(Some(&path), true).unwrap(); + assert!(path.exists()); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + assert_eq!(loaded.public_key(), identity.public_key()); +} diff --git a/net-core/Cargo.toml b/net-core/Cargo.toml @@ -14,8 +14,6 @@ nostr-client = [ "std", "dep:radroots-events", "radroots-events/serde", - "dep:nostr", - "dep:nostr-sdk", "dep:secrecy", "dep:hex", "dep:tempfile", @@ -28,11 +26,9 @@ fs-persistence = ["std"] [dependencies] radroots-events = { workspace = true, optional = true, default-features = true, features = ["std", "serde", "typeshare"] } radroots-log = { workspace = true } -radroots-nostr = { workspace = true, optional = true, default-features = true, features = ["sdk", "events"] } +radroots-nostr = { workspace = true, optional = true, default-features = true, features = ["client", "events"] } directories = { workspace = true, optional = true } hex = { workspace = true, optional = true } -nostr = { workspace = true, optional = true } -nostr-sdk = { workspace = true, optional = true } secrecy = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } diff --git a/net-core/src/keys.rs b/net-core/src/keys.rs @@ -3,6 +3,13 @@ use crate::config::{KeyFormat, KeyPersistenceConfig}; #[cfg(feature = "nostr-client")] use crate::error::{NetError, Result}; #[cfg(feature = "nostr-client")] +use radroots_nostr::prelude::{ + RadrootsNostrKeys, + RadrootsNostrSecretKey, + RadrootsNostrSecp256k1SecretKey, + RadrootsNostrToBech32, +}; +#[cfg(feature = "nostr-client")] use serde::Deserialize; #[cfg(feature = "nostr-client")] use std::path::{Path, PathBuf}; @@ -34,7 +41,7 @@ pub struct KeysState { #[cfg(feature = "nostr-client")] #[derive(Debug, Clone, Default)] pub struct KeysManager { - pub keys: Option<nostr::Keys>, + pub keys: Option<RadrootsNostrKeys>, pub state: KeysState, } @@ -60,10 +67,10 @@ impl KeysManager { } pub fn load_from_secret_bytes(&mut self, sk: &[u8; 32]) -> Result<()> { - use nostr::secp256k1::SecretKey as SecpSecret; - let secp = SecpSecret::from_slice(&sk[..]).map_err(|_| NetError::InvalidHex32)?; - let nostr_sk = nostr::SecretKey::from(secp); - let keys = nostr::Keys::new(nostr_sk); + let secp = + RadrootsNostrSecp256k1SecretKey::from_slice(&sk[..]).map_err(|_| NetError::InvalidHex32)?; + let nostr_sk = RadrootsNostrSecretKey::from(secp); + let keys = RadrootsNostrKeys::new(nostr_sk); self.set_keys(keys); Ok(()) } @@ -71,9 +78,9 @@ impl KeysManager { pub fn load_from_hex32(&mut self, hex: &str) -> Result<()> { use secrecy::{ExposeSecret, SecretString}; let secret = SecretString::new(hex.to_owned().into()); - let k = nostr::SecretKey::from_str(secret.expose_secret()) + let k = RadrootsNostrSecretKey::from_str(secret.expose_secret()) .map_err(|_| NetError::InvalidHex32)?; - let keys = nostr::Keys::new(k); + let keys = RadrootsNostrKeys::new(k); self.set_keys(keys); Ok(()) } @@ -82,14 +89,12 @@ impl KeysManager { use secrecy::{ExposeSecret, SecretString}; let secret = SecretString::new(nsec.to_owned().into()); let keys = - nostr::Keys::parse(secret.expose_secret()).map_err(|_| NetError::InvalidBech32)?; + RadrootsNostrKeys::parse(secret.expose_secret()).map_err(|_| NetError::InvalidBech32)?; self.set_keys(keys); Ok(()) } - pub fn set_keys(&mut self, keys: nostr::Keys) { - use nostr::nips::nip19::ToBech32; - + pub fn set_keys(&mut self, keys: RadrootsNostrKeys) { let npub = keys.public_key().to_bech32().ok(); self.keys = Some(keys); self.state.loaded = true; @@ -102,7 +107,7 @@ impl KeysManager { *self = Self::default(); } - pub fn require(&self) -> Result<&nostr::Keys> { + pub fn require(&self) -> Result<&RadrootsNostrKeys> { self.keys.as_ref().ok_or(NetError::MissingKey) } @@ -191,7 +196,6 @@ impl KeysManager { } fn save_nsec_text(&self, path: impl AsRef<Path>, no_overwrite: bool) -> Result<()> { - use nostr::nips::nip19::ToBech32; if no_overwrite && path.as_ref().exists() { return Err(NetError::OverwriteDenied); } @@ -223,8 +227,8 @@ impl KeysManager { Ok(()) } - pub fn generate_in_memory(&mut self) -> &nostr::Keys { - let keys = nostr::Keys::generate(); + pub fn generate_in_memory(&mut self) -> &RadrootsNostrKeys { + let keys = RadrootsNostrKeys::generate(); self.set_keys(keys); self.keys.as_ref().unwrap() } diff --git a/net-core/src/nostr_client/events/post.rs b/net-core/src/nostr_client/events/post.rs @@ -1,11 +1,16 @@ use crate::error::{NetError, Result}; use radroots_events::post::RadrootsPostEventMetadata; +use radroots_nostr::prelude::{ + radroots_nostr_build_post_event, + radroots_nostr_build_post_reply_event, + radroots_nostr_fetch_post_events, +}; use crate::nostr_client::manager::NostrClientManager; impl NostrClientManager { pub async fn publish_post_event(&self, content: String) -> Result<String> { - let builder = radroots_nostr::events::post::build_post_event(content); + let builder = radroots_nostr_build_post_event(content); let out = self .inner .client @@ -28,7 +33,7 @@ impl NostrClientManager { content: String, root_event_id_hex: Option<String>, ) -> Result<String> { - let builder = radroots_nostr::events::post::build_post_reply_event( + let builder = radroots_nostr_build_post_reply_event( &parent_event_id_hex, &parent_author_hex, content, @@ -71,10 +76,9 @@ impl NostrClientManager { limit: u16, since_unix: Option<u64>, ) -> Result<Vec<RadrootsPostEventMetadata>> { - let items = - radroots_nostr::events::post::fetch_post_events(&self.inner.client, limit, since_unix) - .await - .map_err(|e| NetError::Msg(e.to_string()))?; + let items = radroots_nostr_fetch_post_events(&self.inner.client, limit, since_unix) + .await + .map_err(|e| NetError::Msg(e.to_string()))?; Ok(items) } diff --git a/net-core/src/nostr_client/events/profile.rs b/net-core/src/nostr_client/events/profile.rs @@ -1,14 +1,20 @@ use crate::error::{NetError, Result}; use radroots_events::profile::RadrootsProfileEventMetadata; +use radroots_nostr::prelude::{ + radroots_nostr_fetch_metadata_for_author, + radroots_nostr_post_metadata_event, + RadrootsNostrMetadata, + RadrootsNostrPublicKey, +}; use crate::nostr_client::manager::NostrClientManager; impl NostrClientManager { pub async fn fetch_profile_event( &self, - author: nostr::PublicKey, + author: RadrootsNostrPublicKey, ) -> Result<Option<RadrootsProfileEventMetadata>> { - let ev = radroots_nostr::events::metadata::fetch_metadata_for_author( + let ev = radroots_nostr_fetch_metadata_for_author( &self.inner.client, author, core::time::Duration::from_secs(5), @@ -28,7 +34,7 @@ impl NostrClientManager { pub fn fetch_profile_event_blocking( &self, - author: nostr::PublicKey, + author: RadrootsNostrPublicKey, ) -> Result<Option<RadrootsProfileEventMetadata>> { let rt = self.inner.rt.clone(); let this = self.clone(); @@ -45,7 +51,7 @@ impl NostrClientManager { let rt = self.inner.rt.clone(); let inner_for_task = self.inner.clone(); rt.block_on(async move { - let mut md = nostr::Metadata::new(); + let mut md = RadrootsNostrMetadata::new(); if let Some(v) = name { md = md.name(v); } @@ -58,10 +64,9 @@ impl NostrClientManager { if let Some(v) = about { md = md.about(v); } - let _ = - radroots_nostr::events::metadata::post_metadata_event(&inner_for_task.client, &md) - .await - .map_err(|e| NetError::Msg(e.to_string()))?; + let _ = radroots_nostr_post_metadata_event(&inner_for_task.client, &md) + .await + .map_err(|e| NetError::Msg(e.to_string()))?; Ok::<(), NetError>(()) })?; Ok("ok".to_string()) diff --git a/net-core/src/nostr_client/inner.rs b/net-core/src/nostr_client/inner.rs @@ -1,16 +1,22 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use nostr_sdk::prelude::*; use radroots_events::post::RadrootsPostEventMetadata; +use radroots_nostr::prelude::{ + RadrootsNostrClient, + RadrootsNostrKeys, + RadrootsNostrMonitor, + RadrootsNostrRelayStatus, + RadrootsNostrRelayUrl, +}; use tokio::runtime::Handle; use tokio::sync::broadcast; use tokio::task::JoinHandle; pub(super) struct Inner { - pub client: Client, + pub client: RadrootsNostrClient, pub relays: Arc<Mutex<Vec<String>>>, - pub statuses: Arc<Mutex<HashMap<RelayUrl, RelayStatus>>>, + pub statuses: Arc<Mutex<HashMap<RadrootsNostrRelayUrl, RadrootsNostrRelayStatus>>>, pub last_error: Arc<Mutex<Option<String>>>, pub rt: Handle, pub post_events_tx: broadcast::Sender<RadrootsPostEventMetadata>, @@ -18,9 +24,9 @@ pub(super) struct Inner { } impl Inner { - pub fn new(keys: nostr::Keys, rt: Handle) -> Arc<Self> { - let monitor = Monitor::new(2048); - let client = Client::builder().signer(keys).monitor(monitor).build(); + pub fn new(keys: RadrootsNostrKeys, rt: Handle) -> Arc<Self> { + let monitor = RadrootsNostrMonitor::new(2048); + let client = RadrootsNostrClient::new_with_monitor(keys, monitor); let (tx, _) = broadcast::channel(2048); Arc::new(Self { diff --git a/net-core/src/nostr_client/manager.rs b/net-core/src/nostr_client/manager.rs @@ -2,6 +2,12 @@ use std::sync::Arc; use tokio::runtime::Handle; use super::inner::Inner; +use radroots_nostr::prelude::{ + RadrootsNostrFilter, + RadrootsNostrKind, + RadrootsNostrKeys, + RadrootsNostrTimestamp, +}; #[derive(Clone)] pub struct NostrClientManager { @@ -9,7 +15,7 @@ pub struct NostrClientManager { } impl NostrClientManager { - pub fn new(keys: nostr::Keys, rt: Handle) -> Self { + pub fn new(keys: RadrootsNostrKeys, rt: Handle) -> Self { let inner = Inner::new(keys, rt); let this = Self { inner: inner.clone(), @@ -37,13 +43,12 @@ impl NostrClientManager { let inner = inner.clone(); async move { use futures::StreamExt; - use nostr_sdk::prelude::*; - let mut since = since_unix.unwrap_or_else(|| Timestamp::now().as_u64()); + let mut since = since_unix.unwrap_or_else(|| RadrootsNostrTimestamp::now().as_u64()); loop { - let filter = Filter::new() - .kind(Kind::TextNote) - .since(Timestamp::from(since)); + let filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::TextNote) + .since(RadrootsNostrTimestamp::from(since)); let mut stream = match inner .client @@ -57,7 +62,11 @@ impl NostrClientManager { } }; - while let Some(event) = stream.next().await { + while let Some((_, event)) = stream.next().await { + let event = match event { + Ok(ev) => ev, + Err(_) => continue, + }; let meta = radroots_nostr::event_adapters::to_post_event_metadata(&event); let ts = event.created_at.as_u64(); since = ts.saturating_add(1); diff --git a/net-core/src/nostr_client/status.rs b/net-core/src/nostr_client/status.rs @@ -1,4 +1,7 @@ -use nostr_sdk::prelude::MonitorNotification; +use radroots_nostr::prelude::{ + RadrootsNostrMonitorNotification, + RadrootsNostrRelayStatus, +}; use tracing::{info, warn}; use super::manager::NostrClientManager; @@ -15,7 +18,7 @@ impl NostrClientManager { let mut rx = monitor.subscribe(); while let Ok(notification) = rx.recv().await { match notification { - MonitorNotification::StatusChanged { relay_url, status } => { + RadrootsNostrMonitorNotification::StatusChanged { relay_url, status } => { if let Ok(mut map) = inner_for_task.statuses.lock() { map.insert(relay_url.clone(), status); } else if let Ok(mut last) = inner_for_task.last_error.lock() { @@ -49,8 +52,8 @@ impl NostrClientManager { for (_url, st) in map.iter() { match st { - nostr_sdk::prelude::RelayStatus::Connected => connected += 1, - nostr_sdk::prelude::RelayStatus::Connecting => connecting += 1, + RadrootsNostrRelayStatus::Connected => connected += 1, + RadrootsNostrRelayStatus::Connecting => connecting += 1, _ => {} } } diff --git a/nostr/Cargo.toml b/nostr/Cargo.toml @@ -7,9 +7,9 @@ rust-version.workspace = true license.workspace = true [features] -default = ["std", "sdk"] +default = ["std"] std = [] -sdk = ["dep:nostr-sdk"] +client = ["std", "dep:nostr-sdk", "dep:radroots-identity"] codec = ["dep:radroots-events", "dep:radroots-events-codec"] events = ["dep:radroots-events", "radroots-events/std", "radroots-events/serde"] http = ["dep:reqwest"] @@ -17,6 +17,7 @@ http = ["dep:reqwest"] [dependencies] radroots-events = { workspace = true, optional = true, default-features = false } radroots-events-codec = { workspace = true, optional = true, default-features = false } +radroots-identity = { workspace = true, optional = true, default-features = true } nostr = { workspace = true, features = ["nip04"] } nostr-sdk = { workspace = true, optional = true } reqwest = { workspace = true, optional = true, default-features = false, features = ["json", "rustls-tls"] } diff --git a/nostr/src/client.rs b/nostr/src/client.rs @@ -1,22 +1,145 @@ +#![forbid(unsafe_code)] + +use core::ops::Deref; use core::time::Duration; -use nostr::{event::Event, event::EventBuilder, event::EventId, filter::Filter}; -use nostr_sdk::{Client, prelude::*}; +use std::collections::HashMap; + +use nostr_sdk::Client; +use radroots_identity::RadrootsIdentity; + +use crate::error::RadrootsNostrError; +use crate::types::{ + RadrootsNostrEvent, + RadrootsNostrEventBuilder, + RadrootsNostrEventId, + RadrootsNostrFilter, + RadrootsNostrKeys, + RadrootsNostrMonitor, + RadrootsNostrOutput, + RadrootsNostrRelay, + RadrootsNostrRelayUrl, + RadrootsNostrSubscribeAutoCloseOptions, + RadrootsNostrSubscriptionId, +}; +use crate::types::RadrootsNostrMetadata; + +#[derive(Clone)] +pub struct RadrootsNostrClient { + inner: Client, +} + +impl RadrootsNostrClient { + pub fn new(keys: RadrootsNostrKeys) -> Self { + Self { + inner: Client::new(keys), + } + } + + pub fn new_with_monitor(keys: RadrootsNostrKeys, monitor: RadrootsNostrMonitor) -> Self { + let inner = Client::builder().signer(keys).monitor(monitor).build(); + Self { inner } + } + + pub fn from_identity(identity: &RadrootsIdentity) -> Self { + Self::new(identity.keys().clone()) + } + + pub fn from_identity_owned(identity: RadrootsIdentity) -> Self { + Self::new(identity.into_keys()) + } + + pub fn from_inner(inner: Client) -> Self { + Self { inner } + } + + pub fn into_inner(self) -> Client { + self.inner + } + + pub async fn add_relay(&self, url: &str) -> Result<bool, RadrootsNostrError> { + Ok(self.inner.add_relay(url).await?) + } -use crate::error::NostrUtilsError; + pub async fn add_write_relay(&self, url: &str) -> Result<bool, RadrootsNostrError> { + Ok(self.inner.add_write_relay(url).await?) + } + + pub async fn add_read_relay(&self, url: &str) -> Result<bool, RadrootsNostrError> { + Ok(self.inner.add_read_relay(url).await?) + } + + pub async fn remove_relay(&self, url: &str) -> Result<(), RadrootsNostrError> { + self.inner.force_remove_relay(url).await?; + Ok(()) + } + + pub async fn relays(&self) -> HashMap<RadrootsNostrRelayUrl, RadrootsNostrRelay> { + self.inner.relays().await + } + + pub async fn fetch_events( + &self, + filter: RadrootsNostrFilter, + timeout: Duration, + ) -> Result<Vec<RadrootsNostrEvent>, RadrootsNostrError> { + let events = self.inner.fetch_events(filter, timeout).await?; + Ok(events.to_vec()) + } + + pub async fn subscribe( + &self, + filter: RadrootsNostrFilter, + opts: Option<RadrootsNostrSubscribeAutoCloseOptions>, + ) -> Result<RadrootsNostrOutput<RadrootsNostrSubscriptionId>, RadrootsNostrError> { + Ok(self.inner.subscribe(filter, opts).await?) + } + + pub async fn send_event_builder( + &self, + event: RadrootsNostrEventBuilder, + ) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { + Ok(self.inner.send_event_builder(event).await?) + } + + pub async fn send_event( + &self, + event: &RadrootsNostrEvent, + ) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { + Ok(self.inner.send_event(event).await?) + } + + pub async fn set_metadata( + &self, + md: &RadrootsNostrMetadata, + ) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { + Ok(self.inner.set_metadata(md).await?) + } +} + +impl Deref for RadrootsNostrClient { + type Target = Client; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} -pub async fn nostr_send_event( - client: &Client, - event: EventBuilder, -) -> Result<Output<EventId>, NostrUtilsError> { +pub async fn radroots_nostr_send_event( + client: &RadrootsNostrClient, + event: RadrootsNostrEventBuilder, +) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { Ok(client.send_event_builder(event).await?) } -pub async fn nostr_fetch_event_by_id(client: &Client, id: &str) -> Result<Event, NostrUtilsError> { - let event_id = EventId::parse(id)?; - let filter = Filter::new().id(event_id); +pub async fn radroots_nostr_fetch_event_by_id( + client: &RadrootsNostrClient, + id: &str, +) -> Result<RadrootsNostrEvent, RadrootsNostrError> { + let event_id = RadrootsNostrEventId::parse(id)?; + let filter = RadrootsNostrFilter::new().id(event_id); let events = client.fetch_events(filter, Duration::from_secs(10)).await?; let event = events .first() - .ok_or_else(|| NostrUtilsError::EventNotFound(event_id.to_hex()))?; + .ok_or_else(|| RadrootsNostrError::EventNotFound(event_id.to_hex()))?; Ok(event.clone()) } diff --git a/nostr/src/codec_adapters.rs b/nostr/src/codec_adapters.rs @@ -1,7 +1,7 @@ extern crate alloc; use alloc::{string::String, vec::Vec}; -use nostr::event::Event; +use crate::types::RadrootsNostrEvent; use radroots_events_codec::job::{ error::JobParseError, feedback::decode as fb_decode, request::decode as req_decode, @@ -9,36 +9,36 @@ use radroots_events_codec::job::{ }; use crate::util::created_at_u32_saturating; -fn event_id(e: &Event) -> String { +fn event_id(e: &RadrootsNostrEvent) -> String { e.id.to_hex() } -fn author(e: &Event) -> String { +fn author(e: &RadrootsNostrEvent) -> String { e.pubkey.to_hex() } -fn published_at(e: &Event) -> u32 { +fn published_at(e: &RadrootsNostrEvent) -> u32 { created_at_u32_saturating(e.created_at) } -fn kind_u32(e: &Event) -> u32 { +fn kind_u32(e: &RadrootsNostrEvent) -> u32 { e.kind.as_u16() as u32 } -fn content(e: &Event) -> String { +fn content(e: &RadrootsNostrEvent) -> String { e.content.clone() } -fn tags_vec(e: &Event) -> Vec<Vec<String>> { +fn tags_vec(e: &RadrootsNostrEvent) -> Vec<Vec<String>> { e.tags.iter().map(|t| t.as_slice().to_vec()).collect() } -fn sig_hex(e: &Event) -> String { +fn sig_hex(e: &RadrootsNostrEvent) -> String { e.sig.to_string() } pub fn to_job_request_metadata( - e: &Event, + e: &RadrootsNostrEvent, ) -> Result<radroots_events::job_request::RadrootsJobRequestEventMetadata, JobParseError> { req_decode::metadata_from_event( event_id(e), @@ -50,7 +50,7 @@ pub fn to_job_request_metadata( } pub fn to_job_result_metadata( - e: &Event, + e: &RadrootsNostrEvent, ) -> Result<radroots_events::job_result::RadrootsJobResultEventMetadata, JobParseError> { res_decode::metadata_from_event( event_id(e), @@ -63,7 +63,7 @@ pub fn to_job_result_metadata( } pub fn to_job_feedback_metadata( - e: &Event, + e: &RadrootsNostrEvent, ) -> Result<radroots_events::job_feedback::RadrootsJobFeedbackEventMetadata, JobParseError> { fb_decode::metadata_from_event( @@ -77,7 +77,7 @@ pub fn to_job_feedback_metadata( } pub fn to_job_request_index( - e: &Event, + e: &RadrootsNostrEvent, ) -> Result<radroots_events::job_request::RadrootsJobRequestEventIndex, JobParseError> { req_decode::index_from_event( event_id(e), @@ -91,7 +91,7 @@ pub fn to_job_request_index( } pub fn to_job_result_index( - e: &Event, + e: &RadrootsNostrEvent, ) -> Result<radroots_events::job_result::RadrootsJobResultEventIndex, JobParseError> { res_decode::index_from_event( event_id(e), @@ -105,7 +105,7 @@ pub fn to_job_result_index( } pub fn to_job_feedback_index( - e: &Event, + e: &RadrootsNostrEvent, ) -> Result<radroots_events::job_feedback::RadrootsJobFeedbackEventIndex, JobParseError> { fb_decode::index_from_event( event_id(e), diff --git a/nostr/src/error.rs b/nostr/src/error.rs @@ -1,12 +1,12 @@ use thiserror::Error; #[derive(Debug, Error)] -pub enum NostrUtilsError { - #[cfg(feature = "sdk")] +pub enum RadrootsNostrError { + #[cfg(feature = "client")] #[error("Client error: {0}")] ClientError(#[from] nostr_sdk::client::Error), - #[cfg(feature = "sdk")] + #[cfg(feature = "client")] #[error("Database error: {0}")] DatabaseError(#[from] nostr_sdk::prelude::DatabaseError), @@ -24,7 +24,7 @@ pub enum NostrUtilsError { } #[derive(Debug, Error)] -pub enum NostrTagsResolveError { +pub enum RadrootsNostrTagsResolveError { #[error("Missing public key 'p' tag in encrypted event: {0:?}")] MissingPTag(nostr::Event), diff --git a/nostr/src/event_adapters.rs b/nostr/src/event_adapters.rs @@ -4,13 +4,13 @@ use radroots_events::post::{RadrootsPost, RadrootsPostEventMetadata}; use radroots_events::profile::{RadrootsProfile, RadrootsProfileEventMetadata}; #[cfg(feature = "events")] -use nostr::event::Event; +use crate::types::{RadrootsNostrEvent, RadrootsNostrMetadata}; #[cfg(feature = "events")] use crate::util::created_at_u32_saturating; #[cfg(feature = "events")] -pub fn to_post_event_metadata(e: &Event) -> RadrootsPostEventMetadata { +pub fn to_post_event_metadata(e: &RadrootsNostrEvent) -> RadrootsPostEventMetadata { RadrootsPostEventMetadata { id: e.id.to_string(), author: e.pubkey.to_string(), @@ -23,7 +23,7 @@ pub fn to_post_event_metadata(e: &Event) -> RadrootsPostEventMetadata { } #[cfg(feature = "events")] -pub fn to_profile_event_metadata(e: &Event) -> Option<RadrootsProfileEventMetadata> { +pub fn to_profile_event_metadata(e: &RadrootsNostrEvent) -> Option<RadrootsProfileEventMetadata> { if let Ok(p) = serde_json::from_str::<RadrootsProfile>(&e.content) { return Some(RadrootsProfileEventMetadata { id: e.id.to_string(), @@ -34,7 +34,7 @@ pub fn to_profile_event_metadata(e: &Event) -> Option<RadrootsProfileEventMetada }); } - if let Ok(md) = serde_json::from_str::<nostr::Metadata>(&e.content) { + if let Ok(md) = serde_json::from_str::<RadrootsNostrMetadata>(&e.content) { let p = RadrootsProfile { name: md.name.unwrap_or_default(), display_name: md.display_name, diff --git a/nostr/src/event_convert.rs b/nostr/src/event_convert.rs @@ -1,11 +1,11 @@ #![forbid(unsafe_code)] -use nostr::event::Event; +use crate::types::RadrootsNostrEvent as RadrootsNostrRawEvent; use radroots_events::{RadrootsNostrEvent, RadrootsNostrEventPtr}; use crate::util::event_created_at_u32_saturating; -pub fn radroots_event_from_nostr(event: &Event) -> RadrootsNostrEvent { +pub fn radroots_event_from_nostr(event: &RadrootsNostrRawEvent) -> RadrootsNostrEvent { RadrootsNostrEvent { id: event.id.to_string(), author: event.pubkey.to_string(), @@ -17,7 +17,7 @@ pub fn radroots_event_from_nostr(event: &Event) -> RadrootsNostrEvent { } } -pub fn radroots_event_ptr_from_nostr(event: &Event) -> RadrootsNostrEventPtr { +pub fn radroots_event_ptr_from_nostr(event: &RadrootsNostrRawEvent) -> RadrootsNostrEventPtr { RadrootsNostrEventPtr { id: event.id.to_string(), relays: None, diff --git a/nostr/src/events/jobs.rs b/nostr/src/events/jobs.rs @@ -1,33 +1,32 @@ -use nostr::{ - event::{Event, EventBuilder, Tag}, - nips::nip90::{DataVendingMachineStatus, JobFeedbackData}, -}; +use nostr::nips::nip90::{DataVendingMachineStatus, JobFeedbackData}; -use crate::error::NostrUtilsError; +use crate::error::RadrootsNostrError; +use crate::types::{RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrTag}; -pub fn nostr_build_event_job_result( - job_request: &Event, +pub fn radroots_nostr_build_event_job_result( + job_request: &RadrootsNostrEvent, payload: impl Into<String>, millisats: u64, bolt11: Option<String>, - tags: Option<Vec<Tag>>, -) -> Result<EventBuilder, NostrUtilsError> { - let builder = EventBuilder::job_result(job_request.clone(), payload, millisats, bolt11)? - .tags(tags.unwrap_or_default()); + tags: Option<Vec<RadrootsNostrTag>>, +) -> Result<RadrootsNostrEventBuilder, RadrootsNostrError> { + let builder = + RadrootsNostrEventBuilder::job_result(job_request.clone(), payload, millisats, bolt11)? + .tags(tags.unwrap_or_default()); Ok(builder) } -pub fn nostr_build_event_job_feedback( - job_request: &Event, +pub fn radroots_nostr_build_event_job_feedback( + job_request: &RadrootsNostrEvent, status: &str, extra_info: Option<String>, - tags: Option<Vec<Tag>>, -) -> Result<EventBuilder, NostrUtilsError> { + tags: Option<Vec<RadrootsNostrTag>>, +) -> Result<RadrootsNostrEventBuilder, RadrootsNostrError> { let status = status .parse::<DataVendingMachineStatus>() .unwrap_or(DataVendingMachineStatus::Error); let feedback_data = JobFeedbackData::new(&job_request.clone(), status) .extra_info(extra_info.unwrap_or_default()); - let builder = EventBuilder::job_feedback(feedback_data).tags(tags.unwrap_or_default()); + let builder = RadrootsNostrEventBuilder::job_feedback(feedback_data).tags(tags.unwrap_or_default()); Ok(builder) } diff --git a/nostr/src/events/metadata.rs b/nostr/src/events/metadata.rs @@ -1,35 +1,49 @@ -use crate::error::NostrUtilsError; +use crate::types::{RadrootsNostrEventBuilder, RadrootsNostrMetadata}; + +#[cfg(feature = "client")] use core::time::Duration; -use nostr::{ - Kind, Metadata, event::Event, event::EventBuilder, event::EventId, filter::Filter, - key::PublicKey, +#[cfg(feature = "client")] +use crate::client::RadrootsNostrClient; +#[cfg(feature = "client")] +use crate::error::RadrootsNostrError; +#[cfg(feature = "client")] +use crate::types::{ + RadrootsNostrEvent, + RadrootsNostrEventId, + RadrootsNostrFilter, + RadrootsNostrKind, + RadrootsNostrOutput, + RadrootsNostrPublicKey, }; -use nostr_sdk::{Client, prelude::Output}; -pub fn build_metadata_event(md: &Metadata) -> EventBuilder { - EventBuilder::metadata(md) +pub fn radroots_nostr_build_metadata_event(md: &RadrootsNostrMetadata) -> RadrootsNostrEventBuilder { + RadrootsNostrEventBuilder::metadata(md) } -pub async fn post_metadata_event( - client: &Client, - md: &Metadata, -) -> Result<Output<EventId>, NostrUtilsError> { - let builder = build_metadata_event(md); +#[cfg(feature = "client")] +pub async fn radroots_nostr_post_metadata_event( + client: &RadrootsNostrClient, + md: &RadrootsNostrMetadata, +) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { + let builder = radroots_nostr_build_metadata_event(md); Ok(client.send_event_builder(builder).await?) } -pub async fn fetch_metadata_for_author( - client: &Client, - author: PublicKey, +#[cfg(feature = "client")] +pub async fn radroots_nostr_fetch_metadata_for_author( + client: &RadrootsNostrClient, + author: RadrootsNostrPublicKey, timeout: Duration, -) -> Result<Option<Event>, NostrUtilsError> { - let filter = Filter::new().authors(vec![author]).kind(Kind::Metadata); +) -> Result<Option<RadrootsNostrEvent>, RadrootsNostrError> { + let filter = RadrootsNostrFilter::new() + .authors(vec![author]) + .kind(RadrootsNostrKind::Metadata); let stored = client.database().query(filter.clone()).await?; let fetched = client.fetch_events(filter, timeout).await?; - let mut latest: Option<Event> = None; + let mut latest: Option<RadrootsNostrEvent> = None; for ev in stored.into_iter().chain(fetched.into_iter()) { - if ev.kind != Kind::Metadata { + if ev.kind != RadrootsNostrKind::Metadata { continue; } match &latest { diff --git a/nostr/src/events/mod.rs b/nostr/src/events/mod.rs @@ -5,24 +5,33 @@ pub mod post; extern crate alloc; use alloc::{string::String, vec::Vec}; -use nostr::event::{EventBuilder, Kind, Tag, TagKind}; +use crate::error::RadrootsNostrError; +use crate::types::{ + RadrootsNostrEventBuilder, + RadrootsNostrKind, + RadrootsNostrTag, + RadrootsNostrTagKind, +}; -use crate::error::NostrUtilsError; - -pub fn build_nostr_event( +pub fn radroots_nostr_build_event( kind_u32: u32, content: impl Into<String>, tag_slices: Vec<Vec<String>>, -) -> Result<EventBuilder, NostrUtilsError> { - let mut tags: Vec<Tag> = Vec::new(); +) -> Result<RadrootsNostrEventBuilder, RadrootsNostrError> { + let mut tags: Vec<RadrootsNostrTag> = Vec::new(); for mut s in tag_slices { if s.is_empty() { continue; } let key = s.remove(0); let values = s; - tags.push(Tag::custom(TagKind::Custom(key.into()), values)); + tags.push(RadrootsNostrTag::custom( + RadrootsNostrTagKind::Custom(key.into()), + values, + )); } - let builder = EventBuilder::new(Kind::Custom(kind_u32 as u16), content.into()).tags(tags); + let builder = + RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(kind_u32 as u16), content.into()) + .tags(tags); Ok(builder) } diff --git a/nostr/src/events/post.rs b/nostr/src/events/post.rs @@ -1,52 +1,58 @@ -use crate::error::NostrUtilsError; +use crate::error::RadrootsNostrError; +use crate::types::{ + RadrootsNostrEventBuilder, + RadrootsNostrEventId, + RadrootsNostrPublicKey, + RadrootsNostrTag, +}; -#[cfg(all(feature = "sdk", feature = "events"))] +#[cfg(all(feature = "client", feature = "events"))] use core::time::Duration; -use nostr::{ - event::{EventBuilder, EventId, Tag}, - key::PublicKey, -}; -#[cfg(all(feature = "sdk", feature = "events"))] -use nostr_sdk::prelude::{Client, Filter, Kind, Timestamp}; +#[cfg(all(feature = "client", feature = "events"))] +use crate::client::RadrootsNostrClient; +#[cfg(all(feature = "client", feature = "events"))] +use crate::types::{RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrTimestamp}; -pub fn build_post_event(content: impl Into<String>) -> EventBuilder { - EventBuilder::text_note(content) +pub fn radroots_nostr_build_post_event(content: impl Into<String>) -> RadrootsNostrEventBuilder { + RadrootsNostrEventBuilder::text_note(content) } -pub fn build_post_reply_event( +pub fn radroots_nostr_build_post_reply_event( parent_event_id_hex: &str, parent_author_hex: &str, content: impl Into<String>, root_event_id_hex: Option<&str>, -) -> Result<EventBuilder, NostrUtilsError> { - let parent_id = EventId::from_hex(parent_event_id_hex)?; - let parent_pubkey = PublicKey::from_hex(parent_author_hex)?; - let mut tags: Vec<Tag> = Vec::new(); +) -> Result<RadrootsNostrEventBuilder, RadrootsNostrError> { + let parent_id = RadrootsNostrEventId::from_hex(parent_event_id_hex)?; + let parent_pubkey = RadrootsNostrPublicKey::from_hex(parent_author_hex)?; + let mut tags: Vec<RadrootsNostrTag> = Vec::new(); if let Some(root_hex) = root_event_id_hex { if !root_hex.is_empty() { - if let Ok(root_id) = EventId::from_hex(root_hex) { - tags.push(Tag::event(root_id)); + if let Ok(root_id) = RadrootsNostrEventId::from_hex(root_hex) { + tags.push(RadrootsNostrTag::event(root_id)); } } } - tags.push(Tag::event(parent_id)); - tags.push(Tag::public_key(parent_pubkey)); + tags.push(RadrootsNostrTag::event(parent_id)); + tags.push(RadrootsNostrTag::public_key(parent_pubkey)); - Ok(EventBuilder::text_note(content).tags(tags)) + Ok(RadrootsNostrEventBuilder::text_note(content).tags(tags)) } -#[cfg(all(feature = "sdk", feature = "events"))] -pub async fn fetch_post_events( - client: &Client, +#[cfg(all(feature = "client", feature = "events"))] +pub async fn radroots_nostr_fetch_post_events( + client: &RadrootsNostrClient, limit: u16, since_unix: Option<u64>, -) -> Result<Vec<radroots_events::post::RadrootsPostEventMetadata>, NostrUtilsError> { - let mut filter = Filter::new().kind(Kind::TextNote).limit(limit.into()); +) -> Result<Vec<radroots_events::post::RadrootsPostEventMetadata>, RadrootsNostrError> { + let mut filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::TextNote) + .limit(limit.into()); if let Some(s) = since_unix { - filter = filter.since(Timestamp::from(s)); + filter = filter.since(RadrootsNostrTimestamp::from(s)); } let events = client.fetch_events(filter, Duration::from_secs(10)).await?; diff --git a/nostr/src/filter.rs b/nostr/src/filter.rs @@ -1,13 +1,13 @@ -use nostr::{event::Kind, filter::Filter, types::Timestamp}; +use crate::types::{RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrTimestamp}; -pub fn nostr_kind(kind: u16) -> Kind { - Kind::Custom(kind) +pub fn radroots_nostr_kind(kind: u16) -> RadrootsNostrKind { + RadrootsNostrKind::Custom(kind) } -pub fn nostr_filter_kind(kind: u16) -> Filter { - Filter::new().kind(Kind::Custom(kind)) +pub fn radroots_nostr_filter_kind(kind: u16) -> RadrootsNostrFilter { + RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom(kind)) } -pub fn nostr_filter_new_events(filter: Filter) -> Filter { - filter.since(Timestamp::now()) +pub fn radroots_nostr_filter_new_events(filter: RadrootsNostrFilter) -> RadrootsNostrFilter { + filter.since(RadrootsNostrTimestamp::now()) } diff --git a/nostr/src/job_adapter.rs b/nostr/src/job_adapter.rs @@ -1,18 +1,18 @@ #![forbid(unsafe_code)] -use nostr::event::Event; use radroots_events_codec::job::traits::{JobEventBorrow, JobEventLike}; +use crate::types::{RadrootsNostrEvent, RadrootsNostrKind}; #[derive(Clone, Debug)] -pub struct NostrEventAdapter<'a> { - evt: &'a Event, +pub struct RadrootsNostrEventAdapter<'a> { + evt: &'a RadrootsNostrEvent, id_hex: String, author_hex: String, } -impl<'a> NostrEventAdapter<'a> { +impl<'a> RadrootsNostrEventAdapter<'a> { #[inline] - pub fn new(evt: &'a Event) -> Self { + pub fn new(evt: &'a RadrootsNostrEvent) -> Self { Self { evt, id_hex: evt.id.to_hex(), @@ -30,7 +30,7 @@ impl<'a> NostrEventAdapter<'a> { } } -impl<'a> JobEventBorrow<'a> for NostrEventAdapter<'a> { +impl<'a> JobEventBorrow<'a> for RadrootsNostrEventAdapter<'a> { #[inline] fn raw_id(&'a self) -> &'a str { &self.id_hex @@ -46,13 +46,13 @@ impl<'a> JobEventBorrow<'a> for NostrEventAdapter<'a> { #[inline] fn raw_kind(&'a self) -> u32 { match self.evt.kind { - nostr::event::Kind::Custom(v) => v as u32, + RadrootsNostrKind::Custom(v) => v as u32, _ => 0, } } } -impl JobEventLike for NostrEventAdapter<'_> { +impl JobEventLike for RadrootsNostrEventAdapter<'_> { fn raw_id(&self) -> String { self.id_hex.clone() } @@ -64,7 +64,7 @@ impl JobEventLike for NostrEventAdapter<'_> { } fn raw_kind(&self) -> u32 { match self.evt.kind { - nostr::event::Kind::Custom(v) => v as u32, + RadrootsNostrKind::Custom(v) => v as u32, _ => 0, } } diff --git a/nostr/src/lib.rs b/nostr/src/lib.rs @@ -2,14 +2,16 @@ extern crate alloc; -#[cfg(feature = "sdk")] +#[cfg(feature = "client")] pub mod client; pub mod error; pub mod events; pub mod filter; pub mod parse; +#[cfg(feature = "client")] pub mod relays; +pub mod types; pub mod tags; pub mod util; @@ -29,27 +31,84 @@ pub mod event_adapters; pub mod event_convert; pub mod prelude { - pub use crate::events::build_nostr_event; + pub use crate::events::radroots_nostr_build_event; - #[cfg(feature = "sdk")] - pub use crate::client::{nostr_fetch_event_by_id, nostr_send_event}; + #[cfg(feature = "client")] + pub use crate::client::{ + radroots_nostr_fetch_event_by_id, + radroots_nostr_send_event, + RadrootsNostrClient, + }; - pub use crate::error::{NostrTagsResolveError, NostrUtilsError}; - pub use crate::filter::{nostr_filter_kind, nostr_filter_new_events, nostr_kind}; + pub use crate::error::{RadrootsNostrError, RadrootsNostrTagsResolveError}; + pub use crate::filter::{ + radroots_nostr_filter_kind, + radroots_nostr_filter_new_events, + radroots_nostr_kind, + }; pub use crate::events::{ - jobs::{nostr_build_event_job_feedback, nostr_build_event_job_result}, - metadata::{build_metadata_event, fetch_metadata_for_author, post_metadata_event}, - post::{build_post_event, build_post_reply_event}, + jobs::{ + radroots_nostr_build_event_job_feedback, + radroots_nostr_build_event_job_result, + }, + metadata::radroots_nostr_build_metadata_event, + post::{ + radroots_nostr_build_post_event, + radroots_nostr_build_post_reply_event, + }, + }; + + #[cfg(feature = "client")] + pub use crate::events::metadata::{ + radroots_nostr_fetch_metadata_for_author, + radroots_nostr_post_metadata_event, }; - #[cfg(all(feature = "sdk", feature = "events"))] - pub use crate::events::post::fetch_post_events; + #[cfg(all(feature = "client", feature = "events"))] + pub use crate::events::post::radroots_nostr_fetch_post_events; - pub use crate::parse::{parse_pubkey, parse_pubkeys}; - pub use crate::relays::{add_relay, connect, remove_relay}; + pub use crate::parse::{radroots_nostr_parse_pubkey, radroots_nostr_parse_pubkeys}; + #[cfg(feature = "client")] + pub use crate::relays::{ + radroots_nostr_add_relay, + radroots_nostr_connect, + radroots_nostr_remove_relay, + }; pub use crate::tags::*; - pub use crate::util::npub_string; + pub use crate::types::{ + RadrootsNostrCoordinate, + RadrootsNostrEvent, + RadrootsNostrEventBuilder, + RadrootsNostrEventId, + RadrootsNostrFilter, + RadrootsNostrFromBech32, + RadrootsNostrKind, + RadrootsNostrKeys, + RadrootsNostrMetadata, + RadrootsNostrPublicKey, + RadrootsNostrRelayUrl, + RadrootsNostrSecretKey, + RadrootsNostrSecp256k1SecretKey, + RadrootsNostrSubscriptionId, + RadrootsNostrTag, + RadrootsNostrTagKind, + RadrootsNostrTagStandard, + RadrootsNostrTimestamp, + RadrootsNostrToBech32, + RadrootsNostrUrl, + }; + #[cfg(feature = "client")] + pub use crate::types::{ + RadrootsNostrMonitor, + RadrootsNostrMonitorNotification, + RadrootsNostrOutput, + RadrootsNostrRelay, + RadrootsNostrRelayPoolNotification, + RadrootsNostrRelayStatus, + RadrootsNostrSubscribeAutoCloseOptions, + }; + pub use crate::util::radroots_nostr_npub_string; #[cfg(feature = "http")] pub use crate::nip11::fetch_nip11; @@ -61,5 +120,5 @@ pub mod prelude { pub use crate::event_convert::{radroots_event_from_nostr, radroots_event_ptr_from_nostr}; #[cfg(feature = "codec")] - pub use crate::job_adapter::NostrEventAdapter; + pub use crate::job_adapter::RadrootsNostrEventAdapter; } diff --git a/nostr/src/parse.rs b/nostr/src/parse.rs @@ -1,4 +1,4 @@ -use nostr::{key::PublicKey, nips::nip19::FromBech32}; +use crate::types::{RadrootsNostrFromBech32, RadrootsNostrPublicKey}; #[derive(Debug, thiserror::Error)] pub enum ParseError { @@ -6,12 +6,14 @@ pub enum ParseError { Invalid(String), } -pub fn parse_pubkey(s: &str) -> Result<PublicKey, ParseError> { - PublicKey::from_bech32(s) - .or_else(|_| PublicKey::from_hex(s)) +pub fn radroots_nostr_parse_pubkey(s: &str) -> Result<RadrootsNostrPublicKey, ParseError> { + RadrootsNostrPublicKey::from_bech32(s) + .or_else(|_| RadrootsNostrPublicKey::from_hex(s)) .map_err(|_| ParseError::Invalid(s.to_string())) } -pub fn parse_pubkeys(input: &[String]) -> Result<Vec<PublicKey>, ParseError> { - input.iter().map(|s| parse_pubkey(s)).collect() +pub fn radroots_nostr_parse_pubkeys( + input: &[String], +) -> Result<Vec<RadrootsNostrPublicKey>, ParseError> { + input.iter().map(|s| radroots_nostr_parse_pubkey(s)).collect() } diff --git a/nostr/src/relays.rs b/nostr/src/relays.rs @@ -1,16 +1,22 @@ -use crate::error::NostrUtilsError; -use nostr_sdk::Client; +use crate::client::RadrootsNostrClient; +use crate::error::RadrootsNostrError; -pub async fn add_relay(client: &Client, url: &str) -> Result<(), NostrUtilsError> { +pub async fn radroots_nostr_add_relay( + client: &RadrootsNostrClient, + url: &str, +) -> Result<(), RadrootsNostrError> { client.add_relay(url).await?; Ok(()) } -pub async fn remove_relay(client: &Client, url: &str) -> Result<(), NostrUtilsError> { +pub async fn radroots_nostr_remove_relay( + client: &RadrootsNostrClient, + url: &str, +) -> Result<(), RadrootsNostrError> { client.force_remove_relay(url).await?; Ok(()) } -pub async fn connect(client: &Client) { +pub async fn radroots_nostr_connect(client: &RadrootsNostrClient) { client.connect().await; } diff --git a/nostr/src/tags.rs b/nostr/src/tags.rs @@ -1,47 +1,55 @@ extern crate alloc; use alloc::{borrow::Cow, string::String, vec::Vec}; -use nostr::{ - event::{Event, Tag, TagKind, TagStandard}, - key::{Keys, PublicKey}, - nips::nip04, - types::RelayUrl, -}; +use nostr::nips::nip04; -use crate::error::NostrTagsResolveError; +use crate::error::RadrootsNostrTagsResolveError; +use crate::types::{ + RadrootsNostrEvent, + RadrootsNostrKeys, + RadrootsNostrPublicKey, + RadrootsNostrRelayUrl, + RadrootsNostrTag, + RadrootsNostrTagKind, + RadrootsNostrTagStandard, +}; -pub fn nostr_tag_first_value(tag: &Tag, key: &str) -> Option<String> { - if tag.kind() == TagKind::custom(key) { +pub fn radroots_nostr_tag_first_value(tag: &RadrootsNostrTag, key: &str) -> Option<String> { + if tag.kind() == RadrootsNostrTagKind::custom(key) { tag.content().map(|v| v.to_string()) } else { None } } -pub fn nostr_tag_at_value(tag: &Tag, index: usize) -> Option<String> { +pub fn radroots_nostr_tag_at_value(tag: &RadrootsNostrTag, index: usize) -> Option<String> { tag.as_slice().get(index).cloned() } -pub fn nostr_tag_slice(tag: &Tag, start: usize) -> Option<Vec<String>> { +pub fn radroots_nostr_tag_slice(tag: &RadrootsNostrTag, start: usize) -> Option<Vec<String>> { tag.as_slice().get(start..).map(|s| s.to_vec()) } -pub fn nostr_tag_relays_parse(tag: &Tag) -> Option<&Vec<RelayUrl>> { +pub fn radroots_nostr_tag_relays_parse( + tag: &RadrootsNostrTag, +) -> Option<&Vec<RadrootsNostrRelayUrl>> { match tag.as_standardized()? { - TagStandard::Relays(urls) => Some(urls), + RadrootsNostrTagStandard::Relays(urls) => Some(urls), _ => None, } } -pub fn nostr_tags_match<'a>(tag: &'a Tag) -> Option<(&'a str, &'a [String])> { - if let TagKind::Custom(Cow::Borrowed(key)) = tag.kind() { +pub fn radroots_nostr_tags_match<'a>( + tag: &'a RadrootsNostrTag, +) -> Option<(&'a str, &'a [String])> { + if let RadrootsNostrTagKind::Custom(Cow::Borrowed(key)) = tag.kind() { Some((key, &tag.as_slice()[1..])) } else { None } } -pub fn nostr_tag_match_l(tag: &Tag) -> Option<(&str, f64)> { +pub fn radroots_nostr_tag_match_l(tag: &RadrootsNostrTag) -> Option<(&str, f64)> { let values = tag.as_slice(); if values.len() >= 3 && values[0].eq_ignore_ascii_case("l") { if let Ok(value) = values[1].parse::<f64>() { @@ -51,7 +59,9 @@ pub fn nostr_tag_match_l(tag: &Tag) -> Option<(&str, f64)> { None } -pub fn nostr_tag_match_location(tag: &Tag) -> Option<(&str, &str, &str)> { +pub fn radroots_nostr_tag_match_location( + tag: &RadrootsNostrTag, +) -> Option<(&str, &str, &str)> { let values = tag.as_slice(); if values.len() >= 4 && values[0] == "location" { Some((values[1].as_str(), values[2].as_str(), values[3].as_str())) @@ -60,47 +70,54 @@ pub fn nostr_tag_match_location(tag: &Tag) -> Option<(&str, &str, &str)> { } } -pub fn nostr_tag_match_geohash(tag: &Tag) -> Option<String> { +pub fn radroots_nostr_tag_match_geohash(tag: &RadrootsNostrTag) -> Option<String> { match tag.as_standardized()? { - TagStandard::Geohash(geohash) => Some(geohash.clone()), + RadrootsNostrTagStandard::Geohash(geohash) => Some(geohash.clone()), _ => None, } } -pub fn nostr_tag_match_title(tag: &Tag) -> Option<String> { +pub fn radroots_nostr_tag_match_title(tag: &RadrootsNostrTag) -> Option<String> { match tag.as_standardized()? { - TagStandard::Title(title) => Some(title.clone()), + RadrootsNostrTagStandard::Title(title) => Some(title.clone()), _ => None, } } -pub fn nostr_tag_match_summary(tag: &Tag) -> Option<String> { +pub fn radroots_nostr_tag_match_summary(tag: &RadrootsNostrTag) -> Option<String> { match tag.as_standardized()? { - TagStandard::Summary(summary) => Some(summary.clone()), + RadrootsNostrTagStandard::Summary(summary) => Some(summary.clone()), _ => None, } } -pub fn nostr_tags_resolve(event: &Event, keys: &Keys) -> Result<Vec<Tag>, NostrTagsResolveError> { - if !event.tags.iter().any(|t| t.kind() == TagKind::Encrypted) { +pub fn radroots_nostr_tags_resolve( + event: &RadrootsNostrEvent, + keys: &RadrootsNostrKeys, +) -> Result<Vec<RadrootsNostrTag>, RadrootsNostrTagsResolveError> { + if !event + .tags + .iter() + .any(|t| t.kind() == RadrootsNostrTagKind::Encrypted) + { return Ok(event.clone().tags.to_vec()); } let recipient = event .tags .iter() .find_map(|tag| { - if tag.kind() == TagKind::p() { - tag.content()?.parse::<PublicKey>().ok() + if tag.kind() == RadrootsNostrTagKind::p() { + tag.content()?.parse::<RadrootsNostrPublicKey>().ok() } else { None } }) - .ok_or_else(|| NostrTagsResolveError::MissingPTag(event.clone()))?; + .ok_or_else(|| RadrootsNostrTagsResolveError::MissingPTag(event.clone()))?; if recipient != keys.public_key() { - return Err(NostrTagsResolveError::NotRecipient); + return Err(RadrootsNostrTagsResolveError::NotRecipient); } let cleartext = nip04::decrypt(keys.secret_key(), &event.pubkey, &event.content) - .map_err(|e| NostrTagsResolveError::DecryptionError(e.to_string()))?; + .map_err(|e| RadrootsNostrTagsResolveError::DecryptionError(e.to_string()))?; let decrypted_tags: nostr::event::tag::list::Tags = serde_json::from_str(&cleartext)?; Ok(decrypted_tags.to_vec()) } diff --git a/nostr/src/types.rs b/nostr/src/types.rs @@ -0,0 +1,46 @@ +#![forbid(unsafe_code)] + +pub type RadrootsNostrCoordinate = nostr::nips::nip01::Coordinate; +pub type RadrootsNostrEvent = nostr::Event; +pub type RadrootsNostrEventBuilder = nostr::EventBuilder; +pub type RadrootsNostrEventId = nostr::EventId; +pub type RadrootsNostrFilter = nostr::Filter; +pub type RadrootsNostrKind = nostr::Kind; +pub type RadrootsNostrKeys = nostr::Keys; +pub type RadrootsNostrMetadata = nostr::Metadata; +pub type RadrootsNostrPublicKey = nostr::PublicKey; +pub type RadrootsNostrRelayUrl = nostr::RelayUrl; +pub type RadrootsNostrSecretKey = nostr::SecretKey; +pub type RadrootsNostrSubscriptionId = nostr::SubscriptionId; +pub type RadrootsNostrTag = nostr::Tag; +pub type RadrootsNostrTagKind<'a> = nostr::TagKind<'a>; +pub type RadrootsNostrTagStandard = nostr::TagStandard; +pub type RadrootsNostrTimestamp = nostr::Timestamp; +pub type RadrootsNostrUrl = nostr::Url; + +pub use nostr::nips::nip19::{ + FromBech32 as RadrootsNostrFromBech32, + ToBech32 as RadrootsNostrToBech32, +}; +pub use nostr::secp256k1::SecretKey as RadrootsNostrSecp256k1SecretKey; + +#[cfg(feature = "client")] +pub type RadrootsNostrMonitor = nostr_sdk::prelude::Monitor; + +#[cfg(feature = "client")] +pub type RadrootsNostrMonitorNotification = nostr_sdk::prelude::MonitorNotification; + +#[cfg(feature = "client")] +pub type RadrootsNostrOutput<T> = nostr_sdk::prelude::Output<T>; + +#[cfg(feature = "client")] +pub type RadrootsNostrRelay = nostr_sdk::Relay; + +#[cfg(feature = "client")] +pub type RadrootsNostrRelayPoolNotification = nostr_sdk::RelayPoolNotification; + +#[cfg(feature = "client")] +pub type RadrootsNostrRelayStatus = nostr_sdk::RelayStatus; + +#[cfg(feature = "client")] +pub type RadrootsNostrSubscribeAutoCloseOptions = nostr_sdk::SubscribeAutoCloseOptions; diff --git a/nostr/src/util.rs b/nostr/src/util.rs @@ -1,14 +1,19 @@ -use nostr::{event::Event, key::PublicKey, nips::nip19::ToBech32, Timestamp}; +use crate::types::{ + RadrootsNostrEvent, + RadrootsNostrPublicKey, + RadrootsNostrTimestamp, + RadrootsNostrToBech32, +}; -pub fn npub_string(pk: &PublicKey) -> Option<String> { +pub fn radroots_nostr_npub_string(pk: &RadrootsNostrPublicKey) -> Option<String> { pk.to_bech32().ok() } -pub fn created_at_u32_saturating(ts: Timestamp) -> u32 { +pub fn created_at_u32_saturating(ts: RadrootsNostrTimestamp) -> u32 { u32::try_from(ts.as_u64()).unwrap_or(u32::MAX) } -pub fn event_created_at_u32_saturating(event: &Event) -> u32 { +pub fn event_created_at_u32_saturating(event: &RadrootsNostrEvent) -> u32 { created_at_u32_saturating(event.created_at) } diff --git a/runtime/src/backoff.rs b/runtime/src/backoff.rs @@ -0,0 +1,100 @@ +use core::time::Duration; +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn default_base_ms() -> u64 { + 500 +} + +fn default_max_ms() -> u64 { + 30_000 +} + +fn default_factor() -> u32 { + 2 +} + +fn default_jitter_ms() -> u64 { + 0 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackoffConfig { + #[serde(default = "default_base_ms", alias = "reconnect_base_ms")] + pub base_ms: u64, + #[serde(default = "default_max_ms", alias = "reconnect_max_ms")] + pub max_ms: u64, + #[serde(default = "default_factor", alias = "reconnect_factor")] + pub factor: u32, + #[serde(default = "default_jitter_ms", alias = "reconnect_jitter_ms")] + pub jitter_ms: u64, +} + +impl Default for BackoffConfig { + fn default() -> Self { + Self { + base_ms: default_base_ms(), + max_ms: default_max_ms(), + factor: default_factor(), + jitter_ms: default_jitter_ms(), + } + } +} + +impl BackoffConfig { + pub fn delay_for_attempt(&self, attempt: u32) -> Duration { + let base = self.base_ms.max(1); + let max = self.max_ms.max(base); + let factor = self.factor.max(1) as u64; + + let mut delay = base; + let steps = attempt.saturating_sub(1).min(10); + for _ in 0..steps { + delay = delay.saturating_mul(factor).min(max); + } + + if self.jitter_ms > 0 { + let jitter = jitter_ms(self.jitter_ms); + delay = delay.saturating_add(jitter).min(max); + } + + Duration::from_millis(delay) + } +} + +#[derive(Debug, Clone)] +pub struct Backoff { + cfg: BackoffConfig, + attempt: u32, +} + +impl Backoff { + pub fn new(cfg: BackoffConfig) -> Self { + Self { cfg, attempt: 0 } + } + + pub fn reset(&mut self) { + self.attempt = 0; + } + + pub fn next_delay(&mut self) -> Duration { + let attempt = self.attempt.saturating_add(1); + self.attempt = attempt; + self.cfg.delay_for_attempt(attempt) + } + + pub fn attempt(&self) -> u32 { + self.attempt + } +} + +fn jitter_ms(max: u64) -> u64 { + if max == 0 { + return 0; + } + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .subsec_nanos() as u64; + nanos % (max + 1) +} diff --git a/runtime/src/cli.rs b/runtime/src/cli.rs @@ -40,6 +40,42 @@ where Ok((args, cfg)) } +pub fn parse_and_load_path_with_init<Args, C, FP, FL>( + path_of: FP, + logs_dir_of: FL, + default_level: Option<&str>, +) -> Result<(Args, C), RuntimeError> +where + Args: Parser, + C: DeserializeOwned, + FP: Fn(&Args) -> Option<&Path>, + FL: Fn(&C) -> &str, +{ + let (args, cfg) = parse_and_load_path::<Args, C, FP>(path_of)?; + crate::tracing::init_with(logs_dir_of(&cfg), default_level)?; + Ok((args, cfg)) +} + +pub fn parse_and_load_path_with_env_overrides_and_init<Args, C, FP, FO, FL>( + path_of: FP, + env_prefix: Option<&str>, + overrides_of: FO, + logs_dir_of: FL, + default_level: Option<&str>, +) -> Result<(Args, C), RuntimeError> +where + Args: Parser, + C: DeserializeOwned, + FP: Fn(&Args) -> Option<&Path>, + FO: Fn(&Args) -> Option<Map<String, Value>>, + FL: Fn(&C) -> &str, +{ + let (args, cfg) = + parse_and_load_path_with_env_overrides::<Args, C, FP, FO>(path_of, env_prefix, overrides_of)?; + crate::tracing::init_with(logs_dir_of(&cfg), default_level)?; + Ok((args, cfg)) +} + #[inline] fn resolve_path(p: Option<&Path>) -> PathBuf { p.unwrap_or_else(|| Path::new("config.toml")).to_path_buf() diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs @@ -1,5 +1,6 @@ #[cfg(feature = "cli")] pub mod cli; +pub mod backoff; pub mod config; pub mod error; pub mod json; @@ -8,6 +9,10 @@ pub mod tracing; #[cfg(feature = "cli")] pub use cli::{parse_and_load_path, parse_and_load_path_with_env_overrides}; +#[cfg(feature = "cli")] +pub use cli::{parse_and_load_path_with_env_overrides_and_init, parse_and_load_path_with_init}; + +pub use backoff::{Backoff, BackoffConfig}; pub use config::{ load_required_file, load_required_file_with_env, load_required_file_with_env_and_overrides, diff --git a/trade/Cargo.toml b/trade/Cargo.toml @@ -12,7 +12,7 @@ default = ["std", "serde", "serde_json", "ts-rs"] std = [] serde = ["dep:serde", "radroots-core/serde", "radroots-events/serde", "radroots-events-codec/serde"] serde_json = ["serde", "dep:serde_json"] -ts-rs = ["dep:ts-rs"] +ts-rs = ["dep:ts-rs", "radroots-events/ts-rs", "radroots-events/std"] [dependencies] radroots-core = { workspace = true, default-features = false } diff --git a/trade/bindings/ts/package.json b/trade/bindings/ts/package.json @@ -23,7 +23,7 @@ "build:cjs": "tsc -p tsconfig.cjs.json", "build": "npm run build:esm && npm run build:cjs", "prebuild": "npm run clean && npm run prepend-imports", - "prepend-imports": "bash -c 'f=./src/types.ts; line=\"import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from \\\"@radroots/core-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")' && bash -c 'f=./src/types.ts; line=\"import type { RadrootsListingDiscount, RadrootsListingQuantity, RadrootsNostrEventPtr } from \\\"@radroots/events-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")'", + "prepend-imports": "bash -c 'f=./src/types.ts; line=\"import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from \\\"@radroots/core-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")' && bash -c 'f=./src/types.ts; line=\"import type { RadrootsListingDiscount, RadrootsListingImage, RadrootsNostrEventPtr } from \\\"@radroots/events-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")'", "clean": "rimraf dist", "dev": "npm run watch", "watch": "tsc -w" @@ -41,4 +41,4 @@ "publishConfig": { "access": "public" } -} +} +\ No newline at end of file diff --git a/trade/bindings/ts/src/types.ts b/trade/bindings/ts/src/types.ts @@ -1,23 +1,57 @@ -import type { RadrootsListingDiscount, RadrootsListingQuantity, RadrootsNostrEventPtr } from "@radroots/events-bindings"; +import type { RadrootsListingDiscount, RadrootsListingImage, RadrootsNostrEventPtr } from "@radroots/events-bindings"; -import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from "@radroots/core-bindings"; +import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from "@radroots/core-bindings"; // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +export type RadrootsListing = { d_tag: string, product: RadrootsListingProduct, quantities: Array<RadrootsListingQuantity>, prices: RadrootsCoreQuantityPrice[], discounts?: RadrootsListingDiscount[] | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, }; + +export type RadrootsListingAvailability = { "kind": "window", "amount": { start?: number | null, end?: number | null, } } | { "kind": "status", "amount": { status: RadrootsListingStatus, } }; + +export type RadrootsListingDeliveryMethod = { "kind": "pickup" } | { "kind": "local_delivery" } | { "kind": "shipping" } | { "kind": "other", "amount": { method: string, } }; + +export type RadrootsListingLocation = { primary: string, city?: string | null, region?: string | null, country?: string | null, lat?: number | null, lng?: number | null, geohash?: string | null, }; + +export type RadrootsListingProduct = { key: string, title: string, category: string, summary?: string | null, process?: string | null, lot?: string | null, location?: string | null, profile?: string | null, year?: string | null, }; + +export type RadrootsListingQuantity = { value: RadrootsCoreQuantity, label?: string | null, count?: number | null, }; + +export type RadrootsListingStatus = { "kind": "active" } | { "kind": "sold" } | { "kind": "other", "amount": { value: string, } }; + +export type RadrootsTradeListing = { listing_id: string, listing_addr: string, seller_pubkey: string, title: string, description: string, product_type: string, unit: RadrootsCoreUnit, unit_price: RadrootsCoreMoney, inventory_available: RadrootsCoreDecimal, availability: RadrootsListingAvailability, location: RadrootsListingLocation, delivery_method: RadrootsListingDeliveryMethod, listing: RadrootsListing, }; + export type RadrootsTradeListingSubtotal = { price_amount: RadrootsCoreMoney, price_currency: RadrootsCoreCurrency, quantity_amount: RadrootsCoreDecimal, quantity_unit: RadrootsCoreUnit, }; export type RadrootsTradeListingTotal = { price_amount: RadrootsCoreMoney, price_currency: RadrootsCoreCurrency, quantity_amount: RadrootsCoreDecimal, quantity_unit: RadrootsCoreUnit, }; +export type TradeAnswer = { question_id: string, order_id?: string | null, listing_addr?: string | null, answer_text: string, }; + +export type TradeDiscountDecision = { "kind": "accept", "amount": { value: RadrootsCoreDiscountValue, } } | { "kind": "decline", "amount": { reason?: string | null, } }; + +export type TradeDiscountOffer = { discount_id: string, order_id: string, value: RadrootsCoreDiscountValue, conditions?: string | null, }; + +export type TradeDiscountRequest = { discount_id: string, order_id: string, value: RadrootsCoreDiscountValue, conditions?: string | null, }; + +export type TradeFulfillmentStatus = { "kind": "preparing" } | { "kind": "shipped" } | { "kind": "ready_for_pickup" } | { "kind": "delivered" } | { "kind": "cancelled" }; + +export type TradeFulfillmentUpdate = { status: TradeFulfillmentStatus, tracking?: string | null, eta?: string | null, notes?: string | null, }; + export type TradeListingAcceptRequest = { order_result_event_id: string, listing_event_id: string, }; export type TradeListingAcceptResult = { listing_event_id: string, order_result_event_id: string, accepted_by: string, }; +export type TradeListingCancel = { reason?: string | null, }; + export type TradeListingConveyanceMethod = { "kind": "seller_delivery", "amount": { window: string | null, notes: string | null, } } | { "kind": "buyer_pickup", "amount": { location_hint: string | null, by_when: string | null, } } | { "kind": "third_party", "amount": { provider: string, ref_id: string | null, notes: string | null, } }; export type TradeListingConveyanceRequest = { accept_result_event_id: string, method: TradeListingConveyanceMethod, }; export type TradeListingConveyanceResult = { verified: boolean, method: TradeListingConveyanceMethod, message?: string | null, }; +export type TradeListingDomain = "trade:listing"; + +export type TradeListingEnvelope<T> = { version: number, domain: TradeListingDomain, type: TradeListingMessageType, order_id?: string | null, listing_addr: string, payload: T, }; + export type TradeListingFulfillmentRequest = { payment_result_event_id: string, }; export type TradeListingFulfillmentResult = { state: TradeListingFulfillmentState, tracking?: string | null, eta?: string | null, notes?: string | null, }; @@ -28,12 +62,18 @@ export type TradeListingInvoiceRequest = { accept_result_event_id: string, }; export type TradeListingInvoiceResult = { total_sat: number, bolt11?: string | null, note?: string | null, expires_at?: number | null, }; +export type TradeListingMessagePayload = { "kind": "listing_validate_request", "amount": TradeListingValidateRequest } | { "kind": "listing_validate_result", "amount": TradeListingValidateResult } | { "kind": "order_request", "amount": TradeOrder } | { "kind": "order_response", "amount": TradeOrderResponse } | { "kind": "order_revision", "amount": TradeOrderRevision } | { "kind": "order_revision_accept", "amount": TradeOrderRevisionResponse } | { "kind": "order_revision_decline", "amount": TradeOrderRevisionResponse } | { "kind": "question", "amount": TradeQuestion } | { "kind": "answer", "amount": TradeAnswer } | { "kind": "discount_request", "amount": TradeDiscountRequest } | { "kind": "discount_offer", "amount": TradeDiscountOffer } | { "kind": "discount_accept", "amount": TradeDiscountDecision } | { "kind": "discount_decline", "amount": TradeDiscountDecision } | { "kind": "cancel", "amount": TradeListingCancel } | { "kind": "fulfillment_update", "amount": TradeFulfillmentUpdate } | { "kind": "receipt", "amount": TradeReceipt }; + +export type TradeListingMessageType = "listing_validate_request" | "listing_validate_result" | "order_request" | "order_response" | "order_revision" | "order_revision_accept" | "order_revision_decline" | "question" | "answer" | "discount_request" | "discount_offer" | "discount_accept" | "discount_decline" | "cancel" | "fulfillment_update" | "receipt"; + export type TradeListingOrderRequest = { event: RadrootsNostrEventPtr, payload: TradeListingOrderRequestPayload, }; export type TradeListingOrderRequestPayload = { price: RadrootsCoreQuantityPrice, quantity: RadrootsListingQuantity, }; export type TradeListingOrderResult = { quantity: RadrootsListingQuantity, price: RadrootsCoreQuantityPrice, discounts: RadrootsListingDiscount[], subtotal: RadrootsTradeListingSubtotal, total: RadrootsTradeListingTotal, }; +export type TradeListingParseError = { "MissingTag": string } | { "InvalidTag": string } | { "InvalidNumber": string } | "InvalidUnit" | "InvalidCurrency" | { "InvalidJson": string } | { "InvalidDiscount": string }; + export type TradeListingPaymentProof = { "kind": "zap_event", "amount": { id: string, } } | { "kind": "preimage", "amount": { hex: string, } } | { "kind": "txid", "amount": { id: string, } } | { "kind": "external_ref", "amount": { provider: string, ref_id: string, } }; export type TradeListingPaymentProofRequest = { invoice_result_event_id: string, proof: TradeListingPaymentProof, }; @@ -46,6 +86,32 @@ export type TradeListingReceiptResult = { acknowledged: boolean, at: number, }; export type TradeListingStage = { "kind": "order" } | { "kind": "accept" } | { "kind": "conveyance" } | { "kind": "invoice" } | { "kind": "payment" } | { "kind": "fulfillment" } | { "kind": "receipt" } | { "kind": "cancel" } | { "kind": "refund" }; +export type TradeListingValidateRequest = { listing_event?: RadrootsNostrEventPtr | null, }; + +export type TradeListingValidateResult = { valid: boolean, errors: TradeListingValidationError[], }; + +export type TradeListingValidationError = { "kind": "invalid_kind", "amount": { kind: number, } } | { "kind": "missing_listing_id" } | { "kind": "listing_event_not_found", "amount": { listing_addr: string, } } | { "kind": "listing_event_fetch_failed", "amount": { listing_addr: string, } } | { "kind": "parse_error", "amount": { error: TradeListingParseError, } } | { "kind": "missing_title" } | { "kind": "missing_description" } | { "kind": "missing_product_type" } | { "kind": "missing_price" } | { "kind": "invalid_price" } | { "kind": "missing_inventory" } | { "kind": "invalid_inventory" } | { "kind": "missing_availability" } | { "kind": "missing_location" } | { "kind": "missing_delivery_method" }; + +export type TradeOrder = { order_id: string, listing_addr: string, buyer_pubkey: string, seller_pubkey: string, items: Array<TradeOrderItem>, discounts?: RadrootsCoreDiscountValue[] | null, notes?: string | null, status: TradeOrderStatus, }; + +export type TradeOrderChange = { "kind": "quantity", "amount": { item_index: number, quantity: RadrootsCoreQuantity, } } | { "kind": "price", "amount": { item_index: number, unit_price: RadrootsCoreMoney, } } | { "kind": "item_add", "amount": { item: TradeOrderItem, } } | { "kind": "item_remove", "amount": { item_index: number, } }; + +export type TradeOrderItem = { quantity: RadrootsCoreQuantity, unit_price: RadrootsCoreMoney, }; + +export type TradeOrderResponse = { accepted: boolean, reason?: string | null, }; + +export type TradeOrderRevision = { revision_id: string, order_id: string, changes: Array<TradeOrderChange>, reason?: string | null, }; + +export type TradeOrderRevisionResponse = { accepted: boolean, reason?: string | null, }; + +export type TradeOrderStatus = "draft" | "validated" | "requested" | "questioned" | "revised" | "accepted" | "declined" | "cancelled" | "fulfilled" | "completed"; + +export type TradeQuestion = { question_id: string, order_id?: string | null, listing_addr?: string | null, question_text: string, }; + +export type TradeReceipt = { acknowledged: boolean, at: bigint, note?: string | null, }; + export enum TradeListingMarker { "listing" = "listing", "payload" = "payload", "previous" = "previous", "order_result" = "order_result", "accept_result" = "accept_result", "conveyance_result" = "conveyance_result", "invoice_result" = "invoice_result", "payment_result" = "payment_result", "fulfillment_result" = "fulfillment_result", "receipt_result" = "receipt_result", "cancel_result" = "cancel_result", "refund_result" = "refund_result", "proof" = "proof" } export enum TradeListingKind { "KIND_TRADE_LISTING_ORDER_REQ" = 5301, "KIND_TRADE_LISTING_ORDER_RES" = 6301, "KIND_TRADE_LISTING_ACCEPT_REQ" = 5302, "KIND_TRADE_LISTING_ACCEPT_RES" = 6302, "KIND_TRADE_LISTING_CONVEYANCE_REQ" = 5303, "KIND_TRADE_LISTING_CONVEYANCE_RES" = 6303, "KIND_TRADE_LISTING_INVOICE_REQ" = 5304, "KIND_TRADE_LISTING_INVOICE_RES" = 6304, "KIND_TRADE_LISTING_PAYMENT_REQ" = 5305, "KIND_TRADE_LISTING_PAYMENT_RES" = 6305, "KIND_TRADE_LISTING_FULFILL_REQ" = 5306, "KIND_TRADE_LISTING_FULFILL_RES" = 6306, "KIND_TRADE_LISTING_RECEIPT_REQ" = 5307, "KIND_TRADE_LISTING_RECEIPT_RES" = 6307, "KIND_TRADE_LISTING_CANCEL_REQ" = 5309, "KIND_TRADE_LISTING_CANCEL_RES" = 6309, "KIND_TRADE_LISTING_REFUND_REQ" = 5310, "KIND_TRADE_LISTING_REFUND_RES" = 6310 } + +export enum TradeListingDvmKind { "KIND_TRADE_LISTING_VALIDATE_REQ" = 5321, "KIND_TRADE_LISTING_VALIDATE_RES" = 6321, "KIND_TRADE_LISTING_ORDER_REQ" = 5322, "KIND_TRADE_LISTING_ORDER_RES" = 6322, "KIND_TRADE_LISTING_ORDER_REVISION_REQ" = 5323, "KIND_TRADE_LISTING_ORDER_REVISION_RES" = 6323, "KIND_TRADE_LISTING_QUESTION_REQ" = 5324, "KIND_TRADE_LISTING_ANSWER_RES" = 6324, "KIND_TRADE_LISTING_DISCOUNT_REQ" = 5325, "KIND_TRADE_LISTING_DISCOUNT_OFFER_RES" = 6325, "KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ" = 5326, "KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ" = 5327, "KIND_TRADE_LISTING_CANCEL_REQ" = 5328, "KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ" = 5329, "KIND_TRADE_LISTING_RECEIPT_REQ" = 5330 } diff --git a/trade/src/listing/validation.rs b/trade/src/listing/validation.rs @@ -14,7 +14,7 @@ use radroots_events::{ #[cfg(feature = "ts-rs")] use ts_rs::TS; -use crate::listing::codec::{listing_from_event_parts, TradeListingParseError}; +use crate::listing::codec::{TradeListingParseError, listing_from_event_parts}; use crate::listing::dvm::TradeListingAddress; const LISTING_KIND: u32 = 30402; @@ -45,7 +45,10 @@ pub struct RadrootsTradeListing { #[cfg_attr(feature = "ts-rs", derive(TS))] #[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "kind", content = "amount"))] +#[cfg_attr( + feature = "serde", + serde(rename_all = "snake_case", tag = "kind", content = "amount") +)] #[derive(Clone, Debug, PartialEq, Eq)] pub enum TradeListingValidationError { InvalidKind { kind: u32 }, @@ -164,12 +167,11 @@ pub fn validate_listing_event( return Err(TradeListingValidationError::InvalidPrice); } - let inventory_available = - listing - .inventory_available - .clone() - .or_else(|| derive_inventory(&listing)) - .ok_or(TradeListingValidationError::MissingInventory)?; + let inventory_available = listing + .inventory_available + .clone() + .or_else(|| derive_inventory(&listing)) + .ok_or(TradeListingValidationError::MissingInventory)?; if inventory_available.is_sign_negative() { return Err(TradeListingValidationError::InvalidInventory); } @@ -205,18 +207,18 @@ pub fn validate_listing_event( } fn derive_inventory(listing: &RadrootsListing) -> Option<RadrootsCoreDecimal> { - listing - .quantities - .iter() - .find_map(|qty| qty.count.map(|count| qty.value.amount * RadrootsCoreDecimal::from(count))) + listing.quantities.iter().find_map(|qty| { + qty.count + .map(|count| qty.value.amount * RadrootsCoreDecimal::from(count)) + }) } #[cfg(test)] mod tests { - use super::{validate_listing_event, TradeListingValidationError}; + use super::{TradeListingValidationError, validate_listing_event}; use radroots_core::{ - RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, - RadrootsCoreUnit, + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, + RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::{ RadrootsNostrEvent, @@ -302,7 +304,10 @@ mod tests { let mut event = base_event(&listing); event.tags.clear(); let err = validate_listing_event(&event).unwrap_err(); - assert!(matches!(err, TradeListingValidationError::ParseError { .. })); + assert!(matches!( + err, + TradeListingValidationError::ParseError { .. } + )); } #[test] @@ -315,14 +320,34 @@ mod tests { vec!["title".into(), "Coffee".into()], vec!["category".into(), "coffee".into()], vec!["summary".into(), "Single origin".into()], - vec!["quantity".into(), "1".into(), "lb".into(), "bag".into(), "5".into()], - vec!["price".into(), "20".into(), "US".into(), "1".into(), "lb".into()], - vec!["location".into(), "Farm".into(), "Town".into(), "Region".into()], + vec![ + "quantity".into(), + "1".into(), + "lb".into(), + "bag".into(), + "5".into(), + ], + vec![ + "price".into(), + "20".into(), + "US".into(), + "1".into(), + "lb".into(), + ], + vec![ + "location".into(), + "Farm".into(), + "Town".into(), + "Region".into(), + ], vec!["status".into(), "active".into()], vec!["delivery".into(), "pickup".into()], ]; let err = validate_listing_event(&event).unwrap_err(); - assert!(matches!(err, TradeListingValidationError::ParseError { .. })); + assert!(matches!( + err, + TradeListingValidationError::ParseError { .. } + )); } #[test] @@ -331,9 +356,6 @@ mod tests { listing.quantities[0].count = None; let event = base_event(&listing); let err = validate_listing_event(&event).unwrap_err(); - assert!(matches!( - err, - TradeListingValidationError::MissingInventory - )); + assert!(matches!(err, TradeListingValidationError::MissingInventory)); } }