lib

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

commit a5788d4103ac4fd3497b6e2bf75b68f60d7c710f
parent c13a36cc86b80877a9b286cb056e698b96eb0df5
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Apr 2026 01:00:01 +0000

sdk: add explicit client transport surface

Diffstat:
Acrates/sdk/src/client.rs | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/lib.rs | 4++++
Acrates/sdk/tests/client.rs | 209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 418 insertions(+), 0 deletions(-)

diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -0,0 +1,205 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; +#[cfg(feature = "std")] +use std::{string::String, vec::Vec}; + +use crate::config::{RadrootsSdkConfig, SdkConfigError, SdkTransportMode}; +use crate::{ + NostrTags, RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsProfile, RadrootsProfileType, + RadrootsTradeEnvelope, TradeListingValidateResult, WireEventParts, farm, listing, profile, + trade, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsSdkClient { + config: RadrootsSdkConfig, +} + +impl RadrootsSdkClient { + pub fn from_config(config: RadrootsSdkConfig) -> Result<Self, SdkConfigError> { + config.resolved_relay_urls()?; + config.resolved_radrootsd_endpoint()?; + Ok(Self { config }) + } + + pub fn config(&self) -> &RadrootsSdkConfig { + &self.config + } + + pub fn transport(&self) -> SdkTransportMode { + self.config.transport + } + + pub fn resolved_relay_urls(&self) -> Result<Vec<String>, SdkConfigError> { + self.config.resolved_relay_urls() + } + + pub fn resolved_radrootsd_endpoint(&self) -> Result<String, SdkConfigError> { + self.config.resolved_radrootsd_endpoint() + } + + pub fn profile(&self) -> ProfileClient<'_> { + ProfileClient { client: self } + } + + pub fn farm(&self) -> FarmClient<'_> { + FarmClient { client: self } + } + + pub fn listing(&self) -> ListingClient<'_> { + ListingClient { client: self } + } + + pub fn trade(&self) -> TradeClient<'_> { + TradeClient { client: self } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ProfileClient<'a> { + client: &'a RadrootsSdkClient, +} + +impl<'a> ProfileClient<'a> { + pub fn sdk(&self) -> &'a RadrootsSdkClient { + self.client + } + + pub fn transport(&self) -> SdkTransportMode { + self.client.transport() + } + + #[cfg(feature = "serde_json")] + pub fn build_draft( + &self, + profile_value: &RadrootsProfile, + profile_type: Option<RadrootsProfileType>, + ) -> Result<WireEventParts, profile::ProfileEncodeError> { + profile::build_draft(profile_value, profile_type) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct FarmClient<'a> { + client: &'a RadrootsSdkClient, +} + +impl<'a> FarmClient<'a> { + pub fn sdk(&self) -> &'a RadrootsSdkClient { + self.client + } + + pub fn transport(&self) -> SdkTransportMode { + self.client.transport() + } + + #[cfg(feature = "serde_json")] + pub fn build_draft( + &self, + farm_value: &farm::RadrootsFarm, + ) -> Result<WireEventParts, farm::EventEncodeError> { + farm::build_draft(farm_value) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ListingClient<'a> { + client: &'a RadrootsSdkClient, +} + +impl<'a> ListingClient<'a> { + pub fn sdk(&self) -> &'a RadrootsSdkClient { + self.client + } + + pub fn transport(&self) -> SdkTransportMode { + self.client.transport() + } + + pub fn build_tags( + &self, + listing_value: &listing::RadrootsListing, + ) -> Result<NostrTags, listing::EventEncodeError> { + listing::build_tags(listing_value) + } + + #[cfg(feature = "serde_json")] + pub fn build_draft( + &self, + listing_value: &listing::RadrootsListing, + ) -> Result<WireEventParts, listing::EventEncodeError> { + listing::build_draft(listing_value) + } + + #[cfg(feature = "serde_json")] + pub fn parse_event( + &self, + event: &RadrootsNostrEvent, + ) -> Result<listing::RadrootsListing, listing::RadrootsTradeListingParseError> { + listing::parse_event(event) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TradeClient<'a> { + client: &'a RadrootsSdkClient, +} + +impl<'a> TradeClient<'a> { + pub fn sdk(&self) -> &'a RadrootsSdkClient { + self.client + } + + pub fn transport(&self) -> SdkTransportMode { + self.client.transport() + } + + #[cfg(feature = "serde_json")] + #[allow(clippy::too_many_arguments)] + pub fn build_envelope_draft( + &self, + recipient_pubkey: impl Into<String>, + message_type: trade::RadrootsTradeMessageType, + listing_addr: impl Into<String>, + order_id: Option<String>, + listing_event: Option<&RadrootsNostrEventPtr>, + root_event_id: Option<&str>, + prev_event_id: Option<&str>, + payload: &trade::RadrootsTradeMessagePayload, + ) -> Result<WireEventParts, trade::EventEncodeError> { + trade::build_envelope_draft( + recipient_pubkey, + message_type, + listing_addr, + order_id, + listing_event, + root_event_id, + prev_event_id, + payload, + ) + } + + #[cfg(feature = "serde_json")] + pub fn parse_envelope( + &self, + event: &RadrootsNostrEvent, + ) -> Result<RadrootsTradeEnvelope, trade::RadrootsTradeEnvelopeParseError> { + trade::parse_envelope(event) + } + + #[cfg(feature = "serde_json")] + pub fn parse_listing_address( + &self, + listing_addr: &str, + ) -> Result<trade::RadrootsTradeListingAddress, trade::RadrootsTradeListingAddressError> { + trade::parse_listing_address(listing_addr) + } + + #[cfg(feature = "serde_json")] + pub fn validate_listing_event( + &self, + event: &RadrootsNostrEvent, + ) -> Result<TradeListingValidateResult, trade::RadrootsTradeListingValidationError> { + trade::validate_listing_event(event) + } +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -15,6 +15,7 @@ use std::{string::String, vec::Vec}; feature = "signer-adapters" ))] pub mod adapters; +pub mod client; pub mod config; pub mod farm; #[cfg(feature = "identity-models")] @@ -30,6 +31,9 @@ pub use crate::config::{ RadrootsdAuth, RadrootsdConfig, RelayConfig, RetryPolicy, SdkConfigError, SdkEnvironment, SdkTransportMode, SignerConfig, }; +pub use crate::client::{ + FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, TradeClient, +}; pub use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsNostrEventRef, farm::RadrootsFarm, diff --git a/crates/sdk/tests/client.rs b/crates/sdk/tests/client.rs @@ -0,0 +1,209 @@ +use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, + RadrootsCoreQuantityPrice, RadrootsCoreUnit, +}; +use radroots_events::farm::RadrootsFarm; +use radroots_events::kinds::{KIND_LISTING, KIND_TRADE_LISTING_VALIDATE_REQ}; +use radroots_events::listing::{ + RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, + RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, + RadrootsListingProduct, RadrootsListingStatus, +}; +use radroots_events::trade::{RadrootsTradeListingValidateRequest, RadrootsTradeMessagePayload}; +use radroots_sdk::{ + RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT, RADROOTS_SDK_PRODUCTION_RELAY_URL, + RadrootsNostrEvent, RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkConfigError, + SdkEnvironment, SdkTransportMode, SignerConfig, +}; + +fn sample_farm() -> RadrootsFarm { + RadrootsFarm { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), + name: "North Farm".into(), + about: Some("Organic coffee".into()), + website: None, + picture: None, + banner: None, + location: None, + tags: Some(vec!["coffee".into()]), + } +} + +fn sample_listing() -> RadrootsListing { + RadrootsListing { + d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), + farm: RadrootsListingFarmRef { + pubkey: "seller".into(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), + }, + product: RadrootsListingProduct { + key: "coffee".into(), + title: "Coffee".into(), + category: "coffee".into(), + summary: Some("Single origin coffee".into()), + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: "bin-1".into(), + bins: vec![RadrootsListingBin { + bin_id: "bin-1".into(), + quantity: RadrootsCoreQuantity::new( + RadrootsCoreDecimal::from(1000u32), + RadrootsCoreUnit::MassG, + ), + price_per_canonical_unit: RadrootsCoreQuantityPrice { + amount: RadrootsCoreMoney::new( + RadrootsCoreDecimal::from(20u32), + RadrootsCoreCurrency::USD, + ), + quantity: RadrootsCoreQuantity::new( + RadrootsCoreDecimal::from(1u32), + RadrootsCoreUnit::MassG, + ), + }, + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: Some(RadrootsCoreDecimal::from(5u32)), + availability: Some(RadrootsListingAvailability::Status { + status: RadrootsListingStatus::Active, + }), + delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), + location: Some(RadrootsListingLocation { + primary: "North Farm".into(), + city: None, + region: None, + country: None, + lat: None, + lng: None, + geohash: None, + }), + images: None, + } +} + +#[test] +fn client_default_config_uses_production_relay_direct() { + let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::default()).expect("sdk client"); + + assert_eq!(client.transport(), SdkTransportMode::RelayDirect); + assert_eq!( + client.resolved_relay_urls().expect("resolved relays"), + vec![RADROOTS_SDK_PRODUCTION_RELAY_URL.to_string()] + ); + assert_eq!( + client + .resolved_radrootsd_endpoint() + .expect("resolved radrootsd"), + RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT + ); +} + +#[test] +fn client_rejects_invalid_config_on_construction() { + let mut config = RadrootsSdkConfig::custom(); + config.relay = RelayConfig { + urls: vec!["https://radroots.org".into()], + }; + + let error = RadrootsSdkClient::from_config(config).expect_err("invalid config"); + assert_eq!(error, SdkConfigError::InvalidRelayUrl("https://radroots.org".into())); +} + +#[test] +fn namespace_clients_reflect_explicit_transport_mode() { + let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); + config.transport = SdkTransportMode::Radrootsd; + config.signer = SignerConfig::LocalIdentity; + + let client = RadrootsSdkClient::from_config(config).expect("sdk client"); + + assert_eq!(client.transport(), SdkTransportMode::Radrootsd); + assert_eq!(client.profile().transport(), SdkTransportMode::Radrootsd); + assert_eq!(client.farm().transport(), SdkTransportMode::Radrootsd); + assert_eq!(client.listing().transport(), SdkTransportMode::Radrootsd); + assert_eq!(client.trade().transport(), SdkTransportMode::Radrootsd); +} + +#[test] +fn listing_and_trade_clients_wrap_existing_sdk_facades() { + let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::local()).expect("sdk client"); + let listing_value = sample_listing(); + + let tags = client + .listing() + .build_tags(&listing_value) + .expect("listing tags"); + assert!(!tags.is_empty()); + + let draft = client + .listing() + .build_draft(&listing_value) + .expect("listing draft"); + assert_eq!(draft.kind, KIND_LISTING); + + let event = RadrootsNostrEvent { + id: "listing-1".into(), + author: "seller".into(), + created_at: 1, + kind: draft.kind, + tags: draft.tags, + content: draft.content, + sig: String::new(), + }; + let parsed = client + .listing() + .parse_event(&event) + .expect("parsed listing"); + assert_eq!(parsed.d_tag, listing_value.d_tag); + + let validated = client + .trade() + .validate_listing_event(&event) + .expect("validated listing"); + assert_eq!(validated.listing_id, listing_value.d_tag); + + let listing_addr = format!("{KIND_LISTING}:seller:{}", listing_value.d_tag); + let payload = + RadrootsTradeMessagePayload::ListingValidateRequest(RadrootsTradeListingValidateRequest { + listing_event: None, + }); + let envelope = client + .trade() + .build_envelope_draft( + "buyer", + payload.message_type(), + listing_addr.clone(), + None, + None, + None, + None, + &payload, + ) + .expect("trade draft"); + assert_eq!(envelope.kind, KIND_TRADE_LISTING_VALIDATE_REQ); + let parsed_addr = client + .trade() + .parse_listing_address(&listing_addr) + .expect("listing address"); + assert_eq!(parsed_addr.listing_id, listing_value.d_tag); +} + +#[test] +fn farm_client_wraps_existing_farm_facade() { + let client = + RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); + let farm = sample_farm(); + + let draft = client.farm().build_draft(&farm).expect("farm draft"); + assert!(!draft.tags.is_empty()); +}