lib

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

commit da5c3476bc94b8a2bfbb000667b07148c9a56e11
parent b5d7520c7f1168914f4cedd3260108bb8e65aaf2
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 22:51:40 +0000

sdk: add curated rust facade crate

Diffstat:
MCargo.lock | 10++++++++++
MCargo.toml | 2++
Acrates/sdk/Cargo.toml | 35+++++++++++++++++++++++++++++++++++
Acrates/sdk/README | 8++++++++
Acrates/sdk/src/farm.rs | 9+++++++++
Acrates/sdk/src/lib.rs | 34++++++++++++++++++++++++++++++++++
Acrates/sdk/src/listing.rs | 22++++++++++++++++++++++
Acrates/sdk/src/profile.rs | 12++++++++++++
Acrates/sdk/src/trade.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/tests/facade.rs | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/trade/src/listing/mod.rs | 9+++++++++
Mpolicy/coverage/policy.toml | 1+
12 files changed, 396 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2641,6 +2641,16 @@ dependencies = [ ] [[package]] +name = "radroots_sdk" +version = "0.1.0-alpha.2" +dependencies = [ + "radroots_core", + "radroots_events", + "radroots_events_codec", + "radroots_trade", +] + +[[package]] name = "radroots_secret_vault" version = "0.1.0-alpha.2" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "crates/runtime_paths", "crates/runtime_distribution", "crates/runtime_manager", + "crates/sdk", "crates/trade", "crates/types", "crates/protected_store", @@ -69,6 +70,7 @@ radroots_runtime = { path = "crates/runtime", version = "0.1.0-alpha.2", default radroots_runtime_paths = { path = "crates/runtime_paths", version = "0.1.0-alpha.2", default-features = false } radroots_runtime_distribution = { path = "crates/runtime_distribution", version = "0.1.0-alpha.2", default-features = false } radroots_runtime_manager = { path = "crates/runtime_manager", version = "0.1.0-alpha.2", default-features = false } +radroots_sdk = { path = "crates/sdk", version = "0.1.0-alpha.2", default-features = false } radroots_log = { path = "crates/log", version = "0.1.0-alpha.2", default-features = false } radroots_net = { path = "crates/net", version = "0.1.0-alpha.2", default-features = false } radroots_nostr_runtime = { path = "crates/nostr_runtime", version = "0.1.0-alpha.2", default-features = false } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "radroots_sdk" +publish = ["crates-io"] +version.workspace = true +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "Curated Radroots SDK for profile, farm, listing, and trade event workflows" +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/radroots_sdk" +readme = "README" + +[features] +default = ["std", "serde", "serde_json"] +std = ["radroots_events/std", "radroots_events_codec/std", "radroots_trade/std"] +serde = ["radroots_events/serde", "radroots_trade/serde"] +serde_json = [ + "serde", + "nostr", + "radroots_events_codec/serde_json", + "radroots_trade/serde_json", +] +nostr = ["radroots_events_codec/nostr"] +ts-rs = ["radroots_events/ts-rs", "radroots_trade/ts-rs"] +typeshare = ["radroots_events/typeshare"] + +[dependencies] +radroots_events = { workspace = true, default-features = false } +radroots_events_codec = { workspace = true, default-features = false } +radroots_trade = { workspace = true, default-features = false } + +[dev-dependencies] +radroots_core = { workspace = true, default-features = false, features = ["std"] } diff --git a/crates/sdk/README b/crates/sdk/README @@ -0,0 +1,8 @@ +# radroots_sdk + +Curated Rad Roots Rust SDK for the public marketplace event contract. + +This crate provides the recommended Rust entrypoint for building, parsing, and +validating Rad Roots profile, farm, listing, and trade events. It is a thin +facade over the underlying `rr-rs` substrate crates and does not duplicate the +core event or trade implementations. diff --git a/crates/sdk/src/farm.rs b/crates/sdk/src/farm.rs @@ -0,0 +1,9 @@ +pub use radroots_events::farm::*; +pub use radroots_events_codec::error::EventEncodeError; + +use crate::WireEventParts; + +#[cfg(feature = "serde_json")] +pub fn build_draft(farm: &RadrootsFarm) -> Result<WireEventParts, EventEncodeError> { + radroots_events_codec::farm::encode::to_wire_parts(farm) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -0,0 +1,34 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + +#[cfg(feature = "std")] +use std::{string::String, vec::Vec}; +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +pub mod farm; +pub mod listing; +pub mod profile; +pub mod trade; + +pub use radroots_events::{ + RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsNostrEventRef, + farm::RadrootsFarm, + listing::RadrootsListing, + profile::{RadrootsProfile, RadrootsProfileType}, + trade::{RadrootsTradeMessagePayload, RadrootsTradeMessageType}, +}; +#[cfg(feature = "serde_json")] +pub use radroots_events_codec::trade::{ + RadrootsTradeEnvelopeParseError, RadrootsTradeListingAddress, + RadrootsTradeListingAddressError, +}; +pub use radroots_events_codec::wire::{EventDraft as UnsignedEventDraft, WireEventParts}; +pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; + +pub type NostrTags = Vec<Vec<String>>; +pub type RadrootsTradeEnvelope = + radroots_events::trade::RadrootsTradeEnvelope<RadrootsTradeMessagePayload>; diff --git a/crates/sdk/src/listing.rs b/crates/sdk/src/listing.rs @@ -0,0 +1,22 @@ +pub use radroots_events::listing::*; +pub use radroots_events::trade::{ + RadrootsTradeListingParseError, RadrootsTradeListingValidationError, +}; +pub use radroots_events_codec::error::EventEncodeError; +pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; + +use crate::{NostrTags, RadrootsNostrEvent, WireEventParts}; + +pub fn build_tags(listing: &RadrootsListing) -> Result<NostrTags, EventEncodeError> { + radroots_events_codec::listing::encode::listing_build_tags(listing) +} + +#[cfg(feature = "serde_json")] +pub fn build_draft(listing: &RadrootsListing) -> Result<WireEventParts, EventEncodeError> { + radroots_events_codec::listing::encode::to_wire_parts(listing) +} + +#[cfg(feature = "serde_json")] +pub fn parse_event(event: &RadrootsNostrEvent) -> Result<RadrootsListing, RadrootsTradeListingParseError> { + radroots_trade::listing::parse_listing_event(event) +} diff --git a/crates/sdk/src/profile.rs b/crates/sdk/src/profile.rs @@ -0,0 +1,12 @@ +pub use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; +pub use radroots_events_codec::profile::error::ProfileEncodeError; + +use crate::WireEventParts; + +#[cfg(feature = "serde_json")] +pub fn build_draft( + profile: &RadrootsProfile, + profile_type: Option<RadrootsProfileType>, +) -> Result<WireEventParts, ProfileEncodeError> { + radroots_events_codec::profile::encode::to_wire_parts_with_profile_type(profile, profile_type) +} diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs @@ -0,0 +1,55 @@ +pub use radroots_events::trade::*; +pub use radroots_events_codec::error::EventEncodeError; +#[cfg(feature = "serde_json")] +pub use radroots_events_codec::trade::{ + RadrootsTradeEnvelopeParseError, RadrootsTradeEventContext, RadrootsTradeListingAddress, + RadrootsTradeListingAddressError, +}; +pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; + +use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr, WireEventParts}; +use crate::RadrootsTradeEnvelope as SdkTradeEnvelope; + +#[cfg(feature = "serde_json")] +pub fn build_envelope_draft( + recipient_pubkey: impl Into<String>, + message_type: 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: &RadrootsTradeMessagePayload, +) -> Result<WireEventParts, EventEncodeError> { + radroots_events_codec::trade::trade_envelope_event_build( + 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( + event: &RadrootsNostrEvent, +) -> Result<SdkTradeEnvelope, RadrootsTradeEnvelopeParseError> { + radroots_events_codec::trade::trade_envelope_from_event::<RadrootsTradeMessagePayload>(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_listing_address( + listing_addr: &str, +) -> Result<RadrootsTradeListingAddress, RadrootsTradeListingAddressError> { + RadrootsTradeListingAddress::parse(listing_addr) +} + +#[cfg(feature = "serde_json")] +pub fn validate_listing_event( + event: &RadrootsNostrEvent, +) -> Result<TradeListingValidateResult, RadrootsTradeListingValidationError> { + radroots_trade::listing::validation::validate_listing_event(event) +} diff --git a/crates/sdk/tests/facade.rs b/crates/sdk/tests/facade.rs @@ -0,0 +1,199 @@ +use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, + RadrootsCoreQuantityPrice, RadrootsCoreUnit, +}; +use radroots_events::farm::RadrootsFarm; +use radroots_events::kinds::{ + KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_LISTING_VALIDATE_REQ, +}; +use radroots_events::listing::{ + RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, + RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, + RadrootsListingProduct, RadrootsListingStatus, +}; +use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; +use radroots_events::trade::{RadrootsTradeListingValidateRequest, RadrootsTradeMessagePayload}; +use radroots_sdk::{ + RadrootsNostrEvent, farm, listing, profile, trade, +}; + +fn sample_profile() -> RadrootsProfile { + RadrootsProfile { + name: "North Farm".into(), + display_name: Some("North Farm".into()), + nip05: None, + about: Some("Organic coffee".into()), + website: Some("https://example.com".into()), + picture: None, + banner: None, + lud06: None, + lud16: None, + bot: None, + } +} + +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, + } +} + +fn listing_event(listing_value: &RadrootsListing) -> RadrootsNostrEvent { + let parts = listing::build_draft(listing_value).expect("listing draft"); + RadrootsNostrEvent { + id: "event-1".into(), + author: "seller".into(), + created_at: 1, + kind: parts.kind, + tags: parts.tags, + content: parts.content, + sig: String::new(), + } +} + +#[test] +fn profile_build_draft_wraps_profile_encoder() { + let parts = + profile::build_draft(&sample_profile(), Some(RadrootsProfileType::Farm)).expect("profile"); + + assert_eq!(parts.kind, KIND_PROFILE); + assert!(parts.tags.iter().any(|tag| { + tag.first().map(|value| value.as_str()) == Some("t") + && tag.get(1).map(|value| value.as_str()) == Some("radroots:type:farm") + })); +} + +#[test] +fn farm_build_draft_wraps_farm_encoder() { + let parts = farm::build_draft(&sample_farm()).expect("farm"); + + assert_eq!(parts.kind, KIND_FARM); + assert!(parts + .tags + .iter() + .any(|tag| tag.first().map(|value| value.as_str()) == Some("d"))); +} + +#[test] +fn listing_facade_wraps_build_parse_and_validate() { + let listing_value = sample_listing(); + let tags = listing::build_tags(&listing_value).expect("listing tags"); + assert!(!tags.is_empty()); + + let event = listing_event(&listing_value); + let parsed = listing::parse_event(&event).expect("parsed listing"); + assert_eq!(parsed.d_tag, listing_value.d_tag); + + let validated = trade::validate_listing_event(&event).expect("validated listing"); + assert_eq!(validated.listing_id, listing_value.d_tag); + assert_eq!(event.kind, KIND_LISTING); +} + +#[test] +fn trade_facade_wraps_build_parse_and_address_ops() { + let listing_value = sample_listing(); + let listing_addr = format!("{KIND_LISTING}:seller:{}", listing_value.d_tag); + let payload = + RadrootsTradeMessagePayload::ListingValidateRequest(RadrootsTradeListingValidateRequest { + listing_event: None, + }); + + let parts = trade::build_envelope_draft( + "buyer", + payload.message_type(), + listing_addr.clone(), + None, + None, + None, + None, + &payload, + ) + .expect("trade envelope draft"); + + assert_eq!(parts.kind, KIND_TRADE_LISTING_VALIDATE_REQ); + + let parsed_addr = trade::parse_listing_address(&listing_addr).expect("listing address"); + assert_eq!(parsed_addr.listing_id, listing_value.d_tag); + + let event = RadrootsNostrEvent { + id: "trade-event".into(), + author: "seller".into(), + created_at: 2, + kind: parts.kind, + tags: parts.tags, + content: parts.content, + sig: String::new(), + }; + let envelope = trade::parse_envelope(&event).expect("trade envelope"); + assert_eq!(envelope.message_type, payload.message_type()); + assert_eq!(envelope.listing_addr, listing_addr); +} diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs @@ -7,7 +7,16 @@ pub mod projection; pub mod publish; pub mod validation; +use radroots_events::{RadrootsNostrEvent, listing::RadrootsListing}; + pub(crate) use self::contract as dvm; #[allow(unused_imports)] pub(crate) use self::contract as kinds; pub(crate) use self::contract as order; +pub use radroots_events::trade::RadrootsTradeListingParseError as TradeListingParseError; + +pub fn parse_listing_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsListing, TradeListingParseError> { + self::codec::listing_from_event_parts(&event.tags, &event.content) +} diff --git a/policy/coverage/policy.toml b/policy/coverage/policy.toml @@ -130,6 +130,7 @@ crates = [ "radroots_runtime_paths", "radroots_runtime_distribution", "radroots_runtime_manager", + "radroots_sdk", "radroots_secret_vault", "radroots_simplex_chat_proto", "radroots_simplex_smp_proto",