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:
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",