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:
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));
}
}