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