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:
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();