commit 71aecb031a1caf03e1c73d91d7538074c5ff4882
parent 6b311689a6b95475baf4a707d584c6f3fe7c9463
Author: triesap <triesap@radroots.dev>
Date: Sat, 3 Jan 2026 16:00:55 +0000
jsonrpc: refactor into context-based modules
- Move RPC implementation under src/api/jsonrpc with server builder
- Introduce RpcContext and MethodRegistry for shared state and method tracking
- Restructure config into [config.rpc] with typed RpcConfig and defaults
- Update dependencies and nostr crates; add identity public_key and publish profile on startup
Diffstat:
67 files changed, 1620 insertions(+), 1411 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -313,6 +313,12 @@ dependencies = [
]
[[package]]
+name = "btreecap"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6160c957d8aa33d0a8ba1dbab98e3cb57023ad9374c501441e88559f99e6c4c9"
+
+[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -540,7 +546,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
- "rand_core 0.6.4",
"typenum",
]
@@ -831,6 +836,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
name = "hex-conservative"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1122,9 +1133,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
]
[[package]]
@@ -1373,9 +1381,7 @@ dependencies = [
[[package]]
name = "nostr"
-version = "0.43.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62a97d745f1bd8d5e05a978632bbb87b0614567d5142906fe7c86fb2440faac6"
+version = "0.44.1"
dependencies = [
"aes",
"base64 0.22.1",
@@ -1385,8 +1391,10 @@ dependencies = [
"cbc",
"chacha20",
"chacha20poly1305",
- "getrandom 0.2.16",
+ "hex",
"instant",
+ "once_cell",
+ "rand 0.9.2",
"scrypt",
"secp256k1",
"serde",
@@ -1397,24 +1405,29 @@ dependencies = [
[[package]]
name = "nostr-database"
-version = "0.43.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1c75a8c2175d2785ba73cfddef21d1e30da5fbbdf158569b6808ba44973a15b"
+version = "0.44.0"
dependencies = [
+ "btreecap",
"lru",
"nostr",
"tokio",
]
[[package]]
+name = "nostr-gossip"
+version = "0.44.0"
+dependencies = [
+ "nostr",
+]
+
+[[package]]
name = "nostr-relay-pool"
-version = "0.43.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b2f43b70d13dfc50508a13cd902e11f4625312b2ce0e4b7c4c2283fd04001bd"
+version = "0.44.0"
dependencies = [
"async-utility",
"async-wsocket",
"atomic-destructor",
+ "hex",
"lru",
"negentropy",
"nostr",
@@ -1425,15 +1438,15 @@ dependencies = [
[[package]]
name = "nostr-sdk"
-version = "0.43.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "599f8963d6a1522a13b1a2b0ea6e168acfc367706606f1d33fa595e91fa22db0"
+version = "0.44.1"
dependencies = [
"async-utility",
"nostr",
"nostr-database",
+ "nostr-gossip",
"nostr-relay-pool",
"tokio",
+ "tracing",
]
[[package]]
@@ -1777,6 +1790,7 @@ name = "radroots-identity"
version = "0.1.0"
dependencies = [
"nostr",
+ "radroots-events",
"radroots-runtime",
"serde",
"serde_json",
@@ -1785,6 +1799,16 @@ dependencies = [
]
[[package]]
+name = "radroots-log"
+version = "0.1.0"
+dependencies = [
+ "thiserror 1.0.69",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "radroots-nostr"
version = "0.1.0"
dependencies = [
@@ -1806,6 +1830,7 @@ dependencies = [
"anyhow",
"clap",
"config",
+ "radroots-log",
"serde",
"serde_json",
"tempfile",
@@ -1813,8 +1838,6 @@ dependencies = [
"tokio",
"toml",
"tracing",
- "tracing-appender",
- "tracing-subscriber",
]
[[package]]
@@ -2143,7 +2166,6 @@ version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
- "rand 0.8.5",
"secp256k1-sys",
"serde",
]
@@ -3064,7 +3086,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.61.1",
]
[[package]]
diff --git a/config.toml b/config.toml
@@ -11,7 +11,9 @@ name = "radrootsd"
[config]
logs_dir = "logs"
-rpc_addr = "127.0.0.1:7070"
relays = [
"ws://127.0.0.1:8080"
-]
-\ No newline at end of file
+]
+
+[config.rpc]
+addr = "127.0.0.1:7070"
diff --git a/identity.json b/identity.json
@@ -1,3 +1,4 @@
{
- "secret_key": "e8f44e66f455231d6f22334dfa87d014c75c2718e13de7915dba627f0c78f42f"
+ "secret_key": "e8f44e66f455231d6f22334dfa87d014c75c2718e13de7915dba627f0c78f42f",
+ "public_key": "1bdeb0682b20be945089d730166513fd5867b9af0dcfbbee067c14cae1b295f1"
}
\ No newline at end of file
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
@@ -1,2 +1,2 @@
[toolchain]
-channel = "1.88.0"
-\ No newline at end of file
+channel = "1.88.0"
diff --git a/src/api/jsonrpc/context.rs b/src/api/jsonrpc/context.rs
@@ -0,0 +1,17 @@
+#![forbid(unsafe_code)]
+
+use crate::radrootsd::Radrootsd;
+
+use super::registry::MethodRegistry;
+
+#[derive(Clone)]
+pub struct RpcContext {
+ pub state: Radrootsd,
+ pub methods: MethodRegistry,
+}
+
+impl RpcContext {
+ pub fn new(state: Radrootsd, methods: MethodRegistry) -> Self {
+ Self { state, methods }
+ }
+}
diff --git a/src/api/jsonrpc/error.rs b/src/api/jsonrpc/error.rs
@@ -0,0 +1,30 @@
+#![forbid(unsafe_code)]
+
+use jsonrpsee::types::{ErrorObject, ErrorObjectOwned};
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum RpcError {
+ #[error("failed to add relay {0}: {1}")]
+ AddRelay(String, String),
+ #[error("no relays configured; call relays.add first")]
+ NoRelays,
+ #[error("invalid params: {0}")]
+ InvalidParams(String),
+ #[error("method not found: {0}")]
+ MethodNotFound(String),
+ #[error("{0}")]
+ Other(String),
+}
+
+impl From<RpcError> for ErrorObjectOwned {
+ fn from(err: RpcError) -> Self {
+ match err {
+ RpcError::InvalidParams(msg) => ErrorObject::owned(-32602, msg, None::<()>),
+ RpcError::MethodNotFound(name) => {
+ ErrorObject::owned(-32601, format!("method not found: {name}"), None::<()>)
+ }
+ other => ErrorObject::owned(-32000, other.to_string(), None::<()>),
+ }
+ }
+}
diff --git a/src/rpc/domains/mod.rs b/src/api/jsonrpc/methods/domains/mod.rs
diff --git a/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs b/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs
@@ -0,0 +1,95 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::{Deserialize, Serialize};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, 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<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("trade.listing.dvm.list");
+ m.register_async_method("trade.listing.dvm.list", |params, ctx, _| async move {
+ if ctx.state.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.state.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/api/jsonrpc/methods/domains/trade/listing/get.rs b/src/api/jsonrpc/methods/domains/trade/listing/get.rs
@@ -0,0 +1,43 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::{Deserialize, Serialize};
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, 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<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("trade.listing.get");
+ m.register_async_method("trade.listing.get", |params, ctx, _| async move {
+ if ctx.state.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.state.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/api/jsonrpc/methods/domains/trade/listing/helpers.rs b/src/api/jsonrpc/methods/domains/trade/listing/helpers.rs
@@ -0,0 +1,229 @@
+#![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 super::types::{DvmEventView, ListingEventView, NostrEventView, TradeListingOrderSummary};
+use crate::api::jsonrpc::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_secs(),
+ 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/api/jsonrpc/methods/domains/trade/listing/list.rs b/src/api/jsonrpc/methods/domains/trade/listing/list.rs
@@ -0,0 +1,82 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::{Deserialize, Serialize};
+use std::time::Duration;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, 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<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("trade.listing.list");
+ m.register_async_method("trade.listing.list", |params, ctx, _| async move {
+ if ctx.state.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.state.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
+ .state
+ .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/api/jsonrpc/methods/domains/trade/listing/mod.rs b/src/api/jsonrpc/methods/domains/trade/listing/mod.rs
@@ -0,0 +1,25 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub mod dvm;
+pub mod get;
+pub mod list;
+pub mod orders;
+pub mod series;
+
+mod helpers;
+mod types;
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx);
+ get::register(&mut m, ®istry)?;
+ list::register(&mut m, ®istry)?;
+ dvm::register(&mut m, ®istry)?;
+ series::register(&mut m, ®istry)?;
+ orders::register(&mut m, ®istry)?;
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/methods/domains/trade/listing/orders.rs b/src/api/jsonrpc/methods/domains/trade/listing/orders.rs
@@ -0,0 +1,94 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::{Deserialize, Serialize};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, 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<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("trade.listing.orders.list");
+ m.register_async_method("trade.listing.orders.list", |params, ctx, _| async move {
+ if ctx.state.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.state.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/api/jsonrpc/methods/domains/trade/listing/series.rs b/src/api/jsonrpc/methods/domains/trade/listing/series.rs
@@ -0,0 +1,104 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::{Deserialize, Serialize};
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, 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<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("trade.listing.series.get");
+ m.register_async_method("trade.listing.series.get", |params, ctx, _| async move {
+ if ctx.state.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.state.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.state.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/api/jsonrpc/methods/domains/trade/listing/types.rs
diff --git a/src/api/jsonrpc/methods/domains/trade/mod.rs b/src/api/jsonrpc/methods/domains/trade/mod.rs
@@ -0,0 +1,14 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub mod listing;
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx.clone());
+ m.merge(listing::module(ctx, registry)?)?;
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/methods/events/listing/list.rs b/src/api/jsonrpc/methods/events/listing/list.rs
@@ -0,0 +1,80 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::Deserialize;
+use serde_json::{Value as JsonValue, json};
+use std::time::Duration;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+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 {
+ #[serde(default)]
+ authors: Option<Vec<String>>,
+ #[serde(default)]
+ limit: Option<u64>,
+}
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("events.listing.list");
+ m.register_async_method("events.listing.list", |params, ctx, _| async move {
+ if ctx.state.client.relays().await.is_empty() {
+ return Err(RpcError::NoRelays);
+ }
+
+ let ListListingParams { authors, limit } = params.parse().unwrap_or_default();
+ let limit = limit.unwrap_or(50).min(1000);
+
+ let mut filter = RadrootsNostrFilter::new().limit((limit as u16).into());
+
+ let kinds: Vec<u32> = vec![30402];
+ let kinds_conv = kinds
+ .into_iter()
+ .map(|k| RadrootsNostrKind::Custom(k as u16))
+ .collect::<Vec<_>>();
+ filter = filter.kinds(kinds_conv);
+
+ 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.state.pubkey);
+ }
+
+ let events = ctx
+ .state
+ .client
+ .fetch_events(filter, Duration::from_secs(10))
+ .await
+ .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
+
+ let items = events
+ .into_iter()
+ .map(|ev| {
+ let tags: Vec<Vec<String>> =
+ ev.tags.iter().map(|t| t.as_slice().to_vec()).collect();
+ let listing = listing_from_event_parts(&tags, &ev.content).ok();
+
+ json!({
+ "id": ev.id.to_string(),
+ "author": ev.pubkey.to_string(),
+ "created_at": ev.created_at.as_secs(),
+ "kind": ev.kind.as_u16() as u32,
+ "tags": tags,
+ "content": ev.content,
+ "sig": ev.sig.to_string(),
+ "listing": listing,
+ })
+ })
+ .collect::<Vec<_>>();
+
+ Ok::<JsonValue, RpcError>(json!({ "listings": items }))
+ })?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/events/listing/mod.rs b/src/api/jsonrpc/methods/events/listing/mod.rs
@@ -0,0 +1,14 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub mod list;
+pub mod publish;
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx);
+ list::register(&mut m, ®istry)?;
+ publish::register(&mut m, ®istry)?;
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/methods/events/listing/publish.rs b/src/api/jsonrpc/methods/events/listing/publish.rs
@@ -0,0 +1,49 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::Deserialize;
+use serde_json::{Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+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 {
+ listing: RadrootsListing,
+ #[serde(default)]
+ tags: Option<Vec<Vec<String>>>,
+}
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("events.listing.publish");
+ m.register_async_method("events.listing.publish", |params, ctx, _| async move {
+ if ctx.state.client.relays().await.is_empty() {
+ return Err(RpcError::NoRelays);
+ }
+
+ let PublishListingParams { listing, tags } =
+ params.parse().map_err(|e| RpcError::InvalidParams(e.to_string()))?;
+
+ let content = serde_json::to_string(&listing)
+ .map_err(|e| RpcError::InvalidParams(format!("invalid listing json: {e}")))?;
+ 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 = radroots_nostr_send_event(&ctx.state.client, builder)
+ .await
+ .map_err(|e| RpcError::Other(format!("failed to publish listing: {e}")))?;
+
+ Ok::<JsonValue, RpcError>(json!({
+ "id": out.id().to_string(),
+ "sent": out.success.into_iter().map(|u| u.to_string()).collect::<Vec<_>>(),
+ "failed": out.failed.into_iter().map(|(u,e)| (u.to_string(), e.to_string())).collect::<Vec<_>>(),
+ }))
+ })?;
+ Ok(())
+}
diff --git a/src/rpc/events/mod.rs b/src/api/jsonrpc/methods/events/mod.rs
diff --git a/src/api/jsonrpc/methods/events/post/list.rs b/src/api/jsonrpc/methods/events/post/list.rs
@@ -0,0 +1,71 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::Deserialize;
+use serde_json::{Value as JsonValue, json};
+use std::time::Duration;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, 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<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("events.post.list");
+ m.register_async_method("events.post.list", |params, ctx, _| async move {
+ if ctx.state.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.state.pubkey);
+ }
+
+ let events = ctx
+ .state
+ .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_secs(),
+ "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/api/jsonrpc/methods/events/post/mod.rs b/src/api/jsonrpc/methods/events/post/mod.rs
@@ -0,0 +1,14 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub mod list;
+pub mod publish;
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx);
+ list::register(&mut m, ®istry)?;
+ publish::register(&mut m, ®istry)?;
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/methods/events/post/publish.rs b/src/api/jsonrpc/methods/events/post/publish.rs
@@ -0,0 +1,55 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::Deserialize;
+use serde_json::{Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, 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<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("events.post.publish");
+ m.register_async_method("events.post.publish", |params, ctx, _| async move {
+ let relays = ctx.state.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.state.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/api/jsonrpc/methods/events/profile/list.rs b/src/api/jsonrpc/methods/events/profile/list.rs
@@ -0,0 +1,58 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde_json::{Value as JsonValue, json};
+use std::time::Duration;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+use radroots_nostr::prelude::{
+ radroots_nostr_fetch_metadata_for_author,
+ radroots_nostr_npub_string,
+};
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("events.profile.list");
+ m.register_async_method("events.profile.list", |_params, ctx, _| async move {
+ if ctx.state.client.relays().await.is_empty() {
+ return Err(RpcError::NoRelays);
+ }
+
+ let me_pk = ctx.state.pubkey;
+
+ let latest = radroots_nostr_fetch_metadata_for_author(&ctx.state.client, me_pk, Duration::from_secs(10))
+ .await
+ .map_err(|e| RpcError::Other(format!("metadata fetch failed: {e}")))?;
+
+ 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::RadrootsProfile> =
+ serde_json::from_str(&ev.content).ok();
+
+ json!({
+ "author_hex": me_pk.to_string(),
+ "author_npub": npub,
+ "event_id": ev.id.to_string(),
+ "created_at": ev.created_at.as_secs(),
+ "content": ev.content,
+ "metadata_json": parsed,
+ "radroots_profile": profile,
+ })
+ } else {
+ json!({
+ "author_hex": me_pk.to_string(),
+ "author_npub": npub,
+ "event_id": null,
+ "created_at": null,
+ "content": null,
+ "metadata_json": null,
+ "radroots_profile": null
+ })
+ };
+
+ Ok::<JsonValue, RpcError>(json!({ "profiles": [row] }))
+ })?;
+
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/events/profile/mod.rs b/src/api/jsonrpc/methods/events/profile/mod.rs
@@ -0,0 +1,14 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub mod list;
+pub mod publish;
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx);
+ list::register(&mut m, ®istry)?;
+ publish::register(&mut m, ®istry)?;
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/methods/events/profile/publish.rs b/src/api/jsonrpc/methods/events/profile/publish.rs
@@ -0,0 +1,55 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::Deserialize;
+use serde_json::{Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+
+use radroots_events::profile::RadrootsProfile;
+use radroots_events_codec::profile::encode::to_metadata;
+use radroots_nostr::prelude::{
+ radroots_nostr_build_metadata_event,
+ radroots_nostr_send_event,
+};
+
+#[derive(Debug, Deserialize)]
+struct PublishProfileParams {
+ profile: RadrootsProfile,
+}
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("events.profile.publish");
+ m.register_async_method("events.profile.publish", |params, ctx, _| async move {
+ let relays = ctx.state.client.relays().await;
+ if relays.is_empty() {
+ return Err(RpcError::NoRelays);
+ }
+
+ let PublishProfileParams { profile } = params
+ .parse()
+ .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
+
+ let metadata = to_metadata(&profile).map_err(|e| RpcError::InvalidParams(e.to_string()))?;
+ let builder = radroots_nostr_build_metadata_event(&metadata);
+
+ let output = radroots_nostr_send_event(&ctx.state.client, builder)
+ .await
+ .map_err(|e| RpcError::Other(format!("failed to publish metadata: {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/api/jsonrpc/methods/mod.rs b/src/api/jsonrpc/methods/mod.rs
@@ -0,0 +1,25 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use super::{context::RpcContext, registry::MethodRegistry};
+
+pub mod domains;
+pub mod events;
+pub mod relays;
+pub mod system;
+
+pub fn register_all(
+ root: &mut RpcModule<RpcContext>,
+ ctx: RpcContext,
+ registry: MethodRegistry,
+) -> Result<()> {
+ root.merge(system::module(ctx.clone(), registry.clone())?)?;
+ root.merge(relays::module(ctx.clone(), registry.clone())?)?;
+ root.merge(events::profile::module(ctx.clone(), registry.clone())?)?;
+ root.merge(events::post::module(ctx.clone(), registry.clone())?)?;
+ root.merge(events::listing::module(ctx.clone(), registry.clone())?)?;
+ root.merge(domains::trade::module(ctx, registry)?)?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/relays/add.rs b/src/api/jsonrpc/methods/relays/add.rs
@@ -0,0 +1,28 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use radroots_nostr::prelude::radroots_nostr_add_relay;
+use serde::Deserialize;
+use serde_json::{Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+
+#[derive(Debug, Deserialize)]
+struct AddParams {
+ url: String,
+}
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("relays.add");
+ m.register_async_method("relays.add", |params, ctx, _| async move {
+ let AddParams { url } = params
+ .parse()
+ .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
+
+ radroots_nostr_add_relay(&ctx.state.client, &url)
+ .await
+ .map_err(|e| RpcError::AddRelay(url.clone(), e.to_string()))?;
+
+ Ok::<JsonValue, RpcError>(json!({ "added": url }))
+ })?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/relays/connect.rs b/src/api/jsonrpc/methods/relays/connect.rs
@@ -0,0 +1,43 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde_json::{Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+
+use radroots_nostr::prelude::{radroots_nostr_connect, RadrootsNostrRelayStatus};
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("relays.connect");
+ m.register_async_method("relays.connect", |_p, ctx, _| async move {
+ let relays = ctx.state.client.relays().await;
+ if relays.is_empty() {
+ return Err(RpcError::NoRelays);
+ }
+
+ let mut connected = 0usize;
+ let mut connecting = 0usize;
+ let mut disconnected = 0usize;
+
+ for (_, r) in &relays {
+ match r.status() {
+ RadrootsNostrRelayStatus::Connected => connected += 1,
+ RadrootsNostrRelayStatus::Connecting => connecting += 1,
+ _ => disconnected += 1,
+ }
+ }
+
+ let need_connect = disconnected > 0;
+ if need_connect {
+ let client = ctx.state.client.clone();
+ tokio::spawn(async move { radroots_nostr_connect(&client).await });
+ }
+
+ Ok::<JsonValue, RpcError>(json!({
+ "connected": connected,
+ "connecting": connecting,
+ "disconnected": disconnected,
+ "spawned_connect": need_connect
+ }))
+ })?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/relays/list.rs b/src/api/jsonrpc/methods/relays/list.rs
@@ -0,0 +1,16 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde_json::{Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("relays.list");
+ m.register_async_method("relays.list", |_p, ctx, _| async move {
+ let relays = ctx.state.client.relays().await;
+ Ok::<JsonValue, RpcError>(json!(
+ relays.keys().map(|u| u.to_string()).collect::<Vec<_>>()
+ ))
+ })?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/relays/mod.rs b/src/api/jsonrpc/methods/relays/mod.rs
@@ -0,0 +1,22 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub mod add;
+pub mod connect;
+pub mod list;
+pub mod remove;
+pub mod status;
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx);
+
+ add::register(&mut m, ®istry)?;
+ remove::register(&mut m, ®istry)?;
+ list::register(&mut m, ®istry)?;
+ status::register(&mut m, ®istry)?;
+ connect::register(&mut m, ®istry)?;
+
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/methods/relays/remove.rs b/src/api/jsonrpc/methods/relays/remove.rs
@@ -0,0 +1,28 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use radroots_nostr::prelude::radroots_nostr_remove_relay;
+use serde::Deserialize;
+use serde_json::{Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+
+#[derive(Debug, Deserialize)]
+struct RemoveParams {
+ url: String,
+}
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("relays.remove");
+ m.register_async_method("relays.remove", |params, ctx, _| async move {
+ let RemoveParams { url } = params
+ .parse()
+ .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
+
+ radroots_nostr_remove_relay(&ctx.state.client, &url)
+ .await
+ .map_err(|e| RpcError::Other(format!("failed to remove relay {url}: {e}")))?;
+
+ Ok::<JsonValue, RpcError>(json!({ "removed": url }))
+ })?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/relays/status.rs b/src/api/jsonrpc/methods/relays/status.rs
@@ -0,0 +1,57 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::Deserialize;
+use serde_json::{Map as JsonMap, Value as JsonValue, json};
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+use radroots_nostr::prelude::fetch_nip11;
+
+#[derive(Debug, Deserialize)]
+struct StatusParams {
+ #[serde(default)]
+ include_nip11: bool,
+}
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("relays.status");
+ m.register_async_method("relays.status", |params, ctx, _| async move {
+ let StatusParams { include_nip11 } = params
+ .parse()
+ .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
+
+ let relays = ctx.state.client.relays().await;
+ let mut out = Vec::with_capacity(relays.len());
+
+ for (relay_url, relay) in relays {
+ let url_str = relay_url.to_string();
+ let status_str = format!("{}", relay.status());
+ let parsed = reqwest::Url::parse(&url_str).ok();
+
+ let mut row = JsonMap::new();
+ row.insert("url".into(), json!(url_str));
+ row.insert("status".into(), json!(status_str));
+
+ if let Some(u) = &parsed {
+ row.insert("scheme".into(), json!(u.scheme()));
+ if let Some(h) = u.host_str() {
+ row.insert("host".into(), json!(h));
+ row.insert("onion".into(), json!(h.ends_with(".onion")));
+ }
+ if let Some(p) = u.port() {
+ row.insert("port".into(), json!(p));
+ }
+ }
+
+ if include_nip11 {
+ if let Some(doc) = fetch_nip11(row["url"].as_str().unwrap()).await {
+ row.insert("nip11".into(), json!(doc));
+ }
+ }
+
+ out.push(JsonValue::Object(row));
+ }
+
+ Ok::<JsonValue, RpcError>(json!(out))
+ })?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/system.rs b/src/api/jsonrpc/methods/system.rs
@@ -0,0 +1,27 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde_json::json;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx);
+
+ registry.track("system.ping");
+ m.register_method("system.ping", |_p, _ctx, _| "pong")?;
+
+ registry.track("system.get_info");
+ m.register_method("system.get_info", |_p, ctx, _| {
+ let uptime = ctx.state.started.elapsed().as_secs();
+ json!({
+ "version": ctx.state.info.get("version"),
+ "build": ctx.state.info.get("build"),
+ "uptime_secs": uptime,
+ })
+ })?;
+
+ registry.track("system.help");
+ m.register_method("system.help", |_p, ctx, _| ctx.methods.list())?;
+
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/mod.rs b/src/api/jsonrpc/mod.rs
@@ -0,0 +1,36 @@
+#![forbid(unsafe_code)]
+
+use std::net::SocketAddr;
+
+use anyhow::Result;
+use jsonrpsee::server::{RpcModule, ServerHandle};
+
+use crate::config::RpcConfig;
+use crate::radrootsd::Radrootsd;
+
+mod context;
+mod error;
+mod registry;
+mod server;
+
+pub mod methods;
+
+pub use context::RpcContext;
+pub use error::RpcError;
+pub use registry::MethodRegistry;
+
+pub async fn start_rpc(
+ state: Radrootsd,
+ addr: SocketAddr,
+ rpc_cfg: &RpcConfig,
+) -> Result<ServerHandle> {
+ let registry = MethodRegistry::default();
+ let ctx = RpcContext::new(state, registry.clone());
+ let server = server::build_server(addr, rpc_cfg).await?;
+
+ let mut root = RpcModule::new(ctx.clone());
+ methods::register_all(&mut root, ctx, registry)?;
+
+ let handle = server.start(root);
+ Ok(handle)
+}
diff --git a/src/api/jsonrpc/registry.rs b/src/api/jsonrpc/registry.rs
@@ -0,0 +1,23 @@
+#![forbid(unsafe_code)]
+
+use std::sync::{Arc, RwLock};
+
+#[derive(Clone, Default)]
+pub struct MethodRegistry {
+ inner: Arc<RwLock<Vec<String>>>,
+}
+
+impl MethodRegistry {
+ pub fn track(&self, name: &'static str) {
+ let mut methods = self.inner.write().unwrap_or_else(|e| e.into_inner());
+ if methods.iter().any(|entry| entry == name) {
+ return;
+ }
+ methods.push(name.to_string());
+ methods.sort();
+ }
+
+ pub fn list(&self) -> Vec<String> {
+ self.inner.read().unwrap_or_else(|e| e.into_inner()).clone()
+ }
+}
diff --git a/src/api/jsonrpc/server.rs b/src/api/jsonrpc/server.rs
@@ -0,0 +1,30 @@
+#![forbid(unsafe_code)]
+
+use std::net::SocketAddr;
+
+use anyhow::Result;
+use jsonrpsee::server::{BatchRequestConfig, Server, ServerBuilder, ServerConfigBuilder};
+
+use crate::config::RpcConfig;
+
+pub async fn build_server(addr: SocketAddr, rpc_cfg: &RpcConfig) -> Result<Server> {
+ let mut builder = ServerConfigBuilder::new()
+ .max_request_body_size(rpc_cfg.max_request_body_size)
+ .max_response_body_size(rpc_cfg.max_response_body_size)
+ .max_connections(rpc_cfg.max_connections)
+ .max_subscriptions_per_connection(rpc_cfg.max_subscriptions_per_connection)
+ .set_message_buffer_capacity(rpc_cfg.message_buffer_capacity);
+
+ if let Some(limit) = rpc_cfg.batch_request_limit {
+ let cfg = if limit == 0 {
+ BatchRequestConfig::Disabled
+ } else {
+ BatchRequestConfig::Limit(limit)
+ };
+ builder = builder.set_batch_request_config(cfg);
+ }
+
+ let server_cfg = builder.build();
+ let server = ServerBuilder::with_config(server_cfg).build(addr).await?;
+ Ok(server)
+}
diff --git a/src/api/mod.rs b/src/api/mod.rs
@@ -0,0 +1,3 @@
+#![forbid(unsafe_code)]
+
+pub mod jsonrpc;
diff --git a/src/config.rs b/src/config.rs
@@ -1,13 +1,81 @@
use radroots_nostr::prelude::RadrootsNostrMetadata;
use serde::{Deserialize, Serialize};
+fn default_rpc_addr() -> String {
+ "127.0.0.1:7070".to_string()
+}
+
+fn default_max_request_body_size() -> u32 {
+ 10 * 1024 * 1024
+}
+
+fn default_max_response_body_size() -> u32 {
+ 10 * 1024 * 1024
+}
+
+fn default_max_connections() -> u32 {
+ 100
+}
+
+fn default_max_subscriptions_per_connection() -> u32 {
+ 1024
+}
+
+fn default_message_buffer_capacity() -> u32 {
+ 1024
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct RpcConfig {
+ #[serde(default = "default_rpc_addr")]
+ pub addr: String,
+ #[serde(default = "default_max_request_body_size")]
+ pub max_request_body_size: u32,
+ #[serde(default = "default_max_response_body_size")]
+ pub max_response_body_size: u32,
+ #[serde(default = "default_max_connections")]
+ pub max_connections: u32,
+ #[serde(default = "default_max_subscriptions_per_connection")]
+ pub max_subscriptions_per_connection: u32,
+ #[serde(default = "default_message_buffer_capacity")]
+ pub message_buffer_capacity: u32,
+ #[serde(default)]
+ pub batch_request_limit: Option<u32>,
+}
+
+impl Default for RpcConfig {
+ fn default() -> Self {
+ Self {
+ addr: default_rpc_addr(),
+ max_request_body_size: default_max_request_body_size(),
+ max_response_body_size: default_max_response_body_size(),
+ max_connections: default_max_connections(),
+ max_subscriptions_per_connection: default_max_subscriptions_per_connection(),
+ message_buffer_capacity: default_message_buffer_capacity(),
+ batch_request_limit: None,
+ }
+ }
+}
+
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Configuration {
pub logs_dir: String,
- pub rpc_addr: String,
+ #[serde(default)]
+ pub rpc: RpcConfig,
+ #[serde(default)]
+ pub rpc_addr: Option<String>,
+ #[serde(default)]
pub relays: Vec<String>,
}
+impl Configuration {
+ pub fn rpc_addr(&self) -> &str {
+ self.rpc_addr
+ .as_deref()
+ .unwrap_or(self.rpc.addr.as_str())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
pub metadata: RadrootsNostrMetadata,
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,7 +1,9 @@
+#![forbid(unsafe_code)]
+
+pub mod api;
pub mod cli;
pub mod config;
pub mod radrootsd;
-pub mod rpc;
use anyhow::Result;
@@ -10,13 +12,14 @@ use tracing::info;
use crate::radrootsd::Radrootsd;
use radroots_identity::RadrootsIdentity;
+use radroots_nostr::prelude::radroots_nostr_publish_identity_profile;
pub async fn run_radrootsd(settings: &config::Settings, args: &cli_args) -> Result<()> {
let identity = RadrootsIdentity::load_or_generate(
args.identity.as_ref(),
args.allow_generate_identity,
)?;
- let keys = identity.into_keys();
+ let keys = identity.keys().clone();
let radrootsd = Radrootsd::new(keys, settings.metadata.clone());
@@ -27,6 +30,7 @@ pub async fn run_radrootsd(settings: &config::Settings, args: &cli_args) -> Resu
if !settings.config.relays.is_empty() {
let client = radrootsd.client.clone();
let md = settings.metadata.clone();
+ let identity = identity.clone();
let has_metadata = serde_json::to_value(&md)
.ok()
.and_then(|v| v.as_object().cloned())
@@ -35,7 +39,16 @@ pub async fn run_radrootsd(settings: &config::Settings, args: &cli_args) -> Resu
tokio::spawn(async move {
client.connect().await;
- if has_metadata {
+ let profile_published =
+ match radroots_nostr_publish_identity_profile(&client, &identity).await {
+ Ok(Some(_)) => true,
+ Ok(None) => false,
+ Err(e) => {
+ tracing::warn!("Failed to publish identity profile: {e}");
+ false
+ }
+ };
+ if has_metadata && !profile_published {
if let Err(e) = client.set_metadata(&md).await {
tracing::warn!("Failed to publish metadata on startup: {e}");
} else {
@@ -45,8 +58,8 @@ pub async fn run_radrootsd(settings: &config::Settings, args: &cli_args) -> Resu
});
}
- let addr: std::net::SocketAddr = settings.config.rpc_addr.parse()?;
- let handle = rpc::start_rpc(radrootsd.clone(), addr).await?;
+ let addr: std::net::SocketAddr = settings.config.rpc_addr().parse()?;
+ let handle = api::jsonrpc::start_rpc(radrootsd.clone(), addr, &settings.config.rpc).await?;
info!("JSON-RPC listening on {addr}");
let stop_handle = handle.clone();
diff --git a/src/main.rs b/src/main.rs
@@ -1,3 +1,5 @@
+#![forbid(unsafe_code)]
+
use anyhow::{Context, Result};
use radrootsd::{cli_args, config, run_radrootsd};
use std::process::ExitCode;
diff --git a/src/rpc/domains/trade/listing/dvm.rs b/src/rpc/domains/trade/listing/dvm.rs
@@ -1,94 +0,0 @@
-#![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
@@ -1,42 +0,0 @@
-#![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
@@ -1,231 +0,0 @@
-#![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
@@ -1,80 +0,0 @@
-#![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
@@ -1,25 +0,0 @@
-#![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
@@ -1,93 +0,0 @@
-#![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
@@ -1,103 +0,0 @@
-#![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/mod.rs b/src/rpc/domains/trade/mod.rs
@@ -1,14 +0,0 @@
-#![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/error.rs b/src/rpc/error.rs
@@ -1,28 +0,0 @@
-use jsonrpsee::types::{ErrorObject, ErrorObjectOwned};
-use thiserror::Error;
-
-#[derive(Debug, Error)]
-pub enum RpcError {
- #[error("failed to add relay {0}: {1}")]
- AddRelay(String, String),
- #[error("no relays configured; call relays.add first")]
- NoRelays,
- #[error("invalid params: {0}")]
- InvalidParams(String),
- #[error("method not found: {0}")]
- MethodNotFound(String),
- #[error("{0}")]
- Other(String),
-}
-
-impl From<RpcError> for ErrorObjectOwned {
- fn from(err: RpcError) -> Self {
- match err {
- RpcError::InvalidParams(msg) => ErrorObject::owned(-32602, msg, None::<()>),
- RpcError::MethodNotFound(name) => {
- ErrorObject::owned(-32601, format!("method not found: {name}"), None::<()>)
- }
- other => ErrorObject::owned(-32000, other.to_string(), None::<()>),
- }
- }
-}
diff --git a/src/rpc/events/listing/list.rs b/src/rpc/events/listing/list.rs
@@ -1,78 +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 radroots_nostr::prelude::{
- radroots_nostr_parse_pubkeys,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-use radroots_trade::listing::codec::listing_from_event_parts;
-
-#[derive(Debug, Default, Deserialize)]
-struct ListListingParams {
- #[serde(default)]
- authors: Option<Vec<String>>,
- #[serde(default)]
- limit: Option<u64>,
-}
-
-pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> {
- m.register_async_method("events.listing.list", |params, ctx, _| async move {
- if ctx.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ListListingParams { authors, limit } = params.parse().unwrap_or_default();
- let limit = limit.unwrap_or(50).min(1000);
-
- let mut filter = RadrootsNostrFilter::new().limit((limit as u16).into());
-
- let kinds: Vec<u32> = vec![30402];
- let kinds_conv = kinds
- .into_iter()
- .map(|k| RadrootsNostrKind::Custom(k as u16))
- .collect::<Vec<_>>();
- filter = filter.kinds(kinds_conv);
-
- 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 = events
- .into_iter()
- .map(|ev| {
- let tags: Vec<Vec<String>> =
- ev.tags.iter().map(|t| t.as_slice().to_vec()).collect();
- let listing = listing_from_event_parts(&tags, &ev.content).ok();
-
- 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(),
- "listing": listing,
- })
- })
- .collect::<Vec<_>>();
-
- Ok::<JsonValue, RpcError>(json!({ "listings": items }))
- })?;
- Ok(())
-}
diff --git a/src/rpc/events/listing/mod.rs b/src/rpc/events/listing/mod.rs
@@ -1,14 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::radrootsd::Radrootsd;
-
-pub mod list;
-pub mod publish;
-
-pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> {
- let mut m = RpcModule::new(radrootsd);
- list::register(&mut m)?;
- publish::register(&mut m)?;
- Ok(m)
-}
diff --git a/src/rpc/events/listing/publish.rs b/src/rpc/events/listing/publish.rs
@@ -1,48 +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_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 {
- listing: RadrootsListing,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> {
- m.register_async_method("events.listing.publish", |params, ctx, _| async move {
- if ctx.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishListingParams { listing, tags } =
- params.parse().map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let content = serde_json::to_string(&listing)
- .map_err(|e| RpcError::InvalidParams(format!("invalid listing json: {e}")))?;
- 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 = radroots_nostr_send_event(&ctx.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish listing: {e}")))?;
-
- Ok::<JsonValue, RpcError>(json!({
- "id": out.id().to_string(),
- "sent": out.success.into_iter().map(|u| u.to_string()).collect::<Vec<_>>(),
- "failed": out.failed.into_iter().map(|(u,e)| (u.to_string(), e.to_string())).collect::<Vec<_>>(),
- }))
- })?;
- Ok(())
-}
diff --git a/src/rpc/events/post/list.rs b/src/rpc/events/post/list.rs
@@ -1,69 +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 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/post/mod.rs b/src/rpc/events/post/mod.rs
@@ -1,14 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::radrootsd::Radrootsd;
-
-pub mod list;
-pub mod publish;
-
-pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> {
- let mut m = RpcModule::new(radrootsd);
- list::register(&mut m)?;
- publish::register(&mut m)?;
- Ok(m)
-}
diff --git a/src/rpc/events/post/publish.rs b/src/rpc/events/post/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::{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
@@ -1,57 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde_json::{Value as JsonValue, json};
-use std::time::Duration;
-
-use crate::{radrootsd::Radrootsd, rpc::RpcError};
-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 {
- if ctx.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let me_pk = ctx.pubkey;
-
- 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 = 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::RadrootsProfile> =
- serde_json::from_str(&ev.content).ok();
-
- json!({
- "author_hex": me_pk.to_string(),
- "author_npub": npub,
- "event_id": ev.id.to_string(),
- "created_at": ev.created_at.as_u64(),
- "content": ev.content,
- "metadata_json": parsed,
- "radroots_profile": profile,
- })
- } else {
- json!({
- "author_hex": me_pk.to_string(),
- "author_npub": npub,
- "event_id": null,
- "created_at": null,
- "content": null,
- "metadata_json": null,
- "radroots_profile": null
- })
- };
-
- Ok::<JsonValue, RpcError>(json!({ "profiles": [row] }))
- })?;
-
- Ok(())
-}
diff --git a/src/rpc/events/profile/mod.rs b/src/rpc/events/profile/mod.rs
@@ -1,14 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::radrootsd::Radrootsd;
-
-pub mod list;
-pub mod publish;
-
-pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> {
- let mut m = RpcModule::new(radrootsd);
- list::register(&mut m)?;
- publish::register(&mut m)?;
- Ok(m)
-}
diff --git a/src/rpc/events/profile/publish.rs b/src/rpc/events/profile/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_events::profile::RadrootsProfile;
-use radroots_events_codec::profile::encode::to_metadata;
-use radroots_nostr::prelude::{
- radroots_nostr_build_metadata_event,
- radroots_nostr_send_event,
-};
-
-#[derive(Debug, Deserialize)]
-struct PublishProfileParams {
- profile: RadrootsProfile,
-}
-
-pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> {
- m.register_async_method("events.profile.publish", |params, ctx, _| async move {
- let relays = ctx.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishProfileParams { profile } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let metadata = to_metadata(&profile).map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let builder = radroots_nostr_build_metadata_event(&metadata);
-
- let output = radroots_nostr_send_event(&ctx.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish metadata: {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/mod.rs b/src/rpc/mod.rs
@@ -1,29 +0,0 @@
-use std::net::SocketAddr;
-
-use anyhow::Result;
-use jsonrpsee::server::{RpcModule, Server, ServerHandle};
-
-use crate::radrootsd::Radrootsd;
-
-mod error;
-mod domains;
-mod events;
-mod relays;
-mod system;
-
-pub use error::RpcError;
-
-pub async fn start_rpc(radrootsd: Radrootsd, addr: SocketAddr) -> Result<ServerHandle> {
- let server = Server::builder().build(addr).await?;
-
- let mut root = RpcModule::new(radrootsd.clone());
- root.merge(system::module(radrootsd.clone())?)?;
- root.merge(relays::module(radrootsd.clone())?)?;
- root.merge(events::profile::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,28 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::RpcModule;
-use radroots_nostr::prelude::radroots_nostr_add_relay;
-use serde::Deserialize;
-use serde_json::{Value as JsonValue, json};
-
-use crate::radrootsd::Radrootsd;
-use crate::rpc::RpcError;
-
-#[derive(Debug, Deserialize)]
-struct AddParams {
- url: String,
-}
-
-pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> {
- m.register_async_method("relays.add", |params, ctx, _| async move {
- let AddParams { url } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- radroots_nostr_add_relay(&ctx.client, &url)
- .await
- .map_err(|e| RpcError::AddRelay(url.clone(), e.to_string()))?;
-
- Ok::<JsonValue, RpcError>(json!({ "added": url }))
- })?;
- Ok(())
-}
diff --git a/src/rpc/relays/connect.rs b/src/rpc/relays/connect.rs
@@ -1,43 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::RpcModule;
-use serde_json::{Value as JsonValue, json};
-
-use crate::radrootsd::Radrootsd;
-use crate::rpc::RpcError;
-
-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 {
- let relays = ctx.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let mut connected = 0usize;
- let mut connecting = 0usize;
- let mut disconnected = 0usize;
-
- for (_, r) in &relays {
- match r.status() {
- RadrootsNostrRelayStatus::Connected => connected += 1,
- RadrootsNostrRelayStatus::Connecting => connecting += 1,
- _ => disconnected += 1,
- }
- }
-
- let need_connect = disconnected > 0;
- if need_connect {
- let client = ctx.client.clone();
- tokio::spawn(async move { radroots_nostr_connect(&client).await });
- }
-
- Ok::<JsonValue, RpcError>(json!({
- "connected": connected,
- "connecting": connecting,
- "disconnected": disconnected,
- "spawned_connect": need_connect
- }))
- })?;
- Ok(())
-}
diff --git a/src/rpc/relays/list.rs b/src/rpc/relays/list.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::RpcModule;
-use serde_json::{Value as JsonValue, json};
-
-use crate::radrootsd::Radrootsd;
-use crate::rpc::RpcError;
-
-pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> {
- m.register_async_method("relays.list", |_p, ctx, _| async move {
- let relays = ctx.client.relays().await;
- Ok::<JsonValue, RpcError>(json!(
- relays.keys().map(|u| u.to_string()).collect::<Vec<_>>()
- ))
- })?;
- Ok(())
-}
diff --git a/src/rpc/relays/mod.rs b/src/rpc/relays/mod.rs
@@ -1,22 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::RpcModule;
-
-use crate::radrootsd::Radrootsd;
-
-pub mod add;
-pub mod connect;
-pub mod list;
-pub mod remove;
-pub mod status;
-
-pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> {
- let mut m = RpcModule::new(radrootsd);
-
- add::register(&mut m)?;
- remove::register(&mut m)?;
- list::register(&mut m)?;
- status::register(&mut m)?;
- connect::register(&mut m)?;
-
- Ok(m)
-}
diff --git a/src/rpc/relays/remove.rs b/src/rpc/relays/remove.rs
@@ -1,28 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::RpcModule;
-use radroots_nostr::prelude::radroots_nostr_remove_relay;
-use serde::Deserialize;
-use serde_json::{Value as JsonValue, json};
-
-use crate::radrootsd::Radrootsd;
-use crate::rpc::RpcError;
-
-#[derive(Debug, Deserialize)]
-struct RemoveParams {
- url: String,
-}
-
-pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> {
- m.register_async_method("relays.remove", |params, ctx, _| async move {
- let RemoveParams { url } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- radroots_nostr_remove_relay(&ctx.client, &url)
- .await
- .map_err(|e| RpcError::Other(format!("failed to remove relay {url}: {e}")))?;
-
- Ok::<JsonValue, RpcError>(json!({ "removed": url }))
- })?;
- Ok(())
-}
diff --git a/src/rpc/relays/status.rs b/src/rpc/relays/status.rs
@@ -1,57 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::RpcModule;
-use serde::Deserialize;
-use serde_json::{Map as JsonMap, Value as JsonValue, json};
-
-use crate::radrootsd::Radrootsd;
-use crate::rpc::RpcError;
-use radroots_nostr::prelude::fetch_nip11;
-
-#[derive(Debug, Deserialize)]
-struct StatusParams {
- #[serde(default)]
- include_nip11: bool,
-}
-
-pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> {
- m.register_async_method("relays.status", |params, ctx, _| async move {
- let StatusParams { include_nip11 } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let relays = ctx.client.relays().await;
- let mut out = Vec::with_capacity(relays.len());
-
- for (relay_url, relay) in relays {
- let url_str = relay_url.to_string();
- let status_str = format!("{}", relay.status());
- let parsed = reqwest::Url::parse(&url_str).ok();
-
- let mut row = JsonMap::new();
- row.insert("url".into(), json!(url_str));
- row.insert("status".into(), json!(status_str));
-
- if let Some(u) = &parsed {
- row.insert("scheme".into(), json!(u.scheme()));
- if let Some(h) = u.host_str() {
- row.insert("host".into(), json!(h));
- row.insert("onion".into(), json!(h.ends_with(".onion")));
- }
- if let Some(p) = u.port() {
- row.insert("port".into(), json!(p));
- }
- }
-
- if include_nip11 {
- if let Some(doc) = fetch_nip11(row["url"].as_str().unwrap()).await {
- row.insert("nip11".into(), json!(doc));
- }
- }
-
- out.push(JsonValue::Object(row));
- }
-
- Ok::<JsonValue, RpcError>(json!(out))
- })?;
- Ok(())
-}
diff --git a/src/rpc/system.rs b/src/rpc/system.rs
@@ -1,43 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::RpcModule;
-use serde_json::json;
-
-use crate::radrootsd::Radrootsd;
-
-pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> {
- let mut m = RpcModule::new(radrootsd);
-
- m.register_method("system.ping", |_p, _ctx, _| "pong")?;
-
- m.register_method("system.get_info", |_p, ctx, _| {
- let uptime = ctx.started.elapsed().as_secs();
- json!({
- "version": ctx.info.get("version"),
- "build": ctx.info.get("build"),
- "uptime_secs": uptime,
- })
- })?;
-
- m.register_method("system.help", |_p, _ctx, _| {
- vec![
- /* %% radrootsd-methods %% */
- "system.get_info",
- "system.help",
- "system.ping",
- "events.listing.list",
- "events.listing.publish",
- "events.post.list",
- "events.post.publish",
- "events.profile.list",
- "events.profile.publish",
- "relays.add",
- "relays.connect",
- "relays.list",
- "relays.remove",
- "relays.status",
- /* %% radrootsd-methods %% */
- ]
- })?;
-
- Ok(m)
-}