lib

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

commit 112fb63daacc70248ff4e4fb7ce97f94eed00482
parent 204824fc93cde614ff3622297ad391930c7207e1
Author: triesap <tyson@radroots.org>
Date:   Wed,  3 Jun 2026 00:31:37 -0700

sdk: add active trade lifecycle publish helpers

- add SDK draft builders for fulfillment, cancellation, and buyer receipt events
- expose parse helpers for active trade lifecycle envelopes
- wire relay-direct publish methods for lifecycle payloads and drafts
- cover lifecycle publishing with relay-direct SDK tests

Diffstat:
Mcrates/sdk/src/client.rs | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/trade.rs | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/relay_direct.rs | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 507 insertions(+), 3 deletions(-)

diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -2319,6 +2319,36 @@ impl<'a> TradeClient<'a> { } #[cfg(feature = "serde_json")] + pub fn build_fulfillment_update_draft( + &self, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeFulfillmentUpdated, + ) -> Result<trade::RadrootsTradeFulfillmentUpdateDraft, trade::EventEncodeError> { + trade::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) + } + + #[cfg(feature = "serde_json")] + pub fn build_order_cancellation_draft( + &self, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeOrderCancelled, + ) -> Result<trade::RadrootsTradeOrderCancellationDraft, trade::EventEncodeError> { + trade::build_order_cancellation_draft(root_event_id, prev_event_id, payload) + } + + #[cfg(feature = "serde_json")] + pub fn build_buyer_receipt_draft( + &self, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeBuyerReceipt, + ) -> Result<trade::RadrootsTradeBuyerReceiptDraft, trade::EventEncodeError> { + trade::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) + } + + #[cfg(feature = "serde_json")] pub fn parse_order_request( &self, event: &RadrootsNostrEvent, @@ -2340,6 +2370,39 @@ impl<'a> TradeClient<'a> { trade::parse_order_decision(event) } + #[cfg(feature = "serde_json")] + pub fn parse_fulfillment_update( + &self, + event: &RadrootsNostrEvent, + ) -> Result< + trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeFulfillmentUpdated>, + trade::RadrootsActiveTradeEnvelopeParseError, + > { + trade::parse_fulfillment_update(event) + } + + #[cfg(feature = "serde_json")] + pub fn parse_order_cancellation( + &self, + event: &RadrootsNostrEvent, + ) -> Result< + trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderCancelled>, + trade::RadrootsActiveTradeEnvelopeParseError, + > { + trade::parse_order_cancellation(event) + } + + #[cfg(feature = "serde_json")] + pub fn parse_buyer_receipt( + &self, + event: &RadrootsNostrEvent, + ) -> Result< + trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeBuyerReceipt>, + trade::RadrootsActiveTradeEnvelopeParseError, + > { + trade::parse_buyer_receipt(event) + } + #[cfg(all( feature = "identity-models", feature = "relay-client", @@ -2390,6 +2453,75 @@ impl<'a> TradeClient<'a> { feature = "relay-client", feature = "signing" ))] + pub async fn publish_fulfillment_update_with_identity( + &self, + identity: &RadrootsIdentity, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeFulfillmentUpdated, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + let draft = trade::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) + .map_err(|err| SdkPublishError::Encode(err.to_string()))?; + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_fulfillment_update_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] + pub async fn publish_order_cancellation_with_identity( + &self, + identity: &RadrootsIdentity, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeOrderCancelled, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + let draft = trade::build_order_cancellation_draft(root_event_id, prev_event_id, payload) + .map_err(|err| SdkPublishError::Encode(err.to_string()))?; + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_order_cancellation_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] + pub async fn publish_buyer_receipt_with_identity( + &self, + identity: &RadrootsIdentity, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeBuyerReceipt, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + let draft = trade::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) + .map_err(|err| SdkPublishError::Encode(err.to_string()))?; + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_buyer_receipt_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] pub async fn publish_order_request_draft_with_identity( &self, identity: &RadrootsIdentity, @@ -2423,6 +2555,63 @@ impl<'a> TradeClient<'a> { .await } + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] + pub async fn publish_fulfillment_update_draft_with_identity( + &self, + identity: &RadrootsIdentity, + draft: trade::RadrootsTradeFulfillmentUpdateDraft, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_fulfillment_update_draft_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] + pub async fn publish_order_cancellation_draft_with_identity( + &self, + identity: &RadrootsIdentity, + draft: trade::RadrootsTradeOrderCancellationDraft, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_order_cancellation_draft_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] + pub async fn publish_buyer_receipt_draft_with_identity( + &self, + identity: &RadrootsIdentity, + draft: trade::RadrootsTradeBuyerReceiptDraft, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_buyer_receipt_draft_with_identity", + ) + .await + } + #[cfg(feature = "radrootsd-client")] pub async fn publish_order_request_via_radrootsd( &self, diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs @@ -20,6 +20,21 @@ pub struct RadrootsTradeOrderDecisionDraft { parts: WireEventParts, } +#[derive(Debug, Clone)] +pub struct RadrootsTradeFulfillmentUpdateDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsTradeOrderCancellationDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsTradeBuyerReceiptDraft { + parts: WireEventParts, +} + impl RadrootsTradeOrderRequestDraft { pub fn as_wire_parts(&self) -> &WireEventParts { &self.parts @@ -40,6 +55,36 @@ impl RadrootsTradeOrderDecisionDraft { } } +impl RadrootsTradeFulfillmentUpdateDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsTradeOrderCancellationDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsTradeBuyerReceiptDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + #[cfg(feature = "serde_json")] pub fn build_envelope_draft( recipient_pubkey: impl Into<String>, @@ -92,6 +137,51 @@ pub fn build_order_decision_draft( } #[cfg(feature = "serde_json")] +pub fn build_fulfillment_update_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsTradeFulfillmentUpdated, +) -> Result<RadrootsTradeFulfillmentUpdateDraft, EventEncodeError> { + Ok(RadrootsTradeFulfillmentUpdateDraft { + parts: radroots_events_codec::trade::active_trade_fulfillment_update_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_order_cancellation_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsTradeOrderCancelled, +) -> Result<RadrootsTradeOrderCancellationDraft, EventEncodeError> { + Ok(RadrootsTradeOrderCancellationDraft { + parts: radroots_events_codec::trade::active_trade_order_cancel_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_buyer_receipt_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsTradeBuyerReceipt, +) -> Result<RadrootsTradeBuyerReceiptDraft, EventEncodeError> { + Ok(RadrootsTradeBuyerReceiptDraft { + parts: radroots_events_codec::trade::active_trade_buyer_receipt_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] pub fn parse_envelope( event: &RadrootsNostrEvent, ) -> Result<SdkTradeEnvelope, RadrootsTradeEnvelopeParseError> { @@ -119,6 +209,36 @@ pub fn parse_order_decision( } #[cfg(feature = "serde_json")] +pub fn parse_fulfillment_update( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeFulfillmentUpdated>, + RadrootsActiveTradeEnvelopeParseError, +> { + radroots_events_codec::trade::active_trade_fulfillment_update_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_order_cancellation( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeOrderCancelled>, + RadrootsActiveTradeEnvelopeParseError, +> { + radroots_events_codec::trade::active_trade_order_cancel_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_buyer_receipt( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeBuyerReceipt>, + RadrootsActiveTradeEnvelopeParseError, +> { + radroots_events_codec::trade::active_trade_buyer_receipt_from_event(event) +} + +#[cfg(feature = "serde_json")] pub fn parse_listing_address( listing_addr: &str, ) -> Result<RadrootsTradeListingAddress, RadrootsTradeListingAddressError> { diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs @@ -19,9 +19,11 @@ use radroots_sdk::listing::{ }; use radroots_sdk::profile::{RadrootsProfile, RadrootsProfileType}; use radroots_sdk::trade::{ - RadrootsTradeInventoryCommitment, RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, - RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, - RadrootsTradeOrderRequested, RadrootsTradePricingBasis, + RadrootsActiveTradeFulfillmentState, RadrootsTradeBuyerReceipt, + RadrootsTradeFulfillmentUpdated, RadrootsTradeInventoryCommitment, RadrootsTradeOrderCancelled, + RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, + RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, + RadrootsTradePricingBasis, }; use radroots_sdk::{ RadrootsNostrEventPtr, RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, @@ -271,6 +273,44 @@ fn sample_order_decision( } } +fn sample_fulfillment_update( + buyer_pubkey: String, + seller_pubkey: String, +) -> RadrootsTradeFulfillmentUpdated { + RadrootsTradeFulfillmentUpdated { + order_id: "order-1".into(), + listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), + buyer_pubkey, + seller_pubkey, + status: RadrootsActiveTradeFulfillmentState::ReadyForPickup, + } +} + +fn sample_order_cancellation( + buyer_pubkey: String, + seller_pubkey: String, +) -> RadrootsTradeOrderCancelled { + RadrootsTradeOrderCancelled { + order_id: "order-1".into(), + listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), + buyer_pubkey, + seller_pubkey, + reason: "schedule changed".into(), + } +} + +fn sample_buyer_receipt(buyer_pubkey: String, seller_pubkey: String) -> RadrootsTradeBuyerReceipt { + RadrootsTradeBuyerReceipt { + order_id: "order-1".into(), + listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), + buyer_pubkey, + seller_pubkey, + received: true, + issue: None, + received_at: 1_785_000_000, + } +} + #[tokio::test] async fn relay_direct_farm_publish_accepts_sdk_built_draft() -> TestResult<()> { let relay = AckRelay::spawn().await?; @@ -500,6 +540,161 @@ async fn relay_direct_order_decision_publish_accepts_sdk_built_draft() -> TestRe } #[tokio::test] +async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> TestResult<()> { + let relay = AckRelay::spawn().await?; + let buyer_identity = RadrootsIdentity::generate(); + let seller_identity = RadrootsIdentity::generate(); + let buyer_pubkey = buyer_identity.public_key_hex(); + let seller_pubkey = seller_identity.public_key_hex(); + let root_event_id = "order-request-event-1"; + let decision_event_id = "order-decision-event-1"; + let fulfillment_event_id = "fulfillment-event-1"; + let fulfillment = sample_fulfillment_update(buyer_pubkey.clone(), seller_pubkey.clone()); + let cancellation = sample_order_cancellation(buyer_pubkey.clone(), seller_pubkey.clone()); + let receipt = sample_buyer_receipt(buyer_pubkey.clone(), seller_pubkey.clone()); + let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); + config.transport = SdkTransportMode::RelayDirect; + config.signer = SignerConfig::LocalIdentity; + config.relay = RelayConfig { + urls: vec![relay.url().to_owned()], + }; + let client = RadrootsSdkClient::from_config(config)?; + + let fulfillment_receipt = client + .trade() + .publish_fulfillment_update_with_identity( + &seller_identity, + root_event_id, + decision_event_id, + &fulfillment, + ) + .await?; + let cancellation_receipt = client + .trade() + .publish_order_cancellation_with_identity( + &buyer_identity, + root_event_id, + root_event_id, + &cancellation, + ) + .await?; + let buyer_receipt = client + .trade() + .publish_buyer_receipt_with_identity( + &buyer_identity, + root_event_id, + fulfillment_event_id, + &receipt, + ) + .await?; + + assert_eq!(fulfillment_receipt.event_kind, Some(3433)); + assert_eq!(cancellation_receipt.event_kind, Some(3432)); + assert_eq!(buyer_receipt.event_kind, Some(3434)); + + match fulfillment_receipt.transport_receipt { + SdkTransportReceipt::RelayDirect(relay_receipt) => { + assert_eq!(relay_receipt.event.kind, 3433); + assert_eq!(relay_receipt.event.author, seller_pubkey); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["p".to_owned(), buyer_pubkey.clone()]) + ); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["e_root".to_owned(), root_event_id.to_owned()]) + ); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["e_prev".to_owned(), decision_event_id.to_owned()]) + ); + let envelope = client + .trade() + .parse_fulfillment_update(&relay_receipt.event) + .expect("active fulfillment update"); + assert_eq!(envelope.order_id, fulfillment.order_id); + assert_eq!(envelope.listing_addr, fulfillment.listing_addr); + assert_eq!(envelope.payload.status, fulfillment.status); + } + SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), + } + + match cancellation_receipt.transport_receipt { + SdkTransportReceipt::RelayDirect(relay_receipt) => { + assert_eq!(relay_receipt.event.kind, 3432); + assert_eq!(relay_receipt.event.author, buyer_pubkey); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["p".to_owned(), seller_pubkey.clone()]) + ); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["e_root".to_owned(), root_event_id.to_owned()]) + ); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["e_prev".to_owned(), root_event_id.to_owned()]) + ); + let envelope = client + .trade() + .parse_order_cancellation(&relay_receipt.event) + .expect("active order cancellation"); + assert_eq!(envelope.order_id, cancellation.order_id); + assert_eq!(envelope.listing_addr, cancellation.listing_addr); + assert_eq!(envelope.payload.reason, cancellation.reason); + } + SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), + } + + match buyer_receipt.transport_receipt { + SdkTransportReceipt::RelayDirect(relay_receipt) => { + assert_eq!(relay_receipt.event.kind, 3434); + assert_eq!(relay_receipt.event.author, buyer_pubkey); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["p".to_owned(), seller_pubkey]) + ); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["e_root".to_owned(), root_event_id.to_owned()]) + ); + assert!( + relay_receipt + .event + .tags + .contains(&vec!["e_prev".to_owned(), fulfillment_event_id.to_owned()]) + ); + let envelope = client + .trade() + .parse_buyer_receipt(&relay_receipt.event) + .expect("active buyer receipt"); + assert_eq!(envelope.order_id, receipt.order_id); + assert_eq!(envelope.listing_addr, receipt.listing_addr); + assert_eq!(envelope.payload.received, receipt.received); + } + SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), + } + + Ok(()) +} + +#[tokio::test] async fn relay_direct_order_decision_publish_builds_and_publishes_payload() -> TestResult<()> { let relay = AckRelay::spawn().await?; let buyer_identity = RadrootsIdentity::generate();