radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit 6b311689a6b95475baf4a707d584c6f3fe7c9463
parent be905eddbad3f7929c61328707dc8025f0205818
Author: triesap <triesap@radroots.dev>
Date:   Wed, 24 Dec 2025 14:55:11 +0000

core: refactor RPC and nostr integration

- Add trade.listing RPC domain endpoints (get/list/series/orders/dvm.list)
- Replace direct nostr/nostr-sdk usage with radroots_nostr prelude types and helpers
- Migrate identity handling to RadrootsIdentity and broaden identity file input formats
- Bump toolchain to 1.88, update crate paths, and change default relay to ws://127.0.0.1:8080

Diffstat:
MCargo.lock | 50+++++++++++++++++++++++++++++++++++++++++++++-----
MCargo.toml | 18++++++++----------
Mconfig.toml | 2+-
Ddiff.txt | 0
Midentity.json | 2+-
Mrust-toolchain.toml | 4++--
Msrc/cli.rs | 2+-
Msrc/config.rs | 3++-
Dsrc/identity.rs | 24------------------------
Msrc/lib.rs | 8++++----
Msrc/main.rs | 26+++++++++++++++++---------
Msrc/radrootsd.rs | 18++++++++++++------
Asrc/rpc/domains/mod.rs | 3+++
Asrc/rpc/domains/trade/listing/dvm.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/rpc/domains/trade/listing/get.rs | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/rpc/domains/trade/listing/helpers.rs | 231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/rpc/domains/trade/listing/list.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/rpc/domains/trade/listing/mod.rs | 25+++++++++++++++++++++++++
Asrc/rpc/domains/trade/listing/orders.rs | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/rpc/domains/trade/listing/series.rs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/rpc/domains/trade/listing/types.rs | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/rpc/domains/trade/mod.rs | 14++++++++++++++
Msrc/rpc/events/listing/list.rs | 19++++++++++---------
Msrc/rpc/events/listing/publish.rs | 14++++++++++----
Msrc/rpc/events/mod.rs | 2+-
Dsrc/rpc/events/note/list.rs | 66------------------------------------------------------------------
Dsrc/rpc/events/note/publish.rs | 54------------------------------------------------------
Asrc/rpc/events/post/list.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/rpc/events/note/mod.rs -> src/rpc/events/post/mod.rs | 0
Asrc/rpc/events/post/publish.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/rpc/events/profile/list.rs | 13++++++++-----
Msrc/rpc/events/profile/publish.rs | 11+++++++----
Msrc/rpc/mod.rs | 4+++-
Msrc/rpc/relays/add.rs | 4++--
Msrc/rpc/relays/connect.rs | 9++++-----
Msrc/rpc/relays/remove.rs | 4++--
Msrc/rpc/system.rs | 4++--
37 files changed, 997 insertions(+), 219 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1758,6 +1758,7 @@ version = "0.1.0" dependencies = [ "radroots-core", "serde", + "ts-rs", "typeshare", ] @@ -1778,9 +1779,9 @@ dependencies = [ "nostr", "radroots-runtime", "serde", + "serde_json", "thiserror 1.0.69", "tracing", - "uuid", ] [[package]] @@ -1791,6 +1792,7 @@ dependencies = [ "nostr-sdk", "radroots-events", "radroots-events-codec", + "radroots-identity", "reqwest", "serde", "serde_json", @@ -1823,7 +1825,8 @@ dependencies = [ "radroots-events", "radroots-events-codec", "serde", - "typeshare", + "serde_json", + "ts-rs", ] [[package]] @@ -1833,8 +1836,6 @@ dependencies = [ "anyhow", "clap", "jsonrpsee", - "nostr", - "nostr-sdk", "radroots-core", "radroots-events", "radroots-events-codec", @@ -2367,6 +2368,15 @@ dependencies = [ ] [[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2741,6 +2751,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] +name = "ts-rs" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424" +dependencies = [ + "thiserror 2.0.17", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "termcolor", +] + +[[package]] name = "tungstenite" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2868,7 +2900,6 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", "wasm-bindgen", ] @@ -3028,6 +3059,15 @@ dependencies = [ ] [[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] name = "windows-core" version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -3,24 +3,22 @@ name = "radrootsd" version = "0.1.0" edition = "2024" authors = ["Radroots Authors"] -rust-version = "1.86.0" +rust-version = "1.88.0" license = "AGPL-3.0" description = "Radroots daemon binary" [dependencies] -radroots-core = { path = "../../crates/crates/core", features = ["std", "serde", "typeshare"] } -radroots-events = { path = "../../crates/crates/events", features = ["serde"] } -radroots-events-codec = { path = "../../crates/crates/events-codec", features = ["nostr"] } -radroots-identity = { path = "../../crates/crates/identity" } -radroots-nostr = { path = "../../crates/crates/nostr", features = ["sdk", "codec", "http"] } -radroots-runtime = { path = "../../crates/crates/runtime", features = ["cli"] } -radroots-trade = { path = "../../crates/crates/trade" } +radroots-core = { path = "../crates/core", features = ["std", "serde", "typeshare"] } +radroots-events = { path = "../crates/events", features = ["serde"] } +radroots-events-codec = { path = "../crates/events-codec", features = ["nostr"] } +radroots-identity = { path = "../crates/identity" } +radroots-nostr = { path = "../crates/nostr", features = ["client", "codec", "http"] } +radroots-runtime = { path = "../crates/runtime", features = ["cli"] } +radroots-trade = { path = "../crates/trade" } anyhow = { version = "1" } clap = { version = "4", features = ["derive"] } jsonrpsee = { version = "0.26", features = ["server"] } -nostr = { version = "0.43.0", features = ["nip04"] } -nostr-sdk = { version = "0.43.0" } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false } diff --git a/config.toml b/config.toml @@ -13,5 +13,5 @@ name = "radrootsd" logs_dir = "logs" rpc_addr = "127.0.0.1:7070" relays = [ - "ws://127.0.0.1:21648" + "ws://127.0.0.1:8080" ] \ No newline at end of file diff --git a/diff.txt b/diff.txt diff --git a/identity.json b/identity.json @@ -1,3 +1,3 @@ { - "key": "1ae3ba0030d8e3a9f34bf1f1181cb1aea67d148980f13df86c8976cd0ac1000d" + "secret_key": "e8f44e66f455231d6f22334dfa87d014c75c2718e13de7915dba627f0c78f42f" } \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.86.0" -\ No newline at end of file +channel = "1.88.0" +\ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs @@ -22,7 +22,7 @@ pub struct Args { long, value_name = "PATH", value_hint = ValueHint::FilePath, - help = "Path to the daemon identity JSON file (defaults to identity.json)", + help = "Path to the daemon identity file (json, txt, or raw 32-byte key; defaults to identity.json)", )] pub identity: Option<PathBuf>, diff --git a/src/config.rs b/src/config.rs @@ -1,3 +1,4 @@ +use radroots_nostr::prelude::RadrootsNostrMetadata; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -9,6 +10,6 @@ pub struct Configuration { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { - pub metadata: nostr::Metadata, + pub metadata: RadrootsNostrMetadata, pub config: Configuration, } diff --git a/src/identity.rs b/src/identity.rs @@ -1,24 +0,0 @@ -use radroots_identity::IdentitySpec; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Identity { - pub key: String, -} - -impl IdentitySpec for Identity { - 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) - } -} diff --git a/src/lib.rs b/src/lib.rs @@ -1,6 +1,5 @@ pub mod cli; pub mod config; -pub mod identity; pub mod radrootsd; pub mod rpc; @@ -9,14 +8,15 @@ use anyhow::Result; pub use cli::Args as cli_args; use tracing::info; -use crate::{identity::Identity, radrootsd::Radrootsd}; +use crate::radrootsd::Radrootsd; +use radroots_identity::RadrootsIdentity; pub async fn run_radrootsd(settings: &config::Settings, args: &cli_args) -> Result<()> { - let identity = radroots_identity::load_or_generate::<Identity, _>( + let identity = RadrootsIdentity::load_or_generate( args.identity.as_ref(), args.allow_generate_identity, )?; - let keys = radroots_identity::to_keys(&identity.value)?; + let keys = identity.into_keys(); let radrootsd = Radrootsd::new(keys, settings.metadata.clone()); diff --git a/src/main.rs b/src/main.rs @@ -1,20 +1,28 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use radrootsd::{cli_args, config, run_radrootsd}; +use std::process::ExitCode; use tracing::info; #[tokio::main] -async fn main() { - if let Err(err) = setup().await { - eprintln!("Fatal error: {err:#?}"); - std::process::exit(1); +async fn main() -> ExitCode { + match run().await { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + tracing::error!(error = ?err, "Fatal error"); + eprintln!("Fatal error: {err:#}"); + ExitCode::FAILURE + } } } -async fn setup() -> Result<()> { +async fn run() -> Result<()> { let (args, settings): (cli_args, config::Settings) = - radroots_runtime::parse_and_load_path(|a: &cli_args| Some(a.config.as_path()))?; - - radroots_runtime::init_with(&settings.config.logs_dir, None)?; + radroots_runtime::parse_and_load_path_with_init( + |a: &cli_args| Some(a.config.as_path()), + |cfg: &config::Settings| cfg.config.logs_dir.as_str(), + None, + ) + .context("load configuration")?; info!("Starting radrootsd"); diff --git a/src/radrootsd.rs b/src/radrootsd.rs @@ -1,19 +1,25 @@ -use nostr_sdk::Client; use std::time::Instant; +use radroots_nostr::prelude::{ + RadrootsNostrClient, + RadrootsNostrKeys, + RadrootsNostrMetadata, + RadrootsNostrPublicKey, +}; + #[derive(Clone)] pub struct Radrootsd { pub(crate) started: Instant, - pub client: Client, - pub pubkey: nostr::PublicKey, - pub metadata: nostr::Metadata, + pub client: RadrootsNostrClient, + pub pubkey: RadrootsNostrPublicKey, + pub metadata: RadrootsNostrMetadata, pub info: serde_json::Value, } impl Radrootsd { - pub fn new(keys: nostr::Keys, metadata: nostr::Metadata) -> Self { + pub fn new(keys: RadrootsNostrKeys, metadata: RadrootsNostrMetadata) -> Self { let pubkey = keys.public_key(); - let client = Client::new(keys); + let client = RadrootsNostrClient::new(keys); let info = serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "build": option_env!("GIT_HASH").unwrap_or("unknown"), diff --git a/src/rpc/domains/mod.rs b/src/rpc/domains/mod.rs @@ -0,0 +1,3 @@ +#![forbid(unsafe_code)] + +pub mod trade; diff --git a/src/rpc/domains/trade/listing/dvm.rs b/src/rpc/domains/trade/listing/dvm.rs @@ -0,0 +1,94 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::{Deserialize, Serialize}; + +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use radroots_nostr::prelude::radroots_nostr_parse_pubkeys; +use radroots_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; + +use super::helpers::{fetch_dvm_events, parse_listing_addr}; +use super::types::DvmEventView; + +#[derive(Debug, Deserialize)] +struct TradeListingDvmListParams { + listing_addr: String, + #[serde(default)] + order_id: Option<String>, + #[serde(default)] + authors: Option<Vec<String>>, + #[serde(default)] + recipients: Option<Vec<String>>, + #[serde(default)] + kinds: Option<Vec<u16>>, + #[serde(default)] + limit: Option<u64>, + #[serde(default)] + since: Option<u64>, + #[serde(default)] + until: Option<u64>, + #[serde(default)] + timeout_secs: Option<u64>, +} + +#[derive(Clone, Debug, Serialize)] +struct TradeListingDvmListResponse { + events: Vec<DvmEventView>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("trade.listing.dvm.list", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let TradeListingDvmListParams { + listing_addr, + order_id, + authors, + recipients, + kinds, + limit, + since, + until, + timeout_secs, + } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let addr = parse_listing_addr(&listing_addr)?; + let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec()); + let authors = match authors { + Some(authors) => Some( + radroots_nostr_parse_pubkeys(&authors) + .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?, + ), + None => None, + }; + let recipients = match recipients { + Some(recipients) => Some( + radroots_nostr_parse_pubkeys(&recipients) + .map_err(|e| RpcError::InvalidParams(format!("invalid recipient: {e}")))?, + ), + None => None, + }; + + let events = fetch_dvm_events( + &ctx.client, + &addr, + &kinds, + order_id.as_deref(), + authors.as_deref(), + recipients.as_deref(), + since, + until, + limit, + timeout_secs.unwrap_or(10), + ) + .await?; + + Ok::<TradeListingDvmListResponse, RpcError>(TradeListingDvmListResponse { events }) + })?; + Ok(()) +} diff --git a/src/rpc/domains/trade/listing/get.rs b/src/rpc/domains/trade/listing/get.rs @@ -0,0 +1,42 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::{Deserialize, Serialize}; +use crate::{radrootsd::Radrootsd, rpc::RpcError}; + +use super::helpers::{fetch_latest_listing_event, listing_view, parse_listing_addr}; +use super::types::ListingEventView; + +#[derive(Debug, Deserialize)] +struct TradeListingGetParams { + listing_addr: String, + #[serde(default)] + timeout_secs: Option<u64>, +} + +#[derive(Clone, Debug, Serialize)] +struct TradeListingGetResponse { + listing: Option<ListingEventView>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("trade.listing.get", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let TradeListingGetParams { + listing_addr, + timeout_secs, + } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let addr = parse_listing_addr(&listing_addr)?; + let latest = fetch_latest_listing_event(&ctx.client, &addr, timeout_secs.unwrap_or(10)).await?; + let listing = latest.as_ref().map(listing_view); + Ok::<TradeListingGetResponse, RpcError>(TradeListingGetResponse { listing }) + })?; + Ok(()) +} diff --git a/src/rpc/domains/trade/listing/helpers.rs b/src/rpc/domains/trade/listing/helpers.rs @@ -0,0 +1,231 @@ +#![forbid(unsafe_code)] + +use std::collections::HashMap; +use std::time::Duration; + +use radroots_nostr::prelude::{ + radroots_nostr_parse_pubkey, + RadrootsNostrClient, + RadrootsNostrCoordinate, + RadrootsNostrEvent, + RadrootsNostrFilter, + RadrootsNostrKind, + RadrootsNostrPublicKey, + RadrootsNostrTimestamp, +}; +use radroots_trade::listing::{ + codec::listing_from_event_parts, + dvm::{TradeListingAddress, TradeListingEnvelope}, +}; + +use crate::rpc::domains::trade::listing::types::{ + DvmEventView, ListingEventView, NostrEventView, TradeListingOrderSummary, +}; +use crate::rpc::RpcError; + +pub(crate) const LISTING_KIND: u16 = 30402; + +pub(crate) fn event_tags(event: &RadrootsNostrEvent) -> Vec<Vec<String>> { + event.tags.iter().map(|t| t.as_slice().to_vec()).collect() +} + +pub(crate) fn event_view(event: &RadrootsNostrEvent) -> NostrEventView { + NostrEventView { + id: event.id.to_string(), + author: event.pubkey.to_string(), + created_at: event.created_at.as_u64(), + kind: event.kind.as_u16() as u32, + tags: event_tags(event), + content: event.content.clone(), + sig: event.sig.to_string(), + } +} + +pub(crate) fn listing_view(event: &RadrootsNostrEvent) -> ListingEventView { + let tags = event_tags(event); + let listing = listing_from_event_parts(&tags, &event.content).ok(); + ListingEventView { + event: event_view(event), + listing, + } +} + +pub(crate) fn parse_listing_addr(listing_addr: &str) -> Result<TradeListingAddress, RpcError> { + let addr = TradeListingAddress::parse(listing_addr) + .map_err(|_| RpcError::InvalidParams("invalid listing_addr".to_string()))?; + if addr.kind != LISTING_KIND { + return Err(RpcError::InvalidParams("unsupported listing kind".to_string())); + } + Ok(addr) +} + +pub(crate) fn listing_filter(addr: &TradeListingAddress) -> Result<RadrootsNostrFilter, RpcError> { + let author = radroots_nostr_parse_pubkey(&addr.seller_pubkey) + .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?; + Ok(RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::Custom(addr.kind)) + .author(author) + .identifier(addr.listing_id.clone())) +} + +pub(crate) async fn fetch_latest_listing_event( + client: &RadrootsNostrClient, + listing_addr: &TradeListingAddress, + timeout_secs: u64, +) -> Result<Option<RadrootsNostrEvent>, RpcError> { + let mut filter = listing_filter(listing_addr)?; + filter = filter.limit(25); + let events = client + .fetch_events(filter, Duration::from_secs(timeout_secs)) + .await + .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; + let mut latest: Option<RadrootsNostrEvent> = None; + for event in events { + match &latest { + Some(cur) if event.created_at <= cur.created_at => {} + _ => latest = Some(event), + } + } + Ok(latest) +} + +pub(crate) fn dvm_filter( + listing_addr: &TradeListingAddress, + kinds: &[u16], +) -> Result<RadrootsNostrFilter, RpcError> { + let author = radroots_nostr_parse_pubkey(&listing_addr.seller_pubkey) + .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?; + let coordinate = RadrootsNostrCoordinate::new( + RadrootsNostrKind::Custom(listing_addr.kind), + author, + ) + .identifier(listing_addr.listing_id.clone()); + let kinds = kinds + .iter() + .map(|kind| RadrootsNostrKind::Custom(*kind)) + .collect::<Vec<_>>(); + Ok(RadrootsNostrFilter::new() + .kinds(kinds) + .coordinate(&coordinate)) +} + +pub(crate) fn dvm_event_view(event: &RadrootsNostrEvent) -> DvmEventView { + let envelope = serde_json::from_str::<TradeListingEnvelope<serde_json::Value>>(&event.content) + .ok(); + let envelope_error = envelope + .as_ref() + .and_then(|env| env.validate().err()) + .map(|err| err.to_string()) + .or_else(|| { + if envelope.is_some() { + None + } else { + Some("invalid envelope json".to_string()) + } + }); + DvmEventView { + event: event_view(event), + envelope, + envelope_error, + } +} + +pub(crate) async fn fetch_dvm_events( + client: &RadrootsNostrClient, + listing_addr: &TradeListingAddress, + kinds: &[u16], + order_id: Option<&str>, + authors: Option<&[RadrootsNostrPublicKey]>, + recipients: Option<&[RadrootsNostrPublicKey]>, + since: Option<u64>, + until: Option<u64>, + limit: Option<u64>, + timeout_secs: u64, +) -> Result<Vec<DvmEventView>, RpcError> { + let mut filter = dvm_filter(listing_addr, kinds)?; + + if let Some(order_id) = order_id { + filter = filter.identifier(order_id); + } + if let Some(authors) = authors { + filter = filter.authors(authors.to_vec()); + } + if let Some(recipients) = recipients { + filter = filter.pubkeys(recipients.to_vec()); + } + if let Some(since) = since { + filter = filter.since(RadrootsNostrTimestamp::from_secs(since)); + } + if let Some(until) = until { + filter = filter.until(RadrootsNostrTimestamp::from_secs(until)); + } + if let Some(limit) = limit { + filter = filter.limit(limit.min(1000) as usize); + } + + let events = client + .fetch_events(filter, Duration::from_secs(timeout_secs)) + .await + .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; + + let mut out = events + .into_iter() + .map(|event| dvm_event_view(&event)) + .collect::<Vec<_>>(); + out.sort_by(|a, b| a.event.created_at.cmp(&b.event.created_at)); + Ok(out) +} + +pub(crate) fn order_id_from_event(event: &DvmEventView) -> Option<String> { + if let Some(envelope) = &event.envelope { + if let Some(order_id) = &envelope.order_id { + return Some(order_id.clone()); + } + } + event + .event + .tags + .iter() + .find_map(|tag| match tag.get(0).map(String::as_str) { + Some("d") => tag.get(1).cloned(), + _ => None, + }) +} + +pub(crate) fn order_summaries( + events: &[DvmEventView], + listing_addr: &str, +) -> Vec<TradeListingOrderSummary> { + let mut summary_map: HashMap<String, TradeListingOrderSummary> = HashMap::new(); + + for event in events { + let order_id = match order_id_from_event(event) { + Some(id) => id, + None => continue, + }; + let entry = summary_map.entry(order_id.clone()).or_insert_with(|| { + TradeListingOrderSummary { + order_id, + listing_addr: listing_addr.to_string(), + event_count: 0, + first_seen_at: event.event.created_at, + last_seen_at: event.event.created_at, + last_event_id: event.event.id.clone(), + last_event_kind: event.event.kind, + } + }); + entry.event_count += 1; + if event.event.created_at < entry.first_seen_at { + entry.first_seen_at = event.event.created_at; + } + if event.event.created_at >= entry.last_seen_at { + entry.last_seen_at = event.event.created_at; + entry.last_event_id = event.event.id.clone(); + entry.last_event_kind = event.event.kind; + } + } + + let mut summaries: Vec<TradeListingOrderSummary> = summary_map.into_values().collect(); + summaries.sort_by(|a, b| b.last_seen_at.cmp(&a.last_seen_at)); + summaries +} diff --git a/src/rpc/domains/trade/listing/list.rs b/src/rpc/domains/trade/listing/list.rs @@ -0,0 +1,80 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use radroots_nostr::prelude::{ + radroots_nostr_parse_pubkeys, + RadrootsNostrFilter, + RadrootsNostrKind, + RadrootsNostrTimestamp, +}; + +use super::helpers::{listing_view, LISTING_KIND}; +use super::types::ListingEventView; + +#[derive(Debug, Default, Deserialize)] +struct TradeListingListParams { + #[serde(default)] + authors: Option<Vec<String>>, + #[serde(default)] + limit: Option<u64>, + #[serde(default)] + since: Option<u64>, + #[serde(default)] + until: Option<u64>, +} + +#[derive(Clone, Debug, Serialize)] +struct TradeListingListResponse { + listings: Vec<ListingEventView>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("trade.listing.list", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let TradeListingListParams { + authors, + limit, + since, + until, + } = params.parse().unwrap_or_default(); + + let limit = limit.unwrap_or(50).min(1000) as usize; + + let mut filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::Custom(LISTING_KIND)) + .limit(limit); + if let Some(authors) = authors { + let pks = radroots_nostr_parse_pubkeys(&authors) + .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; + filter = filter.authors(pks); + } else { + filter = filter.author(ctx.pubkey); + } + if let Some(since) = since { + filter = filter.since(RadrootsNostrTimestamp::from_secs(since)); + } + if let Some(until) = until { + filter = filter.until(RadrootsNostrTimestamp::from_secs(until)); + } + + let events = ctx + .client + .fetch_events(filter, Duration::from_secs(10)) + .await + .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; + + let mut listings = events.into_iter().map(|ev| listing_view(&ev)).collect::<Vec<_>>(); + listings.sort_by(|a, b| b.event.created_at.cmp(&a.event.created_at)); + + Ok::<TradeListingListResponse, RpcError>(TradeListingListResponse { listings }) + })?; + Ok(()) +} diff --git a/src/rpc/domains/trade/listing/mod.rs b/src/rpc/domains/trade/listing/mod.rs @@ -0,0 +1,25 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; + +use crate::radrootsd::Radrootsd; + +pub mod dvm; +pub mod get; +pub mod list; +pub mod orders; +pub mod series; + +mod helpers; +mod types; + +pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { + let mut m = RpcModule::new(radrootsd); + get::register(&mut m)?; + list::register(&mut m)?; + dvm::register(&mut m)?; + series::register(&mut m)?; + orders::register(&mut m)?; + Ok(m) +} diff --git a/src/rpc/domains/trade/listing/orders.rs b/src/rpc/domains/trade/listing/orders.rs @@ -0,0 +1,93 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::{Deserialize, Serialize}; + +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use radroots_nostr::prelude::radroots_nostr_parse_pubkeys; +use radroots_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; + +use super::helpers::{fetch_dvm_events, order_summaries, parse_listing_addr}; +use super::types::TradeListingOrderSummary; + +#[derive(Debug, Deserialize)] +struct TradeListingOrdersParams { + listing_addr: String, + #[serde(default)] + authors: Option<Vec<String>>, + #[serde(default)] + recipients: Option<Vec<String>>, + #[serde(default)] + kinds: Option<Vec<u16>>, + #[serde(default)] + limit: Option<u64>, + #[serde(default)] + since: Option<u64>, + #[serde(default)] + until: Option<u64>, + #[serde(default)] + timeout_secs: Option<u64>, +} + +#[derive(Clone, Debug, Serialize)] +struct TradeListingOrdersResponse { + orders: Vec<TradeListingOrderSummary>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("trade.listing.orders.list", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let TradeListingOrdersParams { + listing_addr, + authors, + recipients, + kinds, + limit, + since, + until, + timeout_secs, + } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let addr = parse_listing_addr(&listing_addr)?; + let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec()); + let authors = match authors { + Some(authors) => Some( + radroots_nostr_parse_pubkeys(&authors) + .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?, + ), + None => None, + }; + let recipients = match recipients { + Some(recipients) => Some( + radroots_nostr_parse_pubkeys(&recipients) + .map_err(|e| RpcError::InvalidParams(format!("invalid recipient: {e}")))?, + ), + None => None, + }; + + let events = fetch_dvm_events( + &ctx.client, + &addr, + &kinds, + None, + authors.as_deref(), + recipients.as_deref(), + since, + until, + limit, + timeout_secs.unwrap_or(10), + ) + .await?; + + let orders = order_summaries(&events, &listing_addr); + + Ok::<TradeListingOrdersResponse, RpcError>(TradeListingOrdersResponse { orders }) + })?; + Ok(()) +} diff --git a/src/rpc/domains/trade/listing/series.rs b/src/rpc/domains/trade/listing/series.rs @@ -0,0 +1,103 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::{Deserialize, Serialize}; +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use radroots_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; + +use super::helpers::{ + fetch_dvm_events, fetch_latest_listing_event, listing_view, order_summaries, parse_listing_addr, +}; +use super::types::{TradeListingOrderSummary, TradeListingSeriesView}; + +#[derive(Debug, Deserialize)] +struct TradeListingSeriesParams { + listing_addr: String, + #[serde(default)] + order_id: Option<String>, + #[serde(default)] + include_listing: Option<bool>, + #[serde(default)] + include_dvm: Option<bool>, + #[serde(default)] + limit: Option<u64>, + #[serde(default)] + since: Option<u64>, + #[serde(default)] + until: Option<u64>, + #[serde(default)] + timeout_secs: Option<u64>, +} + +#[derive(Clone, Debug, Serialize)] +struct TradeListingSeriesResponse { + series: TradeListingSeriesView, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("trade.listing.series.get", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let TradeListingSeriesParams { + listing_addr, + order_id, + include_listing, + include_dvm, + limit, + since, + until, + timeout_secs, + } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let addr = parse_listing_addr(&listing_addr)?; + let include_listing = include_listing.unwrap_or(true); + let include_dvm = include_dvm.unwrap_or(true); + + let listing = if include_listing { + fetch_latest_listing_event(&ctx.client, &addr, timeout_secs.unwrap_or(10)) + .await? + .as_ref() + .map(listing_view) + } else { + None + }; + + let dvm_events = if include_dvm { + fetch_dvm_events( + &ctx.client, + &addr, + &TRADE_LISTING_DVM_KINDS, + order_id.as_deref(), + None, + None, + since, + until, + limit, + timeout_secs.unwrap_or(10), + ) + .await? + } else { + Vec::new() + }; + + let orders = if include_dvm { + order_summaries(&dvm_events, &listing_addr) + } else { + Vec::<TradeListingOrderSummary>::new() + }; + + let series = TradeListingSeriesView { + listing, + dvm_events, + orders, + }; + + Ok::<TradeListingSeriesResponse, RpcError>(TradeListingSeriesResponse { series }) + })?; + Ok(()) +} diff --git a/src/rpc/domains/trade/listing/types.rs b/src/rpc/domains/trade/listing/types.rs @@ -0,0 +1,47 @@ +#![forbid(unsafe_code)] + +use radroots_events::listing::RadrootsListing; +use radroots_trade::listing::dvm::TradeListingEnvelope; +use serde::Serialize; + +#[derive(Clone, Debug, Serialize)] +pub struct NostrEventView { + pub id: String, + pub author: String, + pub created_at: u64, + pub kind: u32, + pub tags: Vec<Vec<String>>, + pub content: String, + pub sig: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct ListingEventView { + pub event: NostrEventView, + pub listing: Option<RadrootsListing>, +} + +#[derive(Clone, Debug, Serialize)] +pub struct DvmEventView { + pub event: NostrEventView, + pub envelope: Option<TradeListingEnvelope<serde_json::Value>>, + pub envelope_error: Option<String>, +} + +#[derive(Clone, Debug, Serialize)] +pub struct TradeListingOrderSummary { + pub order_id: String, + pub listing_addr: String, + pub event_count: usize, + pub first_seen_at: u64, + pub last_seen_at: u64, + pub last_event_id: String, + pub last_event_kind: u32, +} + +#[derive(Clone, Debug, Serialize)] +pub struct TradeListingSeriesView { + pub listing: Option<ListingEventView>, + pub dvm_events: Vec<DvmEventView>, + pub orders: Vec<TradeListingOrderSummary>, +} diff --git a/src/rpc/domains/trade/mod.rs b/src/rpc/domains/trade/mod.rs @@ -0,0 +1,14 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; + +use crate::radrootsd::Radrootsd; + +pub mod listing; + +pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { + let mut m = RpcModule::new(radrootsd.clone()); + m.merge(listing::module(radrootsd)?)?; + Ok(m) +} diff --git a/src/rpc/events/listing/list.rs b/src/rpc/events/listing/list.rs @@ -5,8 +5,12 @@ use serde_json::{Value as JsonValue, json}; use std::time::Duration; use crate::{radrootsd::Radrootsd, rpc::RpcError}; -use nostr::{Kind, filter::Filter}; -use radroots_nostr::prelude::parse_pubkeys; +use radroots_nostr::prelude::{ + radroots_nostr_parse_pubkeys, + RadrootsNostrFilter, + RadrootsNostrKind, +}; +use radroots_trade::listing::codec::listing_from_event_parts; #[derive(Debug, Default, Deserialize)] struct ListListingParams { @@ -25,17 +29,17 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { let ListListingParams { authors, limit } = params.parse().unwrap_or_default(); let limit = limit.unwrap_or(50).min(1000); - let mut filter = Filter::new().limit((limit as u16).into()); + let mut filter = RadrootsNostrFilter::new().limit((limit as u16).into()); let kinds: Vec<u32> = vec![30402]; let kinds_conv = kinds .into_iter() - .map(|k| Kind::Custom(k as u16)) + .map(|k| RadrootsNostrKind::Custom(k as u16)) .collect::<Vec<_>>(); filter = filter.kinds(kinds_conv); if let Some(auths) = authors { - let pks = parse_pubkeys(&auths) + let pks = radroots_nostr_parse_pubkeys(&auths) .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; filter = filter.authors(pks); } else { @@ -53,10 +57,7 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { .map(|ev| { let tags: Vec<Vec<String>> = ev.tags.iter().map(|t| t.as_slice().to_vec()).collect(); - let listing = serde_json::from_str::< - radroots_events::listing::models::RadrootsListing, - >(&ev.content) - .ok(); + let listing = listing_from_event_parts(&tags, &ev.content).ok(); json!({ "id": ev.id.to_string(), diff --git a/src/rpc/events/listing/publish.rs b/src/rpc/events/listing/publish.rs @@ -4,8 +4,9 @@ use serde::Deserialize; use serde_json::{Value as JsonValue, json}; use crate::{radrootsd::Radrootsd, rpc::RpcError}; -use radroots_events::listing::models::RadrootsListing; -use radroots_nostr::prelude::{build_nostr_event, nostr_send_event}; +use radroots_events::listing::RadrootsListing; +use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; +use radroots_trade::listing::codec::listing_tags_build; #[derive(Debug, Deserialize)] struct PublishListingParams { @@ -25,10 +26,15 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { let content = serde_json::to_string(&listing) .map_err(|e| RpcError::InvalidParams(format!("invalid listing json: {e}")))?; - let builder = build_nostr_event(30402, content, tags.unwrap_or_default()) + let mut tag_slices = listing_tags_build(&listing) + .map_err(|e| RpcError::InvalidParams(format!("invalid listing tags: {e}")))?; + if let Some(extra_tags) = tags { + tag_slices.extend(extra_tags); + } + let builder = radroots_nostr_build_event(30402, content, tag_slices) .map_err(|e| RpcError::Other(format!("failed to build listing event: {e}")))?; - let out = nostr_send_event(&ctx.client, builder) + let out = radroots_nostr_send_event(&ctx.client, builder) .await .map_err(|e| RpcError::Other(format!("failed to publish listing: {e}")))?; diff --git a/src/rpc/events/mod.rs b/src/rpc/events/mod.rs @@ -1,3 +1,3 @@ pub mod listing; -pub mod note; +pub mod post; pub mod profile; diff --git a/src/rpc/events/note/list.rs b/src/rpc/events/note/list.rs @@ -1,66 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; -use serde_json::{Value as JsonValue, json}; -use std::time::Duration; - -use crate::{radrootsd::Radrootsd, rpc::RpcError}; -use nostr::{Kind, filter::Filter}; -use radroots_nostr::prelude::parse_pubkeys; - -#[derive(Debug, Default, Deserialize)] -struct ListNotesParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, -} - -pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { - m.register_async_method("events.note.list", |params, ctx, _| async move { - if ctx.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ListNotesParams { authors, limit } = params.parse().unwrap_or_default(); - let limit = limit.unwrap_or(50); - - let mut filter = Filter::new() - .kind(Kind::TextNote) - .limit(limit.try_into().unwrap()); - if let Some(auths) = authors { - let pks = parse_pubkeys(&auths) - .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; - filter = filter.authors(pks); - } else { - filter = filter.author(ctx.pubkey); - } - - let events = ctx - .client - .fetch_events(filter, Duration::from_secs(10)) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items: Vec<JsonValue> = events - .into_iter() - .map(|ev| { - let tags: Vec<Vec<String>> = - ev.tags.iter().map(|t| t.as_slice().to_vec()).collect(); - json!({ - "id": ev.id.to_string(), - "author": ev.pubkey.to_string(), - "created_at": ev.created_at.as_u64(), - "kind": ev.kind.as_u16() as u32, - "tags": tags, - "content": ev.content, - "sig": ev.sig.to_string(), - }) - }) - .collect(); - - Ok::<JsonValue, RpcError>(json!({ "notes": items })) - })?; - - Ok(()) -} diff --git a/src/rpc/events/note/publish.rs b/src/rpc/events/note/publish.rs @@ -1,54 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; -use serde_json::{Value as JsonValue, json}; - -use crate::{radrootsd::Radrootsd, rpc::RpcError}; -use radroots_nostr::prelude::{build_nostr_event, nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishNoteParams { - content: String, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { - m.register_async_method("events.note.publish", |params, ctx, _| async move { - let relays = ctx.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishNoteParams { content, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - if content.trim().is_empty() { - return Err(RpcError::InvalidParams("content must not be empty".into())); - } - - let builder = build_nostr_event(1, content, tags.unwrap_or_default()) - .map_err(|e| RpcError::Other(format!("failed to build note: {e}")))?; - - let output = nostr_send_event(&ctx.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish note: {e}")))?; - - let id_hex = output.id().to_string(); - let sent: Vec<String> = output.success.into_iter().map(|u| u.to_string()).collect(); - let failed: Vec<(String, String)> = output - .failed - .into_iter() - .map(|(u, e)| (u.to_string(), e.to_string())) - .collect(); - - Ok::<JsonValue, RpcError>(json!({ - "id": id_hex, - "sent": sent, - "failed": failed - })) - })?; - - Ok(()) -} diff --git a/src/rpc/events/post/list.rs b/src/rpc/events/post/list.rs @@ -0,0 +1,69 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::Deserialize; +use serde_json::{Value as JsonValue, json}; +use std::time::Duration; + +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use radroots_nostr::prelude::{ + radroots_nostr_parse_pubkeys, + RadrootsNostrFilter, + RadrootsNostrKind, +}; + +#[derive(Debug, Default, Deserialize)] +struct ListProfilesParams { + #[serde(default)] + authors: Option<Vec<String>>, + #[serde(default)] + limit: Option<u64>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.post.list", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let ListProfilesParams { authors, limit } = params.parse().unwrap_or_default(); + let limit = limit.unwrap_or(50); + + let mut filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::TextNote) + .limit(limit.try_into().unwrap()); + if let Some(auths) = authors { + let pks = radroots_nostr_parse_pubkeys(&auths) + .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; + filter = filter.authors(pks); + } else { + filter = filter.author(ctx.pubkey); + } + + let events = ctx + .client + .fetch_events(filter, Duration::from_secs(10)) + .await + .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; + + let items: Vec<JsonValue> = events + .into_iter() + .map(|ev| { + let tags: Vec<Vec<String>> = + ev.tags.iter().map(|t| t.as_slice().to_vec()).collect(); + json!({ + "id": ev.id.to_string(), + "author": ev.pubkey.to_string(), + "created_at": ev.created_at.as_u64(), + "kind": ev.kind.as_u16() as u32, + "tags": tags, + "content": ev.content, + "sig": ev.sig.to_string(), + }) + }) + .collect(); + + Ok::<JsonValue, RpcError>(json!({ "Profiles": items })) + })?; + + Ok(()) +} diff --git a/src/rpc/events/note/mod.rs b/src/rpc/events/post/mod.rs diff --git a/src/rpc/events/post/publish.rs b/src/rpc/events/post/publish.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::Deserialize; +use serde_json::{Value as JsonValue, json}; + +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; + +#[derive(Debug, Deserialize)] +struct PublishProfileParams { + content: String, + #[serde(default)] + tags: Option<Vec<Vec<String>>>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.post.publish", |params, ctx, _| async move { + let relays = ctx.client.relays().await; + if relays.is_empty() { + return Err(RpcError::NoRelays); + } + + let PublishProfileParams { content, tags } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + if content.trim().is_empty() { + return Err(RpcError::InvalidParams("content must not be empty".into())); + } + + let builder = radroots_nostr_build_event(1, content, tags.unwrap_or_default()) + .map_err(|e| RpcError::Other(format!("failed to build note: {e}")))?; + + let output = radroots_nostr_send_event(&ctx.client, builder) + .await + .map_err(|e| RpcError::Other(format!("failed to publish note: {e}")))?; + + let id_hex = output.id().to_string(); + let sent: Vec<String> = output.success.into_iter().map(|u| u.to_string()).collect(); + let failed: Vec<(String, String)> = output + .failed + .into_iter() + .map(|(u, e)| (u.to_string(), e.to_string())) + .collect(); + + Ok::<JsonValue, RpcError>(json!({ + "id": id_hex, + "sent": sent, + "failed": failed + })) + })?; + + Ok(()) +} diff --git a/src/rpc/events/profile/list.rs b/src/rpc/events/profile/list.rs @@ -4,7 +4,10 @@ use serde_json::{Value as JsonValue, json}; use std::time::Duration; use crate::{radrootsd::Radrootsd, rpc::RpcError}; -use radroots_nostr::prelude::{fetch_metadata_for_author, npub_string}; +use radroots_nostr::prelude::{ + radroots_nostr_fetch_metadata_for_author, + radroots_nostr_npub_string, +}; pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { m.register_async_method("events.profile.list", |_params, ctx, _| async move { @@ -14,16 +17,16 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { let me_pk = ctx.pubkey; - let latest = fetch_metadata_for_author(&ctx.client, me_pk, Duration::from_secs(10)) + let latest = radroots_nostr_fetch_metadata_for_author(&ctx.client, me_pk, Duration::from_secs(10)) .await .map_err(|e| RpcError::Other(format!("metadata fetch failed: {e}")))?; - let npub = - npub_string(&me_pk).ok_or_else(|| RpcError::Other("bech32 encode failed".into()))?; + let npub = radroots_nostr_npub_string(&me_pk) + .ok_or_else(|| RpcError::Other("bech32 encode failed".into()))?; let row = if let Some(ev) = latest { let parsed: Option<serde_json::Value> = serde_json::from_str(&ev.content).ok(); - let profile: Option<radroots_events::profile::models::RadrootsProfile> = + let profile: Option<radroots_events::profile::RadrootsProfile> = serde_json::from_str(&ev.content).ok(); json!({ diff --git a/src/rpc/events/profile/publish.rs b/src/rpc/events/profile/publish.rs @@ -5,9 +5,12 @@ use serde_json::{Value as JsonValue, json}; use crate::{radrootsd::Radrootsd, rpc::RpcError}; -use radroots_events::profile::models::RadrootsProfile; +use radroots_events::profile::RadrootsProfile; use radroots_events_codec::profile::encode::to_metadata; -use radroots_nostr::prelude::{build_metadata_event, nostr_send_event}; +use radroots_nostr::prelude::{ + radroots_nostr_build_metadata_event, + radroots_nostr_send_event, +}; #[derive(Debug, Deserialize)] struct PublishProfileParams { @@ -26,9 +29,9 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { .map_err(|e| RpcError::InvalidParams(e.to_string()))?; let metadata = to_metadata(&profile).map_err(|e| RpcError::InvalidParams(e.to_string()))?; - let builder = build_metadata_event(&metadata); + let builder = radroots_nostr_build_metadata_event(&metadata); - let output = nostr_send_event(&ctx.client, builder) + let output = radroots_nostr_send_event(&ctx.client, builder) .await .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?; diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs @@ -6,6 +6,7 @@ use jsonrpsee::server::{RpcModule, Server, ServerHandle}; use crate::radrootsd::Radrootsd; mod error; +mod domains; mod events; mod relays; mod system; @@ -19,8 +20,9 @@ pub async fn start_rpc(radrootsd: Radrootsd, addr: SocketAddr) -> Result<ServerH root.merge(system::module(radrootsd.clone())?)?; root.merge(relays::module(radrootsd.clone())?)?; root.merge(events::profile::module(radrootsd.clone())?)?; - root.merge(events::note::module(radrootsd.clone())?)?; + root.merge(events::post::module(radrootsd.clone())?)?; root.merge(events::listing::module(radrootsd.clone())?)?; + root.merge(domains::trade::module(radrootsd.clone())?)?; let handle = server.start(root); Ok(handle) diff --git a/src/rpc/relays/add.rs b/src/rpc/relays/add.rs @@ -1,6 +1,6 @@ use anyhow::Result; use jsonrpsee::RpcModule; -use radroots_nostr::prelude::add_relay; +use radroots_nostr::prelude::radroots_nostr_add_relay; use serde::Deserialize; use serde_json::{Value as JsonValue, json}; @@ -18,7 +18,7 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { .parse() .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - add_relay(&ctx.client, &url) + radroots_nostr_add_relay(&ctx.client, &url) .await .map_err(|e| RpcError::AddRelay(url.clone(), e.to_string()))?; diff --git a/src/rpc/relays/connect.rs b/src/rpc/relays/connect.rs @@ -5,8 +5,7 @@ use serde_json::{Value as JsonValue, json}; use crate::radrootsd::Radrootsd; use crate::rpc::RpcError; -use nostr_sdk::RelayStatus; -use radroots_nostr::prelude::connect; +use radroots_nostr::prelude::{radroots_nostr_connect, RadrootsNostrRelayStatus}; pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { m.register_async_method("relays.connect", |_p, ctx, _| async move { @@ -21,8 +20,8 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { for (_, r) in &relays { match r.status() { - RelayStatus::Connected => connected += 1, - RelayStatus::Connecting => connecting += 1, + RadrootsNostrRelayStatus::Connected => connected += 1, + RadrootsNostrRelayStatus::Connecting => connecting += 1, _ => disconnected += 1, } } @@ -30,7 +29,7 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { let need_connect = disconnected > 0; if need_connect { let client = ctx.client.clone(); - tokio::spawn(async move { connect(&client).await }); + tokio::spawn(async move { radroots_nostr_connect(&client).await }); } Ok::<JsonValue, RpcError>(json!({ diff --git a/src/rpc/relays/remove.rs b/src/rpc/relays/remove.rs @@ -1,6 +1,6 @@ use anyhow::Result; use jsonrpsee::RpcModule; -use radroots_nostr::prelude::remove_relay; +use radroots_nostr::prelude::radroots_nostr_remove_relay; use serde::Deserialize; use serde_json::{Value as JsonValue, json}; @@ -18,7 +18,7 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { .parse() .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - remove_relay(&ctx.client, &url) + radroots_nostr_remove_relay(&ctx.client, &url) .await .map_err(|e| RpcError::Other(format!("failed to remove relay {url}: {e}")))?; diff --git a/src/rpc/system.rs b/src/rpc/system.rs @@ -26,8 +26,8 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { "system.ping", "events.listing.list", "events.listing.publish", - "events.note.list", - "events.note.publish", + "events.post.list", + "events.post.publish", "events.profile.list", "events.profile.publish", "relays.add",