lib

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

commit 96dbf6c70437a816d655cf20f8636d680fadd349
parent 112fb63daacc70248ff4e4fb7ce97f94eed00482
Author: triesap <tyson@radroots.org>
Date:   Wed,  3 Jun 2026 00:55:17 -0700

sdk: add active trade revision publish helpers

- expose revision proposal and decision draft builders and parsers

- publish revision proposal and decision events through relay-direct identities

- cover signed 3424 and 3425 relay publication

Diffstat:
Mcrates/sdk/src/client.rs | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/trade.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/relay_direct.rs | 182++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 389 insertions(+), 1 deletion(-)

diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -2319,6 +2319,26 @@ impl<'a> TradeClient<'a> { } #[cfg(feature = "serde_json")] + pub fn build_order_revision_proposal_draft( + &self, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeOrderRevisionProposed, + ) -> Result<trade::RadrootsTradeOrderRevisionProposalDraft, trade::EventEncodeError> { + trade::build_order_revision_proposal_draft(root_event_id, prev_event_id, payload) + } + + #[cfg(feature = "serde_json")] + pub fn build_order_revision_decision_draft( + &self, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeOrderRevisionDecisionEvent, + ) -> Result<trade::RadrootsTradeOrderRevisionDecisionDraft, trade::EventEncodeError> { + trade::build_order_revision_decision_draft(root_event_id, prev_event_id, payload) + } + + #[cfg(feature = "serde_json")] pub fn build_fulfillment_update_draft( &self, root_event_id: &str, @@ -2371,6 +2391,28 @@ impl<'a> TradeClient<'a> { } #[cfg(feature = "serde_json")] + pub fn parse_order_revision_proposal( + &self, + event: &RadrootsNostrEvent, + ) -> Result< + trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderRevisionProposed>, + trade::RadrootsActiveTradeEnvelopeParseError, + > { + trade::parse_order_revision_proposal(event) + } + + #[cfg(feature = "serde_json")] + pub fn parse_order_revision_decision( + &self, + event: &RadrootsNostrEvent, + ) -> Result< + trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderRevisionDecisionEvent>, + trade::RadrootsActiveTradeEnvelopeParseError, + > { + trade::parse_order_revision_decision(event) + } + + #[cfg(feature = "serde_json")] pub fn parse_fulfillment_update( &self, event: &RadrootsNostrEvent, @@ -2430,6 +2472,54 @@ impl<'a> TradeClient<'a> { feature = "relay-client", feature = "signing" ))] + pub async fn publish_order_revision_proposal_with_identity( + &self, + identity: &RadrootsIdentity, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeOrderRevisionProposed, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + let draft = + trade::build_order_revision_proposal_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_revision_proposal_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] + pub async fn publish_order_revision_decision_with_identity( + &self, + identity: &RadrootsIdentity, + root_event_id: &str, + prev_event_id: &str, + payload: &trade::RadrootsTradeOrderRevisionDecisionEvent, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + let draft = + trade::build_order_revision_decision_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_revision_decision_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] pub async fn publish_order_decision_with_identity( &self, identity: &RadrootsIdentity, @@ -2476,6 +2566,44 @@ impl<'a> TradeClient<'a> { feature = "relay-client", feature = "signing" ))] + pub async fn publish_order_revision_proposal_draft_with_identity( + &self, + identity: &RadrootsIdentity, + draft: trade::RadrootsTradeOrderRevisionProposalDraft, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_order_revision_proposal_draft_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] + pub async fn publish_order_revision_decision_draft_with_identity( + &self, + identity: &RadrootsIdentity, + draft: trade::RadrootsTradeOrderRevisionDecisionDraft, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + self.client + .publish_parts_via_relay_with_identity( + identity, + draft.into_wire_parts(), + "trade.publish_order_revision_decision_draft_with_identity", + ) + .await + } + + #[cfg(all( + feature = "identity-models", + feature = "relay-client", + feature = "signing" + ))] pub async fn publish_order_cancellation_with_identity( &self, identity: &RadrootsIdentity, diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs @@ -21,6 +21,16 @@ pub struct RadrootsTradeOrderDecisionDraft { } #[derive(Debug, Clone)] +pub struct RadrootsTradeOrderRevisionProposalDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsTradeOrderRevisionDecisionDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] pub struct RadrootsTradeFulfillmentUpdateDraft { parts: WireEventParts, } @@ -55,6 +65,26 @@ impl RadrootsTradeOrderDecisionDraft { } } +impl RadrootsTradeOrderRevisionProposalDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsTradeOrderRevisionDecisionDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + impl RadrootsTradeFulfillmentUpdateDraft { pub fn as_wire_parts(&self) -> &WireEventParts { &self.parts @@ -137,6 +167,36 @@ pub fn build_order_decision_draft( } #[cfg(feature = "serde_json")] +pub fn build_order_revision_proposal_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsTradeOrderRevisionProposed, +) -> Result<RadrootsTradeOrderRevisionProposalDraft, EventEncodeError> { + Ok(RadrootsTradeOrderRevisionProposalDraft { + parts: radroots_events_codec::trade::active_trade_order_revision_proposal_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_order_revision_decision_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsTradeOrderRevisionDecisionEvent, +) -> Result<RadrootsTradeOrderRevisionDecisionDraft, EventEncodeError> { + Ok(RadrootsTradeOrderRevisionDecisionDraft { + parts: radroots_events_codec::trade::active_trade_order_revision_decision_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] pub fn build_fulfillment_update_draft( root_event_id: &str, prev_event_id: &str, @@ -209,6 +269,26 @@ pub fn parse_order_decision( } #[cfg(feature = "serde_json")] +pub fn parse_order_revision_proposal( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionProposed>, + RadrootsActiveTradeEnvelopeParseError, +> { + radroots_events_codec::trade::active_trade_order_revision_proposal_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_order_revision_decision( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionDecisionEvent>, + RadrootsActiveTradeEnvelopeParseError, +> { + radroots_events_codec::trade::active_trade_order_revision_decision_from_event(event) +} + +#[cfg(feature = "serde_json")] pub fn parse_fulfillment_update( event: &RadrootsNostrEvent, ) -> Result< diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs @@ -23,7 +23,8 @@ use radroots_sdk::trade::{ RadrootsTradeFulfillmentUpdated, RadrootsTradeInventoryCommitment, RadrootsTradeOrderCancelled, RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, - RadrootsTradePricingBasis, + RadrootsTradeOrderRevisionDecision, RadrootsTradeOrderRevisionDecisionEvent, + RadrootsTradeOrderRevisionProposed, RadrootsTradePricingBasis, }; use radroots_sdk::{ RadrootsNostrEventPtr, RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, @@ -273,6 +274,65 @@ fn sample_order_decision( } } +fn sample_order_revision_proposal( + buyer_pubkey: String, + seller_pubkey: String, + root_event_id: String, + prev_event_id: String, +) -> RadrootsTradeOrderRevisionProposed { + RadrootsTradeOrderRevisionProposed { + revision_id: "revision-1".into(), + order_id: "order-1".into(), + listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), + buyer_pubkey, + seller_pubkey, + root_event_id, + prev_event_id, + items: vec![RadrootsTradeOrderItem { + bin_id: "bin-1".into(), + bin_count: 3, + }], + economics: RadrootsTradeOrderEconomics { + quote_id: "revision-quote-1".into(), + quote_version: 2, + pricing_basis: RadrootsTradePricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsTradeOrderEconomicItem { + bin_id: "bin-1".into(), + bin_count: 3, + quantity_amount: decimal("1"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("5"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd("15"), + }], + discounts: Vec::new(), + adjustments: Vec::new(), + subtotal: usd("15"), + discount_total: usd("0"), + adjustment_total: usd("0"), + total: usd("15"), + }, + reason: "update count".into(), + } +} + +fn sample_order_revision_decision( + proposal: &RadrootsTradeOrderRevisionProposed, + decision: RadrootsTradeOrderRevisionDecision, +) -> RadrootsTradeOrderRevisionDecisionEvent { + RadrootsTradeOrderRevisionDecisionEvent { + revision_id: proposal.revision_id.clone(), + order_id: proposal.order_id.clone(), + listing_addr: proposal.listing_addr.clone(), + buyer_pubkey: proposal.buyer_pubkey.clone(), + seller_pubkey: proposal.seller_pubkey.clone(), + root_event_id: proposal.root_event_id.clone(), + prev_event_id: "order-revision-proposal-event-1".into(), + decision, + } +} + fn sample_fulfillment_update( buyer_pubkey: String, seller_pubkey: String, @@ -540,6 +600,126 @@ async fn relay_direct_order_decision_publish_accepts_sdk_built_draft() -> TestRe } #[tokio::test] +async fn relay_direct_trade_revision_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 proposal = sample_order_revision_proposal( + buyer_pubkey.clone(), + seller_pubkey.clone(), + root_event_id.to_owned(), + decision_event_id.to_owned(), + ); + let decision = + sample_order_revision_decision(&proposal, RadrootsTradeOrderRevisionDecision::Accepted); + 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 proposal_receipt = client + .trade() + .publish_order_revision_proposal_with_identity( + &seller_identity, + root_event_id, + decision_event_id, + &proposal, + ) + .await?; + let decision_receipt = client + .trade() + .publish_order_revision_decision_with_identity( + &buyer_identity, + root_event_id, + decision.prev_event_id.as_str(), + &decision, + ) + .await?; + + assert_eq!(proposal_receipt.event_kind, Some(3424)); + assert_eq!(decision_receipt.event_kind, Some(3425)); + + match proposal_receipt.transport_receipt { + SdkTransportReceipt::RelayDirect(relay_receipt) => { + assert_eq!(relay_receipt.event.kind, 3424); + 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_order_revision_proposal(&relay_receipt.event) + .expect("active order revision proposal"); + assert_eq!(envelope.order_id, proposal.order_id); + assert_eq!(envelope.listing_addr, proposal.listing_addr); + assert_eq!(envelope.payload.revision_id, "revision-1"); + assert_eq!(envelope.payload.economics.total, usd("15")); + assert_eq!(envelope.payload.reason, "update count"); + } + SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), + } + + match decision_receipt.transport_receipt { + SdkTransportReceipt::RelayDirect(relay_receipt) => { + assert_eq!(relay_receipt.event.kind, 3425); + 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(), + "order-revision-proposal-event-1".to_owned() + ])); + let envelope = client + .trade() + .parse_order_revision_decision(&relay_receipt.event) + .expect("active order revision decision"); + assert_eq!(envelope.order_id, decision.order_id); + assert_eq!(envelope.listing_addr, decision.listing_addr); + assert_eq!(envelope.payload.revision_id, decision.revision_id); + assert_eq!( + envelope.payload.decision, + RadrootsTradeOrderRevisionDecision::Accepted + ); + } + SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), + } + + Ok(()) +} + +#[tokio::test] async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> TestResult<()> { let relay = AckRelay::spawn().await?; let buyer_identity = RadrootsIdentity::generate();