lib

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

commit 0887e5c8dc3f59ae401df410a25c9e94fdf627ac
parent 0d6a53f85eca65d7ff9984602b8a9588de12745b
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Apr 2026 08:22:51 +0000

sdk: add bridge workflow inspection

Diffstat:
Mcrates/sdk/src/adapters/radrootsd.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/client.rs | 308+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/lib.rs | 17++++++++++-------
Mcrates/sdk/tests/radrootsd.rs | 220++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 710 insertions(+), 8 deletions(-)

diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -49,6 +49,22 @@ pub enum SdkRadrootsdSignerSessionRole { OutboundRemoteSigner, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SdkRadrootsdBridgeDeliveryPolicy { + Any, + Quorum, + All, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SdkRadrootsdBridgeJobStatus { + Accepted, + Published, + Failed, +} + #[derive(Clone, PartialEq, Eq, Serialize)] pub struct SdkRadrootsdSignerSessionConnectRequest { pub url: String, @@ -219,6 +235,40 @@ pub struct SdkRadrootsdBridgePublishResponse { pub job: SdkRadrootsdBridgeJob, } +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub struct SdkRadrootsdBridgeStatusResponse { + pub enabled: bool, + pub ready: bool, + pub auth_mode: String, + pub signer_mode: String, + pub default_signer_mode: String, + pub supported_signer_modes: Vec<String>, + pub available_nip46_signer_sessions: usize, + pub relay_count: usize, + pub delivery_policy: SdkRadrootsdBridgeDeliveryPolicy, + #[serde(default)] + pub delivery_quorum: Option<usize>, + pub publish_max_attempts: usize, + pub publish_initial_backoff_millis: u64, + pub publish_max_backoff_millis: u64, + pub job_status_retention: usize, + pub retained_jobs: usize, + pub retained_idempotency_keys: usize, + pub accepted_jobs: usize, + pub published_jobs: usize, + pub failed_jobs: usize, + pub recovered_failed_jobs: usize, + pub methods: Vec<String>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SdkRadrootsdBridgeRelayPublishResult { + pub relay_url: String, + pub acknowledged: bool, + #[serde(default)] + pub detail: Option<String>, +} + #[derive(Clone, PartialEq, Eq, Deserialize)] pub struct SdkRadrootsdBridgeJob { pub job_id: String, @@ -260,6 +310,75 @@ impl fmt::Debug for SdkRadrootsdBridgeJob { } } +#[derive(Clone, PartialEq, Eq, Deserialize)] +pub struct SdkRadrootsdBridgeJobView { + pub job_id: String, + pub command: String, + #[serde(default)] + pub idempotency_key: Option<String>, + pub status: SdkRadrootsdBridgeJobStatus, + pub terminal: bool, + pub recovered_after_restart: bool, + pub requested_at_unix: u64, + #[serde(default)] + pub completed_at_unix: Option<u64>, + pub signer_mode: String, + #[serde(default)] + pub signer_session_id: Option<String>, + pub event_kind: u32, + #[serde(default)] + pub event_id: Option<String>, + #[serde(default)] + pub event_addr: Option<String>, + pub delivery_policy: SdkRadrootsdBridgeDeliveryPolicy, + #[serde(default)] + pub delivery_quorum: Option<usize>, + pub relay_count: usize, + pub acknowledged_relay_count: usize, + pub required_acknowledged_relay_count: usize, + pub attempt_count: usize, + #[serde(default)] + pub attempt_summaries: Vec<String>, + #[serde(default)] + pub relay_results: Vec<SdkRadrootsdBridgeRelayPublishResult>, + pub relay_outcome_summary: String, +} + +impl fmt::Debug for SdkRadrootsdBridgeJobView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdBridgeJobView"); + debug.field("job_id", &self.job_id); + debug.field("command", &self.command); + debug.field("idempotency_key", &self.idempotency_key); + debug.field("status", &self.status); + debug.field("terminal", &self.terminal); + debug.field("recovered_after_restart", &self.recovered_after_restart); + debug.field("requested_at_unix", &self.requested_at_unix); + debug.field("completed_at_unix", &self.completed_at_unix); + debug.field("signer_mode", &self.signer_mode.as_str()); + debug.field( + "signer_session_id", + &self.signer_session_id.as_ref().map(|_| "<redacted>"), + ); + debug.field("event_kind", &self.event_kind); + debug.field("event_id", &self.event_id); + debug.field("event_addr", &self.event_addr); + debug.field("delivery_policy", &self.delivery_policy); + debug.field("delivery_quorum", &self.delivery_quorum); + debug.field("relay_count", &self.relay_count); + debug.field("acknowledged_relay_count", &self.acknowledged_relay_count); + debug.field( + "required_acknowledged_relay_count", + &self.required_acknowledged_relay_count, + ); + debug.field("attempt_count", &self.attempt_count); + debug.field("attempt_summaries", &self.attempt_summaries); + debug.field("relay_results", &self.relay_results); + debug.field("relay_outcome_summary", &self.relay_outcome_summary); + debug.finish() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsdError { InvalidAuthHeader(String), @@ -306,6 +425,11 @@ struct SdkRadrootsdSignerSessionRequireAuthParams<'a> { auth_url: &'a str, } +#[derive(Debug, Serialize)] +struct SdkRadrootsdBridgeJobParams<'a> { + job_id: &'a str, +} + pub async fn publish_listing( endpoint: &str, auth: &RadrootsdAuth, @@ -428,6 +552,55 @@ pub(crate) async fn close_signer_session( .await } +pub(crate) async fn bridge_status( + endpoint: &str, + auth: &RadrootsdAuth, + timeout: Duration, +) -> Result<SdkRadrootsdBridgeStatusResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-bridge-status", + "bridge.status", + &json!({}), + timeout, + ) + .await +} + +pub(crate) async fn bridge_job_status( + endpoint: &str, + auth: &RadrootsdAuth, + job_id: &str, + timeout: Duration, +) -> Result<SdkRadrootsdBridgeJobView, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-bridge-job-status", + "bridge.job.status", + &SdkRadrootsdBridgeJobParams { job_id }, + timeout, + ) + .await +} + +pub(crate) async fn list_bridge_jobs( + endpoint: &str, + auth: &RadrootsdAuth, + timeout: Duration, +) -> Result<Vec<SdkRadrootsdBridgeJobView>, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-bridge-job-list", + "bridge.job.list", + &json!({}), + timeout, + ) + .await +} + fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { let mut headers = HeaderMap::new(); match auth { diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -98,6 +98,15 @@ impl fmt::Debug for SdkRadrootsdPublishReceipt { } } +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdPublishReceipt { + pub fn job(&self) -> Option<SdkRadrootsdBridgeJobRef> { + self.job_id + .as_ref() + .map(|job_id| SdkRadrootsdBridgeJobRef::new(job_id.clone())) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SdkPublishError { Config(SdkConfigError), @@ -217,6 +226,44 @@ impl fmt::Display for SdkRadrootsdSessionError { impl std::error::Error for SdkRadrootsdSessionError {} #[cfg(feature = "radrootsd-client")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SdkRadrootsdBridgeError { + Config(SdkConfigError), + UnsupportedTransport { + transport: SdkTransportMode, + operation: &'static str, + }, + Radrootsd(String), +} + +#[cfg(feature = "radrootsd-client")] +impl From<SdkConfigError> for SdkRadrootsdBridgeError { + fn from(value: SdkConfigError) -> Self { + Self::Config(value) + } +} + +#[cfg(feature = "radrootsd-client")] +impl fmt::Display for SdkRadrootsdBridgeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Config(err) => write!(f, "{err}"), + Self::UnsupportedTransport { + transport, + operation, + } => write!( + f, + "{operation} requires a different sdk transport mode than {transport:?}" + ), + Self::Radrootsd(message) => write!(f, "{message}"), + } + } +} + +#[cfg(all(feature = "radrootsd-client", feature = "std"))] +impl std::error::Error for SdkRadrootsdBridgeError {} + +#[cfg(feature = "radrootsd-client")] #[derive(Clone, PartialEq, Eq)] pub struct SdkRadrootsdSignerSessionRef { session_id: String, @@ -239,6 +286,144 @@ impl SdkRadrootsdSignerSessionRef { } #[cfg(feature = "radrootsd-client")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SdkRadrootsdBridgeJobRef { + job_id: String, +} + +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdBridgeJobRef { + pub fn new(job_id: impl Into<String>) -> Self { + Self { + job_id: job_id.into(), + } + } + + pub fn job_id(&self) -> &str { + self.job_id.as_str() + } +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SdkRadrootsdBridgeStatus { + pub enabled: bool, + pub ready: bool, + pub auth_mode: String, + pub signer_mode: String, + pub default_signer_mode: String, + pub supported_signer_modes: Vec<String>, + pub available_nip46_signer_sessions: usize, + pub relay_count: usize, + pub delivery_policy: radrootsd::SdkRadrootsdBridgeDeliveryPolicy, + pub delivery_quorum: Option<usize>, + pub publish_max_attempts: usize, + pub publish_initial_backoff_millis: u64, + pub publish_max_backoff_millis: u64, + pub job_status_retention: usize, + pub retained_jobs: usize, + pub retained_idempotency_keys: usize, + pub accepted_jobs: usize, + pub published_jobs: usize, + pub failed_jobs: usize, + pub recovered_failed_jobs: usize, + pub methods: Vec<String>, +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SdkRadrootsdBridgeJobView { + job: SdkRadrootsdBridgeJobRef, + pub command: String, + pub idempotency_key: Option<String>, + pub status: radrootsd::SdkRadrootsdBridgeJobStatus, + pub terminal: bool, + pub recovered_after_restart: bool, + pub requested_at_unix: u64, + pub completed_at_unix: Option<u64>, + pub signer_mode: String, + pub signer_session_id: Option<String>, + pub event_kind: u32, + pub event_id: Option<String>, + pub event_addr: Option<String>, + pub delivery_policy: radrootsd::SdkRadrootsdBridgeDeliveryPolicy, + pub delivery_quorum: Option<usize>, + pub relay_count: usize, + pub acknowledged_relay_count: usize, + pub required_acknowledged_relay_count: usize, + pub attempt_count: usize, + pub attempt_summaries: Vec<String>, + pub relay_results: Vec<radrootsd::SdkRadrootsdBridgeRelayPublishResult>, + pub relay_outcome_summary: String, +} + +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdBridgeJobView { + pub fn job(&self) -> &SdkRadrootsdBridgeJobRef { + &self.job + } +} + +#[cfg(feature = "radrootsd-client")] +impl From<radrootsd::SdkRadrootsdBridgeStatusResponse> for SdkRadrootsdBridgeStatus { + fn from(value: radrootsd::SdkRadrootsdBridgeStatusResponse) -> Self { + Self { + enabled: value.enabled, + ready: value.ready, + auth_mode: value.auth_mode, + signer_mode: value.signer_mode, + default_signer_mode: value.default_signer_mode, + supported_signer_modes: value.supported_signer_modes, + available_nip46_signer_sessions: value.available_nip46_signer_sessions, + relay_count: value.relay_count, + delivery_policy: value.delivery_policy, + delivery_quorum: value.delivery_quorum, + publish_max_attempts: value.publish_max_attempts, + publish_initial_backoff_millis: value.publish_initial_backoff_millis, + publish_max_backoff_millis: value.publish_max_backoff_millis, + job_status_retention: value.job_status_retention, + retained_jobs: value.retained_jobs, + retained_idempotency_keys: value.retained_idempotency_keys, + accepted_jobs: value.accepted_jobs, + published_jobs: value.published_jobs, + failed_jobs: value.failed_jobs, + recovered_failed_jobs: value.recovered_failed_jobs, + methods: value.methods, + } + } +} + +#[cfg(feature = "radrootsd-client")] +impl From<radrootsd::SdkRadrootsdBridgeJobView> for SdkRadrootsdBridgeJobView { + fn from(value: radrootsd::SdkRadrootsdBridgeJobView) -> Self { + Self { + job: SdkRadrootsdBridgeJobRef::new(value.job_id), + command: value.command, + idempotency_key: value.idempotency_key, + status: value.status, + terminal: value.terminal, + recovered_after_restart: value.recovered_after_restart, + requested_at_unix: value.requested_at_unix, + completed_at_unix: value.completed_at_unix, + signer_mode: value.signer_mode, + signer_session_id: value.signer_session_id, + event_kind: value.event_kind, + event_id: value.event_id, + event_addr: value.event_addr, + delivery_policy: value.delivery_policy, + delivery_quorum: value.delivery_quorum, + relay_count: value.relay_count, + acknowledged_relay_count: value.acknowledged_relay_count, + required_acknowledged_relay_count: value.required_acknowledged_relay_count, + attempt_count: value.attempt_count, + attempt_summaries: value.attempt_summaries, + relay_results: value.relay_results, + relay_outcome_summary: value.relay_outcome_summary, + } + } +} + +#[cfg(feature = "radrootsd-client")] #[derive(Clone, PartialEq, Eq)] pub struct SdkRadrootsdSignerSessionHandle { session: SdkRadrootsdSignerSessionRef, @@ -811,6 +996,87 @@ impl RadrootsSdkClient { .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; Ok(response.into()) } + + #[cfg(feature = "radrootsd-client")] + fn require_radrootsd_bridge_endpoint( + &self, + operation: &'static str, + ) -> Result<&str, SdkRadrootsdBridgeError> { + match &self.resolved_transport_target { + SdkResolvedTransportTarget::Radrootsd { endpoint } => Ok(endpoint.as_str()), + SdkResolvedTransportTarget::RelayDirect { .. } => { + Err(SdkRadrootsdBridgeError::UnsupportedTransport { + transport: self.transport(), + operation, + }) + } + } + } + + #[cfg(feature = "radrootsd-client")] + async fn radrootsd_bridge_status( + &self, + ) -> Result<SdkRadrootsdBridgeStatus, SdkRadrootsdBridgeError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdBridgeError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.bridge.status", + }); + } + + let response = radrootsd::bridge_status( + self.require_radrootsd_bridge_endpoint("radrootsd.bridge.status")?, + &self.config.radrootsd.auth, + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; + Ok(response.into()) + } + + #[cfg(feature = "radrootsd-client")] + async fn radrootsd_bridge_job_status( + &self, + job: &SdkRadrootsdBridgeJobRef, + ) -> Result<SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdBridgeError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.bridge.job", + }); + } + + let response = radrootsd::bridge_job_status( + self.require_radrootsd_bridge_endpoint("radrootsd.bridge.job")?, + &self.config.radrootsd.auth, + job.job_id(), + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; + Ok(response.into()) + } + + #[cfg(feature = "radrootsd-client")] + async fn radrootsd_bridge_jobs( + &self, + ) -> Result<Vec<SdkRadrootsdBridgeJobView>, SdkRadrootsdBridgeError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdBridgeError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.bridge.jobs", + }); + } + + let response = radrootsd::list_bridge_jobs( + self.require_radrootsd_bridge_endpoint("radrootsd.bridge.jobs")?, + &self.config.radrootsd.auth, + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; + Ok(response.into_iter().map(Into::into).collect()) + } } #[cfg(feature = "radrootsd-client")] @@ -838,6 +1104,12 @@ impl<'a> RadrootsdClient<'a> { client: self.client, } } + + pub fn bridge(&self) -> RadrootsdBridgeClient<'a> { + RadrootsdBridgeClient { + client: self.client, + } + } } #[cfg(feature = "radrootsd-client")] @@ -927,6 +1199,42 @@ impl<'a> RadrootsdSignerSessionClient<'a> { } } +#[cfg(feature = "radrootsd-client")] +#[derive(Debug, Clone, Copy)] +pub struct RadrootsdBridgeClient<'a> { + client: &'a RadrootsSdkClient, +} + +#[cfg(feature = "radrootsd-client")] +impl<'a> RadrootsdBridgeClient<'a> { + pub fn sdk(&self) -> &'a RadrootsSdkClient { + self.client + } + + pub fn transport(&self) -> SdkTransportMode { + self.client.transport() + } + + pub fn signer(&self) -> SignerConfig { + self.client.signer() + } + + pub async fn status(&self) -> Result<SdkRadrootsdBridgeStatus, SdkRadrootsdBridgeError> { + self.client.radrootsd_bridge_status().await + } + + pub async fn job( + &self, + job: &SdkRadrootsdBridgeJobRef, + ) -> Result<SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeError> { + self.client.radrootsd_bridge_job_status(job).await + } + + pub async fn jobs(&self) -> Result<Vec<SdkRadrootsdBridgeJobView>, SdkRadrootsdBridgeError> { + self.client.radrootsd_bridge_jobs().await + } +} + #[derive(Debug, Clone, Copy)] pub struct ProfileClient<'a> { client: &'a RadrootsSdkClient, diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -27,8 +27,10 @@ pub mod trade; #[cfg(feature = "radrootsd-client")] pub use crate::adapters::radrootsd::{ - SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, - SdkRadrootsdSignerSessionMode, SdkRadrootsdSignerSessionRole, + SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeJobStatus, + SdkRadrootsdBridgeRelayPublishResult, SdkRadrootsdSignerAuthority, + SdkRadrootsdSignerSessionConnectRequest, SdkRadrootsdSignerSessionMode, + SdkRadrootsdSignerSessionRole, }; pub use crate::client::{ FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError, @@ -37,11 +39,12 @@ pub use crate::client::{ }; #[cfg(feature = "radrootsd-client")] pub use crate::client::{ - RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdListingPublishOptions, - SdkRadrootsdSessionError, SdkRadrootsdSignerSessionAuthorizeResult, - SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSignerSessionHandle, - SdkRadrootsdSignerSessionRef, SdkRadrootsdSignerSessionRequireAuthResult, - SdkRadrootsdSignerSessionView, + RadrootsdBridgeClient, RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdBridgeError, + SdkRadrootsdBridgeJobRef, SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeStatus, + SdkRadrootsdListingPublishOptions, SdkRadrootsdSessionError, + SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSignerSessionCloseResult, + SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionRef, + SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSignerSessionView, }; pub use crate::config::{ NetworkConfig, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, RADROOTS_SDK_LOCAL_RELAY_URL, diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -17,7 +17,8 @@ use radroots_sdk::listing::{ }; use radroots_sdk::{ RadrootsNostrEvent, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, - SdkConfigError, SdkEnvironment, SdkPublishError, SdkRadrootsdListingPublishOptions, + SdkConfigError, SdkEnvironment, SdkPublishError, SdkRadrootsdBridgeDeliveryPolicy, + SdkRadrootsdBridgeError, SdkRadrootsdBridgeJobStatus, SdkRadrootsdListingPublishOptions, SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView, SdkTransportMode, SdkTransportReceipt, SignerConfig, @@ -298,6 +299,70 @@ fn sample_session_view_json(session_id: &str) -> Value { }) } +fn sample_bridge_status_json() -> Value { + json!({ + "enabled": true, + "ready": true, + "auth_mode": "bearer_token", + "signer_mode": "selectable_per_request", + "default_signer_mode": "embedded_service_identity", + "supported_signer_modes": ["embedded_service_identity", "nip46_session"], + "available_nip46_signer_sessions": 2, + "relay_count": 1, + "delivery_policy": "quorum", + "delivery_quorum": 1, + "publish_max_attempts": 3, + "publish_initial_backoff_millis": 250, + "publish_max_backoff_millis": 4000, + "job_status_retention": 64, + "retained_jobs": 4, + "retained_idempotency_keys": 2, + "accepted_jobs": 1, + "published_jobs": 2, + "failed_jobs": 1, + "recovered_failed_jobs": 0, + "methods": ["bridge.status", "bridge.job.status", "bridge.job.list", "bridge.listing.publish"] + }) +} + +fn sample_bridge_job_json(job_id: &str) -> Value { + json!({ + "job_id": job_id, + "command": "bridge.listing.publish", + "idempotency_key": "idem-bridge-1", + "status": "published", + "terminal": true, + "recovered_after_restart": false, + "requested_at_unix": 1720000000u64, + "completed_at_unix": 1720000001u64, + "signer_mode": "nip46_session", + "signer_session_id": "session-123", + "event_kind": 30402, + "event_id": "event-bridge-1", + "event_addr": "30402:seller:listing-bridge-1", + "delivery_policy": "quorum", + "delivery_quorum": 1, + "relay_count": 2, + "acknowledged_relay_count": 1, + "required_acknowledged_relay_count": 1, + "attempt_count": 1, + "attempt_summaries": ["attempt 1: 1/2 relays acknowledged"], + "relay_results": [ + { + "relay_url": "wss://radroots.org", + "acknowledged": true, + "detail": null + }, + { + "relay_url": "wss://backup.radroots.org", + "acknowledged": false, + "detail": "timeout" + } + ], + "relay_outcome_summary": "quorum satisfied with 1/2 relay acknowledgements" + }) +} + async fn connected_bunker_session_handle( session_id: &str, ) -> TestResult<SdkRadrootsdSignerSessionHandle> { @@ -1089,6 +1154,159 @@ async fn radrootsd_listing_publish_rejects_relay_transport_mode() -> TestResult< Ok(()) } +#[tokio::test] +async fn radrootsd_bridge_status_returns_typed_status() -> TestResult<()> { + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-bridge-status", + "result": sample_bridge_status_json() + }), + ) + .await?; + let client = radrootsd_test_client(server.endpoint())?; + let status = client.radrootsd().bridge().status().await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "bridge.status"); + assert!(status.enabled); + assert!(status.ready); + assert_eq!( + status.delivery_policy, + SdkRadrootsdBridgeDeliveryPolicy::Quorum + ); + assert_eq!(status.delivery_quorum, Some(1)); + assert_eq!(status.available_nip46_signer_sessions, 2); + assert!( + status + .methods + .contains(&"bridge.listing.publish".to_owned()) + ); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_bridge_job_status_accepts_typed_job_ref_from_publish_receipt() -> TestResult<()> +{ + let (publish_server, publish_request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-listing-publish", + "result": { + "deduplicated": false, + "job": { + "job_id": "job-bridge-1", + "command": "bridge.listing.publish", + "status": "published", + "terminal": true, + "recovered_after_restart": false, + "signer_mode": "nip46_session:session-123", + "signer_session_id": "session-123", + "event_kind": 30402, + "event_id": "event-bridge-1", + "event_addr": "30402:seller:listing-bridge-1", + "relay_count": 1, + "acknowledged_relay_count": 1 + } + } + }), + ) + .await?; + let handle = connected_bunker_session_handle("session-123").await?; + let publish_client = radrootsd_test_client(publish_server.endpoint())?; + let publish_receipt = publish_client + .listing() + .publish_listing_via_radrootsd(&sample_listing(), &handle) + .await?; + let publish_request_json = publish_request_rx.await?; + assert_eq!(publish_request_json["method"], "bridge.listing.publish"); + + let job = match &publish_receipt.transport_receipt { + SdkTransportReceipt::Radrootsd(receipt) => receipt.job(), + SdkTransportReceipt::RelayDirect(_) => None, + } + .expect("publish receipt should expose a bridge job ref"); + + let (job_server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-bridge-job-status", + "result": sample_bridge_job_json("job-bridge-1") + }), + ) + .await?; + let job_client = radrootsd_test_client(job_server.endpoint())?; + let job_view = job_client.radrootsd().bridge().job(&job).await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "bridge.job.status"); + assert_eq!(request_json["params"]["job_id"], "job-bridge-1"); + assert_eq!(job_view.job().job_id(), "job-bridge-1"); + assert_eq!(job_view.status, SdkRadrootsdBridgeJobStatus::Published); + assert_eq!( + job_view.delivery_policy, + SdkRadrootsdBridgeDeliveryPolicy::Quorum + ); + assert_eq!(job_view.attempt_count, 1); + assert_eq!(job_view.relay_results.len(), 2); + assert_eq!(job_view.relay_results[0].relay_url, "wss://radroots.org"); + assert!(job_view.relay_results[0].acknowledged); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_bridge_job_list_returns_typed_views() -> TestResult<()> { + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-bridge-job-list", + "result": [ + sample_bridge_job_json("job-bridge-1"), + sample_bridge_job_json("job-bridge-2") + ] + }), + ) + .await?; + let client = radrootsd_test_client(server.endpoint())?; + let jobs = client.radrootsd().bridge().jobs().await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "bridge.job.list"); + assert_eq!(jobs.len(), 2); + assert_eq!(jobs[0].job().job_id(), "job-bridge-1"); + assert_eq!(jobs[1].job().job_id(), "job-bridge-2"); + assert_eq!(jobs[0].status, SdkRadrootsdBridgeJobStatus::Published); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_bridge_status_rejects_relay_transport_mode() -> TestResult<()> { + let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; + let error = client + .radrootsd() + .bridge() + .status() + .await + .expect_err("unsupported transport"); + + assert!(matches!( + error, + SdkRadrootsdBridgeError::UnsupportedTransport { + transport: SdkTransportMode::RelayDirect, + operation: "radrootsd.bridge.status", + } + )); + + Ok(()) +} + #[test] fn radrootsd_listing_request_from_event_rejects_listing_draft_kind() -> TestResult<()> { let draft = radroots_sdk::listing::build_draft(&sample_listing())?;