lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 6527cc9fbd1748846498f504d8408095346eab38
parent 322319c3f07954c17e262b57be35470e9b5274ec
Author: triesap <tyson@radroots.org>
Date:   Thu,  7 May 2026 16:57:10 +0000

sdk: strengthen relay direct receipt

- make relay-client include the JSON encoding surface it exposes
- sign relay publish parts once and publish the preserved event
- return signed-event and relay delivery truth in SDK receipts
- prove listing relay publish receipt mapping with targeted tests

Diffstat:
Mcrates/events/src/lib.rs | 2+-
Mcrates/sdk/Cargo.toml | 2+-
Mcrates/sdk/src/adapters/relay.rs | 18++++++++++--------
Mcrates/sdk/src/client.rs | 145++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mcrates/sdk/tests/relay_direct.rs | 12++++++++++++
5 files changed, 149 insertions(+), 30 deletions(-)

diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs @@ -42,7 +42,7 @@ pub mod trade; #[cfg_attr(feature = "ts-rs", derive(TS))] #[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsNostrEvent { pub id: String, pub author: String, diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -31,7 +31,7 @@ identity-models = [ ] identity-storage = ["identity-models", "std", "radroots_identity/std"] signing = ["dep:radroots_nostr", "nostr"] -relay-client = ["signing", "std", "radroots_nostr/client"] +relay-client = ["signing", "std", "serde_json", "radroots_nostr/client"] radrootsd-client = ["std", "serde_json", "dep:reqwest"] signer-adapters = [ "identity-models", diff --git a/crates/sdk/src/adapters/relay.rs b/crates/sdk/src/adapters/relay.rs @@ -1,7 +1,6 @@ use core::time::Duration; -use crate::WireEventParts; -use crate::adapters::signing::{SignedNostrEvent, event_builder_from_parts}; +use crate::adapters::signing::SignedNostrEvent; use crate::identity::RadrootsIdentity; use radroots_nostr::prelude::{ RadrootsNostrClient, RadrootsNostrClientOptions, RadrootsNostrError, RadrootsNostrEventId, @@ -51,13 +50,16 @@ pub async fn connected_client_from_identity( Ok(client) } -pub async fn publish_parts( - client: &RelayClient, - parts: WireEventParts, -) -> Result<RelayOutput<RelayEventId>, RelayError> { - client - .send_event_builder(event_builder_from_parts(parts)?) +pub async fn connected_relay_urls(client: &RelayClient) -> Vec<String> { + let mut relay_urls = client + .relays() .await + .into_values() + .filter(|relay| relay.is_connected()) + .map(|relay| relay.url().to_string()) + .collect::<Vec<_>>(); + relay_urls.sort(); + relay_urls } pub async fn publish_signed_event( diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -11,7 +11,7 @@ use crate::adapters::radrootsd; feature = "relay-client", feature = "signing" ))] -use crate::adapters::relay; +use crate::adapters::{relay, signing}; use crate::config::SignerConfig; use crate::config::{RadrootsSdkConfig, SdkConfigError, SdkTransportMode}; #[cfg(all( @@ -51,8 +51,15 @@ pub enum SdkTransportReceipt { Radrootsd(SdkRadrootsdPublishReceipt), } -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SdkRelayPublishReceipt { + pub event: RadrootsNostrEvent, + pub event_id: String, + pub event_kind: u32, + pub created_at: u32, + pub signature: String, + pub target_relays: Vec<String>, + pub connected_relays: Vec<String>, pub acknowledged_relays: Vec<String>, pub failed_relays: Vec<SdkRelayFailure>, } @@ -1043,7 +1050,6 @@ impl RadrootsSdkClient { } self.require_signer_mode(SignerConfig::LocalIdentity, operation)?; - let event_kind = u32::from(parts.kind); let relay_urls = match &self.resolved_transport_target { SdkResolvedTransportTarget::RelayDirect { relay_urls } => relay_urls.clone(), SdkResolvedTransportTarget::Radrootsd { .. } => { @@ -1060,10 +1066,13 @@ impl RadrootsSdkClient { ) .await .map_err(|err| SdkPublishError::Relay(err.to_string()))?; - let output = relay::publish_parts(&client, parts) + let connected_relays = relay::connected_relay_urls(&client).await; + let signed_event = signing::sign_parts_with_identity(identity, parts) + .map_err(|err| SdkPublishError::Relay(err.to_string()))?; + let output = relay::publish_signed_event(&client, &signed_event) .await .map_err(|err| SdkPublishError::Relay(err.to_string()))?; - sdk_publish_receipt_from_relay_output(event_kind, output) + sdk_publish_receipt_from_relay_output(signed_event, relay_urls, connected_relays, output) } #[cfg(feature = "radrootsd-client")] @@ -2239,15 +2248,24 @@ impl<'a> TradeClient<'a> { feature = "signing" ))] fn sdk_publish_receipt_from_relay_output( - event_kind: u32, + signed_event: signing::SignedNostrEvent, + target_relays: Vec<String>, + connected_relays: Vec<String>, output: relay::RelayOutput<relay::RelayEventId>, ) -> Result<SdkPublishReceipt, SdkPublishError> { + let event = sdk_event_from_signed_event(&signed_event); + let event_id = event.id.clone(); + let event_kind = event.kind; + let created_at = event.created_at; + let signature = event.sig.clone(); + let target_relays = sorted_unique_strings(target_relays); + let connected_relays = sorted_unique_strings(connected_relays); let mut acknowledged_relays = output .success .into_iter() .map(|relay| relay.to_string()) .collect::<Vec<_>>(); - acknowledged_relays.sort(); + acknowledged_relays = sorted_unique_strings(acknowledged_relays); let mut failed_relays = output .failed @@ -2269,14 +2287,53 @@ fn sdk_publish_receipt_from_relay_output( Ok(SdkPublishReceipt { transport: SdkTransportMode::RelayDirect, event_kind: Some(event_kind), - event_id: Some(output.val.to_string()), + event_id: Some(event_id.clone()), transport_receipt: SdkTransportReceipt::RelayDirect(SdkRelayPublishReceipt { + event, + event_id, + event_kind, + created_at, + signature, + target_relays, + connected_relays, acknowledged_relays, failed_relays, }), }) } +#[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" +))] +fn sdk_event_from_signed_event(event: &signing::SignedNostrEvent) -> RadrootsNostrEvent { + RadrootsNostrEvent { + id: event.id.to_string(), + author: event.pubkey.to_string(), + created_at: u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX), + kind: event.kind.as_u16() as u32, + tags: event + .tags + .iter() + .map(|tag| tag.as_slice().to_vec()) + .collect(), + content: event.content.clone(), + sig: event.sig.to_string(), + } +} + +#[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" +))] +fn sorted_unique_strings(mut values: Vec<String>) -> Vec<String> { + values.sort(); + values.dedup(); + values +} + #[cfg(feature = "radrootsd-client")] fn sdk_publish_receipt_from_radrootsd_bridge_response( response: radrootsd::SdkRadrootsdBridgePublishResponse, @@ -2310,17 +2367,30 @@ mod tests { use super::{ SdkPublishError, SdkRelayFailure, SdkTransportMode, sdk_publish_receipt_from_relay_output, }; + use crate::WireEventParts; use crate::adapters::relay::RelayOutput; + use crate::adapters::signing::sign_parts_with_identity; + use crate::identity::RadrootsIdentity; use radroots_nostr::prelude::RadrootsNostrEventId; use std::collections::{HashMap, HashSet}; #[test] fn relay_output_maps_to_normalized_publish_receipt() { + let identity = RadrootsIdentity::generate(); + let signed_event = sign_parts_with_identity( + &identity, + WireEventParts { + kind: 30402, + content: "listing".to_owned(), + tags: vec![vec!["d".to_owned(), "AAAAAAAAAAAAAAAAAAAAAg".to_owned()]], + }, + ) + .expect("signed event"); + let event_id = signed_event.id.to_string(); + let event_created_at = u32::try_from(signed_event.created_at.as_secs()).unwrap(); + let event_signature = signed_event.sig.to_string(); let output = RelayOutput { - val: RadrootsNostrEventId::parse( - "5f3cf27d85c9571a2dca28269f6547f625364a7e06e5e853ee1bc74d2c4aa3d4", - ) - .expect("event id"), + val: RadrootsNostrEventId::parse(event_id.as_str()).expect("event id"), success: HashSet::from([ nostr::RelayUrl::parse("ws://127.0.0.1:8080").expect("relay a"), nostr::RelayUrl::parse("ws://127.0.0.1:8081").expect("relay b"), @@ -2331,23 +2401,57 @@ mod tests { )]), }; - let receipt = sdk_publish_receipt_from_relay_output(30402, output).expect("receipt"); + let receipt = sdk_publish_receipt_from_relay_output( + signed_event, + vec![ + "ws://127.0.0.1:8081".to_owned(), + "ws://127.0.0.1:8080".to_owned(), + ], + vec!["ws://127.0.0.1:8080".to_owned()], + output, + ) + .expect("receipt"); assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); assert_eq!(receipt.event_kind, Some(30402)); + assert_eq!(receipt.event_id, Some(event_id.clone())); + let relay_receipt = match receipt.transport_receipt { + super::SdkTransportReceipt::RelayDirect(relay_receipt) => relay_receipt, + super::SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), + }; + assert_eq!(relay_receipt.event.id, event_id); + assert_eq!(relay_receipt.event_id, relay_receipt.event.id); + assert_eq!(relay_receipt.event_kind, 30402); + assert_eq!(relay_receipt.created_at, event_created_at); + assert_eq!(relay_receipt.signature, event_signature); + assert_eq!( + relay_receipt.target_relays, + vec![ + "ws://127.0.0.1:8080".to_owned(), + "ws://127.0.0.1:8081".to_owned(), + ] + ); assert_eq!( - receipt.event_id, - Some("5f3cf27d85c9571a2dca28269f6547f625364a7e06e5e853ee1bc74d2c4aa3d4".to_owned()) + relay_receipt.connected_relays, + vec!["ws://127.0.0.1:8080".to_owned()] ); } #[test] fn relay_output_without_acknowledgement_is_rejected() { + let identity = RadrootsIdentity::generate(); + let signed_event = sign_parts_with_identity( + &identity, + WireEventParts { + kind: 30402, + content: "listing".to_owned(), + tags: vec![], + }, + ) + .expect("signed event"); let output = RelayOutput { - val: RadrootsNostrEventId::parse( - "5f3cf27d85c9571a2dca28269f6547f625364a7e06e5e853ee1bc74d2c4aa3d4", - ) - .expect("event id"), + val: RadrootsNostrEventId::parse(signed_event.id.to_string().as_str()) + .expect("event id"), success: HashSet::new(), failed: HashMap::from([( nostr::RelayUrl::parse("ws://127.0.0.1:8082").expect("relay c"), @@ -2355,7 +2459,8 @@ mod tests { )]), }; - let error = sdk_publish_receipt_from_relay_output(30402, output).expect_err("error"); + let error = sdk_publish_receipt_from_relay_output(signed_event, vec![], vec![], output) + .expect_err("error"); assert_eq!( error, diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs @@ -185,6 +185,18 @@ async fn relay_direct_listing_publish_accepts_sdk_built_draft() -> TestResult<() match receipt.transport_receipt { SdkTransportReceipt::RelayDirect(relay_receipt) => { assert_eq!( + receipt.event_id.as_deref(), + Some(relay_receipt.event_id.as_str()) + ); + assert_eq!(receipt.event_kind, Some(relay_receipt.event_kind)); + assert_eq!(relay_receipt.event.kind, 30402); + assert_eq!(relay_receipt.event_id, relay_receipt.event.id); + assert_eq!(relay_receipt.signature, relay_receipt.event.sig); + assert_eq!(relay_receipt.created_at, relay_receipt.event.created_at); + assert_eq!(relay_receipt.event.author, identity.public_key_hex()); + assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); + assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); + assert_eq!( relay_receipt.acknowledged_relays, vec![relay.url().to_owned()] );