commit a0b0c1ae5ddc5ea36885339878f4e562a6c87e93
parent c804f77f4ef827a05a4c4b6d1ba8f75f58902315
Author: triesap <tyson@radroots.org>
Date: Tue, 26 May 2026 02:07:33 +0000
sdk: publish order decisions via relay
Diffstat:
3 files changed, 246 insertions(+), 0 deletions(-)
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -2309,6 +2309,16 @@ impl<'a> TradeClient<'a> {
}
#[cfg(feature = "serde_json")]
+ pub fn build_order_decision_draft(
+ &self,
+ root_event_id: &str,
+ prev_event_id: &str,
+ payload: &trade::RadrootsTradeOrderDecisionEvent,
+ ) -> Result<trade::RadrootsTradeOrderDecisionDraft, trade::EventEncodeError> {
+ trade::build_order_decision_draft(root_event_id, prev_event_id, payload)
+ }
+
+ #[cfg(feature = "serde_json")]
pub fn parse_order_request(
&self,
event: &RadrootsNostrEvent,
@@ -2319,6 +2329,17 @@ impl<'a> TradeClient<'a> {
trade::parse_order_request(event)
}
+ #[cfg(feature = "serde_json")]
+ pub fn parse_order_decision(
+ &self,
+ event: &RadrootsNostrEvent,
+ ) -> Result<
+ trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderDecisionEvent>,
+ trade::RadrootsActiveTradeEnvelopeParseError,
+ > {
+ trade::parse_order_decision(event)
+ }
+
#[cfg(all(
feature = "identity-models",
feature = "relay-client",
@@ -2346,6 +2367,29 @@ impl<'a> TradeClient<'a> {
feature = "relay-client",
feature = "signing"
))]
+ pub async fn publish_order_decision_with_identity(
+ &self,
+ identity: &RadrootsIdentity,
+ root_event_id: &str,
+ prev_event_id: &str,
+ payload: &trade::RadrootsTradeOrderDecisionEvent,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ let draft = trade::build_order_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_decision_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,
@@ -2360,6 +2404,25 @@ impl<'a> TradeClient<'a> {
.await
}
+ #[cfg(all(
+ feature = "identity-models",
+ feature = "relay-client",
+ feature = "signing"
+ ))]
+ pub async fn publish_order_decision_draft_with_identity(
+ &self,
+ identity: &RadrootsIdentity,
+ draft: trade::RadrootsTradeOrderDecisionDraft,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ self.client
+ .publish_parts_via_relay_with_identity(
+ identity,
+ draft.into_wire_parts(),
+ "trade.publish_order_decision_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
@@ -15,6 +15,11 @@ pub struct RadrootsTradeOrderRequestDraft {
parts: WireEventParts,
}
+#[derive(Debug, Clone)]
+pub struct RadrootsTradeOrderDecisionDraft {
+ parts: WireEventParts,
+}
+
impl RadrootsTradeOrderRequestDraft {
pub fn as_wire_parts(&self) -> &WireEventParts {
&self.parts
@@ -25,6 +30,16 @@ impl RadrootsTradeOrderRequestDraft {
}
}
+impl RadrootsTradeOrderDecisionDraft {
+ 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>,
@@ -62,6 +77,21 @@ pub fn build_order_request_draft(
}
#[cfg(feature = "serde_json")]
+pub fn build_order_decision_draft(
+ root_event_id: &str,
+ prev_event_id: &str,
+ payload: &RadrootsTradeOrderDecisionEvent,
+) -> Result<RadrootsTradeOrderDecisionDraft, EventEncodeError> {
+ Ok(RadrootsTradeOrderDecisionDraft {
+ parts: radroots_events_codec::trade::active_trade_order_decision_event_build(
+ root_event_id,
+ prev_event_id,
+ payload,
+ )?,
+ })
+}
+
+#[cfg(feature = "serde_json")]
pub fn parse_envelope(
event: &RadrootsNostrEvent,
) -> Result<SdkTradeEnvelope, RadrootsTradeEnvelopeParseError> {
@@ -79,6 +109,16 @@ pub fn parse_order_request(
}
#[cfg(feature = "serde_json")]
+pub fn parse_order_decision(
+ event: &RadrootsNostrEvent,
+) -> Result<
+ RadrootsActiveTradeEnvelope<RadrootsTradeOrderDecisionEvent>,
+ RadrootsActiveTradeEnvelopeParseError,
+> {
+ radroots_events_codec::trade::active_trade_order_decision_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,6 +19,7 @@ use radroots_sdk::listing::{
};
use radroots_sdk::profile::{RadrootsProfile, RadrootsProfileType};
use radroots_sdk::trade::{
+ RadrootsTradeInventoryCommitment, RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent,
RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem,
RadrootsTradeOrderRequested, RadrootsTradePricingBasis,
};
@@ -252,6 +253,24 @@ fn sample_order_request(
}
}
+fn sample_order_decision(
+ buyer_pubkey: String,
+ seller_pubkey: String,
+) -> RadrootsTradeOrderDecisionEvent {
+ RadrootsTradeOrderDecisionEvent {
+ order_id: "order-1".into(),
+ listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"),
+ buyer_pubkey,
+ seller_pubkey,
+ decision: RadrootsTradeOrderDecision::Accepted {
+ inventory_commitments: vec![RadrootsTradeInventoryCommitment {
+ bin_id: "bin-1".into(),
+ bin_count: 2,
+ }],
+ },
+ }
+}
+
#[tokio::test]
async fn relay_direct_farm_publish_accepts_sdk_built_draft() -> TestResult<()> {
let relay = AckRelay::spawn().await?;
@@ -390,6 +409,130 @@ async fn relay_direct_order_request_publish_accepts_sdk_built_draft() -> TestRes
}
#[tokio::test]
+async fn relay_direct_order_decision_publish_accepts_sdk_built_draft() -> TestResult<()> {
+ let relay = AckRelay::spawn().await?;
+ let buyer_identity = RadrootsIdentity::generate();
+ let seller_identity = RadrootsIdentity::generate();
+ let root_event_id = "order-request-event-1";
+ let payload = sample_order_decision(
+ buyer_identity.public_key_hex(),
+ seller_identity.public_key_hex(),
+ );
+ 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 draft =
+ client
+ .trade()
+ .build_order_decision_draft(root_event_id, root_event_id, &payload)?;
+ assert_eq!(draft.as_wire_parts().kind, 3423);
+
+ let receipt = client
+ .trade()
+ .publish_order_decision_draft_with_identity(&seller_identity, draft)
+ .await?;
+
+ assert_eq!(receipt.transport, SdkTransportMode::RelayDirect);
+ assert_eq!(receipt.event_kind, Some(3423));
+ assert!(receipt.event_id.is_some());
+ match receipt.transport_receipt {
+ SdkTransportReceipt::RelayDirect(relay_receipt) => {
+ assert_eq!(
+ receipt.event_id.as_deref(),
+ Some(relay_receipt.event_id.as_str())
+ );
+ assert_eq!(receipt.event_kind, Some(relay_receipt.event_kind));
+ assert_eq!(relay_receipt.event.kind, 3423);
+ assert_eq!(relay_receipt.event.author, seller_identity.public_key_hex());
+ assert!(
+ relay_receipt
+ .event
+ .tags
+ .contains(&vec!["p".to_owned(), buyer_identity.public_key_hex()])
+ );
+ assert!(
+ relay_receipt
+ .event
+ .tags
+ .contains(&vec!["a".to_owned(), payload.listing_addr.clone()])
+ );
+ assert!(
+ relay_receipt
+ .event
+ .tags
+ .contains(&vec!["d".to_owned(), payload.order_id.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(),])
+ );
+ assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]);
+ assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]);
+ assert_eq!(
+ relay_receipt.acknowledged_relays,
+ vec![relay.url().to_owned()]
+ );
+ assert!(relay_receipt.failed_relays.is_empty());
+ let envelope = client
+ .trade()
+ .parse_order_decision(&relay_receipt.event)
+ .expect("active order decision");
+ assert_eq!(envelope.order_id, payload.order_id);
+ assert_eq!(envelope.listing_addr, payload.listing_addr);
+ assert_eq!(envelope.payload.decision, payload.decision);
+ }
+ 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();
+ let seller_identity = RadrootsIdentity::generate();
+ let payload = sample_order_decision(
+ buyer_identity.public_key_hex(),
+ seller_identity.public_key_hex(),
+ );
+ 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 receipt = client
+ .trade()
+ .publish_order_decision_with_identity(
+ &seller_identity,
+ "order-request-event-1",
+ "order-request-event-1",
+ &payload,
+ )
+ .await?;
+
+ assert_eq!(receipt.transport, SdkTransportMode::RelayDirect);
+ assert_eq!(receipt.event_kind, Some(3423));
+
+ Ok(())
+}
+
+#[tokio::test]
async fn relay_direct_order_request_publish_builds_and_publishes_payload() -> TestResult<()> {
let relay = AckRelay::spawn().await?;
let buyer_identity = RadrootsIdentity::generate();