lib

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

commit 8bbfbb46cad58b92ffff4b71fb367d4175fd5b01
parent 33c02a4ad3b1e9400f5614c2ba05c9884e308683
Author: triesap <tyson@radroots.org>
Date:   Thu, 16 Apr 2026 01:31:14 +0000

sdk: add farm bridge publish wrappers

Diffstat:
Mcrates/sdk/src/adapters/radrootsd.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/client.rs | 255++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/sdk/src/lib.rs | 3++-
Mcrates/sdk/tests/radrootsd.rs | 177++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 511 insertions(+), 8 deletions(-)

diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -2,8 +2,10 @@ use core::fmt; use core::time::Duration; use crate::config::RadrootsdAuth; +use crate::farm::RadrootsFarm; use crate::listing; use crate::listing::RadrootsListing; +use crate::profile::{RadrootsProfile, RadrootsProfileType}; use crate::trade; use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr}; use radroots_events::kinds::KIND_LISTING; @@ -112,6 +114,54 @@ impl fmt::Debug for SdkRadrootsdSignerSessionConnectRequest { } #[derive(Clone, Serialize)] +pub struct SdkRadrootsdProfilePublishRequest { + pub profile: RadrootsProfile, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub profile_type: Option<RadrootsProfileType>, + 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 SdkRadrootsdProfilePublishRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdProfilePublishRequest"); + debug.field("profile", &self.profile); + debug.field("profile_type", &self.profile_type); + debug.field("signer_session_id", &"<redacted>"); + debug.field("signer_authority", &self.signer_authority); + debug.field("idempotency_key", &self.idempotency_key); + debug.finish() + } +} + +#[derive(Clone, Serialize)] +pub struct SdkRadrootsdFarmPublishRequest { + pub farm: RadrootsFarm, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub kind: Option<u32>, + 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 SdkRadrootsdFarmPublishRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdFarmPublishRequest"); + debug.field("farm", &self.farm); + debug.field("kind", &self.kind); + debug.field("signer_session_id", &"<redacted>"); + debug.field("signer_authority", &self.signer_authority); + debug.field("idempotency_key", &self.idempotency_key); + debug.finish() + } +} + +#[derive(Clone, Serialize)] pub struct SdkRadrootsdListingPublishRequest { pub listing: RadrootsListing, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1007,6 +1057,40 @@ pub async fn publish_listing( .await } +pub(crate) async fn publish_profile( + endpoint: &str, + auth: &RadrootsdAuth, + request: &SdkRadrootsdProfilePublishRequest, + timeout: Duration, +) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-profile-publish", + "bridge.profile.publish", + request, + timeout, + ) + .await +} + +pub(crate) async fn publish_farm( + endpoint: &str, + auth: &RadrootsdAuth, + request: &SdkRadrootsdFarmPublishRequest, + timeout: Duration, +) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-farm-publish", + "bridge.farm.publish", + request, + timeout, + ) + .await +} + pub(crate) async fn publish_order_request( endpoint: &str, auth: &RadrootsdAuth, diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -35,7 +35,7 @@ use crate::{ ))] use core::time::Duration; #[cfg(feature = "radrootsd-client")] -use radroots_events::kinds::KIND_LISTING; +use radroots_events::kinds::{KIND_FARM, KIND_LISTING}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SdkPublishReceipt { @@ -620,6 +620,132 @@ impl From<radrootsd::SdkRadrootsdSignerSessionConnectResponse> for SdkRadrootsdS #[cfg(feature = "radrootsd-client")] #[derive(Clone, PartialEq, Eq)] +pub struct SdkRadrootsdProfilePublishOptions { + session: SdkRadrootsdSignerSessionRef, + idempotency_key: Option<String>, + signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, +} + +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdProfilePublishOptions { + 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 SdkRadrootsdProfilePublishOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdProfilePublishOptions"); + 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 SdkRadrootsdFarmPublishOptions { + session: SdkRadrootsdSignerSessionRef, + idempotency_key: Option<String>, + signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, +} + +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdFarmPublishOptions { + 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 SdkRadrootsdFarmPublishOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdFarmPublishOptions"); + 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 SdkRadrootsdListingPublishOptions { session: SdkRadrootsdSignerSessionRef, idempotency_key: Option<String>, @@ -957,6 +1083,72 @@ impl RadrootsSdkClient { } #[cfg(feature = "radrootsd-client")] + async fn publish_profile_via_radrootsd( + &self, + request: &radrootsd::SdkRadrootsdProfilePublishRequest, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkPublishError::UnsupportedTransport { + transport: self.transport(), + operation: "profile.publish_via_radrootsd", + }); + } + self.require_signer_mode(SignerConfig::Nip46, "profile.publish_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: "profile.publish_via_radrootsd", + }); + } + }; + let response = radrootsd::publish_profile( + 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_farm_via_radrootsd( + &self, + request: &radrootsd::SdkRadrootsdFarmPublishRequest, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkPublishError::UnsupportedTransport { + transport: self.transport(), + operation: "farm.publish_via_radrootsd", + }); + } + self.require_signer_mode(SignerConfig::Nip46, "farm.publish_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: "farm.publish_via_radrootsd", + }); + } + }; + let response = radrootsd::publish_farm( + 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_order_request_via_radrootsd( &self, request: &radrootsd::SdkRadrootsdOrderRequestPublishRequest, @@ -1641,6 +1833,38 @@ impl<'a> ProfileClient<'a> { ) -> Result<WireEventParts, profile::ProfileEncodeError> { profile::build_draft(profile_value, profile_type) } + + #[cfg(feature = "radrootsd-client")] + pub async fn publish_profile_via_radrootsd( + &self, + profile_value: &RadrootsProfile, + profile_type: Option<RadrootsProfileType>, + session: &SdkRadrootsdSignerSessionHandle, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + self.publish_profile_via_radrootsd_with_options( + profile_value, + profile_type, + &SdkRadrootsdProfilePublishOptions::from_signer_session(session), + ) + .await + } + + #[cfg(feature = "radrootsd-client")] + pub async fn publish_profile_via_radrootsd_with_options( + &self, + profile_value: &RadrootsProfile, + profile_type: Option<RadrootsProfileType>, + options: &SdkRadrootsdProfilePublishOptions, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + let request = radrootsd::SdkRadrootsdProfilePublishRequest { + profile: profile_value.clone(), + profile_type, + 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_profile_via_radrootsd(&request).await + } } #[derive(Debug, Clone, Copy)] @@ -1668,6 +1892,35 @@ impl<'a> FarmClient<'a> { ) -> Result<WireEventParts, farm::EventEncodeError> { farm::build_draft(farm_value) } + + #[cfg(feature = "radrootsd-client")] + pub async fn publish_farm_via_radrootsd( + &self, + farm_value: &farm::RadrootsFarm, + session: &SdkRadrootsdSignerSessionHandle, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + self.publish_farm_via_radrootsd_with_options( + farm_value, + &SdkRadrootsdFarmPublishOptions::from_signer_session(session), + ) + .await + } + + #[cfg(feature = "radrootsd-client")] + pub async fn publish_farm_via_radrootsd_with_options( + &self, + farm_value: &farm::RadrootsFarm, + options: &SdkRadrootsdFarmPublishOptions, + ) -> Result<SdkPublishReceipt, SdkPublishError> { + let request = radrootsd::SdkRadrootsdFarmPublishRequest { + farm: farm_value.clone(), + kind: Some(KIND_FARM), + 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_farm_via_radrootsd(&request).await + } } #[derive(Debug, Clone, Copy)] diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -42,7 +42,8 @@ pub use crate::client::{ pub use crate::client::{ RadrootsdBridgeClient, RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdBridgeError, SdkRadrootsdBridgeJobRef, SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeStatus, - SdkRadrootsdListingPublishOptions, SdkRadrootsdOrderRequestPublishOptions, + SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, + SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdProfilePublishOptions, SdkRadrootsdPublicTradeMessage, SdkRadrootsdPublicTradePublishOptions, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSignerSessionHandle, diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -4,8 +4,8 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; -use radroots_events::farm::RadrootsFarmRef; -use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT}; +use radroots_events::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; +use radroots_events::kinds::{KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_PROFILE}; use radroots_sdk::adapters::radrootsd::{ SdkRadrootsdBridgeJob, SdkRadrootsdBridgePublishResponse, SdkRadrootsdListingPublishRequest, SdkRadrootsdPublicTradePublishRequest, SdkRadrootsdSignerAuthority, @@ -22,10 +22,11 @@ use radroots_sdk::trade::{ RadrootsTradeOrderRevision, RadrootsTradeOrderRevisionResponse, }; use radroots_sdk::{ - RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, - RadrootsdConfig, SdkConfigError, SdkEnvironment, SdkPublishError, - SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeError, SdkRadrootsdBridgeJobStatus, - SdkRadrootsdListingPublishOptions, SdkRadrootsdOrderRequestPublishOptions, + RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsProfile, RadrootsProfileType, + RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, SdkConfigError, + SdkEnvironment, SdkPublishError, SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeError, + SdkRadrootsdBridgeJobStatus, SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, + SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdProfilePublishOptions, SdkRadrootsdPublicTradeMessage, SdkRadrootsdPublicTradePublishOptions, SdkRadrootsdPublicTradePublishValidationError, SdkRadrootsdPublicTradeRoute, SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle, @@ -379,6 +380,40 @@ fn sample_listing() -> RadrootsListing { } } +fn sample_profile() -> RadrootsProfile { + RadrootsProfile { + name: "North Farm".into(), + display_name: Some("North Farm".into()), + nip05: None, + about: Some("Coffee farm".into()), + website: Some("https://example.invalid/north-farm".into()), + picture: None, + banner: None, + lud06: None, + lud16: None, + bot: None, + } +} + +fn sample_farm() -> RadrootsFarm { + RadrootsFarm { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), + name: "North Farm".into(), + about: Some("Coffee farm".into()), + website: Some("https://example.invalid/north-farm".into()), + picture: None, + banner: None, + location: Some(RadrootsFarmLocation { + primary: Some("North Farm".into()), + city: Some("San Francisco".into()), + region: Some("CA".into()), + country: Some("US".into()), + gcs: None, + }), + tags: Some(vec!["coffee".into()]), + } +} + fn sample_trade_order() -> RadrootsTradeOrder { RadrootsTradeOrder { order_id: "order-1".to_owned(), @@ -1197,6 +1232,136 @@ async fn radrootsd_listing_publish_accepts_typed_listing_value() -> TestResult<( } #[tokio::test] +async fn radrootsd_profile_publish_accepts_typed_profile_value() -> TestResult<()> { + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-profile-publish", + "result": { + "deduplicated": false, + "job": { + "job_id": "job-profile-1", + "command": "bridge.profile.publish", + "status": "published", + "terminal": true, + "recovered_after_restart": false, + "signer_mode": "nip46_session:session-profile-1", + "signer_session_id": "session-profile-1", + "event_kind": 0, + "event_id": "event-profile-1", + "relay_count": 1, + "acknowledged_relay_count": 1 + } + } + }), + ) + .await?; + + let handle = connected_bunker_session_handle("session-profile-1").await?; + let client = radrootsd_test_client(server.endpoint())?; + let options = SdkRadrootsdProfilePublishOptions::from_signer_session(&handle) + .with_idempotency_key("profile-idem-1") + .with_signer_authority(SdkRadrootsdSignerAuthority { + provider_runtime_id: "runtime-profile".to_owned(), + account_identity_id: "identity-profile".to_owned(), + provider_signer_session_id: Some("provider-session-profile".to_owned()), + }); + + let receipt = client + .profile() + .publish_profile_via_radrootsd_with_options( + &sample_profile(), + Some(RadrootsProfileType::Farm), + &options, + ) + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "bridge.profile.publish"); + assert_eq!( + request_json["params"]["signer_session_id"], + "session-profile-1" + ); + assert_eq!(request_json["params"]["profile_type"], "farm"); + assert_eq!(request_json["params"]["profile"]["name"], "North Farm"); + assert_eq!(request_json["params"]["idempotency_key"], "profile-idem-1"); + assert_eq!( + request_json["params"]["signer_authority"]["provider_runtime_id"], + "runtime-profile" + ); + assert_eq!(receipt.event_kind, Some(KIND_PROFILE)); + assert_eq!(receipt.event_id, Some("event-profile-1".to_owned())); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_farm_publish_accepts_typed_farm_value() -> TestResult<()> { + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-farm-publish", + "result": { + "deduplicated": false, + "job": { + "job_id": "job-farm-1", + "command": "bridge.farm.publish", + "status": "published", + "terminal": true, + "recovered_after_restart": false, + "signer_mode": "nip46_session:session-farm-1", + "signer_session_id": "session-farm-1", + "event_kind": 30340, + "event_id": "event-farm-1", + "event_addr": "30340:seller:AAAAAAAAAAAAAAAAAAAAAA", + "relay_count": 1, + "acknowledged_relay_count": 1 + } + } + }), + ) + .await?; + + let handle = connected_bunker_session_handle("session-farm-1").await?; + let client = radrootsd_test_client(server.endpoint())?; + let options = SdkRadrootsdFarmPublishOptions::from_signer_session(&handle) + .with_idempotency_key("farm-idem-1"); + + let receipt = client + .farm() + .publish_farm_via_radrootsd_with_options(&sample_farm(), &options) + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "bridge.farm.publish"); + assert_eq!( + request_json["params"]["signer_session_id"], + "session-farm-1" + ); + assert_eq!(request_json["params"]["kind"], KIND_FARM); + assert_eq!( + request_json["params"]["farm"]["d_tag"], + "AAAAAAAAAAAAAAAAAAAAAA" + ); + assert_eq!(request_json["params"]["idempotency_key"], "farm-idem-1"); + assert_eq!(receipt.event_kind, Some(KIND_FARM)); + assert_eq!(receipt.event_id, Some("event-farm-1".to_owned())); + match receipt.transport_receipt { + SdkTransportReceipt::Radrootsd(receipt) => { + assert_eq!( + receipt.event_addr, + Some("30340:seller:AAAAAAAAAAAAAAAAAAAAAA".to_owned()) + ); + } + SdkTransportReceipt::RelayDirect(_) => panic!("unexpected relay receipt"), + } + + Ok(()) +} + +#[tokio::test] async fn radrootsd_listing_publish_with_options_forwards_typed_continuity_metadata() -> TestResult<()> { let (server, request_rx) = JsonRpcServer::spawn(