commit 67f46d7d2f1849604b6ae5d1d39656731c16f820
parent 2b5e92190530b66d3c8e0bb832ae1e674187610f
Author: triesap <triesap@radroots.dev>
Date: Tue, 6 Jan 2026 00:08:35 +0000
core: reset rpc surface and add nip46 status stub
- archive legacy domains/events/system rpc modules
- register only relays and nip46 status endpoints
- add nip46/jsonrpc stub module skeleton
- add empty build/events/validate/nip46 module roots
Diffstat:
80 files changed, 42 insertions(+), 6731 deletions(-)
diff --git a/src/api/jsonrpc/methods/domains/mod.rs b/src/api/jsonrpc/methods/domains/mod.rs
@@ -1,3 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub mod trade;
diff --git a/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs b/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs
@@ -1,79 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::params::{parse_pubkeys_opt, timeout_or, EventListParams};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-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)]
- recipients: Option<Vec<String>>,
- #[serde(default)]
- kinds: Option<Vec<u16>>,
- #[serde(default, flatten)]
- query: EventListParams,
-}
-
-#[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,
- recipients,
- kinds,
- query,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = query;
-
- let addr = parse_listing_addr(&listing_addr)?;
- let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec());
- let authors = parse_pubkeys_opt("author", authors)?;
- let recipients = parse_pubkeys_opt("recipient", recipients)?;
-
- let events = fetch_dvm_events(
- &ctx.state.client,
- &addr,
- &kinds,
- order_id.as_deref(),
- authors.as_deref(),
- recipients.as_deref(),
- since,
- until,
- limit,
- timeout_or(timeout_secs),
- )
- .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
@@ -1,43 +0,0 @@
-#![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
@@ -1,313 +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 super::types::{DvmEventView, ListingEventView, TradeListingOrderSummary};
-use crate::api::jsonrpc::nostr::{event_tags, event_view, event_view_with_tags};
-use crate::api::jsonrpc::params::MAX_LIMIT;
-use crate::api::jsonrpc::RpcError;
-
-pub(crate) const LISTING_KIND: u16 = 30402;
-
-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_with_tags(event, tags),
- 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(MAX_LIMIT) 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
-}
-
-#[cfg(test)]
-mod tests {
- use super::{dvm_event_view, order_id_from_event, order_summaries, LISTING_KIND};
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use radroots_trade::listing::dvm::{TradeListingEnvelope, TradeListingMessageType};
- use serde_json::json;
-
- fn dvm_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- kind: u16,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 6);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": kind,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- #[test]
- fn dvm_event_view_parses_envelope_and_prefers_envelope_order_id() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let listing_addr = format!("{LISTING_KIND}:{pubkey}:AAAAAAAAAAAAAAAAAAAAAg");
- let envelope = TradeListingEnvelope::new(
- TradeListingMessageType::OrderRequest,
- listing_addr,
- Some("env-order".to_string()),
- json!({}),
- );
- let content = serde_json::to_string(&envelope).expect("envelope");
- let id = format!("{:064x}", 1);
- let tags = vec![vec!["d".to_string(), "tag-order".to_string()]];
- let event = dvm_event(&id, pubkey, 100, 5321, tags, &content);
-
- let view = dvm_event_view(&event);
-
- assert!(view.envelope.is_some());
- assert!(view.envelope_error.is_none());
- assert_eq!(order_id_from_event(&view).as_deref(), Some("env-order"));
- }
-
- #[test]
- fn dvm_event_view_invalid_json_sets_error() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let id = format!("{:064x}", 2);
- let event = dvm_event(&id, pubkey, 120, 5321, Vec::new(), "not-json");
-
- let view = dvm_event_view(&event);
-
- assert!(view.envelope.is_none());
- assert_eq!(view.envelope_error.as_deref(), Some("invalid envelope json"));
- }
-
- #[test]
- fn order_summaries_counts_and_sorts() {
- let pubkey = "3bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let listing_addr = format!("{LISTING_KIND}:{pubkey}:AAAAAAAAAAAAAAAAAAAAAg");
- let order_a = vec![vec!["d".to_string(), "order-a".to_string()]];
- let order_b = vec![vec!["d".to_string(), "order-b".to_string()]];
-
- let id_a1 = format!("{:064x}", 3);
- let id_a2 = format!("{:064x}", 4);
- let id_b1 = format!("{:064x}", 5);
-
- let ev_a1 = dvm_event(&id_a1, pubkey, 10, 5321, order_a.clone(), "");
- let ev_a2 = dvm_event(&id_a2, pubkey, 20, 6321, order_a.clone(), "");
- let ev_b1 = dvm_event(&id_b1, pubkey, 15, 5321, order_b, "");
-
- let views = vec![
- dvm_event_view(&ev_a1),
- dvm_event_view(&ev_a2),
- dvm_event_view(&ev_b1),
- ];
-
- let summaries = order_summaries(&views, &listing_addr);
-
- assert_eq!(summaries.len(), 2);
- assert_eq!(summaries[0].order_id, "order-a");
- assert_eq!(summaries[0].event_count, 2);
- assert_eq!(summaries[0].first_seen_at, 10);
- assert_eq!(summaries[0].last_seen_at, 20);
- assert_eq!(summaries[0].last_event_id, id_a2);
- assert_eq!(summaries[0].last_event_kind, 6321);
- assert_eq!(summaries[1].order_id, "order-b");
- assert_eq!(summaries[1].event_count, 1);
- assert_eq!(summaries[1].last_seen_at, 15);
- }
-}
diff --git a/src/api/jsonrpc/methods/domains/trade/listing/list.rs b/src/api/jsonrpc/methods/domains/trade/listing/list.rs
@@ -1,72 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_nostr::prelude::{
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-use super::helpers::{listing_view, LISTING_KIND};
-use super::types::ListingEventView;
-
-#[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 EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(LISTING_KIND))
- .limit(limit);
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .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
@@ -1,29 +0,0 @@
-#![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 order;
-pub mod orders;
-pub mod series;
-pub mod validate;
-
-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)?;
- order::register(&mut m, ®istry)?;
- orders::register(&mut m, ®istry)?;
- validate::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/domains/trade/listing/order.rs b/src/api/jsonrpc/methods/domains/trade/listing/order.rs
@@ -1,181 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use radroots_nostr::prelude::{
- radroots_nostr_build_event,
- radroots_nostr_parse_pubkey,
- radroots_nostr_send_event,
- RadrootsNostrPublicKey,
-};
-use radroots_trade::listing::dvm::{
- TradeListingEnvelope,
- TradeListingMessageType,
- TradeOrderResponse,
-};
-use radroots_trade::listing::order::TradeOrder;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-use super::helpers::parse_listing_addr;
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "snake_case", tag = "type")]
-enum TradeListingOrderPayload {
- OrderRequest { order: TradeOrder },
- OrderResponse {
- order_id: String,
- accepted: bool,
- #[serde(default)]
- reason: Option<String>,
- },
-}
-
-#[derive(Debug, Deserialize)]
-struct TradeListingOrderParams {
- listing_addr: String,
- recipient_pubkey: String,
- #[serde(flatten)]
- payload: TradeListingOrderPayload,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("trade.listing.order");
- m.register_async_method("trade.listing.order", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let TradeListingOrderParams {
- listing_addr,
- recipient_pubkey,
- payload,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let addr = parse_listing_addr(&listing_addr)?;
- let listing_addr = addr.as_str();
-
- let recipient = radroots_nostr_parse_pubkey(&recipient_pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid recipient_pubkey: {e}")))?;
- let recipient_pubkey = recipient.to_string();
-
- let (message_type, order_id, content) = match payload {
- TradeListingOrderPayload::OrderRequest { order } => {
- validate_order_request(&order, &addr, &ctx.state.pubkey, &listing_addr)?;
- let order_id = order.order_id.trim().to_string();
- let envelope = TradeListingEnvelope::new(
- TradeListingMessageType::OrderRequest,
- listing_addr.clone(),
- Some(order_id.clone()),
- order,
- );
- envelope
- .validate()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let content = serde_json::to_string(&envelope)
- .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?;
- (TradeListingMessageType::OrderRequest, order_id, content)
- }
- TradeListingOrderPayload::OrderResponse {
- order_id,
- accepted,
- reason,
- } => {
- validate_order_response(&order_id, &addr, &ctx.state.pubkey)?;
- let order_id = order_id.trim().to_string();
- let response = TradeOrderResponse { accepted, reason };
- let envelope = TradeListingEnvelope::new(
- TradeListingMessageType::OrderResponse,
- listing_addr.clone(),
- Some(order_id.clone()),
- response,
- );
- envelope
- .validate()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let content = serde_json::to_string(&envelope)
- .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?;
- (TradeListingMessageType::OrderResponse, order_id, content)
- }
- };
-
- let tags = vec![
- vec!["p".to_string(), recipient_pubkey],
- vec!["a".to_string(), listing_addr.clone()],
- vec!["d".to_string(), order_id],
- ];
-
- let builder = radroots_nostr_build_event(message_type.kind() as u32, content, tags)
- .map_err(|e| RpcError::Other(format!("failed to build order event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish order event: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
- Ok(())
-}
-
-fn validate_order_request(
- order: &TradeOrder,
- addr: &radroots_trade::listing::dvm::TradeListingAddress,
- runtime_pubkey: &RadrootsNostrPublicKey,
- listing_addr: &str,
-) -> Result<(), RpcError> {
- let order_id = order.order_id.trim();
- if order_id.is_empty() {
- return Err(RpcError::InvalidParams("order_id must not be empty".to_string()));
- }
-
- if order.listing_addr.trim() != listing_addr {
- return Err(RpcError::InvalidParams(
- "order listing_addr must match listing_addr".to_string(),
- ));
- }
-
- let buyer_pubkey = radroots_nostr_parse_pubkey(&order.buyer_pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid buyer_pubkey: {e}")))?;
- if &buyer_pubkey != runtime_pubkey {
- return Err(RpcError::InvalidParams(
- "buyer_pubkey must match runtime key".to_string(),
- ));
- }
-
- let seller_pubkey = radroots_nostr_parse_pubkey(&order.seller_pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid seller_pubkey: {e}")))?;
- let listing_seller = radroots_nostr_parse_pubkey(&addr.seller_pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?;
- if seller_pubkey != listing_seller {
- return Err(RpcError::InvalidParams(
- "seller_pubkey must match listing_addr seller".to_string(),
- ));
- }
-
- Ok(())
-}
-
-fn validate_order_response(
- order_id: &str,
- addr: &radroots_trade::listing::dvm::TradeListingAddress,
- runtime_pubkey: &RadrootsNostrPublicKey,
-) -> Result<(), RpcError> {
- if order_id.trim().is_empty() {
- return Err(RpcError::InvalidParams("order_id must not be empty".to_string()));
- }
-
- let listing_seller = radroots_nostr_parse_pubkey(&addr.seller_pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?;
- if &listing_seller != runtime_pubkey {
- return Err(RpcError::InvalidParams(
- "order_response must be authored by the listing seller".to_string(),
- ));
- }
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/domains/trade/listing/orders.rs b/src/api/jsonrpc/methods/domains/trade/listing/orders.rs
@@ -1,78 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::params::{parse_pubkeys_opt, timeout_or, EventListParams};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-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)]
- recipients: Option<Vec<String>>,
- #[serde(default)]
- kinds: Option<Vec<u16>>,
- #[serde(default, flatten)]
- query: EventListParams,
-}
-
-#[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,
- recipients,
- kinds,
- query,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = query;
-
- let addr = parse_listing_addr(&listing_addr)?;
- let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec());
- let authors = parse_pubkeys_opt("author", authors)?;
- let recipients = parse_pubkeys_opt("recipient", recipients)?;
-
- let events = fetch_dvm_events(
- &ctx.state.client,
- &addr,
- &kinds,
- None,
- authors.as_deref(),
- recipients.as_deref(),
- since,
- until,
- limit,
- timeout_or(timeout_secs),
- )
- .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
@@ -1,105 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-use crate::api::jsonrpc::params::timeout_or;
-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_or(timeout_secs))
- .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_or(timeout_secs),
- )
- .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/api/jsonrpc/methods/domains/trade/listing/types.rs b/src/api/jsonrpc/methods/domains/trade/listing/types.rs
@@ -1,38 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_events::listing::RadrootsListing;
-use radroots_trade::listing::dvm::TradeListingEnvelope;
-use serde::Serialize;
-
-use crate::api::jsonrpc::nostr::NostrEventView;
-
-#[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/api/jsonrpc/methods/domains/trade/listing/validate.rs b/src/api/jsonrpc/methods/domains/trade/listing/validate.rs
@@ -1,363 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::time::Duration;
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use radroots_events::kinds::KIND_FARM;
-use radroots_events::listing::RadrootsListingFarmRef;
-use radroots_events::{RadrootsNostrEvent as RadrootsWireEvent, RadrootsNostrEventPtr};
-use radroots_nostr::prelude::{
- radroots_nostr_build_event,
- radroots_nostr_parse_pubkey,
- radroots_nostr_send_event,
- RadrootsNostrClient,
- RadrootsNostrEvent as RadrootsRawEvent,
- RadrootsNostrEventId,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-use radroots_nostr::util::event_created_at_u32_saturating;
-use radroots_trade::listing::dvm::{
- TradeListingEnvelope,
- TradeListingMessageType,
- TradeListingValidateRequest,
- TradeListingValidateResult,
-};
-use radroots_trade::listing::validation::{validate_listing_event, TradeListingValidationError};
-
-use crate::api::jsonrpc::nostr::{event_tags, publish_response, PublishResponse};
-use crate::api::jsonrpc::params::timeout_or;
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-use super::helpers::{fetch_latest_listing_event, parse_listing_addr};
-
-#[derive(Debug, Deserialize)]
-struct TradeListingValidateParams {
- listing_addr: String,
- #[serde(default)]
- listing_event: Option<RadrootsNostrEventPtr>,
- #[serde(default)]
- timeout_secs: Option<u64>,
- #[serde(default)]
- recipient_pubkey: Option<String>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("trade.listing.validate.request");
- m.register_async_method("trade.listing.validate.request", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let TradeListingValidateRequestParams {
- listing_addr,
- recipient_pubkey,
- listing_event,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let addr = parse_listing_addr(&listing_addr)?;
- let listing_addr = addr.as_str();
-
- let recipient = radroots_nostr_parse_pubkey(&recipient_pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid recipient_pubkey: {e}")))?;
-
- let payload = TradeListingValidateRequest { listing_event };
- let envelope = TradeListingEnvelope::new(
- TradeListingMessageType::ListingValidateRequest,
- listing_addr.clone(),
- None,
- payload,
- );
- envelope
- .validate()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let content = serde_json::to_string(&envelope)
- .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?;
- let tags = vec![
- vec!["p".to_string(), recipient.to_string()],
- vec!["a".to_string(), listing_addr.clone()],
- ];
-
- let builder = radroots_nostr_build_event(
- TradeListingMessageType::ListingValidateRequest.kind() as u32,
- content,
- tags,
- )
- .map_err(|e| RpcError::Other(format!("failed to build validate request event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish validate request: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- registry.track("trade.listing.validate");
- m.register_async_method("trade.listing.validate", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let TradeListingValidateParams {
- listing_addr,
- listing_event,
- timeout_secs,
- recipient_pubkey,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let addr = parse_listing_addr(&listing_addr)?;
- let listing_addr = addr.as_str();
- let timeout_secs = timeout_or(timeout_secs);
-
- let listing_event = if let Some(ptr) = listing_event {
- let event_id = RadrootsNostrEventId::parse(&ptr.id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid listing_event id: {e}")))?;
- match fetch_event_by_id(&ctx.state.client, event_id, timeout_secs).await {
- Ok(event) => event,
- Err(_) => {
- let errors = vec![TradeListingValidationError::ListingEventFetchFailed {
- listing_addr: listing_addr.clone(),
- }];
- let result = TradeListingValidateResult {
- valid: false,
- errors,
- };
- return Ok::<TradeListingValidateResult, RpcError>(result);
- }
- }
- } else {
- match fetch_latest_listing_event(&ctx.state.client, &addr, timeout_secs).await {
- Ok(event) => event,
- Err(err) => {
- if matches!(err, RpcError::InvalidParams(_)) {
- return Err(err);
- }
- let errors = vec![TradeListingValidationError::ListingEventFetchFailed {
- listing_addr: listing_addr.clone(),
- }];
- let result = TradeListingValidateResult {
- valid: false,
- errors,
- };
- return Ok::<TradeListingValidateResult, RpcError>(result);
- }
- }
- };
-
- let errors = if let Some(event) = listing_event {
- let rr_event = radroots_event_from_nostr(&event);
- match validate_listing_event(&rr_event) {
- Ok(listing) => validate_farm_dependencies(&ctx.state.client, &listing.listing.farm, timeout_secs).await,
- Err(err) => vec![err],
- }
- } else {
- vec![TradeListingValidationError::ListingEventNotFound {
- listing_addr: listing_addr.clone(),
- }]
- };
-
- let result = TradeListingValidateResult {
- valid: errors.is_empty(),
- errors,
- };
-
- if let Some(recipient_pubkey) = recipient_pubkey {
- publish_validate_result(
- &ctx.state.client,
- &listing_addr,
- &recipient_pubkey,
- &result,
- )
- .await?;
- }
- Ok::<TradeListingValidateResult, RpcError>(result)
- })?;
- Ok(())
-}
-
-#[derive(Debug, Deserialize)]
-struct TradeListingValidateRequestParams {
- listing_addr: String,
- recipient_pubkey: String,
- #[serde(default)]
- listing_event: Option<RadrootsNostrEventPtr>,
-}
-
-async fn publish_validate_result(
- client: &RadrootsNostrClient,
- listing_addr: &str,
- recipient_pubkey: &str,
- result: &TradeListingValidateResult,
-) -> Result<(), RpcError> {
- let recipient = radroots_nostr_parse_pubkey(recipient_pubkey)
- .map_err(|e| RpcError::InvalidParams(format!("invalid recipient_pubkey: {e}")))?;
- let envelope = TradeListingEnvelope::new(
- TradeListingMessageType::ListingValidateResult,
- listing_addr.to_string(),
- None,
- result.clone(),
- );
- envelope
- .validate()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let content = serde_json::to_string(&envelope)
- .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?;
- let tags = vec![
- vec!["p".to_string(), recipient.to_string()],
- vec!["a".to_string(), listing_addr.to_string()],
- ];
-
- let builder = radroots_nostr_build_event(
- TradeListingMessageType::ListingValidateResult.kind() as u32,
- content,
- tags,
- )
- .map_err(|e| RpcError::Other(format!("failed to build validate result event: {e}")))?;
-
- let output = radroots_nostr_send_event(client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish validate result: {e}")))?;
- if !output.failed.is_empty() {
- return Err(RpcError::Other(format!(
- "validate result delivery failed: {:?}",
- output.failed
- )));
- }
- Ok(())
-}
-
-async fn fetch_event_by_id(
- client: &RadrootsNostrClient,
- event_id: RadrootsNostrEventId,
- timeout_secs: u64,
-) -> Result<Option<RadrootsRawEvent>, RpcError> {
- let filter = RadrootsNostrFilter::new().id(event_id);
- let events = client
- .fetch_events(filter, Duration::from_secs(timeout_secs))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
- Ok(events.into_iter().next())
-}
-
-async fn fetch_latest_event_by_kind(
- client: &RadrootsNostrClient,
- filter: RadrootsNostrFilter,
- kind: RadrootsNostrKind,
- timeout_secs: u64,
-) -> Result<Option<RadrootsRawEvent>, RpcError> {
- 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<RadrootsRawEvent> = None;
- for ev in events {
- if ev.kind != kind {
- continue;
- }
- match &latest {
- Some(cur) if ev.created_at <= cur.created_at => {}
- _ => latest = Some(ev),
- }
- }
- Ok(latest)
-}
-
-async fn validate_farm_dependencies(
- client: &RadrootsNostrClient,
- farm: &RadrootsListingFarmRef,
- timeout_secs: u64,
-) -> Vec<TradeListingValidationError> {
- let mut errors = Vec::new();
- let farm_pubkey = farm.pubkey.trim();
- let farm_d_tag = farm.d_tag.trim();
- let author = match radroots_nostr_parse_pubkey(farm_pubkey) {
- Ok(author) => author,
- Err(_) => {
- errors.push(TradeListingValidationError::MissingFarmProfile);
- errors.push(TradeListingValidationError::MissingFarmRecord);
- return errors;
- }
- };
-
- let profile_filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Metadata)
- .author(author.clone());
- let profile_event =
- match fetch_latest_event_by_kind(client, profile_filter, RadrootsNostrKind::Metadata, timeout_secs).await {
- Ok(event) => event,
- Err(_) => None,
- };
- let has_profile = profile_event
- .map(|event| tag_has_value(&event_tags(&event), "t", "radroots:type:farm"))
- .unwrap_or(false);
- if !has_profile {
- errors.push(TradeListingValidationError::MissingFarmProfile);
- }
-
- if !farm_d_tag.is_empty() {
- let record_filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_FARM as u16))
- .author(author)
- .identifier(farm_d_tag.to_string());
- let record_event = match fetch_latest_event_by_kind(
- client,
- record_filter,
- RadrootsNostrKind::Custom(KIND_FARM as u16),
- timeout_secs,
- )
- .await
- {
- Ok(event) => event,
- Err(_) => None,
- };
- if record_event.is_none() {
- errors.push(TradeListingValidationError::MissingFarmRecord);
- }
- } else {
- errors.push(TradeListingValidationError::MissingFarmRecord);
- }
-
- errors
-}
-
-fn radroots_event_from_nostr(event: &RadrootsRawEvent) -> RadrootsWireEvent {
- RadrootsWireEvent {
- id: event.id.to_string(),
- author: event.pubkey.to_string(),
- created_at: event_created_at_u32_saturating(event),
- kind: event.kind.as_u16() as u32,
- tags: event_tags(event),
- content: event.content.clone(),
- sig: event.sig.to_string(),
- }
-}
-
-fn tag_has_value(tags: &[Vec<String>], key: &str, value: &str) -> bool {
- tags.iter().any(|tag| {
- tag.get(0).map(|k| k.as_str()) == Some(key)
- && tag.get(1).map(|v| v.as_str()) == Some(value)
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::tag_has_value;
-
- #[test]
- fn tag_has_value_matches_exact() {
- let tags = vec![
- vec!["t".to_string(), "radroots:type:farm".to_string()],
- vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAg".to_string()],
- ];
- assert!(tag_has_value(&tags, "t", "radroots:type:farm"));
- assert!(!tag_has_value(&tags, "t", "radroots:type:individual"));
- }
-
-}
diff --git a/src/api/jsonrpc/methods/domains/trade/mod.rs b/src/api/jsonrpc/methods/domains/trade/mod.rs
@@ -1,14 +0,0 @@
-#![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/comment/get.rs b/src/api/jsonrpc/methods/events/comment/get.rs
@@ -1,58 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_COMMENT;
-use radroots_nostr::prelude::{
- RadrootsNostrEventId,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-use super::list::{build_comment_rows, CommentRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct CommentGetParams {
- id: String,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct CommentGetResponse {
- comment: Option<CommentRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.comment.get");
- m.register_async_method("events.comment.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let CommentGetParams { id, timeout_secs } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let id = require_non_empty("id", id)?;
- let event_id = RadrootsNostrEventId::parse(&id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_COMMENT as u16))
- .id(event_id);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let comment = event.and_then(|event| build_comment_rows(vec![event]).into_iter().next());
-
- Ok::<CommentGetResponse, RpcError>(CommentGetResponse { comment })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/comment/list.rs b/src/api/jsonrpc/methods/events/comment/list.rs
@@ -1,275 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::api::jsonrpc::methods::events::helpers::require_non_empty;
-use radroots_events::comment::RadrootsComment;
-use radroots_events::kinds::KIND_COMMENT;
-use radroots_events_codec::comment::decode::comment_from_tags;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct CommentRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- comment: Option<RadrootsComment>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct CommentListResponse {
- comments: Vec<CommentRow>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-struct CommentListParams {
- #[serde(flatten)]
- base: EventListParams,
- #[serde(default)]
- root_id: Option<String>,
- #[serde(default)]
- parent_id: Option<String>,
-}
-
-pub(crate) fn build_comment_rows<I>(events: I) -> Vec<CommentRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let comment = parse_comment_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- CommentRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- comment,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_comment_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsComment> {
- let kind = event.kind.as_u16() as u32;
- comment_from_tags(kind, tags, &event.content).ok()
-}
-
-fn comment_matches_filter(
- comment: &RadrootsComment,
- root_id: Option<&str>,
- parent_id: Option<&str>,
-) -> bool {
- if let Some(root_id) = root_id {
- if comment.root.id != root_id {
- return false;
- }
- }
- if let Some(parent_id) = parent_id {
- if comment.parent.id != parent_id {
- return false;
- }
- }
- true
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.comment.list");
- m.register_async_method("events.comment.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let CommentListParams {
- base,
- root_id,
- parent_id,
- } = params
- .parse::<Option<CommentListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = base;
-
- let root_id = match root_id {
- Some(value) => Some(require_non_empty("root_id", value)?),
- None => None,
- };
- let parent_id = match parent_id {
- Some(value) => Some(require_non_empty("parent_id", value)?),
- None => None,
- };
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_COMMENT as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let mut items = build_comment_rows(events);
- if root_id.is_some() || parent_id.is_some() {
- items.retain(|row| {
- row.comment
- .as_ref()
- .map(|comment| comment_matches_filter(comment, root_id.as_deref(), parent_id.as_deref()))
- .unwrap_or(false)
- });
- }
- if items.len() > limit {
- items.truncate(limit);
- }
-
- Ok::<CommentListResponse, RpcError>(CommentListResponse { comments: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_comment_rows;
- use radroots_events::comment::RadrootsComment;
- use radroots_events::kinds::{KIND_COMMENT, KIND_POST};
- use radroots_events::RadrootsNostrEventRef;
- use radroots_events_codec::comment::encode::comment_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn comment_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 10);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_COMMENT,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_comment(root_id: &str, parent_id: &str, author: &str, content: &str) -> RadrootsComment {
- let root = RadrootsNostrEventRef {
- id: root_id.to_string(),
- author: author.to_string(),
- kind: KIND_POST,
- d_tag: None,
- relays: None,
- };
- let parent = RadrootsNostrEventRef {
- id: parent_id.to_string(),
- author: author.to_string(),
- kind: KIND_POST,
- d_tag: None,
- relays: None,
- };
- RadrootsComment {
- root,
- parent,
- content: content.to_string(),
- }
- }
-
- #[test]
- fn comment_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let comment = sample_comment("root-1", "parent-1", pubkey, "hello");
- let tags = comment_build_tags(&comment).expect("tags");
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let older = comment_event(&old_id, pubkey, 100, tags.clone(), &comment.content);
- let newer = comment_event(&new_id, pubkey, 200, tags.clone(), &comment.content);
-
- let comments = build_comment_rows(vec![older, newer]);
-
- assert_eq!(comments.len(), 2);
- assert_eq!(comments[0].id, new_id);
- assert_eq!(comments[0].created_at, 200);
- assert_eq!(comments[1].id, old_id);
- assert_eq!(comments[1].created_at, 100);
- }
-
- #[test]
- fn comment_list_decodes_comment() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let comment = sample_comment("root-1", "parent-1", pubkey, "hello");
- let tags = comment_build_tags(&comment).expect("tags");
- let id = format!("{:064x}", 3);
- let event = comment_event(&id, pubkey, 300, tags.clone(), &comment.content);
-
- let comments = build_comment_rows(vec![event]);
-
- assert_eq!(comments.len(), 1);
- assert_eq!(comments[0].tags, tags);
- let parsed = comments[0].comment.as_ref().expect("comment");
- assert_eq!(parsed.content, "hello");
- assert_eq!(parsed.root.id, "root-1");
- assert_eq!(parsed.parent.id, "parent-1");
- }
-
- #[test]
- fn comment_filters_match_root_and_parent() {
- let pubkey = "3bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let comment = sample_comment("root-1", "parent-1", pubkey, "hello");
-
- assert!(super::comment_matches_filter(&comment, Some("root-1"), None));
- assert!(super::comment_matches_filter(&comment, None, Some("parent-1")));
- assert!(super::comment_matches_filter(&comment, Some("root-1"), Some("parent-1")));
- assert!(!super::comment_matches_filter(&comment, Some("root-2"), None));
- assert!(!super::comment_matches_filter(&comment, None, Some("parent-2")));
- }
-}
diff --git a/src/api/jsonrpc/methods/events/comment/mod.rs b/src/api/jsonrpc/methods/events/comment/mod.rs
@@ -1,18 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod get;
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/comment/publish.rs b/src/api/jsonrpc/methods/events/comment/publish.rs
@@ -1,51 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::comment::RadrootsComment;
-use radroots_events::kinds::KIND_COMMENT;
-use radroots_events_codec::comment::encode::to_wire_parts;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishCommentParams {
- comment: RadrootsComment,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.comment.publish");
- m.register_async_method("events.comment.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishCommentParams { comment, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let parts = to_wire_parts(&comment)
- .map_err(|e| RpcError::InvalidParams(format!("invalid comment: {e}")))?;
- let mut tag_slices = parts.tags;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(KIND_COMMENT, parts.content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build comment: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish comment: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/get.rs b/src/api/jsonrpc/methods/events/dvm_feedback/get.rs
@@ -1,55 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_JOB_FEEDBACK;
-use radroots_nostr::prelude::{RadrootsNostrEventId, RadrootsNostrFilter};
-
-use super::list::{build_dvm_feedback_rows, DvmFeedbackRow};
-use crate::api::jsonrpc::methods::events::helpers::{fetch_latest_event, require_non_empty};
-
-#[derive(Debug, Deserialize)]
-struct DvmFeedbackGetParams {
- id: String,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct DvmFeedbackGetResponse {
- feedback: Option<DvmFeedbackRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_feedback.get");
- m.register_async_method("events.dvm_feedback.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let DvmFeedbackGetParams { id, timeout_secs } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let id = require_non_empty("id", id)?;
- let event_id = RadrootsNostrEventId::parse(&id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?;
-
- let filter = RadrootsNostrFilter::new().id(event_id);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let feedback = event.and_then(|event| {
- let kind = event.kind.as_u16() as u32;
- if kind != KIND_JOB_FEEDBACK {
- return None;
- }
- build_dvm_feedback_rows(vec![event]).into_iter().next()
- });
-
- Ok::<DvmFeedbackGetResponse, RpcError>(DvmFeedbackGetResponse { feedback })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/list.rs b/src/api/jsonrpc/methods/events/dvm_feedback/list.rs
@@ -1,237 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::job_feedback::RadrootsJobFeedback;
-use radroots_events::kinds::KIND_JOB_FEEDBACK;
-use radroots_events_codec::job::feedback::decode::job_feedback_from_tags;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrEventId,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct DvmFeedbackRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- feedback: Option<RadrootsJobFeedback>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct DvmFeedbackListResponse {
- feedbacks: Vec<DvmFeedbackRow>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-struct DvmFeedbackListParams {
- #[serde(default)]
- authors: Option<Vec<String>>,
- #[serde(default)]
- limit: Option<u64>,
- #[serde(default)]
- since: Option<u64>,
- #[serde(default)]
- until: Option<u64>,
- #[serde(default)]
- timeout_secs: Option<u64>,
- #[serde(default)]
- request_id: Option<String>,
-}
-
-pub(crate) fn build_dvm_feedback_rows<I>(events: I) -> Vec<DvmFeedbackRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let feedback = parse_dvm_feedback_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- DvmFeedbackRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- feedback,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_dvm_feedback_event(
- event: &RadrootsNostrEvent,
- tags: &[Vec<String>],
-) -> Option<RadrootsJobFeedback> {
- let kind = event.kind.as_u16() as u32;
- if kind != KIND_JOB_FEEDBACK {
- return None;
- }
- job_feedback_from_tags(kind, tags, &event.content).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_feedback.list");
- m.register_async_method("events.dvm_feedback.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let DvmFeedbackListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- request_id,
- } = params
- .parse::<Option<DvmFeedbackListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_JOB_FEEDBACK as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- if let Some(request_id) = request_id {
- let request_id = request_id.trim();
- if request_id.is_empty() {
- return Err(RpcError::InvalidParams(
- "request_id cannot be empty".to_string(),
- ));
- }
- let request_id = RadrootsNostrEventId::parse(request_id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid request_id: {e}")))?;
- filter = filter.event(request_id);
- }
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_dvm_feedback_rows(events);
-
- Ok::<DvmFeedbackListResponse, RpcError>(DvmFeedbackListResponse { feedbacks: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_dvm_feedback_rows;
- use radroots_events::job::JobFeedbackStatus;
- use radroots_events::job_feedback::RadrootsJobFeedback;
- use radroots_events::kinds::KIND_JOB_FEEDBACK;
- use radroots_events::RadrootsNostrEventPtr;
- use radroots_events_codec::job::feedback::encode::job_feedback_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn dvm_feedback_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 11);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_JOB_FEEDBACK,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_feedback() -> RadrootsJobFeedback {
- RadrootsJobFeedback {
- kind: KIND_JOB_FEEDBACK as u16,
- status: JobFeedbackStatus::Success,
- extra_info: Some("ok".to_string()),
- request_event: RadrootsNostrEventPtr {
- id: "req".to_string(),
- relays: None,
- },
- customer_pubkey: None,
- payment: None,
- content: Some("payload".to_string()),
- encrypted: false,
- }
- }
-
- #[test]
- fn dvm_feedback_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let feedback = sample_feedback();
- let tags = job_feedback_build_tags(&feedback);
- let older = dvm_feedback_event(&old_id, pubkey, 100, tags.clone(), "payload");
- let newer = dvm_feedback_event(&new_id, pubkey, 200, tags.clone(), "payload");
-
- let feedbacks = build_dvm_feedback_rows(vec![older, newer]);
-
- assert_eq!(feedbacks.len(), 2);
- assert_eq!(feedbacks[0].id, new_id);
- assert_eq!(feedbacks[0].created_at, 200);
- assert_eq!(feedbacks[1].id, old_id);
- assert_eq!(feedbacks[1].created_at, 100);
- }
-
- #[test]
- fn dvm_feedback_list_decodes_feedback() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let feedback = sample_feedback();
- let content = feedback.content.clone().unwrap();
- let tags = job_feedback_build_tags(&feedback);
- let id = format!("{:064x}", 3);
- let event = dvm_feedback_event(&id, pubkey, 300, tags.clone(), &content);
-
- let feedbacks = build_dvm_feedback_rows(vec![event]);
-
- assert_eq!(feedbacks.len(), 1);
- assert_eq!(feedbacks[0].tags, tags);
- let decoded = feedbacks[0].feedback.as_ref().expect("feedback");
- assert_eq!(decoded, &feedback);
- }
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/mod.rs b/src/api/jsonrpc/methods/events/dvm_feedback/mod.rs
@@ -1,18 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod get;
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/publish.rs b/src/api/jsonrpc/methods/events/dvm_feedback/publish.rs
@@ -1,51 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::job_feedback::RadrootsJobFeedback;
-use radroots_events_codec::job::encode::canonicalize_tags;
-use radroots_events_codec::job::feedback::encode::to_wire_parts;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishDvmFeedbackParams {
- feedback: RadrootsJobFeedback,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_feedback.publish");
- m.register_async_method("events.dvm_feedback.publish", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishDvmFeedbackParams { feedback, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let content = feedback.content.clone().unwrap_or_default();
- let mut parts = to_wire_parts(&feedback, &content)
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- parts.tags.extend(extra_tags);
- canonicalize_tags(&mut parts.tags);
- }
-
- let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .map_err(|e| RpcError::Other(format!("failed to build dvm feedback event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish dvm feedback: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_request/get.rs b/src/api/jsonrpc/methods/events/dvm_request/get.rs
@@ -1,55 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::is_request_kind;
-use radroots_nostr::prelude::{RadrootsNostrEventId, RadrootsNostrFilter};
-
-use super::list::{build_dvm_request_rows, DvmRequestRow};
-use crate::api::jsonrpc::methods::events::helpers::{fetch_latest_event, require_non_empty};
-
-#[derive(Debug, Deserialize)]
-struct DvmRequestGetParams {
- id: String,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct DvmRequestGetResponse {
- request: Option<DvmRequestRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_request.get");
- m.register_async_method("events.dvm_request.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let DvmRequestGetParams { id, timeout_secs } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let id = require_non_empty("id", id)?;
- let event_id = RadrootsNostrEventId::parse(&id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?;
-
- let filter = RadrootsNostrFilter::new().id(event_id);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let request = event.and_then(|event| {
- let kind = event.kind.as_u16() as u32;
- if !is_request_kind(kind) {
- return None;
- }
- build_dvm_request_rows(vec![event]).into_iter().next()
- });
-
- Ok::<DvmRequestGetResponse, RpcError>(DvmRequestGetResponse { request })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_request/list.rs b/src/api/jsonrpc/methods/events/dvm_request/list.rs
@@ -1,252 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::job_request::RadrootsJobRequest;
-use radroots_events::kinds::{is_request_kind, KIND_JOB_REQUEST_MAX, KIND_JOB_REQUEST_MIN};
-use radroots_events_codec::job::request::decode::job_request_from_tags;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct DvmRequestRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- request: Option<RadrootsJobRequest>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct DvmRequestListResponse {
- requests: Vec<DvmRequestRow>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-struct DvmRequestListParams {
- #[serde(default)]
- authors: Option<Vec<String>>,
- #[serde(default)]
- limit: Option<u64>,
- #[serde(default)]
- since: Option<u64>,
- #[serde(default)]
- until: Option<u64>,
- #[serde(default)]
- timeout_secs: Option<u64>,
- #[serde(default)]
- kinds: Option<Vec<u32>>,
-}
-
-fn dvm_request_kinds_or(kinds: Option<Vec<u32>>) -> Result<Vec<RadrootsNostrKind>, RpcError> {
- let kinds = match kinds {
- Some(kinds) => {
- if kinds.is_empty() {
- return Err(RpcError::InvalidParams(
- "dvm request kinds cannot be empty".to_string(),
- ));
- }
- kinds
- }
- None => (KIND_JOB_REQUEST_MIN..=KIND_JOB_REQUEST_MAX).collect(),
- };
-
- let mut out = Vec::with_capacity(kinds.len());
- for kind in kinds {
- if !is_request_kind(kind) {
- return Err(RpcError::InvalidParams(format!(
- "invalid dvm request kind: {kind}",
- )));
- }
- let kind = u16::try_from(kind)
- .map_err(|_| RpcError::InvalidParams(format!("dvm request kind out of range: {kind}")))?;
- out.push(RadrootsNostrKind::Custom(kind));
- }
- Ok(out)
-}
-
-pub(crate) fn build_dvm_request_rows<I>(events: I) -> Vec<DvmRequestRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let request = parse_dvm_request_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- DvmRequestRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- request,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_dvm_request_event(
- event: &RadrootsNostrEvent,
- tags: &[Vec<String>],
-) -> Option<RadrootsJobRequest> {
- let kind = event.kind.as_u16() as u32;
- if !is_request_kind(kind) {
- return None;
- }
- job_request_from_tags(kind, tags).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_request.list");
- m.register_async_method("events.dvm_request.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let DvmRequestListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- kinds,
- } = params
- .parse::<Option<DvmRequestListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
- let kinds = dvm_request_kinds_or(kinds)?;
-
- let mut filter = RadrootsNostrFilter::new().limit(limit).kinds(kinds);
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_dvm_request_rows(events);
-
- Ok::<DvmRequestListResponse, RpcError>(DvmRequestListResponse { requests: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_dvm_request_rows;
- use radroots_events::job::JobInputType;
- use radroots_events::job_request::{RadrootsJobInput, RadrootsJobRequest};
- use radroots_events::kinds::KIND_JOB_REQUEST_MIN;
- use radroots_events_codec::job::request::encode::job_request_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn dvm_request_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 9);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": kind,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_request() -> RadrootsJobRequest {
- RadrootsJobRequest {
- kind: (KIND_JOB_REQUEST_MIN + 1) as u16,
- inputs: vec![RadrootsJobInput {
- data: "https://example.com".to_string(),
- input_type: JobInputType::Url,
- relay: None,
- marker: None,
- }],
- output: None,
- params: Vec::new(),
- bid_sat: None,
- relays: Vec::new(),
- providers: Vec::new(),
- topics: Vec::new(),
- encrypted: false,
- }
- }
-
- #[test]
- fn dvm_request_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let req = sample_request();
- let tags = job_request_build_tags(&req);
- let older = dvm_request_event(&old_id, pubkey, 100, KIND_JOB_REQUEST_MIN + 1, tags.clone(), "");
- let newer = dvm_request_event(&new_id, pubkey, 200, KIND_JOB_REQUEST_MIN + 1, tags.clone(), "");
-
- let requests = build_dvm_request_rows(vec![older, newer]);
-
- assert_eq!(requests.len(), 2);
- assert_eq!(requests[0].id, new_id);
- assert_eq!(requests[0].created_at, 200);
- assert_eq!(requests[1].id, old_id);
- assert_eq!(requests[1].created_at, 100);
- }
-
- #[test]
- fn dvm_request_list_decodes_request() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let req = sample_request();
- let tags = job_request_build_tags(&req);
- let id = format!("{:064x}", 3);
- let event = dvm_request_event(&id, pubkey, 300, KIND_JOB_REQUEST_MIN + 1, tags.clone(), "payload");
-
- let requests = build_dvm_request_rows(vec![event]);
-
- assert_eq!(requests.len(), 1);
- assert_eq!(requests[0].tags, tags);
- let decoded = requests[0].request.as_ref().expect("request");
- assert_eq!(decoded, &req);
- }
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_request/mod.rs b/src/api/jsonrpc/methods/events/dvm_request/mod.rs
@@ -1,18 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod get;
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_request/publish.rs b/src/api/jsonrpc/methods/events/dvm_request/publish.rs
@@ -1,65 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::job_request::RadrootsJobRequest;
-use radroots_events_codec::job::encode::canonicalize_tags;
-use radroots_events_codec::job::request::encode::to_wire_parts;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-use radroots_trade::listing::dvm_kinds::is_trade_listing_dvm_request_kind;
-
-#[derive(Debug, Deserialize)]
-struct PublishDvmRequestParams {
- request: RadrootsJobRequest,
- #[serde(default)]
- content: Option<String>,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_request.publish");
- m.register_async_method("events.dvm_request.publish", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishDvmRequestParams {
- request,
- content,
- tags,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- if is_trade_listing_dvm_request_kind(request.kind) {
- return Err(RpcError::InvalidParams(
- "trade listing request kinds must use trade.listing.validate.request or trade.listing.order"
- .to_string(),
- ));
- }
-
- let content = content.unwrap_or_default();
- let mut parts = to_wire_parts(&request, &content)
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- parts.tags.extend(extra_tags);
- canonicalize_tags(&mut parts.tags);
- }
-
- let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .map_err(|e| RpcError::Other(format!("failed to build dvm request event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish dvm request: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_result/get.rs b/src/api/jsonrpc/methods/events/dvm_result/get.rs
@@ -1,55 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::is_result_kind;
-use radroots_nostr::prelude::{RadrootsNostrEventId, RadrootsNostrFilter};
-
-use super::list::{build_dvm_result_rows, DvmResultRow};
-use crate::api::jsonrpc::methods::events::helpers::{fetch_latest_event, require_non_empty};
-
-#[derive(Debug, Deserialize)]
-struct DvmResultGetParams {
- id: String,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct DvmResultGetResponse {
- result: Option<DvmResultRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_result.get");
- m.register_async_method("events.dvm_result.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let DvmResultGetParams { id, timeout_secs } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let id = require_non_empty("id", id)?;
- let event_id = RadrootsNostrEventId::parse(&id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?;
-
- let filter = RadrootsNostrFilter::new().id(event_id);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let result = event.and_then(|event| {
- let kind = event.kind.as_u16() as u32;
- if !is_result_kind(kind) {
- return None;
- }
- build_dvm_result_rows(vec![event]).into_iter().next()
- });
-
- Ok::<DvmResultGetResponse, RpcError>(DvmResultGetResponse { result })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_result/list.rs b/src/api/jsonrpc/methods/events/dvm_result/list.rs
@@ -1,272 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::job_result::RadrootsJobResult;
-use radroots_events::kinds::{is_result_kind, KIND_JOB_RESULT_MAX, KIND_JOB_RESULT_MIN};
-use radroots_events_codec::job::result::decode::job_result_from_tags;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrEventId,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct DvmResultRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- result: Option<RadrootsJobResult>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct DvmResultListResponse {
- results: Vec<DvmResultRow>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-struct DvmResultListParams {
- #[serde(default)]
- authors: Option<Vec<String>>,
- #[serde(default)]
- limit: Option<u64>,
- #[serde(default)]
- since: Option<u64>,
- #[serde(default)]
- until: Option<u64>,
- #[serde(default)]
- timeout_secs: Option<u64>,
- #[serde(default)]
- kinds: Option<Vec<u32>>,
- #[serde(default)]
- request_id: Option<String>,
-}
-
-fn dvm_result_kinds_or(kinds: Option<Vec<u32>>) -> Result<Vec<RadrootsNostrKind>, RpcError> {
- let kinds = match kinds {
- Some(kinds) => {
- if kinds.is_empty() {
- return Err(RpcError::InvalidParams(
- "dvm result kinds cannot be empty".to_string(),
- ));
- }
- kinds
- }
- None => (KIND_JOB_RESULT_MIN..=KIND_JOB_RESULT_MAX).collect(),
- };
-
- let mut out = Vec::with_capacity(kinds.len());
- for kind in kinds {
- if !is_result_kind(kind) {
- return Err(RpcError::InvalidParams(format!(
- "invalid dvm result kind: {kind}",
- )));
- }
- let kind = u16::try_from(kind)
- .map_err(|_| RpcError::InvalidParams(format!("dvm result kind out of range: {kind}")))?;
- out.push(RadrootsNostrKind::Custom(kind));
- }
- Ok(out)
-}
-
-pub(crate) fn build_dvm_result_rows<I>(events: I) -> Vec<DvmResultRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let result = parse_dvm_result_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- DvmResultRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- result,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_dvm_result_event(
- event: &RadrootsNostrEvent,
- tags: &[Vec<String>],
-) -> Option<RadrootsJobResult> {
- let kind = event.kind.as_u16() as u32;
- if !is_result_kind(kind) {
- return None;
- }
- job_result_from_tags(kind, tags, &event.content).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_result.list");
- m.register_async_method("events.dvm_result.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let DvmResultListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- kinds,
- request_id,
- } = params
- .parse::<Option<DvmResultListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
- let kinds = dvm_result_kinds_or(kinds)?;
-
- let mut filter = RadrootsNostrFilter::new().limit(limit).kinds(kinds);
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- if let Some(request_id) = request_id {
- let request_id = request_id.trim();
- if request_id.is_empty() {
- return Err(RpcError::InvalidParams(
- "request_id cannot be empty".to_string(),
- ));
- }
- let request_id = RadrootsNostrEventId::parse(request_id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid request_id: {e}")))?;
- filter = filter.event(request_id);
- }
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_dvm_result_rows(events);
-
- Ok::<DvmResultListResponse, RpcError>(DvmResultListResponse { results: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_dvm_result_rows;
- use radroots_events::job_request::RadrootsJobInput;
- use radroots_events::job_result::RadrootsJobResult;
- use radroots_events::kinds::KIND_JOB_RESULT_MIN;
- use radroots_events::RadrootsNostrEventPtr;
- use radroots_events_codec::job::result::encode::job_result_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn dvm_result_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 10);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": kind,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_result() -> RadrootsJobResult {
- RadrootsJobResult {
- kind: (KIND_JOB_RESULT_MIN + 1) as u16,
- request_event: RadrootsNostrEventPtr {
- id: "req".to_string(),
- relays: None,
- },
- request_json: None,
- inputs: vec![RadrootsJobInput {
- data: "https://example.com".to_string(),
- input_type: radroots_events::job::JobInputType::Url,
- relay: None,
- marker: None,
- }],
- customer_pubkey: None,
- payment: None,
- content: Some("payload".to_string()),
- encrypted: false,
- }
- }
-
- #[test]
- fn dvm_result_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let result = sample_result();
- let tags = job_result_build_tags(&result);
- let older = dvm_result_event(&old_id, pubkey, 100, KIND_JOB_RESULT_MIN + 1, tags.clone(), "payload");
- let newer = dvm_result_event(&new_id, pubkey, 200, KIND_JOB_RESULT_MIN + 1, tags.clone(), "payload");
-
- let results = build_dvm_result_rows(vec![older, newer]);
-
- assert_eq!(results.len(), 2);
- assert_eq!(results[0].id, new_id);
- assert_eq!(results[0].created_at, 200);
- assert_eq!(results[1].id, old_id);
- assert_eq!(results[1].created_at, 100);
- }
-
- #[test]
- fn dvm_result_list_decodes_result() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let result = sample_result();
- let content = result.content.clone().unwrap();
- let tags = job_result_build_tags(&result);
- let id = format!("{:064x}", 3);
- let event = dvm_result_event(&id, pubkey, 300, KIND_JOB_RESULT_MIN + 1, tags.clone(), &content);
-
- let results = build_dvm_result_rows(vec![event]);
-
- assert_eq!(results.len(), 1);
- assert_eq!(results[0].tags, tags);
- let decoded = results[0].result.as_ref().expect("result");
- assert_eq!(decoded, &result);
- }
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_result/mod.rs b/src/api/jsonrpc/methods/events/dvm_result/mod.rs
@@ -1,18 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod get;
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/dvm_result/publish.rs b/src/api/jsonrpc/methods/events/dvm_result/publish.rs
@@ -1,59 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::job_result::RadrootsJobResult;
-use radroots_events_codec::job::encode::canonicalize_tags;
-use radroots_events_codec::job::result::encode::to_wire_parts;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-use radroots_trade::listing::dvm_kinds::is_trade_listing_dvm_result_kind;
-
-#[derive(Debug, Deserialize)]
-struct PublishDvmResultParams {
- result: RadrootsJobResult,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.dvm_result.publish");
- m.register_async_method("events.dvm_result.publish", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishDvmResultParams { result, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- if is_trade_listing_dvm_result_kind(result.kind) {
- return Err(RpcError::InvalidParams(
- "trade listing result kinds must use trade.listing.validate or trade.listing.order"
- .to_string(),
- ));
- }
-
- let content = result.content.clone().unwrap_or_default();
- let mut parts = to_wire_parts(&result, &content)
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- parts.tags.extend(extra_tags);
- canonicalize_tags(&mut parts.tags);
- }
-
- let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .map_err(|e| RpcError::Other(format!("failed to build dvm result event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish dvm result: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/farm/get.rs b/src/api/jsonrpc/methods/events/farm/get.rs
@@ -1,61 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_FARM;
-use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_farm_rows, FarmRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct FarmGetParams {
- d_tag: String,
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct FarmGetResponse {
- farm: Option<FarmRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.farm.get");
- m.register_async_method("events.farm.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let FarmGetParams {
- d_tag,
- author,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
- let d_tag = require_non_empty("d_tag", d_tag)?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_FARM as u16))
- .author(author)
- .identifiers([d_tag]);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let farm = event.and_then(|event| build_farm_rows(vec![event]).into_iter().next());
-
- Ok::<FarmGetResponse, RpcError>(FarmGetResponse { farm })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/farm/list.rs b/src/api/jsonrpc/methods/events/farm/list.rs
@@ -1,197 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::farm::RadrootsFarm;
-use radroots_events::kinds::KIND_FARM;
-use radroots_events_codec::farm::decode::farm_from_event;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct FarmRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- farm: Option<RadrootsFarm>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct FarmListResponse {
- farms: Vec<FarmRow>,
-}
-
-pub(crate) fn build_farm_rows<I>(events: I) -> Vec<FarmRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let farm = parse_farm_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- FarmRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- farm,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_farm_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsFarm> {
- let kind = event.kind.as_u16() as u32;
- farm_from_event(kind, tags, &event.content).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.farm.list");
- m.register_async_method("events.farm.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_FARM as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_farm_rows(events);
-
- Ok::<FarmListResponse, RpcError>(FarmListResponse { farms: items })
- })?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_farm_rows;
- use radroots_events::farm::RadrootsFarm;
- use radroots_events::kinds::KIND_FARM;
- use radroots_events_codec::farm::encode::farm_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn farm_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 7);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_FARM,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_farm(d_tag: &str, name: &str) -> RadrootsFarm {
- RadrootsFarm {
- d_tag: d_tag.to_string(),
- name: name.to_string(),
- about: None,
- website: None,
- picture: None,
- banner: None,
- location: None,
- tags: None,
- }
- }
-
- #[test]
- fn farm_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let farm = sample_farm("AAAAAAAAAAAAAAAAAAAAAA", "Farm One");
- let content = serde_json::to_string(&farm).expect("content");
- let tags = farm_build_tags(&farm).expect("tags");
- let older = farm_event(&old_id, pubkey, 100, tags.clone(), &content);
- let newer = farm_event(&new_id, pubkey, 200, tags.clone(), &content);
-
- let farms = build_farm_rows(vec![older, newer]);
-
- assert_eq!(farms.len(), 2);
- assert_eq!(farms[0].id, new_id);
- assert_eq!(farms[0].created_at, 200);
- assert_eq!(farms[1].id, old_id);
- assert_eq!(farms[1].created_at, 100);
- }
-
- #[test]
- fn farm_list_uses_tag_d_when_missing_in_content() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let farm = sample_farm("AAAAAAAAAAAAAAAAAAAAAA", "Farm One");
- let tags = farm_build_tags(&farm).expect("tags");
- let content_farm = sample_farm("", "Farm One");
- let content = serde_json::to_string(&content_farm).expect("content");
- let id = format!("{:064x}", 3);
- let event = farm_event(&id, pubkey, 300, tags.clone(), &content);
-
- let farms = build_farm_rows(vec![event]);
-
- assert_eq!(farms.len(), 1);
- assert_eq!(farms[0].tags, tags);
- let parsed = farms[0].farm.as_ref().expect("farm");
- assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAA");
- assert_eq!(parsed.name, "Farm One");
- }
-}
diff --git a/src/api/jsonrpc/methods/events/farm/mod.rs b/src/api/jsonrpc/methods/events/farm/mod.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod publish;
-pub mod list;
-pub mod get;
-
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/farm/publish.rs b/src/api/jsonrpc/methods/events/farm/publish.rs
@@ -1,50 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::farm::RadrootsFarm;
-use radroots_events::kinds::KIND_FARM;
-use radroots_events_codec::farm::encode::farm_build_tags;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishFarmParams {
- farm: RadrootsFarm,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.farm.publish");
- m.register_async_method("events.farm.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishFarmParams { farm, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let content = serde_json::to_string(&farm)
- .map_err(|e| RpcError::InvalidParams(format!("invalid farm json: {e}")))?;
- let mut tag_slices =
- farm_build_tags(&farm).map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(KIND_FARM, content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build farm event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish farm: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/follow/get.rs b/src/api/jsonrpc/methods/events/follow/get.rs
@@ -1,56 +0,0 @@
-#![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::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_follow_rows, FollowRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
-};
-
-#[derive(Debug, Deserialize)]
-struct FollowGetParams {
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct FollowGetResponse {
- follow: Option<FollowRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.follow.get");
- m.register_async_method("events.follow.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let FollowGetParams {
- author,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::ContactList)
- .author(author);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let follow = event.and_then(|event| build_follow_rows(vec![event]).into_iter().next());
-
- Ok::<FollowGetResponse, RpcError>(FollowGetResponse { follow })
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/follow/list.rs b/src/api/jsonrpc/methods/events/follow/list.rs
@@ -1,214 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::collections::HashMap;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::follow::RadrootsFollow;
-use radroots_events_codec::follow::decode::follow_from_tags;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
- RadrootsNostrPublicKey,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct FollowRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- follow: Option<RadrootsFollow>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct FollowListResponse {
- follows: Vec<FollowRow>,
-}
-
-pub(crate) fn build_follow_rows<I>(events: I) -> Vec<FollowRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut latest_by_author: HashMap<RadrootsNostrPublicKey, RadrootsNostrEvent> = HashMap::new();
- for event in events {
- match latest_by_author.get(&event.pubkey) {
- Some(cur) if event.created_at <= cur.created_at => {}
- _ => {
- latest_by_author.insert(event.pubkey, event);
- }
- }
- }
-
- let mut items = latest_by_author
- .into_values()
- .map(|ev| {
- let tags = event_tags(&ev);
- let follow = parse_follow_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- FollowRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- follow,
- }
- })
- .collect::<Vec<_>>();
-
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_follow_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsFollow> {
- let kind = event.kind.as_u16() as u32;
- let published_at = u32::try_from(event.created_at.as_secs()).ok()?;
- follow_from_tags(kind, tags, published_at).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.follow.list");
- m.register_async_method("events.follow.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::ContactList)
- .limit(limit);
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
-
- filter = apply_time_bounds(filter, since, until);
-
- let stored = ctx
- .state
- .client
- .database()
- .query(filter.clone())
- .await
- .map_err(|e| RpcError::Other(format!("query failed: {e}")))?;
- let fetched = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let mut items = build_follow_rows(stored.into_iter().chain(fetched.into_iter()));
- if items.len() > limit {
- items.truncate(limit);
- }
-
- Ok::<FollowListResponse, RpcError>(FollowListResponse { follows: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_follow_rows;
- use radroots_events::follow::{RadrootsFollow, RadrootsFollowProfile};
- use radroots_events::kinds::KIND_FOLLOW;
- use radroots_events_codec::follow::encode::follow_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn follow_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 9);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_FOLLOW,
- "tags": tags,
- "content": "",
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- #[test]
- fn follow_list_picks_latest_per_author() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let tags = vec![vec!["p".to_string(), "target".to_string()]];
- let older = follow_event(&old_id, pubkey, 100, tags.clone());
- let newer = follow_event(&new_id, pubkey, 200, tags.clone());
-
- let follows = build_follow_rows(vec![older, newer]);
-
- assert_eq!(follows.len(), 1);
- assert_eq!(follows[0].id, new_id);
- assert_eq!(follows[0].created_at, 200);
- }
-
- #[test]
- fn follow_list_decodes_follow_entries() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let follow = RadrootsFollow {
- list: vec![RadrootsFollowProfile {
- published_at: 0,
- public_key: "pubkey".to_string(),
- relay_url: Some("wss://relay".to_string()),
- contact_name: Some("alice".to_string()),
- }],
- };
- let tags = follow_build_tags(&follow).expect("tags");
- let id = format!("{:064x}", 3);
- let event = follow_event(&id, pubkey, 300, tags.clone());
-
- let follows = build_follow_rows(vec![event]);
-
- assert_eq!(follows.len(), 1);
- assert_eq!(follows[0].tags, tags);
- let parsed = follows[0].follow.as_ref().expect("follow");
- assert_eq!(parsed.list.len(), 1);
- assert_eq!(parsed.list[0].public_key, "pubkey");
- assert_eq!(parsed.list[0].relay_url.as_deref(), Some("wss://relay"));
- assert_eq!(parsed.list[0].contact_name.as_deref(), Some("alice"));
- assert_eq!(parsed.list[0].published_at, 300);
- }
-}
diff --git a/src/api/jsonrpc/methods/events/follow/mod.rs b/src/api/jsonrpc/methods/events/follow/mod.rs
@@ -1,20 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod get;
-pub mod list;
-pub mod publish;
-pub mod update;
-
-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)?;
- get::register(&mut m, ®istry)?;
- update::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/follow/publish.rs b/src/api/jsonrpc/methods/events/follow/publish.rs
@@ -1,51 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::follow::RadrootsFollow;
-use radroots_events::kinds::KIND_FOLLOW;
-use radroots_events_codec::follow::encode::to_wire_parts;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishFollowParams {
- follow: RadrootsFollow,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.follow.publish");
- m.register_async_method("events.follow.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishFollowParams { follow, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let parts = to_wire_parts(&follow)
- .map_err(|e| RpcError::InvalidParams(format!("invalid follow: {e}")))?;
- let mut tag_slices = parts.tags;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(KIND_FOLLOW, parts.content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build follow: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish follow: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/follow/update.rs b/src/api/jsonrpc/methods/events/follow/update.rs
@@ -1,143 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{event_tags, publish_response, PublishResponse};
-use crate::api::jsonrpc::params::parse_pubkeys;
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use crate::api::jsonrpc::methods::events::helpers::fetch_latest_event;
-use radroots_events::follow::RadrootsFollow;
-use radroots_events_codec::follow::decode::follow_from_tags;
-use radroots_events_codec::follow::encode::{follow_apply, FollowMutation, to_wire_parts};
-use radroots_nostr::prelude::{
- radroots_nostr_build_event,
- radroots_nostr_send_event,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Copy, Debug, Deserialize)]
-#[serde(rename_all = "snake_case")]
-enum FollowUpdateAction {
- Follow,
- Unfollow,
- Toggle,
-}
-
-#[derive(Debug, Deserialize)]
-struct FollowUpdateParams {
- public_key: String,
- #[serde(default)]
- relay_url: Option<String>,
- #[serde(default)]
- contact_name: Option<String>,
- #[serde(default)]
- action: Option<FollowUpdateAction>,
- #[serde(default)]
- timeout_secs: Option<u64>,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.follow.update");
- m.register_async_method("events.follow.update", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let FollowUpdateParams {
- public_key,
- relay_url,
- contact_name,
- action,
- timeout_secs,
- tags,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let public_key = normalize_pubkey(public_key)?;
- let relay_url = normalize_optional(relay_url);
- let contact_name = normalize_optional(contact_name);
-
- let base_follow = load_latest_follow(&ctx, timeout_secs).await?;
-
- let mutation = match action.unwrap_or(FollowUpdateAction::Toggle) {
- FollowUpdateAction::Follow => FollowMutation::Follow {
- public_key,
- relay_url,
- contact_name,
- },
- FollowUpdateAction::Unfollow => FollowMutation::Unfollow { public_key },
- FollowUpdateAction::Toggle => FollowMutation::Toggle {
- public_key,
- relay_url,
- contact_name,
- },
- };
-
- let updated = follow_apply(&base_follow, mutation)
- .map_err(|e| RpcError::InvalidParams(format!("invalid follow mutation: {e}")))?;
- let mut parts = to_wire_parts(&updated)
- .map_err(|e| RpcError::InvalidParams(format!("invalid follow: {e}")))?;
- if let Some(extra_tags) = tags {
- parts.tags.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .map_err(|e| RpcError::Other(format!("failed to build follow: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish follow: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
-
-fn normalize_optional(value: Option<String>) -> Option<String> {
- value.and_then(|value| {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed.to_string())
- }
- })
-}
-
-fn normalize_pubkey(value: String) -> Result<String, RpcError> {
- let mut parsed = parse_pubkeys("public_key", &[value])?;
- parsed
- .pop()
- .map(|key| key.to_string())
- .ok_or_else(|| RpcError::InvalidParams("public_key cannot be empty".to_string()))
-}
-
-async fn load_latest_follow(
- ctx: &RpcContext,
- timeout_secs: Option<u64>,
-) -> Result<RadrootsFollow, RpcError> {
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::ContactList)
- .author(ctx.state.pubkey);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- match event {
- Some(event) => {
- let tags = event_tags(&event);
- let published_at = u32::try_from(event.created_at.as_secs()).map_err(|_| {
- RpcError::Other("follow event created_at overflow".to_string())
- })?;
- follow_from_tags(event.kind.as_u16() as u32, &tags, published_at)
- .map_err(|e| RpcError::Other(format!("invalid follow event: {e}")))
- }
- None => Ok(RadrootsFollow { list: Vec::new() }),
- }
-}
diff --git a/src/api/jsonrpc/methods/events/helpers.rs b/src/api/jsonrpc/methods/events/helpers.rs
@@ -1,105 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::time::Duration;
-
-use crate::api::jsonrpc::params::{parse_pubkeys, timeout_or};
-use crate::api::jsonrpc::RpcError;
-use radroots_nostr::prelude::{
- RadrootsNostrClient,
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrPublicKey,
-};
-
-pub(crate) fn parse_author_or_default(
- author: Option<String>,
- default: RadrootsNostrPublicKey,
-) -> Result<RadrootsNostrPublicKey, RpcError> {
- match author {
- Some(author) => {
- let authors = vec![author];
- let mut parsed = parse_pubkeys("author", &authors)?;
- parsed
- .pop()
- .ok_or_else(|| RpcError::InvalidParams("author cannot be empty".to_string()))
- }
- None => Ok(default),
- }
-}
-
-pub(crate) fn require_non_empty(label: &str, value: String) -> Result<String, RpcError> {
- if value.trim().is_empty() {
- Err(RpcError::InvalidParams(format!("{label} cannot be empty")))
- } else {
- Ok(value)
- }
-}
-
-pub(crate) async fn fetch_latest_event(
- client: &RadrootsNostrClient,
- filter: RadrootsNostrFilter,
- timeout_secs: Option<u64>,
-) -> Result<Option<RadrootsNostrEvent>, RpcError> {
- let stored = client
- .database()
- .query(filter.clone())
- .await
- .map_err(|e| RpcError::Other(format!("query failed: {e}")))?;
- let fetched = client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
- Ok(select_latest_event(
- stored.into_iter().chain(fetched.into_iter()),
- ))
-}
-
-fn select_latest_event<I>(events: I) -> Option<RadrootsNostrEvent>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut latest: Option<RadrootsNostrEvent> = None;
- for event in events {
- let replace = match latest.as_ref() {
- Some(current) => event.created_at > current.created_at,
- None => true,
- };
- if replace {
- latest = Some(event);
- }
- }
- latest
-}
-
-#[cfg(test)]
-mod tests {
- use super::select_latest_event;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn event_with_created_at(id: &str, created_at: u64) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 1);
- let event_json = json!({
- "id": id,
- "pubkey": "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4",
- "created_at": created_at,
- "kind": 1,
- "tags": [],
- "content": "content",
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- #[test]
- fn select_latest_event_picks_newest() {
- let older_id = format!("{:064x}", 1);
- let newer_id = format!("{:064x}", 2);
- let older = event_with_created_at(&older_id, 100);
- let newer = event_with_created_at(&newer_id, 200);
- let latest = select_latest_event(vec![older, newer]).expect("latest");
- assert_eq!(latest.id.to_string(), newer_id);
- assert_eq!(latest.created_at.as_secs(), 200);
- }
-
-}
diff --git a/src/api/jsonrpc/methods/events/list_set/get.rs b/src/api/jsonrpc/methods/events/list_set/get.rs
@@ -1,73 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::{is_nip51_list_set_kind, KIND_LIST_SET_GENERIC};
-use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_list_set_rows, ListSetRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct ListSetGetParams {
- d_tag: String,
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- kind: Option<u32>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ListSetGetResponse {
- list_set: Option<ListSetRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.list_set.get");
- m.register_async_method("events.list_set.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ListSetGetParams {
- d_tag,
- author,
- kind,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
- let d_tag = require_non_empty("d_tag", d_tag)?;
-
- let kind = kind.unwrap_or(KIND_LIST_SET_GENERIC);
- if !is_nip51_list_set_kind(kind) {
- return Err(RpcError::InvalidParams(format!(
- "invalid list_set kind: {kind}"
- )));
- }
- let kind = u16::try_from(kind)
- .map_err(|_| RpcError::InvalidParams(format!("list_set kind out of range: {kind}")))?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(kind))
- .author(author)
- .identifiers([d_tag]);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let list_set = event.and_then(|event| build_list_set_rows(vec![event]).into_iter().next());
-
- Ok::<ListSetGetResponse, RpcError>(ListSetGetResponse { list_set })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/list_set/list.rs b/src/api/jsonrpc/methods/events/list_set/list.rs
@@ -1,360 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-use std::collections::HashSet;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{apply_time_bounds, limit_or, parse_pubkeys_opt, timeout_or};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::{is_nip51_list_set_kind, KIND_LIST_SET_GENERIC};
-use radroots_events::list_set::RadrootsListSet;
-use radroots_events_codec::list_set::decode::list_set_from_tags;
-use radroots_nostr::prelude::{
- RadrootsNostrClient,
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct ListSetRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- list_set: Option<RadrootsListSet>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ListSetListResponse {
- list_sets: Vec<ListSetRow>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-struct ListSetListParams {
- #[serde(default)]
- authors: Option<Vec<String>>,
- #[serde(default)]
- limit: Option<u64>,
- #[serde(default)]
- since: Option<u64>,
- #[serde(default)]
- until: Option<u64>,
- #[serde(default)]
- timeout_secs: Option<u64>,
- #[serde(default)]
- kinds: Option<Vec<u32>>,
- #[serde(default)]
- d_tags: Option<Vec<String>>,
-}
-
-fn list_set_kinds_or(kinds: Option<Vec<u32>>) -> Result<Vec<RadrootsNostrKind>, RpcError> {
- let kinds = kinds.unwrap_or_else(|| vec![KIND_LIST_SET_GENERIC]);
- if kinds.is_empty() {
- return Err(RpcError::InvalidParams(
- "list_set kinds cannot be empty".to_string(),
- ));
- }
- let mut out = Vec::with_capacity(kinds.len());
- for kind in kinds {
- if !is_nip51_list_set_kind(kind) {
- return Err(RpcError::InvalidParams(format!(
- "invalid list_set kind: {kind}"
- )));
- }
- let kind = u16::try_from(kind).map_err(|_| {
- RpcError::InvalidParams(format!("list_set kind out of range: {kind}"))
- })?;
- out.push(RadrootsNostrKind::Custom(kind));
- }
- Ok(out)
-}
-
-pub(crate) fn build_list_set_rows<I>(events: I) -> Vec<ListSetRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let list_set = parse_list_set_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- ListSetRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- list_set,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_list_set_event(
- event: &RadrootsNostrEvent,
- tags: &[Vec<String>],
-) -> Option<RadrootsListSet> {
- let kind = event.kind.as_u16() as u32;
- list_set_from_tags(kind, event.content.clone(), tags).ok()
-}
-
-fn merge_list_set_events(
- stored: Vec<RadrootsNostrEvent>,
- fetched: Vec<RadrootsNostrEvent>,
-) -> Vec<RadrootsNostrEvent> {
- let mut seen = HashSet::new();
- let mut combined = Vec::with_capacity(stored.len() + fetched.len());
- for event in stored.into_iter().chain(fetched) {
- let id = event.id.to_string();
- if seen.insert(id) {
- combined.push(event);
- }
- }
- combined
-}
-
-async fn query_list_set_events(
- client: &RadrootsNostrClient,
- base_filter: RadrootsNostrFilter,
- d_tags: Option<Vec<String>>,
-) -> Result<Vec<RadrootsNostrEvent>, RpcError> {
- match d_tags {
- Some(d_tags) if d_tags.len() > 1 => {
- let mut events = Vec::new();
- let mut seen = HashSet::new();
- for d_tag in d_tags.into_iter().filter(|tag| !tag.trim().is_empty()) {
- let filter = base_filter.clone().identifiers([d_tag]);
- let items = client
- .database()
- .query(filter)
- .await
- .map_err(|e| RpcError::Other(format!("query failed: {e}")))?;
- for item in items {
- let id = item.id.to_string();
- if seen.insert(id) {
- events.push(item);
- }
- }
- }
- Ok(events)
- }
- Some(d_tags) => {
- let mut filter = base_filter;
- if let Some(d_tag) = d_tags.into_iter().find(|tag| !tag.trim().is_empty()) {
- filter = filter.identifiers([d_tag]);
- }
- let events = client
- .database()
- .query(filter)
- .await
- .map_err(|e| RpcError::Other(format!("query failed: {e}")))?;
- Ok(events.into_iter().collect())
- }
- None => {
- let events = client
- .database()
- .query(base_filter)
- .await
- .map_err(|e| RpcError::Other(format!("query failed: {e}")))?;
- Ok(events.into_iter().collect())
- }
- }
-}
-
-async fn fetch_list_set_events(
- client: &RadrootsNostrClient,
- base_filter: RadrootsNostrFilter,
- d_tags: Option<Vec<String>>,
- timeout: Duration,
-) -> Result<Vec<RadrootsNostrEvent>, RpcError> {
- match d_tags {
- Some(d_tags) if d_tags.len() > 1 => {
- let mut events = Vec::new();
- let mut seen = HashSet::new();
- for d_tag in d_tags.into_iter().filter(|tag| !tag.trim().is_empty()) {
- let filter = base_filter.clone().identifiers([d_tag]);
- let items = client
- .fetch_events(filter, timeout)
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
- for item in items {
- let id = item.id.to_string();
- if seen.insert(id) {
- events.push(item);
- }
- }
- }
- Ok(events)
- }
- Some(d_tags) => {
- let mut filter = base_filter;
- if let Some(d_tag) = d_tags.into_iter().find(|tag| !tag.trim().is_empty()) {
- filter = filter.identifiers([d_tag]);
- }
- let events = client
- .fetch_events(filter, timeout)
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
- Ok(events)
- }
- None => {
- let events = client
- .fetch_events(base_filter, timeout)
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
- Ok(events)
- }
- }
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.list_set.list");
- m.register_async_method("events.list_set.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ListSetListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- kinds,
- d_tags,
- } = params
- .parse::<Option<ListSetListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
- let kinds = list_set_kinds_or(kinds)?;
-
- let mut filter = RadrootsNostrFilter::new().limit(limit).kinds(kinds);
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
-
- filter = apply_time_bounds(filter, since, until);
-
- let stored = query_list_set_events(&ctx.state.client, filter.clone(), d_tags.clone())
- .await?;
- let fetched = fetch_list_set_events(
- &ctx.state.client,
- filter,
- d_tags,
- Duration::from_secs(timeout_or(timeout_secs)),
- )
- .await?;
-
- let events = merge_list_set_events(stored, fetched);
- let mut items = build_list_set_rows(events);
- if items.len() > limit {
- items.truncate(limit);
- }
-
- Ok::<ListSetListResponse, RpcError>(ListSetListResponse { list_sets: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_list_set_rows;
- use radroots_events::kinds::KIND_LIST_SET_GENERIC;
- use radroots_events::list::RadrootsListEntry;
- use radroots_events::list_set::RadrootsListSet;
- use radroots_events_codec::list_set::encode::list_set_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn list_set_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 12);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_LIST_SET_GENERIC,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_list_set(d_tag: &str, pubkey: &str) -> RadrootsListSet {
- RadrootsListSet {
- d_tag: d_tag.to_string(),
- content: String::new(),
- entries: vec![RadrootsListEntry {
- tag: "p".to_string(),
- values: vec![pubkey.to_string()],
- }],
- title: None,
- description: None,
- image: None,
- }
- }
-
- #[test]
- fn list_set_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let list_set = sample_list_set("member_of.farms", pubkey);
- let content = list_set.content.clone();
- let tags = list_set_build_tags(&list_set).expect("tags");
- let older = list_set_event(&old_id, pubkey, 100, tags.clone(), &content);
- let newer = list_set_event(&new_id, pubkey, 200, tags.clone(), &content);
-
- let list_sets = build_list_set_rows(vec![older, newer]);
-
- assert_eq!(list_sets.len(), 2);
- assert_eq!(list_sets[0].id, new_id);
- assert_eq!(list_sets[0].created_at, 200);
- assert_eq!(list_sets[1].id, old_id);
- assert_eq!(list_sets[1].created_at, 100);
- }
-
- #[test]
- fn list_set_list_decodes_entries() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let list_set = sample_list_set("member_of.farms", pubkey);
- let content = list_set.content.clone();
- let tags = list_set_build_tags(&list_set).expect("tags");
- let id = format!("{:064x}", 3);
- let event = list_set_event(&id, pubkey, 300, tags.clone(), &content);
-
- let list_sets = build_list_set_rows(vec![event]);
-
- assert_eq!(list_sets.len(), 1);
- assert_eq!(list_sets[0].tags, tags);
- let parsed = list_sets[0].list_set.as_ref().expect("list set");
- assert_eq!(parsed.d_tag, "member_of.farms");
- assert_eq!(parsed.entries.len(), 1);
- assert_eq!(parsed.entries[0].tag, "p");
- assert_eq!(parsed.entries[0].values, vec![pubkey.to_string()]);
- }
-}
diff --git a/src/api/jsonrpc/methods/events/list_set/mod.rs b/src/api/jsonrpc/methods/events/list_set/mod.rs
@@ -1,18 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod list;
-pub mod publish;
-pub mod get;
-
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/list_set/publish.rs b/src/api/jsonrpc/methods/events/list_set/publish.rs
@@ -1,64 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::{is_nip51_list_set_kind, KIND_LIST_SET_GENERIC};
-use radroots_events::list_set::RadrootsListSet;
-use radroots_events_codec::list_set::encode::list_set_build_tags;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishListSetParams {
- list_set: RadrootsListSet,
- #[serde(default)]
- kind: Option<u32>,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.list_set.publish");
- m.register_async_method("events.list_set.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishListSetParams {
- list_set,
- kind,
- tags,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let kind = kind.unwrap_or(KIND_LIST_SET_GENERIC);
- if !is_nip51_list_set_kind(kind) {
- return Err(RpcError::InvalidParams(format!(
- "invalid list_set kind: {kind}"
- )));
- }
-
- let content = list_set.content.clone();
- let mut tag_slices =
- list_set_build_tags(&list_set).map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(kind, content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build list_set event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish list_set: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/listing/get.rs b/src/api/jsonrpc/methods/events/listing/get.rs
@@ -1,61 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_LISTING;
-use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_listing_rows, ListingRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct ListingGetParams {
- d_tag: String,
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ListingGetResponse {
- listing: Option<ListingRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.listing.get");
- m.register_async_method("events.listing.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ListingGetParams {
- d_tag,
- author,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
- let d_tag = require_non_empty("d_tag", d_tag)?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_LISTING as u16))
- .author(author)
- .identifiers([d_tag]);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let listing = event.and_then(|event| build_listing_rows(vec![event]).into_iter().next());
-
- Ok::<ListingGetResponse, RpcError>(ListingGetResponse { listing })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/listing/list.rs b/src/api/jsonrpc/methods/events/listing/list.rs
@@ -1,238 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_LISTING;
-use radroots_events::listing::RadrootsListing;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-use radroots_trade::listing::codec::listing_from_event_parts;
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct ListingRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- listing: Option<RadrootsListing>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ListingListResponse {
- listings: Vec<ListingRow>,
-}
-
-pub(crate) fn build_listing_rows<I>(events: I) -> Vec<ListingRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let listing = parse_listing_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- ListingRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- listing,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_listing_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsListing> {
- listing_from_event_parts(tags, &event.content).ok()
-}
-
-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 EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_LISTING as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_listing_rows(events);
-
- Ok::<ListingListResponse, RpcError>(ListingListResponse { listings: items })
- })?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_listing_rows;
- use radroots_core::{
- RadrootsCoreCurrency,
- RadrootsCoreDecimal,
- RadrootsCoreMoney,
- RadrootsCoreQuantity,
- RadrootsCoreQuantityPrice,
- RadrootsCoreUnit,
- };
- use radroots_events::kinds::KIND_LISTING;
- use radroots_events::listing::{
- RadrootsListing,
- RadrootsListingBin,
- RadrootsListingFarmRef,
- RadrootsListingProduct,
- };
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use radroots_trade::listing::codec::listing_tags_build;
- use serde_json::json;
-
- fn listing_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 5);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_LISTING,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_listing(farm_pubkey: &str) -> RadrootsListing {
- let quantity = RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1_u64), RadrootsCoreUnit::Each);
- let price = RadrootsCoreQuantityPrice::new(
- RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10_u64), RadrootsCoreCurrency::USD),
- quantity.clone(),
- );
- let bin = RadrootsListingBin {
- bin_id: "bin-1".to_string(),
- quantity,
- price_per_canonical_unit: price,
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- };
- RadrootsListing {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
- farm: RadrootsListingFarmRef {
- pubkey: farm_pubkey.to_string(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- },
- product: RadrootsListingProduct {
- key: "coffee".to_string(),
- title: "Coffee".to_string(),
- category: "beverage".to_string(),
- summary: None,
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: "bin-1".to_string(),
- bins: vec![bin],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
- images: None,
- }
- }
-
- #[test]
- fn listing_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let older = listing_event(&old_id, pubkey, 100, Vec::new(), "");
- let newer = listing_event(&new_id, pubkey, 200, Vec::new(), "");
-
- let listings = build_listing_rows(vec![older, newer]);
-
- assert_eq!(listings.len(), 2);
- assert_eq!(listings[0].id, new_id);
- assert_eq!(listings[0].created_at, 200);
- assert_eq!(listings[1].id, old_id);
- assert_eq!(listings[1].created_at, 100);
- }
-
- #[test]
- fn listing_list_builds_from_tags_when_content_empty() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let listing = sample_listing(pubkey);
- let tags = listing_tags_build(&listing).expect("tags");
- let id = format!("{:064x}", 3);
- let event = listing_event(&id, pubkey, 300, tags.clone(), "");
-
- let listings = build_listing_rows(vec![event]);
-
- assert_eq!(listings.len(), 1);
- assert_eq!(listings[0].tags, tags);
- let parsed = listings[0].listing.as_ref().expect("listing");
- assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAg");
- assert_eq!(parsed.farm.pubkey, pubkey);
- assert_eq!(parsed.primary_bin_id, "bin-1");
- }
-}
diff --git a/src/api/jsonrpc/methods/events/listing/mod.rs b/src/api/jsonrpc/methods/events/listing/mod.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod list;
-pub mod publish;
-pub mod get;
-
-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)?;
- get::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
@@ -1,46 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_LISTING;
-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(KIND_LISTING, 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::<PublishResponse, RpcError>(publish_response(out))
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/mod.rs b/src/api/jsonrpc/methods/events/mod.rs
@@ -1,15 +0,0 @@
-pub mod comment;
-pub mod farm;
-pub mod follow;
-pub mod dvm_feedback;
-pub mod dvm_request;
-pub mod dvm_result;
-pub mod reaction;
-pub mod plot;
-pub mod resource_area;
-pub mod resource_cap;
-pub mod listing;
-pub mod list_set;
-pub mod post;
-pub mod profile;
-pub mod helpers;
diff --git a/src/api/jsonrpc/methods/events/plot/get.rs b/src/api/jsonrpc/methods/events/plot/get.rs
@@ -1,61 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_PLOT;
-use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_plot_rows, PlotRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct PlotGetParams {
- d_tag: String,
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct PlotGetResponse {
- plot: Option<PlotRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.plot.get");
- m.register_async_method("events.plot.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PlotGetParams {
- d_tag,
- author,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
- let d_tag = require_non_empty("d_tag", d_tag)?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_PLOT as u16))
- .author(author)
- .identifiers([d_tag]);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let plot = event.and_then(|event| build_plot_rows(vec![event]).into_iter().next());
-
- Ok::<PlotGetResponse, RpcError>(PlotGetResponse { plot })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/plot/list.rs b/src/api/jsonrpc/methods/events/plot/list.rs
@@ -1,201 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_PLOT;
-use radroots_events::plot::RadrootsPlot;
-use radroots_events_codec::plot::decode::plot_from_event;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct PlotRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- plot: Option<RadrootsPlot>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct PlotListResponse {
- plots: Vec<PlotRow>,
-}
-
-pub(crate) fn build_plot_rows<I>(events: I) -> Vec<PlotRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let plot = parse_plot_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- PlotRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- plot,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_plot_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsPlot> {
- let kind = event.kind.as_u16() as u32;
- plot_from_event(kind, tags, &event.content).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.plot.list");
- m.register_async_method("events.plot.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_PLOT as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_plot_rows(events);
-
- Ok::<PlotListResponse, RpcError>(PlotListResponse { plots: items })
- })?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_plot_rows;
- use radroots_events::farm::RadrootsFarmRef;
- use radroots_events::kinds::KIND_PLOT;
- use radroots_events::plot::RadrootsPlot;
- use radroots_events_codec::plot::encode::plot_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn plot_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 8);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_PLOT,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_plot(d_tag: &str, name: &str, farm_pubkey: &str, farm_d_tag: &str) -> RadrootsPlot {
- RadrootsPlot {
- d_tag: d_tag.to_string(),
- farm: RadrootsFarmRef {
- pubkey: farm_pubkey.to_string(),
- d_tag: farm_d_tag.to_string(),
- },
- name: name.to_string(),
- about: None,
- location: None,
- tags: None,
- }
- }
-
- #[test]
- fn plot_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let farm_pubkey = pubkey;
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let plot = sample_plot("AAAAAAAAAAAAAAAAAAAAAQ", "Plot One", farm_pubkey, "AAAAAAAAAAAAAAAAAAAAAA");
- let content = serde_json::to_string(&plot).expect("content");
- let tags = plot_build_tags(&plot).expect("tags");
- let older = plot_event(&old_id, pubkey, 100, tags.clone(), &content);
- let newer = plot_event(&new_id, pubkey, 200, tags.clone(), &content);
-
- let plots = build_plot_rows(vec![older, newer]);
-
- assert_eq!(plots.len(), 2);
- assert_eq!(plots[0].id, new_id);
- assert_eq!(plots[0].created_at, 200);
- assert_eq!(plots[1].id, old_id);
- assert_eq!(plots[1].created_at, 100);
- }
-
- #[test]
- fn plot_list_uses_tag_fields_when_missing_in_content() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let plot = sample_plot("AAAAAAAAAAAAAAAAAAAAAQ", "Plot One", pubkey, "AAAAAAAAAAAAAAAAAAAAAA");
- let tags = plot_build_tags(&plot).expect("tags");
- let content_plot = sample_plot("", "Plot One", "", "");
- let content = serde_json::to_string(&content_plot).expect("content");
- let id = format!("{:064x}", 3);
- let event = plot_event(&id, pubkey, 300, tags.clone(), &content);
-
- let plots = build_plot_rows(vec![event]);
-
- assert_eq!(plots.len(), 1);
- assert_eq!(plots[0].tags, tags);
- let parsed = plots[0].plot.as_ref().expect("plot");
- assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAQ");
- assert_eq!(parsed.farm.pubkey, pubkey);
- assert_eq!(parsed.farm.d_tag, "AAAAAAAAAAAAAAAAAAAAAA");
- }
-}
diff --git a/src/api/jsonrpc/methods/events/plot/mod.rs b/src/api/jsonrpc/methods/events/plot/mod.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod list;
-pub mod publish;
-pub mod get;
-
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/plot/publish.rs b/src/api/jsonrpc/methods/events/plot/publish.rs
@@ -1,50 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_PLOT;
-use radroots_events::plot::RadrootsPlot;
-use radroots_events_codec::plot::encode::plot_build_tags;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishPlotParams {
- plot: RadrootsPlot,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.plot.publish");
- m.register_async_method("events.plot.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishPlotParams { plot, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let content = serde_json::to_string(&plot)
- .map_err(|e| RpcError::InvalidParams(format!("invalid plot json: {e}")))?;
- let mut tag_slices =
- plot_build_tags(&plot).map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(KIND_PLOT, content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build plot event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish plot: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/post/get.rs b/src/api/jsonrpc/methods/events/post/get.rs
@@ -1,57 +0,0 @@
-#![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::{
- RadrootsNostrEventId,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-use super::list::{build_post_rows, PostRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct PostGetParams {
- id: String,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct PostGetResponse {
- post: Option<PostRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.post.get");
- m.register_async_method("events.post.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PostGetParams { id, timeout_secs } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let id = require_non_empty("id", id)?;
- let event_id = RadrootsNostrEventId::parse(&id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::TextNote)
- .id(event_id);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let post = event.and_then(|event| build_post_rows(vec![event]).into_iter().next());
-
- Ok::<PostGetResponse, RpcError>(PostGetResponse { post })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/post/list.rs b/src/api/jsonrpc/methods/events/post/list.rs
@@ -1,173 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::post::RadrootsPost;
-use radroots_events_codec::post::decode::post_from_content;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct PostRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- post: Option<RadrootsPost>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct PostListResponse {
- posts: Vec<PostRow>,
-}
-
-pub(crate) fn build_post_rows<I>(events: I) -> Vec<PostRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let post = parse_post_event(&ev);
- let event = event_view_with_tags(&ev, tags);
- PostRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- post,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_post_event(event: &RadrootsNostrEvent) -> Option<RadrootsPost> {
- let kind = event.kind.as_u16() as u32;
- post_from_content(kind, &event.content).ok()
-}
-
-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 EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::TextNote)
- .limit(limit);
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_post_rows(events);
-
- Ok::<PostListResponse, RpcError>(PostListResponse { posts: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_post_rows;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn post_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- content: &str,
- tags: Vec<Vec<String>>,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 4);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": 1,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- #[test]
- fn post_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let older = post_event(&old_id, pubkey, 100, "old", Vec::new());
- let newer = post_event(&new_id, pubkey, 200, "new", Vec::new());
-
- let posts = build_post_rows(vec![older, newer]);
-
- assert_eq!(posts.len(), 2);
- assert_eq!(posts[0].id, new_id);
- assert_eq!(posts[0].created_at, 200);
- assert_eq!(posts[1].id, old_id);
- assert_eq!(posts[1].created_at, 100);
- }
-
- #[test]
- fn post_list_preserves_content_and_tags() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let id = format!("{:064x}", 3);
- let tags = vec![vec!["t".to_string(), "radroots".to_string()]];
- let event = post_event(&id, pubkey, 300, "hello", tags.clone());
-
- let posts = build_post_rows(vec![event]);
-
- assert_eq!(posts.len(), 1);
- assert_eq!(posts[0].content, "hello");
- assert_eq!(posts[0].tags, tags);
- assert_eq!(posts[0].post.as_ref().unwrap().content, "hello");
- }
-}
diff --git a/src/api/jsonrpc/methods/events/post/mod.rs b/src/api/jsonrpc/methods/events/post/mod.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod list;
-pub mod publish;
-pub mod get;
-
-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)?;
- get::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
@@ -1,44 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_POST;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishPostParams {
- 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 PublishPostParams { 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(KIND_POST, 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}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/profile/get.rs b/src/api/jsonrpc/methods/events/profile/get.rs
@@ -1,60 +0,0 @@
-#![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::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_profile_rows, ProfileListRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
-};
-
-#[derive(Debug, Deserialize)]
-struct ProfileGetParams {
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ProfileGetResponse {
- profile: Option<ProfileListRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.profile.get");
- m.register_async_method("events.profile.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ProfileGetParams {
- author,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Metadata)
- .author(author);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let profile = match event {
- Some(event) => build_profile_rows(vec![author], vec![event])?
- .into_iter()
- .next(),
- None => None,
- };
-
- Ok::<ProfileGetResponse, RpcError>(ProfileGetResponse { profile })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/profile/list.rs b/src/api/jsonrpc/methods/events/profile/list.rs
@@ -1,213 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use serde_json::Value as JsonValue;
-use std::collections::HashMap;
-use std::time::Duration;
-
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::profile::RadrootsProfile;
-use radroots_nostr::prelude::{
- radroots_nostr_npub_string,
- RadrootsNostrFilter,
- RadrootsNostrKind,
- RadrootsNostrEvent,
- RadrootsNostrPublicKey,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct ProfileListRow {
- author_hex: String,
- author_npub: String,
- event_id: Option<String>,
- created_at: Option<u64>,
- content: Option<String>,
- metadata_json: Option<JsonValue>,
- radroots_profile: Option<RadrootsProfile>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ProfileListResponse {
- profiles: Vec<ProfileListRow>,
-}
-
-pub(crate) fn build_profile_rows<I>(
- authors: Vec<RadrootsNostrPublicKey>,
- events: I,
-) -> Result<Vec<ProfileListRow>, RpcError>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut latest_by_author: HashMap<RadrootsNostrPublicKey, RadrootsNostrEvent> = HashMap::new();
- for event in events {
- match latest_by_author.get(&event.pubkey) {
- Some(cur) if event.created_at <= cur.created_at => {}
- _ => {
- latest_by_author.insert(event.pubkey, event);
- }
- }
- }
-
- authors
- .into_iter()
- .map(|author| {
- let npub = radroots_nostr_npub_string(&author)
- .ok_or_else(|| RpcError::Other("bech32 encode failed".into()))?;
- let row = match latest_by_author.get(&author) {
- Some(event) => {
- let parsed: Option<JsonValue> = serde_json::from_str(&event.content).ok();
- let profile: Option<RadrootsProfile> = serde_json::from_str(&event.content).ok();
- ProfileListRow {
- author_hex: author.to_string(),
- author_npub: npub,
- event_id: Some(event.id.to_string()),
- created_at: Some(event.created_at.as_secs()),
- content: Some(event.content.clone()),
- metadata_json: parsed,
- radroots_profile: profile,
- }
- }
- None => ProfileListRow {
- author_hex: author.to_string(),
- author_npub: npub,
- event_id: None,
- created_at: None,
- content: None,
- metadata_json: None,
- radroots_profile: None,
- },
- };
- Ok(row)
- })
- .collect::<Result<Vec<_>, RpcError>>()
-}
-
-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 EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let authors = match parse_pubkeys_opt("author", authors)? {
- Some(authors) => authors,
- None => vec![ctx.state.pubkey],
- };
-
- let mut filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Metadata)
- .authors(authors.clone())
- .limit(limit_or(limit));
- filter = apply_time_bounds(filter, since, until);
-
- let stored = ctx
- .state
- .client
- .database()
- .query(filter.clone())
- .await
- .map_err(|e| RpcError::Other(format!("metadata query failed: {e}")))?;
- let fetched = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("metadata fetch failed: {e}")))?;
-
- let profiles = build_profile_rows(authors, stored.into_iter().chain(fetched.into_iter()))?;
-
- Ok::<ProfileListResponse, RpcError>(ProfileListResponse { profiles })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_profile_rows;
- use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrPublicKey};
- use serde_json::json;
-
- fn parse_pubkey(hex: &str) -> RadrootsNostrPublicKey {
- RadrootsNostrPublicKey::from_hex(hex).expect("pubkey")
- }
-
- fn event_with_profile(
- pubkey: &RadrootsNostrPublicKey,
- created_at: u64,
- name: &str,
- id: &str,
- ) -> RadrootsNostrEvent {
- let content = serde_json::to_string(&json!({ "name": name })).expect("content");
- let sig = format!("{:0128x}", 2);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey.to_string(),
- "created_at": created_at,
- "kind": 0,
- "tags": [],
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- #[test]
- fn profile_list_picks_latest_per_author() {
- let author = parse_pubkey(
- "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4",
- );
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let older = event_with_profile(&author, 100, "old", &old_id);
- let newer = event_with_profile(&author, 200, "new", &new_id);
-
- let profiles = build_profile_rows(vec![author], vec![older, newer]).expect("profiles");
-
- assert_eq!(profiles.len(), 1);
- let row = &profiles[0];
- assert_eq!(row.created_at, Some(200));
- assert_eq!(row.event_id.as_deref(), Some(new_id.as_str()));
- assert_eq!(row.radroots_profile.as_ref().unwrap().name, "new");
- assert_eq!(row.metadata_json.as_ref().unwrap()["name"], "new");
- }
-
- #[test]
- fn profile_list_preserves_author_order_and_missing_rows() {
- let author_a = parse_pubkey(
- "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4",
- );
- let author_b = parse_pubkey(
- "3bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4",
- );
- let event_id = format!("{:064x}", 3);
- let event_b = event_with_profile(&author_b, 300, "b", &event_id);
-
- let profiles =
- build_profile_rows(vec![author_a, author_b], vec![event_b]).expect("profiles");
-
- assert_eq!(profiles.len(), 2);
- assert_eq!(profiles[0].author_hex, author_a.to_string());
- assert!(profiles[0].event_id.is_none());
- assert_eq!(profiles[1].author_hex, author_b.to_string());
- assert_eq!(profiles[1].event_id.as_deref(), Some(event_id.as_str()));
- }
-}
diff --git a/src/api/jsonrpc/methods/events/profile/mod.rs b/src/api/jsonrpc/methods/events/profile/mod.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod list;
-pub mod publish;
-pub mod get;
-
-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)?;
- get::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
@@ -1,43 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-use radroots_events::profile::{RadrootsProfile, RadrootsProfileType};
-use radroots_events_codec::profile::encode::to_wire_parts_with_profile_type;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishProfileParams {
- profile: RadrootsProfile,
- profile_type: RadrootsProfileType,
-}
-
-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, profile_type } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let parts = to_wire_parts_with_profile_type(&profile, Some(profile_type))
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .map_err(|e| RpcError::Other(format!("failed to build profile event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/reaction/get.rs b/src/api/jsonrpc/methods/events/reaction/get.rs
@@ -1,58 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_REACTION;
-use radroots_nostr::prelude::{
- RadrootsNostrEventId,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-use super::list::{build_reaction_rows, ReactionRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct ReactionGetParams {
- id: String,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ReactionGetResponse {
- reaction: Option<ReactionRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.reaction.get");
- m.register_async_method("events.reaction.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ReactionGetParams { id, timeout_secs } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let id = require_non_empty("id", id)?;
- let event_id = RadrootsNostrEventId::parse(&id)
- .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_REACTION as u16))
- .id(event_id);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let reaction = event.and_then(|event| build_reaction_rows(vec![event]).into_iter().next());
-
- Ok::<ReactionGetResponse, RpcError>(ReactionGetResponse { reaction })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/reaction/list.rs b/src/api/jsonrpc/methods/events/reaction/list.rs
@@ -1,199 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_REACTION;
-use radroots_events::reaction::RadrootsReaction;
-use radroots_events_codec::reaction::decode::reaction_from_tags;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct ReactionRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- reaction: Option<RadrootsReaction>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ReactionListResponse {
- reactions: Vec<ReactionRow>,
-}
-
-pub(crate) fn build_reaction_rows<I>(events: I) -> Vec<ReactionRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let reaction = parse_reaction_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- ReactionRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- reaction,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_reaction_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsReaction> {
- let kind = event.kind.as_u16() as u32;
- reaction_from_tags(kind, tags, &event.content).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.reaction.list");
- m.register_async_method("events.reaction.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_REACTION as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_reaction_rows(events);
-
- Ok::<ReactionListResponse, RpcError>(ReactionListResponse { reactions: items })
- })?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_reaction_rows;
- use radroots_events::kinds::{KIND_REACTION, KIND_POST};
- use radroots_events::reaction::RadrootsReaction;
- use radroots_events::RadrootsNostrEventRef;
- use radroots_events_codec::reaction::encode::reaction_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn reaction_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 11);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_REACTION,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_reaction(event_id: &str, author: &str, content: &str) -> RadrootsReaction {
- let root = RadrootsNostrEventRef {
- id: event_id.to_string(),
- author: author.to_string(),
- kind: KIND_POST,
- d_tag: None,
- relays: None,
- };
- RadrootsReaction {
- root,
- content: content.to_string(),
- }
- }
-
- #[test]
- fn reaction_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let reaction = sample_reaction("root-1", pubkey, "+");
- let tags = reaction_build_tags(&reaction).expect("tags");
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let older = reaction_event(&old_id, pubkey, 100, tags.clone(), &reaction.content);
- let newer = reaction_event(&new_id, pubkey, 200, tags.clone(), &reaction.content);
-
- let reactions = build_reaction_rows(vec![older, newer]);
-
- assert_eq!(reactions.len(), 2);
- assert_eq!(reactions[0].id, new_id);
- assert_eq!(reactions[0].created_at, 200);
- assert_eq!(reactions[1].id, old_id);
- assert_eq!(reactions[1].created_at, 100);
- }
-
- #[test]
- fn reaction_list_decodes_reaction() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let reaction = sample_reaction("root-1", pubkey, "+");
- let tags = reaction_build_tags(&reaction).expect("tags");
- let id = format!("{:064x}", 3);
- let event = reaction_event(&id, pubkey, 300, tags.clone(), &reaction.content);
-
- let reactions = build_reaction_rows(vec![event]);
-
- assert_eq!(reactions.len(), 1);
- assert_eq!(reactions[0].tags, tags);
- let parsed = reactions[0].reaction.as_ref().expect("reaction");
- assert_eq!(parsed.content, "+");
- assert_eq!(parsed.root.id, "root-1");
- }
-}
diff --git a/src/api/jsonrpc/methods/events/reaction/mod.rs b/src/api/jsonrpc/methods/events/reaction/mod.rs
@@ -1,18 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod get;
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/reaction/publish.rs b/src/api/jsonrpc/methods/events/reaction/publish.rs
@@ -1,51 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_REACTION;
-use radroots_events::reaction::RadrootsReaction;
-use radroots_events_codec::reaction::encode::to_wire_parts;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishReactionParams {
- reaction: RadrootsReaction,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.reaction.publish");
- m.register_async_method("events.reaction.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishReactionParams { reaction, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let parts = to_wire_parts(&reaction)
- .map_err(|e| RpcError::InvalidParams(format!("invalid reaction: {e}")))?;
- let mut tag_slices = parts.tags;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(KIND_REACTION, parts.content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build reaction: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish reaction: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/resource_area/get.rs b/src/api/jsonrpc/methods/events/resource_area/get.rs
@@ -1,62 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_RESOURCE_AREA;
-use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_resource_area_rows, ResourceAreaRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct ResourceAreaGetParams {
- d_tag: String,
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ResourceAreaGetResponse {
- resource_area: Option<ResourceAreaRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.resource_area.get");
- m.register_async_method("events.resource_area.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ResourceAreaGetParams {
- d_tag,
- author,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
- let d_tag = require_non_empty("d_tag", d_tag)?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_AREA as u16))
- .author(author)
- .identifiers([d_tag]);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let resource_area =
- event.and_then(|event| build_resource_area_rows(vec![event]).into_iter().next());
-
- Ok::<ResourceAreaGetResponse, RpcError>(ResourceAreaGetResponse { resource_area })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/resource_area/list.rs b/src/api/jsonrpc/methods/events/resource_area/list.rs
@@ -1,247 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_RESOURCE_AREA;
-use radroots_events::resource_area::RadrootsResourceArea;
-use radroots_events_codec::resource_area::decode::resource_area_from_event;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct ResourceAreaRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- resource_area: Option<RadrootsResourceArea>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ResourceAreaListResponse {
- resource_areas: Vec<ResourceAreaRow>,
-}
-
-pub(crate) fn build_resource_area_rows<I>(events: I) -> Vec<ResourceAreaRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let resource_area = parse_resource_area_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- ResourceAreaRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- resource_area,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_resource_area_event(
- event: &RadrootsNostrEvent,
- tags: &[Vec<String>],
-) -> Option<RadrootsResourceArea> {
- let kind = event.kind.as_u16() as u32;
- resource_area_from_event(kind, tags, &event.content).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.resource_area.list");
- m.register_async_method("events.resource_area.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_AREA as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_resource_area_rows(events);
-
- Ok::<ResourceAreaListResponse, RpcError>(ResourceAreaListResponse {
- resource_areas: items,
- })
- })?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_resource_area_rows;
- use radroots_events::farm::{RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon};
- use radroots_events::kinds::KIND_RESOURCE_AREA;
- use radroots_events::resource_area::{
- RadrootsResourceArea, RadrootsResourceAreaLocation,
- };
- use radroots_events_codec::resource_area::encode::resource_area_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn resource_area_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 9);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_RESOURCE_AREA,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_location() -> RadrootsResourceAreaLocation {
- let point = RadrootsGeoJsonPoint {
- r#type: "Point".to_string(),
- coordinates: [-76.9714, -6.0346],
- };
- let polygon = RadrootsGeoJsonPolygon {
- r#type: "Polygon".to_string(),
- coordinates: vec![vec![
- [-76.9714, -6.0346],
- [-76.9712, -6.0346],
- [-76.9712, -6.0344],
- [-76.9714, -6.0344],
- [-76.9714, -6.0346],
- ]],
- };
- let gcs = RadrootsGcsLocation {
- lat: -6.0346,
- lng: -76.9714,
- geohash: "6m6t5x".to_string(),
- point,
- polygon,
- accuracy: None,
- altitude: None,
- tag_0: None,
- label: None,
- area: None,
- elevation: None,
- soil: None,
- climate: None,
- gc_id: None,
- gc_name: None,
- gc_admin1_id: None,
- gc_admin1_name: None,
- gc_country_id: None,
- gc_country_name: None,
- };
- RadrootsResourceAreaLocation {
- primary: Some("Moyobamba".to_string()),
- city: None,
- region: None,
- country: None,
- gcs,
- }
- }
-
- fn sample_resource_area(d_tag: &str, name: &str) -> RadrootsResourceArea {
- RadrootsResourceArea {
- d_tag: d_tag.to_string(),
- name: name.to_string(),
- about: None,
- location: sample_location(),
- tags: None,
- }
- }
-
- #[test]
- fn resource_area_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let area = sample_resource_area("AAAAAAAAAAAAAAAAAAAAAw", "Area One");
- let content = serde_json::to_string(&area).expect("content");
- let tags = resource_area_build_tags(&area).expect("tags");
- let older = resource_area_event(&old_id, pubkey, 100, tags.clone(), &content);
- let newer = resource_area_event(&new_id, pubkey, 200, tags.clone(), &content);
-
- let areas = build_resource_area_rows(vec![older, newer]);
-
- assert_eq!(areas.len(), 2);
- assert_eq!(areas[0].id, new_id);
- assert_eq!(areas[0].created_at, 200);
- assert_eq!(areas[1].id, old_id);
- assert_eq!(areas[1].created_at, 100);
- }
-
- #[test]
- fn resource_area_list_uses_tag_d_when_missing_in_content() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let area = sample_resource_area("AAAAAAAAAAAAAAAAAAAAAw", "Area One");
- let tags = resource_area_build_tags(&area).expect("tags");
- let content_area = sample_resource_area("", "Area One");
- let content = serde_json::to_string(&content_area).expect("content");
- let id = format!("{:064x}", 3);
- let event = resource_area_event(&id, pubkey, 300, tags.clone(), &content);
-
- let areas = build_resource_area_rows(vec![event]);
-
- assert_eq!(areas.len(), 1);
- assert_eq!(areas[0].tags, tags);
- let parsed = areas[0].resource_area.as_ref().expect("area");
- assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAw");
- assert_eq!(parsed.name, "Area One");
- }
-}
diff --git a/src/api/jsonrpc/methods/events/resource_area/mod.rs b/src/api/jsonrpc/methods/events/resource_area/mod.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod list;
-pub mod publish;
-pub mod get;
-
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/resource_area/publish.rs b/src/api/jsonrpc/methods/events/resource_area/publish.rs
@@ -1,51 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_RESOURCE_AREA;
-use radroots_events::resource_area::RadrootsResourceArea;
-use radroots_events_codec::resource_area::encode::resource_area_build_tags;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishResourceAreaParams {
- resource_area: RadrootsResourceArea,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.resource_area.publish");
- m.register_async_method("events.resource_area.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishResourceAreaParams { resource_area, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let content = serde_json::to_string(&resource_area).map_err(|e| {
- RpcError::InvalidParams(format!("invalid resource_area json: {e}"))
- })?;
- let mut tag_slices = resource_area_build_tags(&resource_area)
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(KIND_RESOURCE_AREA, content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build resource_area event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish resource_area: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/resource_cap/get.rs b/src/api/jsonrpc/methods/events/resource_cap/get.rs
@@ -1,62 +0,0 @@
-#![forbid(unsafe_code)]
-
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP;
-use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind};
-
-use super::list::{build_resource_cap_rows, ResourceCapRow};
-use crate::api::jsonrpc::methods::events::helpers::{
- fetch_latest_event,
- parse_author_or_default,
- require_non_empty,
-};
-
-#[derive(Debug, Deserialize)]
-struct ResourceCapGetParams {
- d_tag: String,
- #[serde(default)]
- author: Option<String>,
- #[serde(default)]
- timeout_secs: Option<u64>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ResourceCapGetResponse {
- resource_cap: Option<ResourceCapRow>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.resource_cap.get");
- m.register_async_method("events.resource_cap.get", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let ResourceCapGetParams {
- d_tag,
- author,
- timeout_secs,
- } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let author = parse_author_or_default(author, ctx.state.pubkey)?;
- let d_tag = require_non_empty("d_tag", d_tag)?;
-
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_HARVEST_CAP as u16))
- .author(author)
- .identifiers([d_tag]);
-
- let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?;
- let resource_cap =
- event.and_then(|event| build_resource_cap_rows(vec![event]).into_iter().next());
-
- Ok::<ResourceCapGetResponse, RpcError>(ResourceCapGetResponse { resource_cap })
- })?;
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/events/resource_cap/list.rs b/src/api/jsonrpc/methods/events/resource_cap/list.rs
@@ -1,218 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-use std::time::Duration;
-
-use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags};
-use crate::api::jsonrpc::params::{
- apply_time_bounds,
- limit_or,
- parse_pubkeys_opt,
- timeout_or,
- EventListParams,
-};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP;
-use radroots_events::resource_cap::RadrootsResourceHarvestCap;
-use radroots_events_codec::resource_cap::decode::resource_harvest_cap_from_event;
-use radroots_nostr::prelude::{
- RadrootsNostrEvent,
- RadrootsNostrFilter,
- RadrootsNostrKind,
-};
-
-#[derive(Clone, Debug, Serialize)]
-pub(crate) struct ResourceCapRow {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
- resource_cap: Option<RadrootsResourceHarvestCap>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct ResourceCapListResponse {
- resource_caps: Vec<ResourceCapRow>,
-}
-
-pub(crate) fn build_resource_cap_rows<I>(events: I) -> Vec<ResourceCapRow>
-where
- I: IntoIterator<Item = RadrootsNostrEvent>,
-{
- let mut items = events
- .into_iter()
- .map(|ev| {
- let tags = event_tags(&ev);
- let resource_cap = parse_resource_cap_event(&ev, &tags);
- let event = event_view_with_tags(&ev, tags);
- ResourceCapRow {
- id: event.id,
- author: event.author,
- created_at: event.created_at,
- kind: event.kind,
- tags: event.tags,
- content: event.content,
- sig: event.sig,
- resource_cap,
- }
- })
- .collect::<Vec<_>>();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- items
-}
-
-fn parse_resource_cap_event(
- event: &RadrootsNostrEvent,
- tags: &[Vec<String>],
-) -> Option<RadrootsResourceHarvestCap> {
- let kind = event.kind.as_u16() as u32;
- resource_harvest_cap_from_event(kind, tags, &event.content).ok()
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.resource_cap.list");
- m.register_async_method("events.resource_cap.list", |params, ctx, _| async move {
- if ctx.state.client.relays().await.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let EventListParams {
- authors,
- limit,
- since,
- until,
- timeout_secs,
- } = params
- .parse::<Option<EventListParams>>()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?
- .unwrap_or_default();
-
- let limit = limit_or(limit);
-
- let mut filter = RadrootsNostrFilter::new()
- .limit(limit)
- .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_HARVEST_CAP as u16));
-
- if let Some(authors) = parse_pubkeys_opt("author", authors)? {
- filter = filter.authors(authors);
- } else {
- filter = filter.author(ctx.state.pubkey);
- }
- filter = apply_time_bounds(filter, since, until);
-
- let events = ctx
- .state
- .client
- .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs)))
- .await
- .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?;
-
- let items = build_resource_cap_rows(events);
-
- Ok::<ResourceCapListResponse, RpcError>(ResourceCapListResponse {
- resource_caps: items,
- })
- })?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::build_resource_cap_rows;
- use radroots_core::{RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreUnit};
- use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP;
- use radroots_events::resource_area::RadrootsResourceAreaRef;
- use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct};
- use radroots_events_codec::resource_cap::encode::resource_harvest_cap_build_tags;
- use radroots_nostr::prelude::RadrootsNostrEvent;
- use serde_json::json;
-
- fn resource_cap_event(
- id: &str,
- pubkey: &str,
- created_at: u64,
- tags: Vec<Vec<String>>,
- content: &str,
- ) -> RadrootsNostrEvent {
- let sig = format!("{:0128x}", 10);
- let event_json = json!({
- "id": id,
- "pubkey": pubkey,
- "created_at": created_at,
- "kind": KIND_RESOURCE_HARVEST_CAP,
- "tags": tags,
- "content": content,
- "sig": sig,
- });
- serde_json::from_value(event_json).expect("event")
- }
-
- fn sample_cap(d_tag: &str, area_pubkey: &str, area_d_tag: &str) -> RadrootsResourceHarvestCap {
- let quantity = RadrootsCoreQuantity::new(
- RadrootsCoreDecimal::from(100_u64),
- RadrootsCoreUnit::MassG,
- );
- RadrootsResourceHarvestCap {
- d_tag: d_tag.to_string(),
- resource_area: RadrootsResourceAreaRef {
- pubkey: area_pubkey.to_string(),
- d_tag: area_d_tag.to_string(),
- },
- product: RadrootsResourceHarvestProduct {
- key: "coffee".to_string(),
- category: None,
- },
- start: 100,
- end: 200,
- cap_quantity: quantity,
- display_amount: None,
- display_unit: None,
- display_label: None,
- tags: None,
- }
- }
-
- #[test]
- fn resource_cap_list_sorts_by_created_at_desc() {
- let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let old_id = format!("{:064x}", 1);
- let new_id = format!("{:064x}", 2);
- let cap = sample_cap("CAAAAAAAAAAAAAAAAAAAAA", pubkey, "AAAAAAAAAAAAAAAAAAAAAw");
- let content = serde_json::to_string(&cap).expect("content");
- let tags = resource_harvest_cap_build_tags(&cap).expect("tags");
- let older = resource_cap_event(&old_id, pubkey, 100, tags.clone(), &content);
- let newer = resource_cap_event(&new_id, pubkey, 200, tags.clone(), &content);
-
- let caps = build_resource_cap_rows(vec![older, newer]);
-
- assert_eq!(caps.len(), 2);
- assert_eq!(caps[0].id, new_id);
- assert_eq!(caps[0].created_at, 200);
- assert_eq!(caps[1].id, old_id);
- assert_eq!(caps[1].created_at, 100);
- }
-
- #[test]
- fn resource_cap_list_uses_tag_d_when_missing_in_content() {
- let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4";
- let cap = sample_cap("CAAAAAAAAAAAAAAAAAAAAA", pubkey, "AAAAAAAAAAAAAAAAAAAAAw");
- let tags = resource_harvest_cap_build_tags(&cap).expect("tags");
- let mut content_cap = sample_cap("", pubkey, "AAAAAAAAAAAAAAAAAAAAAw");
- content_cap.display_label = Some("display".to_string());
- let content = serde_json::to_string(&content_cap).expect("content");
- let id = format!("{:064x}", 3);
- let event = resource_cap_event(&id, pubkey, 300, tags.clone(), &content);
-
- let caps = build_resource_cap_rows(vec![event]);
-
- assert_eq!(caps.len(), 1);
- assert_eq!(caps[0].tags, tags);
- let parsed = caps[0].resource_cap.as_ref().expect("cap");
- assert_eq!(parsed.d_tag, "CAAAAAAAAAAAAAAAAAAAAA");
- assert_eq!(parsed.resource_area.d_tag, "AAAAAAAAAAAAAAAAAAAAAw");
- assert_eq!(parsed.product.key, "coffee");
- }
-}
diff --git a/src/api/jsonrpc/methods/events/resource_cap/mod.rs b/src/api/jsonrpc/methods/events/resource_cap/mod.rs
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-pub mod list;
-pub mod publish;
-pub mod get;
-
-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)?;
- get::register(&mut m, ®istry)?;
- Ok(m)
-}
diff --git a/src/api/jsonrpc/methods/events/resource_cap/publish.rs b/src/api/jsonrpc/methods/events/resource_cap/publish.rs
@@ -1,51 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Deserialize;
-
-use crate::api::jsonrpc::nostr::{publish_response, PublishResponse};
-use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP;
-use radroots_events::resource_cap::RadrootsResourceHarvestCap;
-use radroots_events_codec::resource_cap::encode::resource_harvest_cap_build_tags;
-use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event};
-
-#[derive(Debug, Deserialize)]
-struct PublishResourceCapParams {
- resource_cap: RadrootsResourceHarvestCap,
- #[serde(default)]
- tags: Option<Vec<Vec<String>>>,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- registry.track("events.resource_cap.publish");
- m.register_async_method("events.resource_cap.publish", |params, ctx, _| async move {
- let relays = ctx.state.client.relays().await;
- if relays.is_empty() {
- return Err(RpcError::NoRelays);
- }
-
- let PublishResourceCapParams { resource_cap, tags } = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
-
- let content = serde_json::to_string(&resource_cap).map_err(|e| {
- RpcError::InvalidParams(format!("invalid resource_cap json: {e}"))
- })?;
- let mut tag_slices = resource_harvest_cap_build_tags(&resource_cap)
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- if let Some(extra_tags) = tags {
- tag_slices.extend(extra_tags);
- }
-
- let builder = radroots_nostr_build_event(KIND_RESOURCE_HARVEST_CAP, content, tag_slices)
- .map_err(|e| RpcError::Other(format!("failed to build resource_cap event: {e}")))?;
-
- let output = radroots_nostr_send_event(&ctx.state.client, builder)
- .await
- .map_err(|e| RpcError::Other(format!("failed to publish resource_cap: {e}")))?;
-
- Ok::<PublishResponse, RpcError>(publish_response(output))
- })?;
-
- Ok(())
-}
diff --git a/src/api/jsonrpc/methods/mod.rs b/src/api/jsonrpc/methods/mod.rs
@@ -5,32 +5,15 @@ use jsonrpsee::server::RpcModule;
use super::{context::RpcContext, registry::MethodRegistry};
-pub mod domains;
-pub mod events;
+pub mod nip46;
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::follow::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::post::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::comment::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::reaction::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::listing::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::list_set::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::dvm_request::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::dvm_result::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::dvm_feedback::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::farm::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::plot::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::resource_area::module(ctx.clone(), registry.clone())?)?;
- root.merge(events::resource_cap::module(ctx.clone(), registry.clone())?)?;
- root.merge(domains::trade::module(ctx, registry)?)?;
+ root.merge(nip46::module(ctx, registry)?)?;
Ok(())
}
diff --git a/src/api/jsonrpc/methods/nip46/mod.rs b/src/api/jsonrpc/methods/nip46/mod.rs
@@ -0,0 +1,14 @@
+#![forbid(unsafe_code)]
+
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext};
+
+pub mod status;
+
+pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> {
+ let mut m = RpcModule::new(ctx);
+ status::register(&mut m, ®istry)?;
+ Ok(m)
+}
diff --git a/src/api/jsonrpc/methods/nip46/status.rs b/src/api/jsonrpc/methods/nip46/status.rs
@@ -0,0 +1,18 @@
+use anyhow::Result;
+use jsonrpsee::server::RpcModule;
+use serde::Serialize;
+
+use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError};
+
+#[derive(Debug, Serialize)]
+struct Nip46StatusResponse {
+ ready: bool,
+}
+
+pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
+ registry.track("nip46.status");
+ m.register_method("nip46.status", |_p, _ctx, _| {
+ Ok::<Nip46StatusResponse, RpcError>(Nip46StatusResponse { ready: false })
+ })?;
+ Ok(())
+}
diff --git a/src/api/jsonrpc/methods/system.rs b/src/api/jsonrpc/methods/system.rs
@@ -1,34 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use serde::Serialize;
-
-use crate::api::jsonrpc::{MethodRegistry, RpcContext};
-
-#[derive(Clone, Debug, Serialize)]
-struct SystemInfoResponse {
- version: Option<serde_json::Value>,
- build: Option<serde_json::Value>,
- uptime_secs: u64,
-}
-
-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();
- Ok::<SystemInfoResponse, crate::api::jsonrpc::RpcError>(SystemInfoResponse {
- version: ctx.state.info.get("version").cloned(),
- build: ctx.state.info.get("build").cloned(),
- uptime_secs: uptime,
- })
- })?;
-
- registry.track("system.help");
- m.register_method("system.help", |_p, ctx, _| ctx.methods.list())?;
-
- Ok(m)
-}
diff --git a/src/build/mod.rs b/src/build/mod.rs
@@ -0,0 +1 @@
+#![forbid(unsafe_code)]
diff --git a/src/events/mod.rs b/src/events/mod.rs
@@ -0,0 +1 @@
+#![forbid(unsafe_code)]
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,9 +1,13 @@
#![forbid(unsafe_code)]
pub mod api;
+pub mod build;
pub mod cli;
pub mod config;
+pub mod events;
+pub mod nip46;
pub mod radrootsd;
+pub mod validate;
use anyhow::Result;
diff --git a/src/nip46/mod.rs b/src/nip46/mod.rs
@@ -0,0 +1 @@
+#![forbid(unsafe_code)]
diff --git a/src/validate/mod.rs b/src/validate/mod.rs
@@ -0,0 +1 @@
+#![forbid(unsafe_code)]