commit 0887e5c8dc3f59ae401df410a25c9e94fdf627ac
parent 0d6a53f85eca65d7ff9984602b8a9588de12745b
Author: triesap <tyson@radroots.org>
Date: Mon, 13 Apr 2026 08:22:51 +0000
sdk: add bridge workflow inspection
Diffstat:
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())?;