commit c87b20273d86d95d533d2c80088539f9e636e4d4
parent 0887e5c8dc3f59ae401df410a25c9e94fdf627ac
Author: triesap <tyson@radroots.org>
Date: Mon, 13 Apr 2026 08:35:12 +0000
sdk: add trade publish methods
Diffstat:
4 files changed, 840 insertions(+), 12 deletions(-)
diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs
@@ -1,10 +1,11 @@
use core::fmt;
use core::time::Duration;
-use crate::RadrootsNostrEvent;
use crate::config::RadrootsdAuth;
use crate::listing;
use crate::listing::RadrootsListing;
+use crate::trade;
+use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr};
use radroots_events::kinds::KIND_LISTING;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
@@ -134,6 +135,136 @@ impl fmt::Debug for SdkRadrootsdListingPublishRequest {
}
}
+#[derive(Clone, PartialEq, Eq, Serialize)]
+pub(crate) struct SdkRadrootsdOrderRequestPublishRequest {
+ pub order: trade::RadrootsTradeOrder,
+ pub signer_session_id: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub signer_authority: Option<SdkRadrootsdSignerAuthority>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub idempotency_key: Option<String>,
+}
+
+impl fmt::Debug for SdkRadrootsdOrderRequestPublishRequest {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut debug = f.debug_struct("SdkRadrootsdOrderRequestPublishRequest");
+ debug.field("order", &self.order);
+ debug.field("signer_session_id", &"<redacted>");
+ debug.field("signer_authority", &self.signer_authority);
+ debug.field("idempotency_key", &self.idempotency_key);
+ debug.finish()
+ }
+}
+
+#[derive(Clone, PartialEq, Eq, Serialize)]
+pub struct SdkRadrootsdPublicTradePublishRequest {
+ pub listing_addr: String,
+ pub order_id: String,
+ pub counterparty_pubkey: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub listing_event: Option<RadrootsNostrEventPtr>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub root_event_id: Option<String>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub prev_event_id: Option<String>,
+ pub payload: trade::RadrootsTradeMessagePayload,
+}
+
+impl SdkRadrootsdPublicTradePublishRequest {
+ pub fn new(
+ listing_addr: impl Into<String>,
+ order_id: impl Into<String>,
+ counterparty_pubkey: impl Into<String>,
+ payload: trade::RadrootsTradeMessagePayload,
+ ) -> Self {
+ Self {
+ listing_addr: listing_addr.into(),
+ order_id: order_id.into(),
+ counterparty_pubkey: counterparty_pubkey.into(),
+ listing_event: None,
+ root_event_id: None,
+ prev_event_id: None,
+ payload,
+ }
+ }
+
+ pub fn with_listing_event(mut self, listing_event: RadrootsNostrEventPtr) -> Self {
+ self.listing_event = Some(listing_event);
+ self
+ }
+
+ pub fn with_trade_chain(
+ mut self,
+ root_event_id: impl Into<String>,
+ prev_event_id: impl Into<String>,
+ ) -> Self {
+ self.root_event_id = Some(root_event_id.into());
+ self.prev_event_id = Some(prev_event_id.into());
+ self
+ }
+
+ pub fn message_type(&self) -> Option<trade::RadrootsTradeMessageType> {
+ match &self.payload {
+ trade::RadrootsTradeMessagePayload::ListingValidateRequest(_) => None,
+ trade::RadrootsTradeMessagePayload::ListingValidateResult(_) => None,
+ trade::RadrootsTradeMessagePayload::OrderRequest(_) => None,
+ trade::RadrootsTradeMessagePayload::OrderResponse(_) => {
+ Some(trade::RadrootsTradeMessageType::OrderResponse)
+ }
+ trade::RadrootsTradeMessagePayload::OrderRevision(_) => {
+ Some(trade::RadrootsTradeMessageType::OrderRevision)
+ }
+ trade::RadrootsTradeMessagePayload::OrderRevisionAccept(_) => {
+ Some(trade::RadrootsTradeMessageType::OrderRevisionAccept)
+ }
+ trade::RadrootsTradeMessagePayload::OrderRevisionDecline(_) => {
+ Some(trade::RadrootsTradeMessageType::OrderRevisionDecline)
+ }
+ trade::RadrootsTradeMessagePayload::Question(_) => {
+ Some(trade::RadrootsTradeMessageType::Question)
+ }
+ trade::RadrootsTradeMessagePayload::Answer(_) => {
+ Some(trade::RadrootsTradeMessageType::Answer)
+ }
+ trade::RadrootsTradeMessagePayload::DiscountRequest(_) => {
+ Some(trade::RadrootsTradeMessageType::DiscountRequest)
+ }
+ trade::RadrootsTradeMessagePayload::DiscountOffer(_) => {
+ Some(trade::RadrootsTradeMessageType::DiscountOffer)
+ }
+ trade::RadrootsTradeMessagePayload::DiscountAccept(_) => {
+ Some(trade::RadrootsTradeMessageType::DiscountAccept)
+ }
+ trade::RadrootsTradeMessagePayload::DiscountDecline(_) => {
+ Some(trade::RadrootsTradeMessageType::DiscountDecline)
+ }
+ trade::RadrootsTradeMessagePayload::Cancel(_) => {
+ Some(trade::RadrootsTradeMessageType::Cancel)
+ }
+ trade::RadrootsTradeMessagePayload::FulfillmentUpdate(_) => {
+ Some(trade::RadrootsTradeMessageType::FulfillmentUpdate)
+ }
+ trade::RadrootsTradeMessagePayload::Receipt(_) => {
+ Some(trade::RadrootsTradeMessageType::Receipt)
+ }
+ }
+ }
+}
+
+impl fmt::Debug for SdkRadrootsdPublicTradePublishRequest {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut debug = f.debug_struct("SdkRadrootsdPublicTradePublishRequest");
+ debug.field("listing_addr", &self.listing_addr);
+ debug.field("order_id", &self.order_id);
+ debug.field("counterparty_pubkey", &self.counterparty_pubkey);
+ debug.field("listing_event", &self.listing_event);
+ debug.field("root_event_id", &self.root_event_id);
+ debug.field("prev_event_id", &self.prev_event_id);
+ debug.field("payload", &self.payload);
+ debug.finish()
+ }
+}
+
impl SdkRadrootsdListingPublishRequest {
pub fn from_event(
event: &RadrootsNostrEvent,
@@ -430,6 +561,23 @@ struct SdkRadrootsdBridgeJobParams<'a> {
job_id: &'a str,
}
+#[derive(Clone, Serialize)]
+struct SdkRadrootsdPublicTradePublishParams<T> {
+ listing_addr: String,
+ order_id: String,
+ counterparty_pubkey: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ listing_event: Option<RadrootsNostrEventPtr>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ root_event_id: Option<String>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ prev_event_id: Option<String>,
+ payload: T,
+ signer_session_id: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ idempotency_key: Option<String>,
+}
+
pub async fn publish_listing(
endpoint: &str,
auth: &RadrootsdAuth,
@@ -447,6 +595,209 @@ pub async fn publish_listing(
.await
}
+pub(crate) async fn publish_order_request(
+ endpoint: &str,
+ auth: &RadrootsdAuth,
+ request: &SdkRadrootsdOrderRequestPublishRequest,
+ timeout: Duration,
+) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> {
+ jsonrpc_call(
+ endpoint,
+ auth,
+ "radroots-sdk-order-request-publish",
+ "bridge.order.request",
+ request,
+ timeout,
+ )
+ .await
+}
+
+pub(crate) async fn publish_public_trade(
+ endpoint: &str,
+ auth: &RadrootsdAuth,
+ request: &SdkRadrootsdPublicTradePublishRequest,
+ signer_session_id: &str,
+ idempotency_key: Option<&str>,
+ timeout: Duration,
+) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> {
+ match &request.payload {
+ trade::RadrootsTradeMessagePayload::OrderResponse(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.response",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::OrderRevision(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.revision",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::OrderRevisionAccept(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.revision.accept",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::OrderRevisionDecline(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.revision.decline",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::Question(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.question",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::Answer(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.answer",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::DiscountRequest(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.discount.request",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::DiscountOffer(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.discount.offer",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::DiscountAccept(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.discount.accept",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::DiscountDecline(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.discount.decline",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::Cancel(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.cancel",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::FulfillmentUpdate(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.fulfillment.update",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::Receipt(payload) => {
+ public_trade_call(
+ endpoint,
+ auth,
+ "bridge.order.receipt",
+ request,
+ payload,
+ signer_session_id,
+ idempotency_key,
+ timeout,
+ )
+ .await
+ }
+ trade::RadrootsTradeMessagePayload::ListingValidateRequest(_)
+ | trade::RadrootsTradeMessagePayload::ListingValidateResult(_)
+ | trade::RadrootsTradeMessagePayload::OrderRequest(_) => {
+ unreachable!("unsupported trade payload should be rejected by the curated client")
+ }
+ }
+}
+
pub(crate) async fn connect_signer_session(
endpoint: &str,
auth: &RadrootsdAuth,
@@ -624,6 +975,41 @@ pub fn bridge_listing_publish_request_json(
})
}
+async fn public_trade_call<T>(
+ endpoint: &str,
+ auth: &RadrootsdAuth,
+ method: &'static str,
+ request: &SdkRadrootsdPublicTradePublishRequest,
+ payload: &T,
+ signer_session_id: &str,
+ idempotency_key: Option<&str>,
+ timeout: Duration,
+) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError>
+where
+ T: Serialize + Clone,
+{
+ let params = SdkRadrootsdPublicTradePublishParams {
+ listing_addr: request.listing_addr.clone(),
+ order_id: request.order_id.clone(),
+ counterparty_pubkey: request.counterparty_pubkey.clone(),
+ listing_event: request.listing_event.clone(),
+ root_event_id: request.root_event_id.clone(),
+ prev_event_id: request.prev_event_id.clone(),
+ payload: payload.clone(),
+ signer_session_id: signer_session_id.to_owned(),
+ idempotency_key: idempotency_key.map(str::to_owned),
+ };
+ jsonrpc_call(
+ endpoint,
+ auth,
+ "radroots-sdk-public-trade-publish",
+ method,
+ ¶ms,
+ timeout,
+ )
+ .await
+}
+
async fn jsonrpc_call<P, R>(
endpoint: &str,
auth: &RadrootsdAuth,
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -675,6 +675,116 @@ impl fmt::Debug for SdkRadrootsdListingPublishOptions {
}
}
+#[cfg(feature = "radrootsd-client")]
+#[derive(Clone, PartialEq, Eq)]
+pub struct SdkRadrootsdOrderRequestPublishOptions {
+ session: SdkRadrootsdSignerSessionRef,
+ idempotency_key: Option<String>,
+ signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>,
+}
+
+#[cfg(feature = "radrootsd-client")]
+impl SdkRadrootsdOrderRequestPublishOptions {
+ pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self {
+ Self {
+ session: session.session().clone(),
+ idempotency_key: None,
+ signer_authority: None,
+ }
+ }
+
+ pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self {
+ Self {
+ session: session.clone(),
+ idempotency_key: None,
+ signer_authority: None,
+ }
+ }
+
+ pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
+ self.idempotency_key = Some(idempotency_key.into());
+ self
+ }
+
+ pub fn with_signer_authority(
+ mut self,
+ signer_authority: radrootsd::SdkRadrootsdSignerAuthority,
+ ) -> Self {
+ self.signer_authority = Some(signer_authority);
+ self
+ }
+
+ pub fn session(&self) -> &SdkRadrootsdSignerSessionRef {
+ &self.session
+ }
+
+ pub fn idempotency_key(&self) -> Option<&str> {
+ self.idempotency_key.as_deref()
+ }
+
+ pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> {
+ self.signer_authority.as_ref()
+ }
+}
+
+#[cfg(feature = "radrootsd-client")]
+impl fmt::Debug for SdkRadrootsdOrderRequestPublishOptions {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut debug = f.debug_struct("SdkRadrootsdOrderRequestPublishOptions");
+ debug.field("session", &self.session);
+ debug.field("idempotency_key", &self.idempotency_key);
+ debug.field("signer_authority", &self.signer_authority);
+ debug.finish()
+ }
+}
+
+#[cfg(feature = "radrootsd-client")]
+#[derive(Clone, PartialEq, Eq)]
+pub struct SdkRadrootsdPublicTradePublishOptions {
+ session: SdkRadrootsdSignerSessionRef,
+ idempotency_key: Option<String>,
+}
+
+#[cfg(feature = "radrootsd-client")]
+impl SdkRadrootsdPublicTradePublishOptions {
+ pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self {
+ Self {
+ session: session.session().clone(),
+ idempotency_key: None,
+ }
+ }
+
+ pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self {
+ Self {
+ session: session.clone(),
+ idempotency_key: None,
+ }
+ }
+
+ pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
+ self.idempotency_key = Some(idempotency_key.into());
+ self
+ }
+
+ pub fn session(&self) -> &SdkRadrootsdSignerSessionRef {
+ &self.session
+ }
+
+ pub fn idempotency_key(&self) -> Option<&str> {
+ self.idempotency_key.as_deref()
+ }
+}
+
+#[cfg(feature = "radrootsd-client")]
+impl fmt::Debug for SdkRadrootsdPublicTradePublishOptions {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut debug = f.debug_struct("SdkRadrootsdPublicTradePublishOptions");
+ debug.field("session", &self.session);
+ debug.field("idempotency_key", &self.idempotency_key);
+ debug.finish()
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsSdkClient {
config: RadrootsSdkConfig,
@@ -837,9 +947,83 @@ impl RadrootsSdkClient {
)
.await
.map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?;
- Ok(sdk_publish_receipt_from_radrootsd_listing_response(
- response,
- ))
+ Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response))
+ }
+
+ #[cfg(feature = "radrootsd-client")]
+ async fn publish_order_request_via_radrootsd(
+ &self,
+ request: &radrootsd::SdkRadrootsdOrderRequestPublishRequest,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ if self.transport() != SdkTransportMode::Radrootsd {
+ return Err(SdkPublishError::UnsupportedTransport {
+ transport: self.transport(),
+ operation: "trade.publish_order_request_via_radrootsd",
+ });
+ }
+ self.require_signer_mode(
+ SignerConfig::Nip46,
+ "trade.publish_order_request_via_radrootsd",
+ )?;
+
+ let endpoint = match &self.resolved_transport_target {
+ SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(),
+ SdkResolvedTransportTarget::RelayDirect { .. } => {
+ return Err(SdkPublishError::UnsupportedTransport {
+ transport: self.transport(),
+ operation: "trade.publish_order_request_via_radrootsd",
+ });
+ }
+ };
+ let response = radrootsd::publish_order_request(
+ endpoint,
+ &self.config.radrootsd.auth,
+ request,
+ Duration::from_millis(self.config.network.timeout_ms),
+ )
+ .await
+ .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?;
+ Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response))
+ }
+
+ #[cfg(feature = "radrootsd-client")]
+ async fn publish_public_trade_via_radrootsd(
+ &self,
+ request: &radrootsd::SdkRadrootsdPublicTradePublishRequest,
+ signer_session: &SdkRadrootsdSignerSessionRef,
+ idempotency_key: Option<&str>,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ if self.transport() != SdkTransportMode::Radrootsd {
+ return Err(SdkPublishError::UnsupportedTransport {
+ transport: self.transport(),
+ operation: "trade.publish_public_message_via_radrootsd",
+ });
+ }
+ self.require_signer_mode(
+ SignerConfig::Nip46,
+ "trade.publish_public_message_via_radrootsd",
+ )?;
+
+ let endpoint = match &self.resolved_transport_target {
+ SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(),
+ SdkResolvedTransportTarget::RelayDirect { .. } => {
+ return Err(SdkPublishError::UnsupportedTransport {
+ transport: self.transport(),
+ operation: "trade.publish_public_message_via_radrootsd",
+ });
+ }
+ };
+ let response = radrootsd::publish_public_trade(
+ endpoint,
+ &self.config.radrootsd.auth,
+ request,
+ signer_session.session_id(),
+ idempotency_key,
+ Duration::from_millis(self.config.network.timeout_ms),
+ )
+ .await
+ .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?;
+ Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response))
}
#[cfg(feature = "radrootsd-client")]
@@ -1503,6 +1687,71 @@ impl<'a> TradeClient<'a> {
) -> Result<TradeListingValidateResult, trade::RadrootsTradeListingValidationError> {
trade::validate_listing_event(event)
}
+
+ #[cfg(feature = "radrootsd-client")]
+ pub async fn publish_order_request_via_radrootsd(
+ &self,
+ order: &trade::RadrootsTradeOrder,
+ session: &SdkRadrootsdSignerSessionHandle,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ self.publish_order_request_via_radrootsd_with_options(
+ order,
+ &SdkRadrootsdOrderRequestPublishOptions::from_signer_session(session),
+ )
+ .await
+ }
+
+ #[cfg(feature = "radrootsd-client")]
+ pub async fn publish_order_request_via_radrootsd_with_options(
+ &self,
+ order: &trade::RadrootsTradeOrder,
+ options: &SdkRadrootsdOrderRequestPublishOptions,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ let request = radrootsd::SdkRadrootsdOrderRequestPublishRequest {
+ order: order.clone(),
+ signer_session_id: options.session().session_id().to_owned(),
+ signer_authority: options.signer_authority().cloned(),
+ idempotency_key: options.idempotency_key().map(str::to_owned),
+ };
+ self.client
+ .publish_order_request_via_radrootsd(&request)
+ .await
+ }
+
+ #[cfg(feature = "radrootsd-client")]
+ pub async fn publish_public_message_via_radrootsd(
+ &self,
+ request: &radrootsd::SdkRadrootsdPublicTradePublishRequest,
+ session: &SdkRadrootsdSignerSessionHandle,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ self.publish_public_message_via_radrootsd_with_options(
+ request,
+ &SdkRadrootsdPublicTradePublishOptions::from_signer_session(session),
+ )
+ .await
+ }
+
+ #[cfg(feature = "radrootsd-client")]
+ pub async fn publish_public_message_via_radrootsd_with_options(
+ &self,
+ request: &radrootsd::SdkRadrootsdPublicTradePublishRequest,
+ options: &SdkRadrootsdPublicTradePublishOptions,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ match request.message_type() {
+ Some(_) => {
+ self.client
+ .publish_public_trade_via_radrootsd(
+ request,
+ options.session(),
+ options.idempotency_key(),
+ )
+ .await
+ }
+ None => Err(SdkPublishError::Encode(
+ "trade.publish_public_message_via_radrootsd requires a bridge.order.* public trade payload; use trade.publish_order_request_via_radrootsd for order requests".to_owned(),
+ )),
+ }
+ }
}
#[cfg(all(
@@ -1550,7 +1799,7 @@ fn sdk_publish_receipt_from_relay_output(
}
#[cfg(feature = "radrootsd-client")]
-fn sdk_publish_receipt_from_radrootsd_listing_response(
+fn sdk_publish_receipt_from_radrootsd_bridge_response(
response: radrootsd::SdkRadrootsdBridgePublishResponse,
) -> SdkPublishReceipt {
let job = response.job;
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -28,9 +28,9 @@ pub mod trade;
#[cfg(feature = "radrootsd-client")]
pub use crate::adapters::radrootsd::{
SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeJobStatus,
- SdkRadrootsdBridgeRelayPublishResult, SdkRadrootsdSignerAuthority,
- SdkRadrootsdSignerSessionConnectRequest, SdkRadrootsdSignerSessionMode,
- SdkRadrootsdSignerSessionRole,
+ SdkRadrootsdBridgeRelayPublishResult, SdkRadrootsdPublicTradePublishRequest,
+ SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest,
+ SdkRadrootsdSignerSessionMode, SdkRadrootsdSignerSessionRole,
};
pub use crate::client::{
FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError,
@@ -41,7 +41,8 @@ pub use crate::client::{
pub use crate::client::{
RadrootsdBridgeClient, RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdBridgeError,
SdkRadrootsdBridgeJobRef, SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeStatus,
- SdkRadrootsdListingPublishOptions, SdkRadrootsdSessionError,
+ SdkRadrootsdListingPublishOptions, SdkRadrootsdOrderRequestPublishOptions,
+ SdkRadrootsdPublicTradePublishOptions, SdkRadrootsdSessionError,
SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSignerSessionCloseResult,
SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionRef,
SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSignerSessionView,
diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs
@@ -4,21 +4,26 @@ use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
-use radroots_events::kinds::KIND_LISTING_DRAFT;
+use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT};
use radroots_sdk::adapters::radrootsd::{
SdkRadrootsdBridgeJob, SdkRadrootsdBridgePublishResponse, SdkRadrootsdListingPublishRequest,
- SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest,
- SdkRadrootsdSignerSessionMode,
+ SdkRadrootsdPublicTradePublishRequest, SdkRadrootsdSignerAuthority,
+ SdkRadrootsdSignerSessionConnectRequest, SdkRadrootsdSignerSessionMode,
};
use radroots_sdk::listing::{
RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation,
RadrootsListingProduct, RadrootsListingStatus, RadrootsTradeListingParseError,
};
+use radroots_sdk::trade::{
+ RadrootsTradeMessagePayload, RadrootsTradeMessageType, RadrootsTradeOrder,
+ RadrootsTradeOrderItem, RadrootsTradeOrderResponse,
+};
use radroots_sdk::{
RadrootsNostrEvent, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig,
SdkConfigError, SdkEnvironment, SdkPublishError, SdkRadrootsdBridgeDeliveryPolicy,
SdkRadrootsdBridgeError, SdkRadrootsdBridgeJobStatus, SdkRadrootsdListingPublishOptions,
+ SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdPublicTradePublishOptions,
SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle,
SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView, SdkTransportMode,
SdkTransportReceipt, SignerConfig,
@@ -247,6 +252,33 @@ fn sample_listing() -> RadrootsListing {
}
}
+fn sample_trade_order() -> RadrootsTradeOrder {
+ RadrootsTradeOrder {
+ order_id: "order-1".to_owned(),
+ listing_addr: format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"),
+ buyer_pubkey: "buyer".to_owned(),
+ seller_pubkey: "seller".to_owned(),
+ items: vec![RadrootsTradeOrderItem {
+ bin_id: "bin-1".to_owned(),
+ bin_count: 2,
+ }],
+ discounts: Some(Vec::new()),
+ }
+}
+
+fn sample_public_trade_request() -> SdkRadrootsdPublicTradePublishRequest {
+ SdkRadrootsdPublicTradePublishRequest::new(
+ format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"),
+ "order-1",
+ "buyer",
+ RadrootsTradeMessagePayload::OrderResponse(RadrootsTradeOrderResponse {
+ accepted: true,
+ reason: None,
+ }),
+ )
+ .with_trade_chain("root-event-1", "prev-event-1")
+}
+
fn sdk_event(
author: &str,
created_at: u32,
@@ -1155,6 +1187,166 @@ async fn radrootsd_listing_publish_rejects_relay_transport_mode() -> TestResult<
}
#[tokio::test]
+async fn radrootsd_trade_order_request_publish_accepts_session_handle() -> TestResult<()> {
+ let (server, request_rx) = JsonRpcServer::spawn(
+ Some("Bearer sdk-secret"),
+ json!({
+ "jsonrpc": "2.0",
+ "id": "radroots-sdk-order-request-publish",
+ "result": {
+ "deduplicated": false,
+ "job": {
+ "job_id": "job-order-1",
+ "command": "bridge.order.request",
+ "status": "published",
+ "terminal": true,
+ "recovered_after_restart": false,
+ "signer_mode": "nip46_session:session-order-1",
+ "signer_session_id": "session-order-1",
+ "event_kind": RadrootsTradeMessageType::OrderRequest.kind(),
+ "event_id": "event-order-1",
+ "event_addr": format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"),
+ "relay_count": 1,
+ "acknowledged_relay_count": 1
+ }
+ }
+ }),
+ )
+ .await?;
+
+ let handle = connected_bunker_session_handle("session-order-1").await?;
+ let client = radrootsd_test_client(server.endpoint())?;
+ let options = SdkRadrootsdOrderRequestPublishOptions::from_signer_session(&handle)
+ .with_idempotency_key("idem-order-1")
+ .with_signer_authority(SdkRadrootsdSignerAuthority {
+ provider_runtime_id: "runtime-1".to_owned(),
+ account_identity_id: "identity-1".to_owned(),
+ provider_signer_session_id: Some("provider-session-order-1".to_owned()),
+ });
+
+ let receipt = client
+ .trade()
+ .publish_order_request_via_radrootsd_with_options(&sample_trade_order(), &options)
+ .await?;
+ let request_json = request_rx.await?;
+
+ assert_eq!(request_json["method"], "bridge.order.request");
+ assert_eq!(
+ request_json["params"]["signer_session_id"],
+ "session-order-1"
+ );
+ assert_eq!(request_json["params"]["idempotency_key"], "idem-order-1");
+ assert_eq!(request_json["params"]["order"]["order_id"], "order-1");
+ assert_eq!(
+ request_json["params"]["signer_authority"]["provider_runtime_id"],
+ "runtime-1"
+ );
+ assert_eq!(
+ request_json["params"]["signer_authority"]["provider_signer_session_id"],
+ "provider-session-order-1"
+ );
+ assert_eq!(
+ receipt.event_kind,
+ Some(RadrootsTradeMessageType::OrderRequest.kind())
+ );
+ assert_eq!(receipt.event_id, Some("event-order-1".to_owned()));
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn radrootsd_trade_public_message_publish_accepts_typed_request() -> TestResult<()> {
+ let (server, request_rx) = JsonRpcServer::spawn(
+ Some("Bearer sdk-secret"),
+ json!({
+ "jsonrpc": "2.0",
+ "id": "radroots-sdk-public-trade-publish",
+ "result": {
+ "deduplicated": false,
+ "job": {
+ "job_id": "job-response-1",
+ "command": "bridge.order.response",
+ "status": "published",
+ "terminal": true,
+ "recovered_after_restart": false,
+ "signer_mode": "nip46_session:session-response-1",
+ "signer_session_id": "session-response-1",
+ "event_kind": RadrootsTradeMessageType::OrderResponse.kind(),
+ "event_id": "event-response-1",
+ "event_addr": format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"),
+ "relay_count": 1,
+ "acknowledged_relay_count": 1
+ }
+ }
+ }),
+ )
+ .await?;
+
+ let handle = connected_bunker_session_handle("session-response-1").await?;
+ let client = radrootsd_test_client(server.endpoint())?;
+ let request = sample_public_trade_request();
+ let options = SdkRadrootsdPublicTradePublishOptions::from_signer_session(&handle)
+ .with_idempotency_key("idem-response-1");
+
+ let receipt = client
+ .trade()
+ .publish_public_message_via_radrootsd_with_options(&request, &options)
+ .await?;
+ let request_json = request_rx.await?;
+
+ assert_eq!(request_json["method"], "bridge.order.response");
+ assert_eq!(
+ request_json["params"]["signer_session_id"],
+ "session-response-1"
+ );
+ assert_eq!(request_json["params"]["idempotency_key"], "idem-response-1");
+ assert_eq!(
+ request_json["params"]["listing_addr"],
+ format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg")
+ );
+ assert_eq!(request_json["params"]["order_id"], "order-1");
+ assert_eq!(request_json["params"]["counterparty_pubkey"], "buyer");
+ assert_eq!(request_json["params"]["root_event_id"], "root-event-1");
+ assert_eq!(request_json["params"]["prev_event_id"], "prev-event-1");
+ assert_eq!(request_json["params"]["payload"]["accepted"], true);
+ assert_eq!(
+ receipt.event_kind,
+ Some(RadrootsTradeMessageType::OrderResponse.kind())
+ );
+ assert_eq!(receipt.event_id, Some("event-response-1".to_owned()));
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn radrootsd_trade_public_message_publish_rejects_order_request_payload() -> TestResult<()> {
+ let client = radrootsd_test_client("https://rpc.radroots.org/jsonrpc")?;
+ let handle = connected_bunker_session_handle("session-order-request").await?;
+ let request = SdkRadrootsdPublicTradePublishRequest::new(
+ sample_trade_order().listing_addr.clone(),
+ "order-1",
+ "buyer",
+ RadrootsTradeMessagePayload::OrderRequest(sample_trade_order()),
+ );
+
+ let error = client
+ .trade()
+ .publish_public_message_via_radrootsd(&request, &handle)
+ .await
+ .expect_err("order request payload should use the dedicated trade order request path");
+
+ assert!(matches!(error, SdkPublishError::Encode(_)));
+ assert!(
+ error
+ .to_string()
+ .contains("trade.publish_order_request_via_radrootsd"),
+ "unexpected error: {error}"
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
async fn radrootsd_bridge_status_returns_typed_status() -> TestResult<()> {
let (server, request_rx) = JsonRpcServer::spawn(
Some("Bearer sdk-secret"),