lib

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

commit c87b20273d86d95d533d2c80088539f9e636e4d4
parent 0887e5c8dc3f59ae401df410a25c9e94fdf627ac
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Apr 2026 08:35:12 +0000

sdk: add trade publish methods

Diffstat:
Mcrates/sdk/src/adapters/radrootsd.rs | 388++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/sdk/src/client.rs | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/sdk/src/lib.rs | 9+++++----
Mcrates/sdk/tests/radrootsd.rs | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
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, + &params, + 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"),