commit 9531b64e862e0c632576edeb2866c2d6aedb195e
parent 30460646c84f3e588ad0e1705ab49ecd04dd2de3
Author: triesap <tyson@radroots.org>
Date: Tue, 23 Jun 2026 10:25:50 +0000
publish-proxy: add cli proxy transport
- Replace publish mode config with publish transport and proxy token sources.
- Route farm, listing, and sync publish flows through SDK publish transports.
- Remove legacy direct listing publish and bridge/deferred proxy paths.
- Cover proxy readiness, dry-run listing mutations, and retired surface rejection.
Diffstat:
30 files changed, 817 insertions(+), 2724 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3815,6 +3815,13 @@ dependencies = [
]
[[package]]
+name = "radroots_publish_proxy_protocol"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "serde",
+]
+
+[[package]]
name = "radroots_relay_transport"
version = "0.1.0-alpha.2"
dependencies = [
@@ -3905,6 +3912,7 @@ dependencies = [
name = "radroots_sdk"
version = "0.1.0"
dependencies = [
+ "futures",
"hex",
"radroots_authority",
"radroots_event_store",
@@ -3913,8 +3921,10 @@ dependencies = [
"radroots_identity",
"radroots_nostr",
"radroots_outbox",
+ "radroots_publish_proxy_protocol",
"radroots_relay_transport",
"radroots_trade",
+ "reqwest",
"serde",
"serde_json",
"sha2",
diff --git a/Cargo.toml b/Cargo.toml
@@ -41,7 +41,7 @@ radroots_replica_db_schema = { path = "../lib/crates/replica_db_schema" }
radroots_replica_sync = { path = "../lib/crates/replica_sync" }
radroots_runtime = { path = "../lib/crates/runtime" }
radroots_runtime_paths = { path = "../lib/crates/runtime_paths" }
-radroots_sdk = { path = "../sdk/crates/sdk", features = ["local-runtime"] }
+radroots_sdk = { path = "../sdk/crates/sdk", features = ["local-runtime-radrootsd-proxy", "relay-runtime"] }
radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] }
radroots_sql_core = { path = "../lib/crates/sql_core", features = ["native"] }
radroots_sp1_host_trade = { path = "../lib/crates/sp1_host_trade" }
diff --git a/src/cli/global.rs b/src/cli/global.rs
@@ -39,7 +39,7 @@ pub struct RuntimeInvocationArgs {
pub account: Option<String>,
pub identity_path: Option<PathBuf>,
pub signer: Option<String>,
- pub publish_mode: Option<String>,
+ pub publish_transport: Option<String>,
pub relay: Vec<String>,
pub myc_executable: Option<PathBuf>,
pub myc_status_timeout_ms: Option<u64>,
@@ -134,7 +134,6 @@ pub struct FarmUpdateArgs {
pub struct FarmPublishArgs {
pub scope: Option<FarmScopeArg>,
pub idempotency_key: Option<String>,
- pub signer_session_id: Option<String>,
pub print_event: bool,
}
@@ -184,7 +183,6 @@ pub struct ListingRebindArgs {
pub struct ListingMutationArgs {
pub file: PathBuf,
pub idempotency_key: Option<String>,
- pub signer_session_id: Option<String>,
pub print_event: bool,
pub offline: bool,
}
diff --git a/src/cli/input.rs b/src/cli/input.rs
@@ -27,7 +27,7 @@ pub fn runtime_invocation_args_from_target(args: &TargetCliArgs) -> RuntimeInvoc
account: args.account_id.clone(),
identity_path: None,
signer: None,
- publish_mode: args.publish_mode.map(|mode| mode.as_str().to_owned()),
+ publish_transport: args.publish_transport.map(|mode| mode.as_str().to_owned()),
relay: args.relay.clone(),
myc_executable: None,
myc_status_timeout_ms: None,
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
@@ -43,17 +43,18 @@ pub enum TargetOutputFormat {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
-pub enum TargetPublishMode {
- #[value(name = "nostr_relay")]
- NostrRelay,
- Radrootsd,
+pub enum TargetPublishTransport {
+ #[value(name = "direct_nostr_relay")]
+ DirectNostrRelay,
+ #[value(name = "radrootsd_proxy")]
+ RadrootsdProxy,
}
-impl TargetPublishMode {
+impl TargetPublishTransport {
pub fn as_str(self) -> &'static str {
match self {
- Self::NostrRelay => "nostr_relay",
- Self::Radrootsd => "radrootsd",
+ Self::DirectNostrRelay => "direct_nostr_relay",
+ Self::RadrootsdProxy => "radrootsd_proxy",
}
}
}
@@ -62,7 +63,7 @@ impl TargetPublishMode {
#[command(
name = "radroots",
about = "Operate Radroots local-first trade workflows.",
- long_about = "Operate Radroots local-first trade workflows.\n\nPublish modes:\n nostr_relay uses direct relay publish with local signer custody.\n radrootsd is reserved and fails closed for active buyer and seller writes.\n\nRelay mode never silently falls back to radrootsd.",
+ long_about = "Operate Radroots local-first trade workflows.\n\nPublish transports:\n direct_nostr_relay publishes directly to configured relays with local signer custody.\n radrootsd_proxy publishes locally signed events through the local daemon proxy.",
disable_help_subcommand = true
)]
pub struct TargetCliArgs {
@@ -73,12 +74,12 @@ pub struct TargetCliArgs {
#[arg(long = "relay", global = true)]
pub relay: Vec<String>,
#[arg(
- long = "publish-mode",
+ long = "publish-transport",
global = true,
value_enum,
- help = "Select nostr_relay direct relay publish or reserved radrootsd guardrail mode"
+ help = "Select direct_nostr_relay direct relay publish or radrootsd_proxy daemon proxy publish"
)]
- pub publish_mode: Option<TargetPublishMode>,
+ pub publish_transport: Option<TargetPublishTransport>,
#[arg(long = "offline", global = true, action = ArgAction::SetTrue, conflicts_with = "online")]
pub offline: bool,
#[arg(long = "online", global = true, action = ArgAction::SetTrue, conflicts_with = "offline")]
diff --git a/src/main.rs b/src/main.rs
@@ -13,7 +13,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use clap::Parser;
-use serde_json::{Value, json};
+use serde_json::Value;
use crate::cli::input::runtime_invocation_args_from_target;
use crate::cli::{TargetCliArgs, TargetOutputFormat};
@@ -28,13 +28,8 @@ use crate::ops::{
TargetOperationRequest,
};
use crate::out::envelope::OutputEnvelope;
-use crate::registry::{
- NetworkRequirement, network_requirement, requires_local_signer_mode,
- requires_nostr_relay_publish_mode,
-};
-use crate::runtime::config::{
- PublishMode, RADROOTSD_PUBLISH_DEFERRED_REASON, RuntimeConfig, SignerBackend,
-};
+use crate::registry::{NetworkRequirement, network_requirement, requires_local_signer_mode};
+use crate::runtime::config::{RuntimeConfig, SignerBackend};
use crate::runtime::logging::initialize_logging;
static REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
@@ -327,7 +322,7 @@ fn validate_request_contract(
config: &RuntimeConfig,
) -> Result<(), OperationAdapterError> {
validate_pre_runtime_request_contract(request)?;
- validate_publish_mode_contract(request, config)?;
+ validate_publish_transport_contract(request, config)?;
validate_signer_mode_contract(request, config)?;
validate_network_contract(request, config)?;
Ok(())
@@ -362,7 +357,7 @@ fn validate_signer_mode_contract(
) -> Result<(), OperationAdapterError> {
let spec = request.spec();
if matches!(config.signer.backend, SignerBackend::Myc)
- && requires_local_signer_mode_for_publish_mode(spec.operation_id, config)
+ && requires_local_signer_mode_for_publish_transport(spec.operation_id, config)
{
return Err(OperationAdapterError::SignerModeDeferred {
operation_id: spec.operation_id.to_owned(),
@@ -423,84 +418,32 @@ fn validate_network_contract(
}
}
-fn requires_local_signer_mode_for_publish_mode(operation_id: &str, config: &RuntimeConfig) -> bool {
- if matches!(config.publish.mode, PublishMode::Radrootsd)
- && is_publish_mode_routed_operation(operation_id)
- {
- return false;
- }
+fn requires_local_signer_mode_for_publish_transport(
+ operation_id: &str,
+ config: &RuntimeConfig,
+) -> bool {
+ let _ = config;
requires_local_signer_mode(operation_id)
}
fn requires_pre_runtime_relay_target(operation_id: &str) -> bool {
- !is_publish_mode_routed_operation(operation_id)
+ !is_publish_transport_routed_operation(operation_id)
}
fn allows_offline_local_mutation(operation_id: &str) -> bool {
matches!(operation_id, "listing.publish")
}
-fn validate_publish_mode_contract(
+fn validate_publish_transport_contract(
request: &TargetOperationRequest,
config: &RuntimeConfig,
) -> Result<(), OperationAdapterError> {
- let spec = request.spec();
- if matches!(config.publish.mode, PublishMode::Radrootsd)
- && requires_nostr_relay_publish_mode(spec.operation_id)
- {
- let message = format!(
- "`{}` cannot run with publish mode `radrootsd`; {RADROOTSD_PUBLISH_DEFERRED_REASON}",
- spec.cli_path
- );
- let actions = nostr_relay_publish_mode_recovery_actions(spec.operation_id);
- return Err(OperationAdapterError::operation_unavailable_with_detail(
- spec.operation_id,
- message.clone(),
- json!({
- "state": "unavailable",
- "reason": message,
- "actions": actions,
- "publish": {
- "mode": config.publish.mode.as_str(),
- "source": config.publish.source.as_str(),
- "transport_family": config.publish.mode.transport_family(),
- "state": "unavailable",
- "executable": false,
- "provider": {
- "provider_runtime_id": "radrootsd",
- "state": "unavailable",
- }
- }
- }),
- ));
- }
+ let _ = request;
+ let _ = config;
Ok(())
}
-fn nostr_relay_publish_mode_recovery_actions(operation_id: &str) -> Vec<String> {
- match operation_id {
- "farm.publish" => vec![
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com farm publish"
- .to_owned(),
- ],
- "listing.publish" => vec![format!(
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com {}",
- "listing publish <file>"
- )],
- "listing.update" => vec![format!(
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com {}",
- "listing update <file>"
- )],
- "listing.archive" => vec![format!(
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com {}",
- "listing archive <file>"
- )],
- "sync.push" => vec!["radroots --publish-mode nostr_relay sync push".to_owned()],
- _ => Vec::new(),
- }
-}
-
-fn is_publish_mode_routed_operation(operation_id: &str) -> bool {
+fn is_publish_transport_routed_operation(operation_id: &str) -> bool {
matches!(
operation_id,
"farm.publish" | "listing.publish" | "listing.update" | "listing.archive"
@@ -581,8 +524,8 @@ fn render_human_envelope(
{
writeln!(handle, "state: {state}")?;
}
- if let Some(mode) = human_publish_mode(display) {
- writeln!(handle, "publish_mode: {mode}")?;
+ if let Some(mode) = human_publish_transport(display) {
+ writeln!(handle, "publish_transport: {mode}")?;
}
if let Some(state) = human_publish_state(display) {
writeln!(handle, "publish_state: {state}")?;
@@ -624,10 +567,10 @@ fn human_state(result: &Value) -> Option<&str> {
human_string_path(result, &["state"])
}
-fn human_publish_mode(result: &Value) -> Option<&str> {
+fn human_publish_transport(result: &Value) -> Option<&str> {
human_string_path(result, &["publish", "mode"])
.or_else(|| human_string_path(result, &["checks", "publish", "mode"]))
- .or_else(|| human_string_path(result, &["publish_mode"]))
+ .or_else(|| human_string_path(result, &["publish_transport"]))
}
fn human_publish_state(result: &Value) -> Option<&str> {
diff --git a/src/ops/error.rs b/src/ops/error.rs
@@ -829,7 +829,6 @@ fn looks_like_signer_failure(value: &str) -> bool {
"signer",
"sign_event",
"sign event",
- "signer_session_id",
"signer session",
"nip46",
"nip-46",
@@ -848,7 +847,7 @@ fn looks_like_provider_failure(value: &str) -> bool {
"provider failed",
"radrootsd unavailable",
"daemon unavailable",
- "bridge provider",
+ "proxy provider",
],
)
}
@@ -863,9 +862,8 @@ fn looks_like_operation_failure(value: &str) -> bool {
"unsupported operation",
"operation unavailable",
"operation disabled",
- "bridge disabled",
- "bridge is disabled",
- "bridge.listing.publish is disabled",
+ "publish proxy disabled",
+ "publish.event is disabled",
],
)
}
diff --git a/src/ops/exec/basket.rs b/src/ops/exec/basket.rs
@@ -1335,8 +1335,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
const LISTING_ADDR: &str = "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
@@ -1912,8 +1913,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -1936,7 +1938,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/ops/exec/core.rs b/src/ops/exec/core.rs
@@ -27,9 +27,7 @@ use crate::runtime::account::{
resolve_account_selector, secret_backend_status, select_account, snapshot,
unresolved_account_reason,
};
-use crate::runtime::config::{
- PublishMode, RADROOTSD_PUBLISH_DEFERRED_REASON, RuntimeConfig, SignerBackend,
-};
+use crate::runtime::config::{PublishTransport, RuntimeConfig, SignerBackend};
use crate::runtime::logging::LoggingState;
use crate::runtime::sdk::CliSdkAdapterError;
use crate::view::runtime::{
@@ -175,7 +173,7 @@ impl OperationService<HealthCheckRunRequest> for CoreOperationService<'_> {
"signer": signer,
"publish": {
"state": publish.state,
- "mode": publish.mode,
+ "transport": publish.transport,
"executable": publish.executable,
"reason": publish.reason,
},
@@ -196,7 +194,6 @@ impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
let publish = publish_runtime_view(self.config, true, &account);
let write_plane =
crate::runtime::provider::resolve_write_plane_provider(self.config, &publish);
- let bridge_auth_configured = write_plane.bridge_auth_configured;
let actions = config_actions(self.config, &account, &publish);
let mut result = json!({
"output": {
@@ -258,12 +255,15 @@ impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
},
"actions": actions,
});
- if matches!(self.config.publish.mode, PublishMode::Radrootsd) {
- result["rpc"] = json!({
- "url": self.config.rpc.url,
- "bridge_auth_configured": self.config.rpc.bridge_bearer_token.is_some(),
+ if matches!(
+ self.config.publish.transport,
+ PublishTransport::RadrootsdProxy
+ ) {
+ result["radrootsd_proxy"] = json!({
+ "url": self.config.publish.radrootsd_proxy.url,
+ "token_file_configured": self.config.publish.radrootsd_proxy.token_file.is_some(),
+ "token_secret_id_configured": self.config.publish.radrootsd_proxy.token_secret_id.is_some(),
});
- result["write_plane"]["bridge_auth_configured"] = json!(bridge_auth_configured);
}
json_operation_result::<ConfigGetResult>(result)
}
@@ -805,42 +805,46 @@ fn publish_runtime_view(
source: config.relay.source.as_str().to_owned(),
};
- match config.publish.mode {
- PublishMode::NostrRelay => {
- let (state, executable, reason) =
- nostr_relay_publish_readiness(config, relay_ready, signed_write_required, account);
+ match config.publish.transport {
+ PublishTransport::DirectNostrRelay => {
+ let (state, executable, reason) = direct_nostr_relay_publish_readiness(
+ config,
+ relay_ready,
+ signed_write_required,
+ account,
+ );
PublishRuntimeView {
- mode: config.publish.mode.as_str().to_owned(),
+ transport: config.publish.transport.as_str().to_owned(),
source,
- transport_family: config.publish.mode.transport_family().to_owned(),
+ transport_family: config.publish.transport.transport_family().to_owned(),
state: state.to_owned(),
executable,
reason: reason.clone(),
signed_write_required,
relay,
provider: PublishProviderRuntimeView {
- provider_runtime_id: "nostr_relay".to_owned(),
+ provider_runtime_id: "direct_nostr_relay".to_owned(),
state: state.to_owned(),
source: config.relay.source.as_str().to_owned(),
reason,
},
}
}
- PublishMode::Radrootsd => {
+ PublishTransport::RadrootsdProxy => {
let (state, executable, reason) = radrootsd_publish_readiness(config);
PublishRuntimeView {
- mode: config.publish.mode.as_str().to_owned(),
+ transport: config.publish.transport.as_str().to_owned(),
source,
- transport_family: config.publish.mode.transport_family().to_owned(),
+ transport_family: config.publish.transport.transport_family().to_owned(),
state: state.to_owned(),
executable,
reason: reason.clone(),
signed_write_required,
relay,
provider: PublishProviderRuntimeView {
- provider_runtime_id: "radrootsd".to_owned(),
+ provider_runtime_id: "radrootsd_proxy".to_owned(),
state: state.to_owned(),
- source: "publish mode · local first".to_owned(),
+ source: "publish transport · local first".to_owned(),
reason,
},
}
@@ -848,7 +852,7 @@ fn publish_runtime_view(
}
}
-fn nostr_relay_publish_readiness(
+fn direct_nostr_relay_publish_readiness(
config: &RuntimeConfig,
relay_ready: bool,
signed_write_required: bool,
@@ -859,7 +863,7 @@ fn nostr_relay_publish_readiness(
"unconfigured",
false,
Some(
- "nostr_relay publish mode requires at least one configured relay for writes"
+ "direct_nostr_relay publish transport requires at least one configured relay for writes"
.to_owned(),
),
);
@@ -874,7 +878,7 @@ fn nostr_relay_publish_readiness(
"unavailable",
false,
Some(
- "nostr_relay publish mode requires signer mode `local` for signed writes; signer mode `myc` is deferred"
+ "direct_nostr_relay publish transport requires signer mode `local` for signed writes; signer mode `myc` is deferred"
.to_owned(),
),
);
@@ -885,7 +889,7 @@ fn nostr_relay_publish_readiness(
"unconfigured",
false,
Some(
- "nostr_relay publish mode requires a selected or default write-capable local account for signed writes"
+ "direct_nostr_relay publish transport requires a selected or default write-capable local account for signed writes"
.to_owned(),
),
);
@@ -904,12 +908,29 @@ fn nostr_relay_publish_readiness(
("ready", true, None)
}
-fn radrootsd_publish_readiness(_config: &RuntimeConfig) -> (&'static str, bool, Option<String>) {
- (
- "unavailable",
- false,
- Some(RADROOTSD_PUBLISH_DEFERRED_REASON.to_owned()),
- )
+fn radrootsd_publish_readiness(config: &RuntimeConfig) -> (&'static str, bool, Option<String>) {
+ if config.publish.radrootsd_proxy.token_file.is_none()
+ && config.publish.radrootsd_proxy.token_secret_id.is_none()
+ {
+ return (
+ "unconfigured",
+ false,
+ Some("radrootsd_proxy publish transport requires a configured token file or token secret id".to_owned()),
+ );
+ }
+
+ if matches!(config.signer.backend, SignerBackend::Myc) {
+ return (
+ "unavailable",
+ false,
+ Some(
+ "radrootsd_proxy publish transport requires signer mode `local` for signed writes; signer mode `myc` is deferred"
+ .to_owned(),
+ ),
+ );
+ }
+
+ ("ready", true, None)
}
fn signer_health_view(config: &RuntimeConfig, account: &AccountResolution) -> Value {
@@ -1005,8 +1026,8 @@ fn publish_recovery_actions(
}
let mut actions = Vec::new();
- match config.publish.mode {
- PublishMode::NostrRelay => {
+ match config.publish.transport {
+ PublishTransport::DirectNostrRelay => {
if config.relay.urls.is_empty() {
push_unique(
&mut actions,
@@ -1025,16 +1046,29 @@ fn publish_recovery_actions(
}
}
}
- PublishMode::Radrootsd => {
- push_unique(
- &mut actions,
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get",
- );
+ PublishTransport::RadrootsdProxy => {
+ if self::proxy_token_configured(config) {
+ if publish.signed_write_required
+ && matches!(config.signer.backend, SignerBackend::Myc)
+ {
+ push_unique(&mut actions, "radroots signer status get");
+ }
+ } else {
+ push_unique(
+ &mut actions,
+ "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID",
+ );
+ }
}
}
actions
}
+fn proxy_token_configured(config: &RuntimeConfig) -> bool {
+ config.publish.radrootsd_proxy.token_file.is_some()
+ || config.publish.radrootsd_proxy.token_secret_id.is_some()
+}
+
fn push_unique(actions: &mut Vec<String>, action: impl Into<String>) {
let action = action.into();
if !actions.contains(&action) {
@@ -1123,8 +1157,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
use crate::runtime::logging::LoggingState;
@@ -1340,8 +1375,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -1364,7 +1400,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs
@@ -14,7 +14,7 @@ use crate::ops::{
OperationResult, OperationResultData, OperationService,
};
use crate::runtime::RuntimeError;
-use crate::runtime::config::{PublishMode, RuntimeConfig};
+use crate::runtime::config::{PublishTransport, RuntimeConfig};
use crate::view::runtime::{CommandDisposition, FarmPublishView};
pub struct FarmOperationService<'a> {
@@ -172,7 +172,6 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
.idempotency_key
.clone()
.or_else(|| string_input(&request, "idempotency_key")),
- signer_session_id: string_input(&request, "signer_session_id"),
print_event: bool_input(&request, "print_event").unwrap_or(false),
};
if request.context.requires_approval_token() {
@@ -180,7 +179,10 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
request.operation_id(),
));
}
- if matches!(self.config.publish.mode, PublishMode::NostrRelay) {
+ if matches!(
+ self.config.publish.transport,
+ PublishTransport::DirectNostrRelay
+ ) {
require_relay_target(&request, self.config)?;
}
@@ -390,8 +392,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
#[test]
@@ -545,8 +548,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -569,7 +573,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/ops/exec/listing.rs b/src/ops/exec/listing.rs
@@ -158,10 +158,9 @@ impl OperationService<ListingUpdateRequest> for ListingOperationService<'_> {
}
let args = mutation_args(&request)?;
let config = mutation_config(self.config, &request);
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::update(&config, &args),
- )?;
+ let view = crate::runtime::listing::update(&config, &args).map_err(|error| {
+ OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
+ })?;
mutation_result::<ListingUpdateResult>(request.operation_id(), &view)
}
}
@@ -243,10 +242,9 @@ impl OperationService<ListingArchiveRequest> for ListingOperationService<'_> {
}
let args = mutation_args(&request)?;
let config = mutation_config(self.config, &request);
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::archive(&config, &args),
- )?;
+ let view = crate::runtime::listing::archive(&config, &args).map_err(|error| {
+ OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
+ })?;
mutation_result::<ListingArchiveResult>(request.operation_id(), &view)
}
}
@@ -275,7 +273,6 @@ where
.idempotency_key
.clone()
.or_else(|| string_input(request, "idempotency_key")),
- signer_session_id: string_input(request, "signer_session_id"),
print_event: bool_input(request, "print_event").unwrap_or(false),
offline: matches!(request.context.network_mode, OperationNetworkMode::Offline),
})
@@ -479,8 +476,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
#[test]
@@ -616,8 +614,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -640,7 +639,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/ops/exec/market.rs b/src/ops/exec/market.rs
@@ -256,8 +256,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
use crate::view::runtime::{
FindPriceView, FindQuantityView, FindResultProvenanceView, FindResultView, FindView,
@@ -677,8 +678,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -701,7 +703,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/ops/exec/order.rs b/src/ops/exec/order.rs
@@ -1258,8 +1258,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
use crate::view::runtime::OrderDecisionView;
@@ -1801,8 +1802,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -1825,7 +1827,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/ops/exec/runtime.rs b/src/ops/exec/runtime.rs
@@ -1,5 +1,5 @@
use serde::Serialize;
-use serde_json::{Value, json};
+use serde_json::Value;
use crate::cli::global::SyncWatchArgs;
use crate::ops::{
@@ -10,7 +10,7 @@ use crate::ops::{
SyncWatchResult,
};
use crate::runtime::RuntimeError;
-use crate::runtime::config::{PublishMode, RuntimeConfig};
+use crate::runtime::config::RuntimeConfig;
use crate::view::runtime::{CommandDisposition, SyncActionView, SyncStatusView};
pub struct RuntimeOperationService<'a> {
@@ -80,9 +80,6 @@ impl OperationService<SyncPushRequest> for RuntimeOperationService<'_> {
&self,
request: OperationRequest<SyncPushRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if matches!(self.config.publish.mode, PublishMode::Radrootsd) {
- return Err(sync_push_radrootsd_unavailable(self.config));
- }
if request.context.requires_approval_token() {
return Err(OperationAdapterError::approval_required("sync.push"));
}
@@ -111,26 +108,6 @@ impl OperationService<SyncWatchRequest> for RuntimeOperationService<'_> {
}
}
-fn sync_push_radrootsd_unavailable(config: &RuntimeConfig) -> OperationAdapterError {
- OperationAdapterError::operation_unavailable_with_detail(
- "sync.push",
- crate::runtime::sync::RADROOTSD_SYNC_PUSH_UNAVAILABLE_REASON.to_owned(),
- json!({
- "publish": {
- "mode": config.publish.mode.as_str(),
- "source": config.publish.source.as_str(),
- "transport_family": config.publish.mode.transport_family(),
- "state": "unavailable",
- "executable": false,
- "provider": {
- "provider_runtime_id": "radrootsd",
- "state": "unavailable",
- }
- }
- }),
- )
-}
-
fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
where
R: OperationResultData,
@@ -246,13 +223,14 @@ mod tests {
use super::RuntimeOperationService;
use crate::ops::{
OperationAdapter, OperationContext, OperationRequest, RelayListRequest,
- SignerStatusGetRequest, SyncPushRequest, SyncStatusGetRequest,
+ SignerStatusGetRequest, SyncStatusGetRequest,
};
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
#[test]
@@ -313,31 +291,6 @@ mod tests {
assert_eq!(envelope.result["actions"][0], "radroots sync pull");
}
- #[test]
- fn runtime_service_rejects_radrootsd_sync_push_before_approval_or_store_checks() {
- let dir = tempdir().expect("tempdir");
- let mut config = sample_config(dir.path(), Vec::new());
- config.publish.mode = PublishMode::Radrootsd;
- config.publish.source = PublishModeSource::Flags;
- let service = OperationAdapter::new(RuntimeOperationService::new(&config));
-
- let sync = OperationRequest::new(OperationContext::default(), SyncPushRequest::default())
- .expect("sync push request");
- let error = service.execute(sync).expect_err("radrootsd sync push");
- let output = error.to_output_error();
- let detail = output.detail.expect("radrootsd detail");
-
- assert_eq!(output.code, "operation_unavailable");
- assert_eq!(output.exit_code, 3);
- assert_eq!(detail["publish"]["mode"], "radrootsd");
- assert_eq!(detail["publish"]["source"], "cli flags · local first");
- assert_eq!(
- detail["publish"]["provider"]["provider_runtime_id"],
- "radrootsd"
- );
- assert_eq!(detail["class"], "operation");
- }
-
fn sample_config(root: &Path, relays: Vec<String>) -> RuntimeConfig {
let data = root.join("data");
let logs = root.join("logs");
@@ -405,8 +358,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: relays,
@@ -429,7 +383,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/out/envelope.rs b/src/out/envelope.rs
@@ -277,13 +277,15 @@ fn next_actions_from_actions_value(actions_value: Option<&Value>) -> Vec<NextAct
fn next_action_from_action_string(action: &str) -> Option<NextAction> {
let action = action.trim();
- if action == "configure RADROOTS_CLI_RPC_BEARER_TOKEN" {
+ if action
+ == "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
+ {
return Some(NextAction {
kind: NextActionKind::OperatorConfig,
- label: "configure rpc bearer token".to_owned(),
+ label: "configure radrootsd proxy token source".to_owned(),
command: None,
description: Some(action.to_owned()),
- env_var: Some("RADROOTS_CLI_RPC_BEARER_TOKEN".to_owned()),
+ env_var: Some("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE".to_owned()),
config_key: None,
});
}
@@ -324,7 +326,7 @@ fn next_action_label(command: &str) -> String {
"--format"
| "--account-id"
| "--relay"
- | "--publish-mode"
+ | "--publish-transport"
| "--idempotency-key"
| "--correlation-id"
| "--approval-token"
@@ -602,14 +604,14 @@ mod tests {
fn failure_envelope_derives_operator_config_next_actions() {
let mut error = OutputError::new(
"operation_unavailable",
- "publish mode needs operator configuration",
+ "publish transport needs operator configuration",
CliExitCode::RuntimeUnavailable,
);
error.detail = Some(json!({
"actions": [
- "configure RADROOTS_CLI_RPC_BEARER_TOKEN",
+ "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID",
"configure signer.remote_nip46 signer_session_ref",
- "configure RADROOTS_CLI_RPC_BEARER_TOKEN"
+ "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
]
}));
let envelope = OutputEnvelope::failure(
@@ -624,11 +626,14 @@ mod tests {
envelope.next_actions[0].kind,
NextActionKind::OperatorConfig
);
- assert_eq!(envelope.next_actions[0].label, "configure rpc bearer token");
+ assert_eq!(
+ envelope.next_actions[0].label,
+ "configure radrootsd proxy token source"
+ );
assert_eq!(envelope.next_actions[0].command, None);
assert_eq!(
envelope.next_actions[0].env_var.as_deref(),
- Some("RADROOTS_CLI_RPC_BEARER_TOKEN")
+ Some("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE")
);
assert_eq!(
envelope.next_actions[1].kind,
diff --git a/src/registry/mod.rs b/src/registry/mod.rs
@@ -227,7 +227,8 @@ pub fn requires_local_signer_mode(operation_id: &str) -> bool {
)
}
-pub fn requires_nostr_relay_publish_mode(operation_id: &str) -> bool {
+#[cfg(test)]
+pub fn requires_direct_nostr_relay_publish_transport(operation_id: &str) -> bool {
matches!(
operation_id,
"sync.push"
@@ -260,8 +261,8 @@ mod tests {
use super::{
ApprovalPolicy, NetworkRequirement, OPERATION_REGISTRY, OperationRole, RiskLevel,
- get_operation, network_requirement, requires_local_signer_mode,
- requires_nostr_relay_publish_mode,
+ get_operation, network_requirement, requires_direct_nostr_relay_publish_transport,
+ requires_local_signer_mode,
};
const EXPECTED_OPERATION_IDS: &[&str] = &[
@@ -597,10 +598,12 @@ mod tests {
}
#[test]
- fn registry_nostr_relay_publish_requirements_are_explicit() {
+ fn registry_direct_nostr_relay_publish_requirements_are_explicit() {
let publish = OPERATION_REGISTRY
.iter()
- .filter(|operation| requires_nostr_relay_publish_mode(operation.operation_id))
+ .filter(|operation| {
+ requires_direct_nostr_relay_publish_transport(operation.operation_id)
+ })
.map(|operation| operation.operation_id)
.collect::<BTreeSet<_>>();
let expected = [
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -44,14 +44,16 @@ const ENV_CLI_ACCOUNT_SECRET_BACKEND: &str = "RADROOTS_CLI_ACCOUNT_SECRET_BACKEN
const ENV_CLI_ACCOUNT_SECRET_FALLBACK: &str = "RADROOTS_CLI_ACCOUNT_SECRET_FALLBACK";
const ENV_CLI_IDENTITY_PATH: &str = "RADROOTS_CLI_IDENTITY_PATH";
const ENV_CLI_SIGNER_BACKEND: &str = "RADROOTS_CLI_SIGNER_BACKEND";
-const ENV_CLI_PUBLISH_MODE: &str = "RADROOTS_CLI_PUBLISH_MODE";
+const ENV_CLI_PUBLISH_TRANSPORT: &str = "RADROOTS_CLI_PUBLISH_TRANSPORT";
const ENV_CLI_RELAYS_URLS: &str = "RADROOTS_CLI_RELAYS_URLS";
+const ENV_CLI_RADROOTSD_PROXY_URL: &str = "RADROOTS_CLI_RADROOTSD_PROXY_URL";
+const ENV_CLI_RADROOTSD_PROXY_TOKEN_FILE: &str = "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE";
+const ENV_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID: &str =
+ "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID";
const ENV_CLI_MYC_EXECUTABLE: &str = "RADROOTS_CLI_MYC_EXECUTABLE";
const ENV_CLI_MYC_STATUS_TIMEOUT_MS: &str = "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS";
const ENV_CLI_HYF_ENABLED: &str = "RADROOTS_CLI_HYF_ENABLED";
const ENV_CLI_HYF_EXECUTABLE: &str = "RADROOTS_CLI_HYF_EXECUTABLE";
-const ENV_CLI_RPC_URL: &str = "RADROOTS_CLI_RPC_URL";
-const ENV_CLI_RPC_BEARER_TOKEN: &str = "RADROOTS_CLI_RPC_BEARER_TOKEN";
const ENV_CLI_RHI_TRUSTED_WORKER_PUBKEYS: &str = "RADROOTS_CLI_RHI_TRUSTED_WORKER_PUBKEYS";
const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
ENV_CLI_OUTPUT_FORMAT,
@@ -65,14 +67,15 @@ const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
ENV_CLI_ACCOUNT_SECRET_FALLBACK,
ENV_CLI_IDENTITY_PATH,
ENV_CLI_SIGNER_BACKEND,
- ENV_CLI_PUBLISH_MODE,
+ ENV_CLI_PUBLISH_TRANSPORT,
ENV_CLI_RELAYS_URLS,
+ ENV_CLI_RADROOTSD_PROXY_URL,
+ ENV_CLI_RADROOTSD_PROXY_TOKEN_FILE,
+ ENV_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID,
ENV_CLI_MYC_EXECUTABLE,
ENV_CLI_MYC_STATUS_TIMEOUT_MS,
ENV_CLI_HYF_ENABLED,
ENV_CLI_HYF_EXECUTABLE,
- ENV_CLI_RPC_URL,
- ENV_CLI_RPC_BEARER_TOKEN,
ENV_CLI_RHI_TRUSTED_WORKER_PUBKEYS,
];
@@ -181,29 +184,29 @@ pub struct SignerConfig {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum PublishMode {
- NostrRelay,
- Radrootsd,
+pub enum PublishTransport {
+ DirectNostrRelay,
+ RadrootsdProxy,
}
-impl PublishMode {
+impl PublishTransport {
pub fn as_str(self) -> &'static str {
match self {
- Self::NostrRelay => "nostr_relay",
- Self::Radrootsd => "radrootsd",
+ Self::DirectNostrRelay => "direct_nostr_relay",
+ Self::RadrootsdProxy => "radrootsd_proxy",
}
}
pub fn transport_family(self) -> &'static str {
match self {
- Self::NostrRelay => "nostr_relay",
- Self::Radrootsd => "radrootsd",
+ Self::DirectNostrRelay => "direct_nostr_relay",
+ Self::RadrootsdProxy => "radrootsd_proxy",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum PublishModeSource {
+pub enum PublishTransportSource {
Flags,
Environment,
UserConfig,
@@ -211,7 +214,7 @@ pub enum PublishModeSource {
Defaults,
}
-impl PublishModeSource {
+impl PublishTransportSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Flags => "cli flags · local first",
@@ -225,8 +228,26 @@ impl PublishModeSource {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishConfig {
- pub mode: PublishMode,
- pub source: PublishModeSource,
+ pub transport: PublishTransport,
+ pub source: PublishTransportSource,
+ pub radrootsd_proxy: RadrootsdProxyConfig,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsdProxyConfig {
+ pub url: String,
+ pub token_file: Option<PathBuf>,
+ pub token_secret_id: Option<String>,
+}
+
+impl Default for RadrootsdProxyConfig {
+ fn default() -> Self {
+ Self {
+ url: DEFAULT_RPC_URL.to_owned(),
+ token_file: None,
+ token_secret_id: None,
+ }
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -359,7 +380,6 @@ pub struct CapabilityBindingInspection {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RpcConfig {
pub url: String,
- pub bridge_bearer_token: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -463,7 +483,16 @@ struct RelayFileConfig {
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct PublishFileConfig {
- mode: Option<String>,
+ transport: Option<String>,
+ radrootsd_proxy: Option<RadrootsdProxyFileConfig>,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct RadrootsdProxyFileConfig {
+ url: Option<String>,
+ token_file: Option<PathBuf>,
+ token_secret_id: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
@@ -518,8 +547,6 @@ struct CapabilityBindingSpec {
pub(crate) const SIGNER_REMOTE_NIP46_CAPABILITY: &str = "signer.remote_nip46";
pub(crate) const INFERENCE_HYF_STDIO_CAPABILITY: &str = "inference.hyf_stdio";
-pub(crate) const RADROOTSD_PUBLISH_DEFERRED_REASON: &str = "radrootsd publish mode is deferred for the active CLI buyer/seller workflow; use publish mode `nostr_relay` with local signer custody and configured relays";
-
const CAPABILITY_BINDING_SPECS: &[CapabilityBindingSpec] = &[
CapabilityBindingSpec {
capability_id: SIGNER_REMOTE_NIP46_CAPABILITY,
@@ -909,17 +936,14 @@ fn load_cli_config_file(path: &Path) -> Result<Option<CliConfigFile>, RuntimeErr
}
fn resolve_rpc_config(
- env: &dyn Environment,
- env_file: &EnvFileValues,
+ _env: &dyn Environment,
+ _env_file: &EnvFileValues,
user_config: Option<&CliConfigFile>,
workspace_config: Option<&CliConfigFile>,
) -> Result<RpcConfig, RuntimeError> {
- let url = env_value(env, env_file, &[ENV_CLI_RPC_URL])
- .or_else(|| {
- user_config
- .and_then(|config| config.rpc.as_ref())
- .and_then(|rpc| rpc.url.clone())
- })
+ let url = user_config
+ .and_then(|config| config.rpc.as_ref())
+ .and_then(|rpc| rpc.url.clone())
.or_else(|| {
workspace_config
.and_then(|config| config.rpc.as_ref())
@@ -929,7 +953,37 @@ fn resolve_rpc_config(
Ok(RpcConfig {
url: validate_rpc_url(url.as_str())?,
- bridge_bearer_token: env_value(env, env_file, &[ENV_CLI_RPC_BEARER_TOKEN]),
+ })
+}
+
+fn resolve_radrootsd_proxy_config(
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
+) -> Result<RadrootsdProxyConfig, RuntimeError> {
+ let user_proxy = user_config
+ .and_then(|config| config.publish.as_ref())
+ .and_then(|publish| publish.radrootsd_proxy.as_ref());
+ let workspace_proxy = workspace_config
+ .and_then(|config| config.publish.as_ref())
+ .and_then(|publish| publish.radrootsd_proxy.as_ref());
+ let url = env_value(env, env_file, &[ENV_CLI_RADROOTSD_PROXY_URL])
+ .or_else(|| user_proxy.and_then(|proxy| proxy.url.clone()))
+ .or_else(|| workspace_proxy.and_then(|proxy| proxy.url.clone()))
+ .unwrap_or_else(|| DEFAULT_RPC_URL.to_owned());
+ let token_file = env_value(env, env_file, &[ENV_CLI_RADROOTSD_PROXY_TOKEN_FILE])
+ .map(PathBuf::from)
+ .or_else(|| user_proxy.and_then(|proxy| proxy.token_file.clone()))
+ .or_else(|| workspace_proxy.and_then(|proxy| proxy.token_file.clone()));
+ let token_secret_id = env_value(env, env_file, &[ENV_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID])
+ .or_else(|| user_proxy.and_then(|proxy| proxy.token_secret_id.clone()))
+ .or_else(|| workspace_proxy.and_then(|proxy| proxy.token_secret_id.clone()));
+
+ Ok(RadrootsdProxyConfig {
+ url: validate_rpc_url(url.as_str())?,
+ token_file,
+ token_secret_id,
})
}
@@ -1206,43 +1260,50 @@ fn resolve_publish_config(
user_config: Option<&CliConfigFile>,
workspace_config: Option<&CliConfigFile>,
) -> Result<PublishConfig, RuntimeError> {
- if let Some(value) = args.publish_mode.clone() {
+ let radrootsd_proxy =
+ resolve_radrootsd_proxy_config(env, env_file, user_config, workspace_config)?;
+ if let Some(value) = args.publish_transport.clone() {
return Ok(PublishConfig {
- mode: parse_publish_mode("--publish-mode", value)?,
- source: PublishModeSource::Flags,
+ transport: parse_publish_transport("--publish-transport", value)?,
+ source: PublishTransportSource::Flags,
+ radrootsd_proxy,
});
}
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_PUBLISH_MODE]) {
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_PUBLISH_TRANSPORT]) {
return Ok(PublishConfig {
- mode: parse_publish_mode(key.as_str(), value)?,
- source: PublishModeSource::Environment,
+ transport: parse_publish_transport(key.as_str(), value)?,
+ source: PublishTransportSource::Environment,
+ radrootsd_proxy,
});
}
if let Some(value) = user_config
.and_then(|config| config.publish.as_ref())
- .and_then(|publish| publish.mode.clone())
+ .and_then(|publish| publish.transport.clone())
{
return Ok(PublishConfig {
- mode: parse_publish_mode("user config [publish].mode", value)?,
- source: PublishModeSource::UserConfig,
+ transport: parse_publish_transport("user config [publish].transport", value)?,
+ source: PublishTransportSource::UserConfig,
+ radrootsd_proxy,
});
}
if let Some(value) = workspace_config
.and_then(|config| config.publish.as_ref())
- .and_then(|publish| publish.mode.clone())
+ .and_then(|publish| publish.transport.clone())
{
return Ok(PublishConfig {
- mode: parse_publish_mode("workspace config [publish].mode", value)?,
- source: PublishModeSource::WorkspaceConfig,
+ transport: parse_publish_transport("workspace config [publish].transport", value)?,
+ source: PublishTransportSource::WorkspaceConfig,
+ radrootsd_proxy,
});
}
Ok(PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy,
})
}
@@ -1741,14 +1802,14 @@ fn parse_signer_mode(source: &str, value: String) -> Result<SignerBackend, Runti
}
}
-fn parse_publish_mode(source: &str, value: String) -> Result<PublishMode, RuntimeError> {
+fn parse_publish_transport(source: &str, value: String) -> Result<PublishTransport, RuntimeError> {
match value.trim().to_ascii_lowercase().as_str() {
- "nostr_relay" => Ok(PublishMode::NostrRelay),
- "radrootsd" => Ok(PublishMode::Radrootsd),
+ "direct_nostr_relay" => Ok(PublishTransport::DirectNostrRelay),
+ "radrootsd_proxy" => Ok(PublishTransport::RadrootsdProxy),
other => Err(RuntimeError::Config(format!(
"{source} must be `{}` or `{}`, got `{other}`",
- PublishMode::NostrRelay.as_str(),
- PublishMode::Radrootsd.as_str()
+ PublishTransport::DirectNostrRelay.as_str(),
+ PublishTransport::RadrootsdProxy.as_str()
))),
}
}
@@ -1853,7 +1914,7 @@ mod tests {
CapabilityBindingSource, CapabilityBindingTargetKind, DEFAULT_HYF_EXECUTABLE,
DEFAULT_LOG_FILTER, DEFAULT_MYC_STATUS_TIMEOUT_MS, DEFAULT_RPC_URL, EnvFileValues,
Environment, HyfConfig, INFERENCE_HYF_STDIO_CAPABILITY, InteractionConfig, OutputConfig,
- OutputFormat, PathsConfig, PublishConfig, PublishMode, PublishModeSource,
+ OutputFormat, PathsConfig, PublishConfig, PublishTransport, PublishTransportSource,
RelayConfigSource, RelayPublishPolicy, RuntimeConfig, SignerBackend, Verbosity,
parse_env_file_values,
};
@@ -1964,7 +2025,7 @@ mod tests {
log_stdout: true,
identity_path: Some(PathBuf::from("custom-identity.json")),
signer: Some("local".to_owned()),
- publish_mode: Some("nostr_relay".to_owned()),
+ publish_transport: Some("direct_nostr_relay".to_owned()),
relay: vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()],
myc_executable: Some(PathBuf::from("bin/myc-cli")),
myc_status_timeout_ms: Some(2500),
@@ -1982,8 +2043,8 @@ mod tests {
),
("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
(
- "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
- "radrootsd".to_owned(),
+ "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
+ "radrootsd_proxy".to_owned(),
),
(
"RADROOTS_CLI_RELAYS_URLS".to_owned(),
@@ -2088,8 +2149,9 @@ mod tests {
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Flags,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Flags,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
assert_eq!(
@@ -2133,8 +2195,8 @@ mod tests {
),
("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
(
- "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
- "radrootsd".to_owned(),
+ "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
+ "radrootsd_proxy".to_owned(),
),
(
"RADROOTS_CLI_RELAYS_URLS".to_owned(),
@@ -2197,8 +2259,9 @@ mod tests {
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::Radrootsd,
- source: PublishModeSource::Environment,
+ transport: PublishTransport::RadrootsdProxy,
+ source: PublishTransportSource::Environment,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
assert_eq!(
@@ -2281,7 +2344,10 @@ mod tests {
Some(RadrootsSecretBackend::EncryptedFile)
);
assert_eq!(resolved.signer.backend, SignerBackend::Local);
- assert_eq!(resolved.publish.mode, PublishMode::NostrRelay);
+ assert_eq!(
+ resolved.publish.transport,
+ PublishTransport::DirectNostrRelay
+ );
assert_eq!(resolved.relay.urls, Vec::<String>::new());
assert_eq!(resolved.myc.executable, PathBuf::from("myc"));
assert_eq!(
@@ -2294,7 +2360,6 @@ mod tests {
PathBuf::from(DEFAULT_HYF_EXECUTABLE)
);
assert_eq!(resolved.rpc.url, DEFAULT_RPC_URL);
- assert_eq!(resolved.rpc.bridge_bearer_token, None);
assert_eq!(resolved.rhi.trusted_worker_pubkeys, Vec::<String>::new());
}
@@ -2525,14 +2590,14 @@ path = "identity/from-toml.json"
);
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
+ "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
"relay".to_owned(),
)]));
let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("invalid publish mode");
- assert!(error.to_string().contains("RADROOTS_CLI_PUBLISH_MODE"));
- assert!(error.to_string().contains("nostr_relay"));
- assert!(error.to_string().contains("radrootsd"));
+ .expect_err("invalid publish transport");
+ assert!(error.to_string().contains("RADROOTS_CLI_PUBLISH_TRANSPORT"));
+ assert!(error.to_string().contains("direct_nostr_relay"));
+ assert!(error.to_string().contains("radrootsd_proxy"));
let args = RuntimeInvocationArgs {
myc_status_timeout_ms: Some(0),
@@ -2557,7 +2622,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false
RADROOTS_CLI_ACCOUNT_SELECTOR=acct_env_file
RADROOTS_CLI_IDENTITY_PATH=state/identity.json
RADROOTS_CLI_SIGNER_BACKEND=myc
-RADROOTS_CLI_PUBLISH_MODE=radrootsd
+RADROOTS_CLI_PUBLISH_TRANSPORT=radrootsd_proxy
RADROOTS_CLI_RELAYS_URLS=wss://relay.env-file
RADROOTS_CLI_MYC_EXECUTABLE=bin/myc
RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS=4500
@@ -2583,8 +2648,9 @@ RADROOTS_CLI_HYF_EXECUTABLE=bin/hyfd
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::Radrootsd,
- source: PublishModeSource::Environment,
+ transport: PublishTransport::RadrootsdProxy,
+ source: PublishTransportSource::Environment,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
assert_eq!(resolved.relay.urls, vec!["wss://relay.env-file".to_owned()]);
@@ -2740,7 +2806,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false
}
#[test]
- fn publish_mode_precedence_tracks_source() {
+ fn publish_transport_precedence_tracks_source() {
let temp = tempdir().expect("tempdir");
let workspace_root = temp.path().join("workspace");
let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
@@ -2750,12 +2816,12 @@ RADROOTS_CLI_LOGGING_STDOUT=false
fs::create_dir_all(&app_config_dir).expect("app config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[publish]\nmode = \"radrootsd\"\n",
+ "[publish]\ntransport = \"radrootsd_proxy\"\n",
)
.expect("write workspace config");
fs::write(
app_config_dir.join("config.toml"),
- "[publish]\nmode = \"nostr_relay\"\n",
+ "[publish]\ntransport = \"direct_nostr_relay\"\n",
)
.expect("write user config");
@@ -2764,21 +2830,22 @@ RADROOTS_CLI_LOGGING_STDOUT=false
repo_local_root.clone(),
user_home.clone(),
BTreeMap::from([(
- "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
- "radrootsd".to_owned(),
+ "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
+ "radrootsd_proxy".to_owned(),
)]),
);
let args = RuntimeInvocationArgs {
- publish_mode: Some("nostr_relay".to_owned()),
+ publish_transport: Some("direct_nostr_relay".to_owned()),
..runtime_args()
};
let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve flag publish mode");
+ .expect("resolve flag publish transport");
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Flags,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Flags,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
@@ -2787,18 +2854,19 @@ RADROOTS_CLI_LOGGING_STDOUT=false
repo_local_root.clone(),
user_home.clone(),
BTreeMap::from([(
- "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
- "radrootsd".to_owned(),
+ "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
+ "radrootsd_proxy".to_owned(),
)]),
);
let resolved =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve environment publish mode");
+ .expect("resolve environment publish transport");
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::Radrootsd,
- source: PublishModeSource::Environment,
+ transport: PublishTransport::RadrootsdProxy,
+ source: PublishTransportSource::Environment,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
@@ -2810,12 +2878,13 @@ RADROOTS_CLI_LOGGING_STDOUT=false
);
let resolved =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve user publish mode");
+ .expect("resolve user publish transport");
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::UserConfig,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::UserConfig,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
@@ -2828,12 +2897,13 @@ RADROOTS_CLI_LOGGING_STDOUT=false
);
let resolved =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve workspace publish mode");
+ .expect("resolve workspace publish transport");
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::Radrootsd,
- source: PublishModeSource::WorkspaceConfig,
+ transport: PublishTransport::RadrootsdProxy,
+ source: PublishTransportSource::WorkspaceConfig,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
@@ -2841,12 +2911,13 @@ RADROOTS_CLI_LOGGING_STDOUT=false
let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
let resolved =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve default publish mode");
+ .expect("resolve default publish transport");
assert_eq!(
resolved.publish,
PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
}
);
}
@@ -3042,18 +3113,18 @@ RADROOTS_CLI_LOGGING_STDOUT=false
fs::create_dir_all(&repo_local_root).expect("workspace config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[publish]\nmode = \"nostr\"\n",
+ "[publish]\ntransport = \"nostr\"\n",
)
.expect("write workspace config");
let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
let error =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect_err("invalid publish mode");
+ .expect_err("invalid publish transport");
let message = error.to_string();
- assert!(message.contains("workspace config [publish].mode"));
- assert!(message.contains("nostr_relay"));
- assert!(message.contains("radrootsd"));
+ assert!(message.contains("workspace config [publish].transport"));
+ assert!(message.contains("direct_nostr_relay"));
+ assert!(message.contains("radrootsd_proxy"));
}
#[test]
diff --git a/src/runtime/direct_relay.rs b/src/runtime/direct_relay.rs
@@ -1,10 +1,8 @@
use std::time::Duration;
-use radroots_events_codec::wire::WireEventParts;
-use radroots_identity::RadrootsIdentity;
use radroots_nostr::prelude::{
RadrootsNostrClient, RadrootsNostrError, RadrootsNostrEvent, RadrootsNostrFilter,
- RadrootsNostrOutput, radroots_nostr_build_event,
+ RadrootsNostrOutput,
};
const RELAY_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -17,18 +15,6 @@ pub struct DirectRelayFailure {
}
#[derive(Debug, Clone)]
-pub struct DirectRelayPublishReceipt {
- pub event: RadrootsNostrEvent,
- pub event_id: String,
- pub created_at: u32,
- pub signature: String,
- pub target_relays: Vec<String>,
- pub connected_relays: Vec<String>,
- pub acknowledged_relays: Vec<String>,
- pub failed_relays: Vec<DirectRelayFailure>,
-}
-
-#[derive(Debug, Clone)]
pub struct DirectRelayFetchReceipt {
pub target_relays: Vec<String>,
pub connected_relays: Vec<String>,
@@ -37,39 +23,6 @@ pub struct DirectRelayFetchReceipt {
}
#[derive(Debug, thiserror::Error)]
-pub enum DirectRelayPublishError {
- #[error("direct relay publish requires at least one configured relay")]
- MissingRelays,
- #[error("failed to build async runtime for direct relay publish: {0}")]
- Runtime(String),
- #[error("failed to build Nostr event for direct relay publish: {0}")]
- Build(#[source] RadrootsNostrError),
- #[error("failed to sign Nostr event for direct relay publish: {0}")]
- Sign(#[source] RadrootsNostrError),
- #[error("failed to configure relay `{relay}` for direct relay publish: {source}")]
- RelayConfig {
- relay: String,
- #[source]
- source: RadrootsNostrError,
- },
- #[error("direct relay connection failed: {reason}")]
- Connect {
- reason: String,
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- failed_relays: Vec<DirectRelayFailure>,
- },
- #[error("direct relay publish failed for event `{event_id}`: {reason}")]
- Publish {
- event_id: String,
- reason: String,
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- failed_relays: Vec<DirectRelayFailure>,
- },
-}
-
-#[derive(Debug, thiserror::Error)]
pub enum DirectRelayFetchError {
#[error("direct relay fetch requires at least one configured relay")]
MissingRelays,
@@ -91,25 +44,6 @@ pub enum DirectRelayFetchError {
Fetch(#[source] RadrootsNostrError),
}
-pub fn publish_signed_event_with_identity(
- identity: &RadrootsIdentity,
- relay_urls: &[String],
- event: RadrootsNostrEvent,
-) -> Result<DirectRelayPublishReceipt, DirectRelayPublishError> {
- if relay_urls.is_empty() {
- return Err(DirectRelayPublishError::MissingRelays);
- }
-
- let runtime = tokio::runtime::Builder::new_multi_thread()
- .enable_all()
- .build()
- .map_err(|error| DirectRelayPublishError::Runtime(error.to_string()))?;
-
- runtime.block_on(publish_signed_event_with_identity_async(
- identity, relay_urls, event,
- ))
-}
-
pub fn fetch_events_from_relays(
relay_urls: &[String],
filter: RadrootsNostrFilter,
@@ -183,90 +117,6 @@ async fn fetch_events_from_relays_async(
})
}
-async fn publish_signed_event_with_identity_async(
- identity: &RadrootsIdentity,
- relay_urls: &[String],
- event: RadrootsNostrEvent,
-) -> Result<DirectRelayPublishReceipt, DirectRelayPublishError> {
- let event_id = event.id.to_hex();
- let created_at = event_created_at_u32(&event);
- let signature = event.sig.to_string();
- let client = RadrootsNostrClient::from_identity(identity);
-
- for relay_url in relay_urls {
- client.add_write_relay(relay_url).await.map_err(|source| {
- DirectRelayPublishError::RelayConfig {
- relay: relay_url.clone(),
- source,
- }
- })?;
- }
-
- let connection_output = client.try_connect(RELAY_CONNECT_TIMEOUT).await;
- let connected_relays = connection_output
- .success
- .iter()
- .map(ToString::to_string)
- .collect::<Vec<_>>();
- let connection_failed_relays = relay_failures_from_output(&connection_output);
- if connection_output.success.is_empty() {
- return Err(DirectRelayPublishError::Connect {
- reason: summarize_failures(&connection_failed_relays),
- target_relays: relay_urls.to_vec(),
- connected_relays,
- failed_relays: connection_failed_relays,
- });
- }
-
- let publish_output =
- client
- .send_event(&event)
- .await
- .map_err(|source| DirectRelayPublishError::Publish {
- event_id: event_id.clone(),
- reason: source.to_string(),
- target_relays: relay_urls.to_vec(),
- connected_relays: connected_relays.clone(),
- failed_relays: Vec::new(),
- })?;
- let failed_relays = relay_failures_from_output(&publish_output);
- if publish_output.success.is_empty() {
- return Err(DirectRelayPublishError::Publish {
- event_id: event_id.clone(),
- reason: summarize_failures(&failed_relays),
- target_relays: relay_urls.to_vec(),
- connected_relays,
- failed_relays,
- });
- }
-
- Ok(DirectRelayPublishReceipt {
- event,
- event_id,
- created_at,
- signature,
- target_relays: relay_urls.to_vec(),
- connected_relays,
- acknowledged_relays: publish_output
- .success
- .iter()
- .map(ToString::to_string)
- .collect(),
- failed_relays,
- })
-}
-
-pub fn sign_parts_with_identity(
- identity: &RadrootsIdentity,
- parts: WireEventParts,
-) -> Result<RadrootsNostrEvent, DirectRelayPublishError> {
- let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .map_err(DirectRelayPublishError::Build)?;
- builder
- .sign_with_keys(identity.keys())
- .map_err(|error| DirectRelayPublishError::Sign(error.into()))
-}
-
fn relay_failures_from_output<T: std::fmt::Debug>(
output: &RadrootsNostrOutput<T>,
) -> Vec<DirectRelayFailure> {
@@ -292,61 +142,18 @@ fn summarize_failures(failed_relays: &[DirectRelayFailure]) -> String {
.join("; ")
}
-fn event_created_at_u32(event: &radroots_nostr::prelude::RadrootsNostrEvent) -> u32 {
- u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX)
-}
-
#[cfg(test)]
mod tests {
use std::time::Duration;
- use radroots_events_codec::wire::WireEventParts;
- use radroots_identity::RadrootsIdentity;
use radroots_nostr::prelude::RadrootsNostrFilter;
use super::{
- DirectRelayFetchError, event_created_at_u32, fetch_events_from_relays_async,
- fetch_events_from_relays_with_timeout, sign_parts_with_identity,
+ DirectRelayFetchError, fetch_events_from_relays_async,
+ fetch_events_from_relays_with_timeout,
};
#[test]
- fn direct_relay_signed_event_preserves_publish_receipt_parity() {
- let identity = RadrootsIdentity::generate();
- let parts = WireEventParts {
- kind: 30402,
- content: "listing".to_owned(),
- tags: vec![
- vec!["d".to_owned(), "listing-1".to_owned()],
- vec!["title".to_owned(), "eggs".to_owned()],
- ],
- };
- let event = sign_parts_with_identity(&identity, parts.clone()).expect("signed event");
- let receipt = super::DirectRelayPublishReceipt {
- event: event.clone(),
- event_id: event.id.to_hex(),
- created_at: event_created_at_u32(&event),
- signature: event.sig.to_string(),
- target_relays: vec!["ws://127.0.0.1:1234".to_owned()],
- connected_relays: vec!["ws://127.0.0.1:1234".to_owned()],
- acknowledged_relays: vec!["ws://127.0.0.1:1234".to_owned()],
- failed_relays: Vec::new(),
- };
- let tags = receipt
- .event
- .tags
- .iter()
- .map(|tag| tag.as_slice().to_vec())
- .collect::<Vec<_>>();
-
- assert_eq!(receipt.event_id, receipt.event.id.to_hex());
- assert_eq!(receipt.signature, receipt.event.sig.to_string());
- assert_eq!(receipt.created_at, event_created_at_u32(&receipt.event));
- assert_eq!(receipt.event.kind.as_u16() as u32, parts.kind);
- assert_eq!(receipt.event.content, parts.content);
- assert_eq!(tags, parts.tags);
- }
-
- #[test]
fn fetch_events_requires_relays_before_runtime_work() {
let err = fetch_events_from_relays_with_timeout(
&[],
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -13,7 +13,7 @@ use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_sdk::{
FarmEnqueuePublishRequest, FarmEnqueueReceipt, FarmPreparePublishRequest, FarmPublishPlan,
PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRelayOutcomeKind,
- PushOutboxRequest, SdkMutationState, SdkRelayTargetPolicy,
+ PushOutboxRequest, SdkMutationState,
};
use serde_json::json;
@@ -23,15 +23,15 @@ use crate::cli::global::{
};
use crate::runtime::RuntimeError;
use crate::runtime::account::{self, AccountRecordView};
-use crate::runtime::config::{
- PublishMode, RADROOTSD_PUBLISH_DEFERRED_REASON, RuntimeConfig, SignerBackend,
-};
+use crate::runtime::config::{PublishTransport, RuntimeConfig, SignerBackend};
use crate::runtime::farm_config::{
self, FarmConfigDocument, FarmConfigScope, FarmConfigSelection, FarmListingDefaults,
FarmMissingField, FarmPublicationStatus, ResolvedFarmConfig, SUPPORTED_FARM_CONFIG_VERSION,
};
use crate::runtime::local_events::append_local_work;
-use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_relay_url_policy};
+use crate::runtime::sdk::{
+ CliSdkAdapterError, CliSdkSession, sdk_relay_target_policy, sdk_relay_url_policy,
+};
use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority};
use crate::view::runtime::{
FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView,
@@ -43,13 +43,10 @@ use crate::view::runtime::{
const FARM_CONFIG_SOURCE: &str = "farm config · local first";
const FARM_SELLER_ACTOR_SOURCE: &str = "farm_config";
const SDK_FARM_WRITE_SOURCE: &str = "SDK farm publish · local key";
-const RADROOTSD_FARM_WRITE_SOURCE: &str = "radrootsd publish transport · deferred";
const SDK_PROFILE_NOT_SUBMITTED_METHOD: &str = "sdk.farm.profile.not_submitted";
const SDK_FARM_PUBLISH_METHOD: &str = "sdk.farm.publish.v1";
const SDK_PROFILE_NOT_SUBMITTED_REASON: &str =
"profile publish is not part of SDK farm.publish.v1; profile draft was not submitted";
-const RADROOTSD_BRIDGE_PROFILE_PUBLISH_METHOD: &str = "bridge.profile.publish";
-const RADROOTSD_BRIDGE_FARM_PUBLISH_METHOD: &str = "bridge.farm.publish";
static D_TAG_COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -358,7 +355,7 @@ pub fn status(
config_valid: false,
account_state: "not_checked".to_owned(),
listing_defaults_state: "missing".to_owned(),
- publish_mode: config.publish.mode.as_str().to_owned(),
+ publish_transport: config.publish.transport.as_str().to_owned(),
publish_state: "not_checked".to_owned(),
publish_executable: false,
publish_reason: None,
@@ -423,7 +420,7 @@ pub fn status(
config_valid: true,
account_state: account_state.to_owned(),
listing_defaults_state: listing_defaults_state.to_owned(),
- publish_mode: config.publish.mode.as_str().to_owned(),
+ publish_transport: config.publish.transport.as_str().to_owned(),
publish_state: publish.state.to_owned(),
publish_executable: publish.executable,
publish_reason: publish.reason,
@@ -499,22 +496,21 @@ fn farm_publish_readiness(
config: &RuntimeConfig,
account: &AccountRecordView,
) -> FarmPublishReadiness {
- match config.publish.mode {
- PublishMode::NostrRelay => relay_farm_publish_readiness(config, account),
- PublishMode::Radrootsd => radrootsd_farm_publish_readiness(config),
- }
+ relay_farm_publish_readiness(config, account)
}
fn relay_farm_publish_readiness(
config: &RuntimeConfig,
account: &AccountRecordView,
) -> FarmPublishReadiness {
- if config.relay.urls.is_empty() {
+ if matches!(config.publish.transport, PublishTransport::DirectNostrRelay)
+ && config.relay.urls.is_empty()
+ {
return FarmPublishReadiness {
state: "unconfigured",
executable: false,
reason: Some(
- "nostr_relay farm publish requires at least one configured relay".to_owned(),
+ "direct_nostr_relay farm publish requires at least one configured relay".to_owned(),
),
missing: vec!["Configured relay".to_owned()],
actions: vec!["radroots --relay wss://relay.example.com farm publish".to_owned()],
@@ -526,7 +522,7 @@ fn relay_farm_publish_readiness(
state: "unavailable",
executable: false,
reason: Some(
- "nostr_relay farm publish requires signer mode `local`; signer mode `myc` is deferred"
+ "direct_nostr_relay farm publish requires signer mode `local`; signer mode `myc` is deferred"
.to_owned(),
),
missing: vec!["Local signer mode".to_owned()],
@@ -558,19 +554,6 @@ fn relay_farm_publish_readiness(
}
}
-fn radrootsd_farm_publish_readiness(_config: &RuntimeConfig) -> FarmPublishReadiness {
- FarmPublishReadiness {
- state: "unavailable",
- executable: false,
- reason: Some(RADROOTSD_PUBLISH_DEFERRED_REASON.to_owned()),
- missing: vec!["Active direct relay publish mode".to_owned()],
- actions: vec![
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com farm publish"
- .to_owned(),
- ],
- }
-}
-
pub fn publish(
config: &RuntimeConfig,
args: &FarmPublishArgs,
@@ -639,53 +622,15 @@ pub fn publish(
let profile_idempotency_key = component_idempotency_key(args, "profile")?;
let farm_idempotency_key = component_idempotency_key(args, "farm")?;
- if config.output.dry_run {
- return match config.publish.mode {
- PublishMode::NostrRelay => publish_via_sdk(
- config,
- args,
- resolved,
- account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- ),
- PublishMode::Radrootsd => Ok(radrootsd_preflight_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- "unavailable",
- RADROOTSD_PUBLISH_DEFERRED_REASON,
- )),
- };
- }
-
- match config.publish.mode {
- PublishMode::NostrRelay => publish_via_sdk(
- config,
- args,
- resolved,
- account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- ),
- PublishMode::Radrootsd => Ok(radrootsd_preflight_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- "unavailable",
- RADROOTSD_PUBLISH_DEFERRED_REASON,
- )),
- }
+ publish_via_sdk(
+ config,
+ args,
+ resolved,
+ account_pubkey,
+ previews,
+ profile_idempotency_key,
+ farm_idempotency_key,
+ )
}
fn publish_via_sdk(
@@ -742,11 +687,8 @@ fn publish_via_sdk(
resolved.document.selection.account.as_str(),
account_pubkey.as_str(),
)?;
- let mut request = FarmEnqueuePublishRequest::new(
- input.actor,
- input.farm,
- SdkRelayTargetPolicy::UseConfiguredRelays,
- );
+ let mut request =
+ FarmEnqueuePublishRequest::new(input.actor, input.farm, sdk_relay_target_policy(config));
if let Some(idempotency_key) = farm_idempotency_key.as_deref() {
request = request.try_with_idempotency_key(idempotency_key)?;
}
@@ -795,13 +737,6 @@ struct FarmPublishEventDraft {
event: FarmPublishEventView,
}
-impl FarmPublishView {
- fn with_requested_signer_session_id(mut self, signer_session_id: Option<String>) -> Self {
- self.requested_signer_session_id = signer_session_id;
- self
- }
-}
-
fn missing_publish_view(
config: &RuntimeConfig,
scope: FarmConfigScope,
@@ -827,7 +762,6 @@ fn missing_publish_view(
seller_pubkey,
seller_actor_source: FARM_SELLER_ACTOR_SOURCE.to_owned(),
farm_d_tag,
- requested_signer_session_id: args.signer_session_id.clone(),
profile: not_submitted_component(
profile_publish_rpc_method(config),
KIND_PROFILE,
@@ -879,7 +813,7 @@ fn resolve_farm_signing_identity(
fn base_publish_view(
state: &str,
config: &RuntimeConfig,
- args: &FarmPublishArgs,
+ _args: &FarmPublishArgs,
resolved: &ResolvedFarmConfig,
account_pubkey: &str,
profile: FarmPublishComponentView,
@@ -898,7 +832,6 @@ fn base_publish_view(
seller_pubkey: account_pubkey.to_owned(),
seller_actor_source: FARM_SELLER_ACTOR_SOURCE.to_owned(),
farm_d_tag: resolved.document.selection.farm_d_tag.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
profile,
farm,
local_replica: Vec::new(),
@@ -965,7 +898,6 @@ fn preview_component(
job_id: None,
job_status: None,
signer_mode: None,
- signer_session_id: None,
event_id: None,
event_addr: event.as_ref().and_then(|event| event.event_addr.clone()),
idempotency_key: idempotency_key.clone(),
@@ -1297,66 +1229,6 @@ fn profile_not_submitted_component(
}
}
-fn deferred_component(
- rpc_method: &str,
- event_kind: u32,
- idempotency_key: Option<String>,
- args: &FarmPublishArgs,
- reason: &str,
- event: Option<FarmPublishEventView>,
-) -> FarmPublishComponentView {
- FarmPublishComponentView {
- state: "unavailable".to_owned(),
- signer_mode: Some("deferred".to_owned()),
- signer_session_id: None,
- reason: Some(reason.to_owned()),
- ..preview_component(rpc_method, event_kind, idempotency_key, args, event)
- }
-}
-
-fn radrootsd_preflight_publish_view(
- config: &RuntimeConfig,
- args: &FarmPublishArgs,
- resolved: &ResolvedFarmConfig,
- account_pubkey: &str,
- previews: FarmPublishPreviews,
- profile_idempotency_key: Option<String>,
- farm_idempotency_key: Option<String>,
- state: &str,
- reason: &str,
-) -> FarmPublishView {
- let requested_signer_session_id = args.signer_session_id.clone();
- base_publish_view(
- state,
- config,
- args,
- resolved,
- account_pubkey,
- deferred_component(
- RADROOTSD_BRIDGE_PROFILE_PUBLISH_METHOD,
- KIND_PROFILE,
- profile_idempotency_key,
- args,
- reason,
- Some(previews.profile.event),
- ),
- deferred_component(
- RADROOTSD_BRIDGE_FARM_PUBLISH_METHOD,
- KIND_FARM,
- farm_idempotency_key,
- args,
- reason,
- None,
- ),
- Some(reason.to_owned()),
- vec![
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com farm publish"
- .to_owned(),
- ],
- )
- .with_requested_signer_session_id(requested_signer_session_id)
-}
-
fn persist_farm_publication(
config: &RuntimeConfig,
resolved: &mut ResolvedFarmConfig,
@@ -1385,24 +1257,18 @@ fn persist_publication(
}
fn farm_write_source(config: &RuntimeConfig) -> &'static str {
- match config.publish.mode {
- PublishMode::NostrRelay => SDK_FARM_WRITE_SOURCE,
- PublishMode::Radrootsd => RADROOTSD_FARM_WRITE_SOURCE,
- }
+ let _ = config;
+ SDK_FARM_WRITE_SOURCE
}
fn profile_publish_rpc_method(config: &RuntimeConfig) -> &'static str {
- match config.publish.mode {
- PublishMode::NostrRelay => SDK_PROFILE_NOT_SUBMITTED_METHOD,
- PublishMode::Radrootsd => RADROOTSD_BRIDGE_PROFILE_PUBLISH_METHOD,
- }
+ let _ = config;
+ SDK_PROFILE_NOT_SUBMITTED_METHOD
}
fn farm_publish_rpc_method(config: &RuntimeConfig) -> &'static str {
- match config.publish.mode {
- PublishMode::NostrRelay => SDK_FARM_PUBLISH_METHOD,
- PublishMode::Radrootsd => RADROOTSD_BRIDGE_FARM_PUBLISH_METHOD,
- }
+ let _ = config;
+ SDK_FARM_PUBLISH_METHOD
}
fn selected_account_for_draft(
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -23,17 +23,13 @@ use radroots_events::listing::{
use radroots_events::trade_validation::RadrootsTradeValidationListingError;
use radroots_events_codec::d_tag::is_d_tag_base64url;
use radroots_events_codec::listing::encode::to_wire_parts_with_kind;
-use radroots_events_codec::wire::WireEventParts;
use radroots_local_events::{LocalEventRecord, LocalRecordFamily, SourceRuntime};
-use radroots_nostr::prelude::{
- RadrootsNostrEvent as SignedNostrEvent, RadrootsNostrKeys, radroots_event_from_nostr,
-};
-use radroots_replica_db::{ReplicaSql, migrations};
-use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
+use radroots_nostr::prelude::RadrootsNostrKeys;
+use radroots_replica_db::ReplicaSql;
use radroots_sdk::{
ListingEnqueuePublishRequest, ListingEnqueueReceipt, ListingPreparePublishRequest,
ListingPublishPlan, PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt,
- PushOutboxRelayOutcomeKind, PushOutboxRequest, SdkMutationState, SdkRelayTargetPolicy,
+ PushOutboxRelayOutcomeKind, PushOutboxRequest, SdkMutationState,
};
use radroots_sql_core::SqliteExecutor;
use radroots_trade::listing::{RadrootsListingDraftDocumentV1, validation::validate_listing_event};
@@ -46,39 +42,31 @@ use crate::cli::global::{
};
use crate::runtime::RuntimeError;
use crate::runtime::account;
-use crate::runtime::config::{
- PublishMode, RADROOTSD_PUBLISH_DEFERRED_REASON, RuntimeConfig, SignerBackend,
-};
-use crate::runtime::direct_relay::{
- DirectRelayFailure, DirectRelayPublishError, DirectRelayPublishReceipt,
- publish_signed_event_with_identity, sign_parts_with_identity,
-};
+use crate::runtime::config::{RuntimeConfig, SignerBackend};
use crate::runtime::farm_config;
use crate::runtime::local_events::{
- append_local_work, append_signed_event, get_shared_record, list_shared_records_before,
- list_shared_records_latest, mark_signed_event_acknowledged,
- mark_signed_event_failed_for_publish_error, shared_local_events_db_path,
+ append_local_work, get_shared_record, list_shared_records_before, list_shared_records_latest,
+ shared_local_events_db_path,
+};
+use crate::runtime::sdk::{
+ CliSdkAdapterError, CliSdkSession, sdk_relay_target_policy, sdk_relay_url_policy,
};
-use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_relay_url_policy};
-use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority};
use crate::runtime::sync::{
RelayIngestScope, freshness_for_scope_from_executor, market_refresh, missing_freshness,
};
use crate::view::runtime::{
FindPriceView, FindQuantityView, FindResultProvenanceView, ListingAppRecordExportView,
ListingAppRecordListView, ListingAppRecordSummaryView, ListingGetView, ListingListView,
- ListingMutationEventView, ListingMutationLocalReplicaView, ListingMutationView, ListingNewView,
- ListingRebindView, ListingSummaryView, ListingValidateView, ListingValidationIssueView,
- MarketReadinessView, RelayFailureView,
+ ListingMutationEventView, ListingMutationView, ListingNewView, ListingRebindView,
+ ListingSummaryView, ListingValidateView, ListingValidationIssueView, MarketReadinessView,
+ RelayFailureView,
};
const DRAFT_KIND: &str = "listing_draft_v1";
const LISTING_SOURCE: &str = "local draft · local first";
const LISTING_READ_SOURCE: &str = "local replica · local first";
const LISTING_APP_RECORD_SOURCE: &str = "shared local events · app";
-const RELAY_LISTING_WRITE_SOURCE: &str = "direct Nostr relay publish · local key";
const SDK_LISTING_WRITE_SOURCE: &str = "SDK listing publish · local key";
-const RADROOTSD_LISTING_WRITE_SOURCE: &str = "radrootsd publish transport · deferred";
const LISTING_DRAFTS_DIR: &str = "listings/drafts";
const LISTING_SELLER_ACTOR_SOURCE_FARM_CONFIG: &str = "farm_config";
const LISTING_SELLER_ACTOR_SOURCE_RESOLVED_ACCOUNT: &str = "resolved_account";
@@ -245,12 +233,6 @@ struct CanonicalListingDraft {
}
#[derive(Debug, Clone)]
-struct ListingMutationEventDraft {
- event: ListingMutationEventView,
- parts: WireEventParts,
-}
-
-#[derive(Debug, Clone)]
struct SdkListingPublishInput {
canonical: CanonicalListingDraft,
actor: RadrootsActorContext,
@@ -1770,6 +1752,7 @@ pub fn publish_via_sdk(
return Ok(sdk_prepared_publish_view(
config,
args,
+ ListingMutationOperation::Publish,
&input.canonical,
plan,
));
@@ -1780,7 +1763,7 @@ pub fn publish_via_sdk(
let mut request = ListingEnqueuePublishRequest::from_document(
input.actor,
input.document,
- SdkRelayTargetPolicy::UseConfiguredRelays,
+ sdk_relay_target_policy(config),
);
if let Some(idempotency_key) = args.idempotency_key.as_deref() {
request = request.try_with_idempotency_key(idempotency_key)?;
@@ -1803,6 +1786,7 @@ pub fn publish_via_sdk(
Ok(sdk_enqueued_publish_view(
config,
args,
+ ListingMutationOperation::Publish,
&input.canonical,
enqueue_receipt,
push_receipt,
@@ -1870,6 +1854,7 @@ fn sdk_listing_signer(
fn sdk_prepared_publish_view(
config: &RuntimeConfig,
args: &ListingMutationArgs,
+ operation: ListingMutationOperation,
canonical: &CanonicalListingDraft,
plan: ListingPublishPlan,
) -> ListingMutationView {
@@ -1877,7 +1862,7 @@ fn sdk_prepared_publish_view(
let event = sdk_plan_event_view(&plan);
ListingMutationView {
state: "dry_run".to_owned(),
- operation: ListingMutationOperation::Publish.as_str().to_owned(),
+ operation: operation.as_str().to_owned(),
source: SDK_LISTING_WRITE_SOURCE.to_owned(),
file: args.file.display().to_string(),
listing_id: canonical.listing_id.clone(),
@@ -1898,8 +1883,6 @@ fn sdk_prepared_publish_view(
event_id: Some(plan.expected_event_id.as_str().to_owned()),
event_addr: Some(listing_addr),
idempotency_key: args.idempotency_key.clone(),
- signer_session_id: None,
- requested_signer_session_id: args.signer_session_id.clone(),
local_replica: None,
reason: Some("dry run requested; SDK enqueue and relay push skipped".to_owned()),
job: None,
@@ -1911,6 +1894,7 @@ fn sdk_prepared_publish_view(
fn sdk_enqueued_publish_view(
config: &RuntimeConfig,
args: &ListingMutationArgs,
+ operation: ListingMutationOperation,
canonical: &CanonicalListingDraft,
enqueue: ListingEnqueueReceipt,
push: Option<PushOutboxReceipt>,
@@ -1934,7 +1918,7 @@ fn sdk_enqueued_publish_view(
let listing_addr = enqueue.public_listing_addr.as_str().to_owned();
ListingMutationView {
state,
- operation: ListingMutationOperation::Publish.as_str().to_owned(),
+ operation: operation.as_str().to_owned(),
source: SDK_LISTING_WRITE_SOURCE.to_owned(),
file: args.file.display().to_string(),
listing_id: canonical.listing_id.clone(),
@@ -1955,8 +1939,6 @@ fn sdk_enqueued_publish_view(
event_id: Some(event_id),
event_addr: Some(listing_addr),
idempotency_key: args.idempotency_key.clone(),
- signer_session_id: None,
- requested_signer_session_id: args.signer_session_id.clone(),
local_replica: None,
reason,
job: None,
@@ -2114,14 +2096,14 @@ fn sdk_relay_outcome_kind(kind: PushOutboxRelayOutcomeKind) -> &'static str {
pub fn update(
config: &RuntimeConfig,
args: &ListingMutationArgs,
-) -> Result<ListingMutationView, RuntimeError> {
+) -> Result<ListingMutationView, CliSdkAdapterError> {
mutate(config, args, ListingMutationOperation::Update)
}
pub fn archive(
config: &RuntimeConfig,
args: &ListingMutationArgs,
-) -> Result<ListingMutationView, RuntimeError> {
+) -> Result<ListingMutationView, CliSdkAdapterError> {
mutate(config, args, ListingMutationOperation::Archive)
}
@@ -2129,8 +2111,8 @@ fn mutate(
config: &RuntimeConfig,
args: &ListingMutationArgs,
operation: ListingMutationOperation,
-) -> Result<ListingMutationView, RuntimeError> {
- let contents = fs::read_to_string(&args.file)?;
+) -> Result<ListingMutationView, CliSdkAdapterError> {
+ let contents = fs::read_to_string(&args.file).map_err(RuntimeError::from)?;
let parsed = toml::from_str::<ListingDraftDocument>(&contents).map_err(|error| {
RuntimeError::Config(format!(
"invalid listing draft {}: {error}",
@@ -2170,228 +2152,72 @@ fn mutate(
});
}
- let (event_draft, listing_addr) = build_listing_event_draft(&canonical)?;
-
- if config.output.dry_run
- && matches!(config.publish.mode, PublishMode::NostrRelay)
- && matches!(config.signer.backend, SignerBackend::Local)
- {
+ if config.output.dry_run && matches!(config.signer.backend, SignerBackend::Local) {
validate_local_listing_signer(config, &canonical)?;
}
- if config.output.dry_run {
- let requested_signer_session_id = match config.publish.mode {
- PublishMode::NostrRelay => args.signer_session_id.clone(),
- PublishMode::Radrootsd => {
- return Ok(radrootsd_preflight_view(
- config,
- args,
- operation,
- &canonical,
- listing_addr,
- event_draft.event,
- "unavailable",
- RADROOTSD_PUBLISH_DEFERRED_REASON,
- ));
- }
- };
- return Ok(ListingMutationView {
- state: "dry_run".to_owned(),
- operation: operation.as_str().to_owned(),
- source: listing_write_source(config).to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr: listing_addr.clone(),
- seller_account_id: canonical.seller_account_id.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_LISTING,
- dry_run: true,
- deduplicated: false,
- target_relays: Vec::new(),
- connected_relays: Vec::new(),
- acknowledged_relays: Vec::new(),
- failed_relays: Vec::new(),
- job_id: None,
- job_status: None,
- signer_mode: dry_run_signer_mode(config),
- event_id: None,
- event_addr: Some(listing_addr.clone()),
- idempotency_key: args.idempotency_key.clone(),
- signer_session_id: None,
- requested_signer_session_id,
- local_replica: None,
- reason: Some(dry_run_reason(config)),
- job: None,
- event: args.print_event.then_some(event_draft.event),
- actions: vec![format!(
- "radroots listing {} {}",
- operation.as_str(),
- args.file.display()
- )],
- });
- }
-
- match config.publish.mode {
- PublishMode::NostrRelay => mutate_via_direct_relay(
- config,
- args,
- operation,
- &canonical,
- listing_addr,
- event_draft,
- ),
- PublishMode::Radrootsd => Ok(radrootsd_preflight_view(
- config,
- args,
- operation,
- &canonical,
- listing_addr,
- event_draft.event,
- "unavailable",
- RADROOTSD_PUBLISH_DEFERRED_REASON,
- )),
- }
+ mutate_via_sdk_from_canonical(config, args, operation, canonical)
}
-fn mutate_via_direct_relay(
+fn mutate_via_sdk_from_canonical(
config: &RuntimeConfig,
args: &ListingMutationArgs,
operation: ListingMutationOperation,
- canonical: &CanonicalListingDraft,
- listing_addr: String,
- event_draft: ListingMutationEventDraft,
-) -> Result<ListingMutationView, RuntimeError> {
- let signing = if matches!(config.signer.backend, SignerBackend::Local) {
- resolve_listing_signing_identity(config, canonical)?
- } else {
- match resolve_actor_write_authority(config, "seller", canonical.seller_pubkey.as_str()) {
- Ok(_) => {
- return Ok(binding_error_view(
- config,
- args,
- operation,
- canonical,
- listing_addr,
- event_draft.event,
- ActorWriteBindingError::Unconfigured(
- "listing publish requires signer mode `local`".to_owned(),
- ),
- ));
- }
- Err(error) => {
- return Ok(binding_error_view(
- config,
- args,
- operation,
- canonical,
- listing_addr,
- event_draft.event,
- error,
- ));
- }
- }
- };
-
- if config.relay.urls.is_empty() {
- return Ok(direct_relay_error_view(
- config,
- args,
- operation,
- canonical,
- listing_addr,
- event_draft.event,
- DirectRelayPublishError::MissingRelays,
+ canonical: CanonicalListingDraft,
+) -> Result<ListingMutationView, CliSdkAdapterError> {
+ let actor = RadrootsActorContext::local_account(
+ canonical.seller_pubkey.as_str(),
+ canonical.seller_account_id.clone(),
+ [RadrootsActorRole::Seller],
+ )
+ .map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?;
+ let document = RadrootsListingDraftDocumentV1::new(canonical.listing.clone());
+ if config.output.dry_run {
+ let session = CliSdkSession::connect_memory(config)?;
+ let plan = session
+ .sdk()
+ .listings()
+ .prepare_publish(ListingPreparePublishRequest::from_document(actor, document))?;
+ return Ok(sdk_prepared_publish_view(
+ config, args, operation, &canonical, plan,
));
}
- let signed_event = sign_parts_with_identity(&signing.identity, event_draft.parts)
- .map_err(|error| RuntimeError::Network(error.to_string()))?;
- let record = append_signed_event(
- config,
- format!("listing:{}", canonical.listing_id).as_str(),
- Some(canonical.seller_account_id.clone()),
- Some(canonical.seller_pubkey.clone()),
- Some(canonical.farm_d_tag.clone()),
- Some(listing_addr.clone()),
- &signed_event,
- )?;
- let receipt = match publish_signed_event_with_identity(
- &signing.identity,
- &config.relay.urls,
- signed_event,
- ) {
- Ok(receipt) => {
- mark_signed_event_acknowledged(
- config,
- record.record_id.as_str(),
- receipt.target_relays.clone(),
- receipt.connected_relays.clone(),
- receipt.acknowledged_relays.clone(),
- receipt.failed_relays.clone(),
- )?;
- receipt
- }
- Err(
- error @ (DirectRelayPublishError::RelayConfig { .. }
- | DirectRelayPublishError::Connect { .. }
- | DirectRelayPublishError::Publish { .. }),
- ) => {
- mark_signed_event_failed_for_publish_error(config, record.record_id.as_str(), &error)?;
- let mut event = event_draft.event;
- event.event_id = record.event_id.clone();
- event.created_at = record
- .event_created_at
- .and_then(|created_at| u32::try_from(created_at).ok());
- event.signature = record.event_sig.clone();
- return Ok(direct_relay_error_view(
- config,
- args,
- operation,
- canonical,
- listing_addr,
- event,
- error,
- ));
- }
- Err(error) => {
- mark_signed_event_failed_for_publish_error(config, record.record_id.as_str(), &error)?;
- return Err(RuntimeError::Network(error.to_string()));
- }
+ let session = CliSdkSession::connect(config)?;
+ let signer = sdk_listing_signer(config, &canonical)?;
+ let mut request = ListingEnqueuePublishRequest::from_document(
+ actor,
+ document,
+ sdk_relay_target_policy(config),
+ );
+ if let Some(idempotency_key) = args.idempotency_key.as_deref() {
+ request = request.try_with_idempotency_key(idempotency_key)?;
+ }
+ let enqueue_receipt =
+ session.block_on(session.sdk().listings().enqueue_publish(request, &signer))?;
+ let push_receipt = if args.offline {
+ None
+ } else {
+ Some(
+ session.block_on(
+ session.sdk().sync().push_outbox(
+ PushOutboxRequest::new()
+ .with_limit(1)
+ .with_relay_url_policy(sdk_relay_url_policy(config)),
+ ),
+ )?,
+ )
};
-
- Ok(published_mutation_view(
+ Ok(sdk_enqueued_publish_view(
config,
args,
operation,
- canonical,
- listing_addr,
- event_draft.event,
- receipt,
+ &canonical,
+ enqueue_receipt,
+ push_receipt,
))
}
-fn listing_write_source(config: &RuntimeConfig) -> &'static str {
- match config.publish.mode {
- PublishMode::NostrRelay => RELAY_LISTING_WRITE_SOURCE,
- PublishMode::Radrootsd => RADROOTSD_LISTING_WRITE_SOURCE,
- }
-}
-
-fn dry_run_reason(config: &RuntimeConfig) -> String {
- match config.publish.mode {
- PublishMode::NostrRelay => "dry run requested; relay publish skipped".to_owned(),
- PublishMode::Radrootsd => "dry run requested; radrootsd submission skipped".to_owned(),
- }
-}
-
-fn dry_run_signer_mode(config: &RuntimeConfig) -> Option<String> {
- match config.publish.mode {
- PublishMode::NostrRelay => None,
- PublishMode::Radrootsd => Some("nip46".to_owned()),
- }
-}
-
fn scaffold_contents(draft: &ListingDraftDocument) -> Result<String, RuntimeError> {
let toml = toml::to_string_pretty(draft).map_err(|error| {
RuntimeError::Config(format!("failed to render listing draft: {error}"))
@@ -2410,18 +2236,7 @@ fn validation_context(config: &RuntimeConfig) -> Result<ListingValidationContext
fn mutation_validation_context(
config: &RuntimeConfig,
) -> Result<ListingValidationContext, RuntimeError> {
- match config.publish.mode {
- PublishMode::NostrRelay => validation_context(config),
- PublishMode::Radrootsd => radrootsd_mutation_validation_context(config),
- }
-}
-
-fn radrootsd_mutation_validation_context(
- config: &RuntimeConfig,
-) -> Result<ListingValidationContext, RuntimeError> {
- Ok(ListingValidationContext {
- farm_setup_action: farm_setup_action(config)?,
- })
+ validation_context(config)
}
fn canonicalize_draft(
@@ -2979,202 +2794,6 @@ fn invalid_validation_view(
}
}
-fn build_listing_event_draft(
- canonical: &CanonicalListingDraft,
-) -> Result<(ListingMutationEventDraft, String), RuntimeError> {
- let parts = to_wire_parts_with_kind(&canonical.listing, KIND_LISTING)
- .map_err(|error| RuntimeError::Config(format!("invalid listing contract: {error}")))?;
- let event = RadrootsNostrEvent {
- id: String::new(),
- author: canonical.seller_pubkey.clone(),
- created_at: 0,
- kind: parts.kind,
- tags: parts.tags.clone(),
- content: parts.content.clone(),
- sig: String::new(),
- };
- let validated = validate_listing_event(&event)
- .map_err(|error| RuntimeError::Config(format!("invalid listing contract: {error}")))?;
- Ok((
- ListingMutationEventDraft {
- event: ListingMutationEventView {
- kind: KIND_LISTING,
- author: canonical.seller_pubkey.clone(),
- created_at: None,
- content: parts.content.clone(),
- tags: parts.tags.clone(),
- event_id: None,
- signature: None,
- event_addr: validated.listing_addr.clone(),
- },
- parts,
- },
- validated.listing_addr,
- ))
-}
-
-fn radrootsd_preflight_view(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
- canonical: &CanonicalListingDraft,
- listing_addr: String,
- event_preview: ListingMutationEventView,
- state: &str,
- reason: impl Into<String>,
-) -> ListingMutationView {
- ListingMutationView {
- state: state.to_owned(),
- operation: operation.as_str().to_owned(),
- source: listing_write_source(config).to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr: listing_addr.clone(),
- seller_account_id: canonical.seller_account_id.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- target_relays: Vec::new(),
- connected_relays: Vec::new(),
- acknowledged_relays: Vec::new(),
- failed_relays: Vec::new(),
- job_id: None,
- job_status: None,
- signer_mode: Some("deferred".to_owned()),
- event_id: None,
- event_addr: Some(listing_addr),
- idempotency_key: args.idempotency_key.clone(),
- signer_session_id: None,
- requested_signer_session_id: args.signer_session_id.clone(),
- local_replica: None,
- reason: Some(reason.into()),
- job: None,
- event: args.print_event.then_some(event_preview),
- actions: vec![format!(
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com listing {} {}",
- operation.as_str(),
- args.file.display()
- )],
- }
-}
-
-fn direct_relay_error_view(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
- canonical: &CanonicalListingDraft,
- listing_addr: String,
- mut event_preview: ListingMutationEventView,
- error: DirectRelayPublishError,
-) -> ListingMutationView {
- let parts = direct_relay_error_view_parts(config.relay.urls.as_slice(), error);
- let event_id = parts.event_id.or_else(|| event_preview.event_id.clone());
- event_preview.event_id = event_id.clone();
-
- ListingMutationView {
- state: "unavailable".to_owned(),
- operation: operation.as_str().to_owned(),
- source: listing_write_source(config).to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr: listing_addr.clone(),
- seller_account_id: canonical.seller_account_id.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- target_relays: parts.target_relays,
- connected_relays: parts.connected_relays,
- acknowledged_relays: Vec::new(),
- failed_relays: parts.failed_relays,
- job_id: None,
- job_status: None,
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- event_id,
- event_addr: Some(listing_addr),
- idempotency_key: args.idempotency_key.clone(),
- signer_session_id: None,
- requested_signer_session_id: args.signer_session_id.clone(),
- local_replica: None,
- reason: Some(parts.reason),
- job: None,
- event: args.print_event.then_some(event_preview),
- actions: Vec::new(),
- }
-}
-
-#[derive(Debug, Clone)]
-struct DirectRelayErrorViewParts {
- reason: String,
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- failed_relays: Vec<RelayFailureView>,
- event_id: Option<String>,
-}
-
-fn direct_relay_error_view_parts(
- configured_relays: &[String],
- error: DirectRelayPublishError,
-) -> DirectRelayErrorViewParts {
- let (reason, target_relays, connected_relays, failed_relays, event_id) = match error {
- DirectRelayPublishError::MissingRelays => (
- "direct relay publish requires at least one configured relay".to_owned(),
- configured_relays.to_vec(),
- Vec::new(),
- Vec::new(),
- None,
- ),
- DirectRelayPublishError::RelayConfig { relay, source } => (
- format!("failed to configure relay `{relay}` for direct relay publish: {source}"),
- configured_relays.to_vec(),
- Vec::new(),
- vec![RelayFailureView {
- relay,
- reason: source.to_string(),
- }],
- None,
- ),
- DirectRelayPublishError::Connect {
- reason,
- target_relays,
- connected_relays,
- failed_relays,
- } => (
- format!("direct relay connection failed: {reason}"),
- target_relays,
- connected_relays,
- relay_failures(failed_relays),
- None,
- ),
- DirectRelayPublishError::Publish {
- event_id,
- reason,
- target_relays,
- connected_relays,
- failed_relays,
- } => (
- format!("direct relay publish failed for event `{event_id}`: {reason}"),
- target_relays,
- connected_relays,
- relay_failures(failed_relays),
- Some(event_id),
- ),
- DirectRelayPublishError::Runtime(_)
- | DirectRelayPublishError::Build(_)
- | DirectRelayPublishError::Sign(_) => unreachable!(),
- };
- DirectRelayErrorViewParts {
- reason,
- target_relays,
- connected_relays,
- failed_relays,
- event_id,
- }
-}
-
fn validate_local_listing_signer(
config: &RuntimeConfig,
canonical: &CanonicalListingDraft,
@@ -3258,217 +2877,6 @@ fn listing_bound_signing_error(
}
}
-fn binding_error_view(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
- canonical: &CanonicalListingDraft,
- listing_addr: String,
- event_preview: ListingMutationEventView,
- error: ActorWriteBindingError,
-) -> ListingMutationView {
- let reason = error.reason();
- let state = "unconfigured".to_owned();
- let actions = vec!["run radroots signer status get".to_owned()];
-
- ListingMutationView {
- state: state.clone(),
- operation: operation.as_str().to_owned(),
- source: listing_write_source(config).to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr,
- seller_account_id: canonical.seller_account_id.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- target_relays: Vec::new(),
- connected_relays: Vec::new(),
- acknowledged_relays: Vec::new(),
- failed_relays: Vec::new(),
- job_id: None,
- job_status: None,
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- event_id: None,
- event_addr: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- local_replica: None,
- reason: Some(reason),
- job: None,
- event: args.print_event.then_some(event_preview),
- actions,
- }
-}
-
-fn published_mutation_view(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
- canonical: &CanonicalListingDraft,
- listing_addr: String,
- mut event: ListingMutationEventView,
- receipt: DirectRelayPublishReceipt,
-) -> ListingMutationView {
- let DirectRelayPublishReceipt {
- event: published_event,
- event_id,
- created_at,
- signature,
- target_relays,
- connected_relays,
- acknowledged_relays,
- failed_relays,
- } = receipt;
- debug_assert_eq!(event_id, published_event.id.to_hex());
- debug_assert_eq!(signature, published_event.sig.to_string());
- let local_replica =
- listing_local_replica_ingest_view(config, &published_event, Some(listing_addr.clone()));
- event.event_id = Some(event_id.clone());
- event.created_at = Some(created_at);
- event.signature = Some(signature);
- ListingMutationView {
- state: match operation {
- ListingMutationOperation::Archive => "archived",
- ListingMutationOperation::Publish | ListingMutationOperation::Update => "published",
- }
- .to_owned(),
- operation: operation.as_str().to_owned(),
- source: listing_write_source(config).to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr: listing_addr.clone(),
- seller_account_id: canonical.seller_account_id.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- target_relays,
- connected_relays,
- acknowledged_relays,
- failed_relays: relay_failures(failed_relays),
- job_id: None,
- job_status: None,
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- event_id: Some(event_id),
- event_addr: Some(listing_addr),
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- local_replica: Some(local_replica),
- reason: None,
- job: None,
- event: args.print_event.then_some(event),
- actions: Vec::new(),
- }
-}
-
-fn listing_local_replica_ingest_view(
- config: &RuntimeConfig,
- event: &SignedNostrEvent,
- event_addr: Option<String>,
-) -> ListingMutationLocalReplicaView {
- ingest_listing_event_into_local_replica(
- config.local.replica_db_path.as_path(),
- event,
- event_addr,
- )
-}
-
-fn ingest_listing_event_into_local_replica(
- replica_db_path: &Path,
- event: &SignedNostrEvent,
- event_addr: Option<String>,
-) -> ListingMutationLocalReplicaView {
- let event_id = event.id.to_hex();
- if !replica_db_path.exists() {
- return ListingMutationLocalReplicaView {
- state: "unconfigured".to_owned(),
- store_state: "missing".to_owned(),
- ingest_outcome: None,
- event_id: Some(event_id),
- event_addr,
- reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store init".to_owned()],
- };
- }
-
- let executor = match SqliteExecutor::open(replica_db_path) {
- Ok(executor) => executor,
- Err(error) => {
- return listing_local_replica_failed_view(
- event_id,
- event_addr,
- format!("failed to open local replica database: {error}"),
- );
- }
- };
- if let Err(error) = migrations::run_all_up(&executor) {
- return listing_local_replica_failed_view(
- event_id,
- event_addr,
- format!("failed to migrate local replica database: {error}"),
- );
- }
-
- let event = radroots_event_from_nostr(event);
- match radroots_replica_ingest_event(&executor, &event) {
- Ok(RadrootsReplicaIngestOutcome::Applied) => ListingMutationLocalReplicaView {
- state: "applied".to_owned(),
- store_state: "ready".to_owned(),
- ingest_outcome: Some("applied".to_owned()),
- event_id: Some(event_id),
- event_addr,
- reason: None,
- actions: Vec::new(),
- },
- Ok(RadrootsReplicaIngestOutcome::Skipped) => ListingMutationLocalReplicaView {
- state: "skipped".to_owned(),
- store_state: "ready".to_owned(),
- ingest_outcome: Some("skipped".to_owned()),
- event_id: Some(event_id),
- event_addr,
- reason: Some("shared replica ingest skipped the event".to_owned()),
- actions: Vec::new(),
- },
- Err(error) => listing_local_replica_failed_view(
- event_id,
- event_addr,
- format!("failed to ingest listing event into local replica: {error}"),
- ),
- }
-}
-
-fn listing_local_replica_failed_view(
- event_id: String,
- event_addr: Option<String>,
- reason: String,
-) -> ListingMutationLocalReplicaView {
- ListingMutationLocalReplicaView {
- state: "failed".to_owned(),
- store_state: "unavailable".to_owned(),
- ingest_outcome: None,
- event_id: Some(event_id),
- event_addr,
- reason: Some(reason),
- actions: vec!["radroots store status get".to_owned()],
- }
-}
-
-fn relay_failures(failures: Vec<DirectRelayFailure>) -> Vec<RelayFailureView> {
- failures
- .into_iter()
- .map(|failure| RelayFailureView {
- relay: failure.relay,
- reason: failure.reason,
- })
- .collect()
-}
-
fn issue_from_trade_validation(
error: RadrootsTradeValidationListingError,
contents: &str,
@@ -3808,18 +3216,13 @@ fn encode_base64url_no_pad(bytes: [u8; 16]) -> String {
#[cfg(test)]
mod tests {
use super::{
- DRAFT_KIND, ListingDraftDocument, direct_relay_error_view_parts, encode_base64url_no_pad,
- generate_d_tag, ingest_listing_event_into_local_replica, sdk_publish_actions,
- sdk_publish_reason, sdk_publish_state, sdk_push_acknowledged_relays,
+ DRAFT_KIND, ListingDraftDocument, encode_base64url_no_pad, generate_d_tag,
+ sdk_publish_actions, sdk_publish_reason, sdk_publish_state, sdk_push_acknowledged_relays,
sdk_push_failed_relays,
};
use crate::cli::global::ListingMutationArgs;
- use crate::runtime::direct_relay::{DirectRelayFailure, DirectRelayPublishError};
use radroots_events::ids::RadrootsEventId;
use radroots_events_codec::d_tag::is_d_tag_base64url;
- use radroots_events_codec::wire::WireEventParts;
- use radroots_identity::RadrootsIdentity;
- use radroots_nostr::prelude::{RadrootsNostrTimestamp, radroots_nostr_build_event};
use radroots_sdk::{
PushOutboxEventReceipt, PushOutboxEventState, PushOutboxRelayOutcomeKind,
PushOutboxRelayReceipt,
@@ -3839,27 +3242,6 @@ mod tests {
}
#[test]
- fn direct_relay_publish_error_parts_preserve_event_id() {
- let parts = direct_relay_error_view_parts(
- &["ws://127.0.0.1:19000".to_owned()],
- DirectRelayPublishError::Publish {
- event_id: "e".repeat(64),
- reason: "relay rejected event".to_owned(),
- target_relays: vec!["ws://127.0.0.1:19000".to_owned()],
- connected_relays: vec!["ws://127.0.0.1:19000".to_owned()],
- failed_relays: vec![DirectRelayFailure {
- relay: "ws://127.0.0.1:19000".to_owned(),
- reason: "relay rejected event".to_owned(),
- }],
- },
- );
-
- assert_eq!(parts.event_id, Some("e".repeat(64)));
- assert!(parts.reason.contains("direct relay publish failed"));
- assert_eq!(parts.failed_relays.len(), 1);
- }
-
- #[test]
fn sdk_push_receipt_helpers_map_published_and_auth_required_states() {
let accepted = sdk_push_event(
PushOutboxEventState::Published,
@@ -3903,122 +3285,6 @@ mod tests {
}
#[test]
- fn local_replica_ingest_reports_missing_store() {
- let temp = tempfile::tempdir().expect("tempdir");
- let event = signed_test_listing_event(WireEventParts {
- kind: super::KIND_LISTING,
- content: "{}".to_owned(),
- tags: vec![vec!["d".to_owned(), "listing-1".to_owned()]],
- });
-
- let view = ingest_listing_event_into_local_replica(
- &temp.path().join("missing.sqlite"),
- &event,
- Some("30402:pubkey:listing-1".to_owned()),
- );
-
- assert_eq!(view.state, "unconfigured");
- assert_eq!(view.store_state, "missing");
- assert_eq!(view.event_id, Some(event.id.to_hex()));
- assert_eq!(view.actions, vec!["radroots store init".to_owned()]);
- }
-
- #[test]
- fn local_replica_ingest_preserves_shared_ingest_failure() {
- let temp = tempfile::tempdir().expect("tempdir");
- let replica = temp.path().join("replica.sqlite");
- std::fs::File::create(&replica).expect("replica placeholder");
- let event = signed_test_listing_event(WireEventParts {
- kind: super::KIND_LISTING,
- content: "{}".to_owned(),
- tags: vec![vec!["d".to_owned(), "listing-1".to_owned()]],
- });
-
- let view = ingest_listing_event_into_local_replica(
- &replica,
- &event,
- Some("30402:pubkey:listing-1".to_owned()),
- );
-
- assert_eq!(view.state, "failed");
- assert_eq!(view.store_state, "unavailable");
- assert_eq!(view.event_id, Some(event.id.to_hex()));
- assert!(
- view.reason
- .as_deref()
- .unwrap_or_default()
- .contains("failed to ingest listing event into local replica")
- );
- }
-
- #[test]
- fn local_replica_ingest_makes_listing_writes_visible_to_local_reads() {
- let temp = tempfile::tempdir().expect("tempdir");
- let replica = temp.path().join("replica.sqlite");
- std::fs::File::create(&replica).expect("replica placeholder");
- let identity = RadrootsIdentity::generate();
- let seller_pubkey = identity.public_key_hex();
- let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
- let listing_addr = format!(
- "{}:{}:{}",
- super::KIND_LISTING,
- seller_pubkey,
- listing_d_tag
- );
-
- let active = signed_test_listing_event_with_identity(
- &identity,
- test_listing_wire_parts(&seller_pubkey, listing_d_tag, "active", "Pasture Eggs"),
- 1_700_000_001,
- );
- let active_view =
- ingest_listing_event_into_local_replica(&replica, &active, Some(listing_addr.clone()));
- assert_eq!(active_view.state, "applied");
- assert_eq!(active_view.store_state, "ready");
- assert_eq!(active_view.ingest_outcome.as_deref(), Some("applied"));
-
- let db = super::ReplicaSql::new(super::SqliteExecutor::open(&replica).expect("open db"));
- let active_rows = db
- .trade_product_search(&["eggs".to_owned()])
- .expect("search active");
- assert_eq!(active_rows.len(), 1);
- assert_eq!(active_rows[0].title, "Pasture Eggs");
- assert_eq!(
- active_rows[0].listing_addr.as_deref(),
- Some(listing_addr.as_str())
- );
-
- let updated = signed_test_listing_event_with_identity(
- &identity,
- test_listing_wire_parts(&seller_pubkey, listing_d_tag, "active", "Market Eggs"),
- 1_700_000_002,
- );
- let updated_view =
- ingest_listing_event_into_local_replica(&replica, &updated, Some(listing_addr.clone()));
- assert_eq!(updated_view.state, "applied");
- let db = super::ReplicaSql::new(super::SqliteExecutor::open(&replica).expect("open db"));
- let updated_rows = db
- .trade_product_search(&["eggs".to_owned()])
- .expect("search updated");
- assert_eq!(updated_rows.len(), 1);
- assert_eq!(updated_rows[0].title, "Market Eggs");
-
- let archived = signed_test_listing_event_with_identity(
- &identity,
- test_listing_wire_parts(&seller_pubkey, listing_d_tag, "archived", "Market Eggs"),
- 1_700_000_003,
- );
- let archived_view =
- ingest_listing_event_into_local_replica(&replica, &archived, Some(listing_addr));
- assert_eq!(archived_view.state, "applied");
- let db = super::ReplicaSql::new(super::SqliteExecutor::open(&replica).expect("open db"));
- let archived_rows = db
- .trade_product_search(&["eggs".to_owned()])
- .expect("search archived");
- assert!(archived_rows.is_empty());
- }
-
- #[test]
fn listing_draft_kind_constant_is_stable() {
let document = ListingDraftDocument {
version: 1,
@@ -4193,80 +3459,8 @@ mod tests {
ListingMutationArgs {
file: "listing.toml".into(),
idempotency_key: None,
- signer_session_id: None,
print_event: false,
offline,
}
}
-
- fn signed_test_listing_event(
- parts: WireEventParts,
- ) -> radroots_nostr::prelude::RadrootsNostrEvent {
- let identity = RadrootsIdentity::generate();
- signed_test_listing_event_with_identity(&identity, parts, 1_700_000_001)
- }
-
- fn signed_test_listing_event_with_identity(
- identity: &RadrootsIdentity,
- parts: WireEventParts,
- created_at: u64,
- ) -> radroots_nostr::prelude::RadrootsNostrEvent {
- radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .expect("event builder")
- .custom_created_at(RadrootsNostrTimestamp::from_secs(created_at))
- .sign_with_keys(identity.keys())
- .expect("signed event")
- }
-
- fn test_listing_wire_parts(
- seller_pubkey: &str,
- listing_d_tag: &str,
- status: &str,
- title: &str,
- ) -> WireEventParts {
- let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
- WireEventParts {
- kind: super::KIND_LISTING,
- content: format!("# {title}"),
- tags: vec![
- vec!["d".to_owned(), listing_d_tag.to_owned()],
- vec![
- "a".to_owned(),
- format!(
- "{}:{}:{}",
- radroots_events::kinds::KIND_FARM,
- seller_pubkey,
- farm_d_tag
- ),
- ],
- vec!["p".to_owned(), seller_pubkey.to_owned()],
- vec!["key".to_owned(), "pasture-eggs".to_owned()],
- vec!["title".to_owned(), title.to_owned()],
- vec!["category".to_owned(), "eggs".to_owned()],
- vec!["summary".to_owned(), "Pasture-raised eggs".to_owned()],
- vec!["radroots:primary_bin".to_owned(), "bin-a".to_owned()],
- vec![
- "radroots:bin".to_owned(),
- "bin-a".to_owned(),
- "12".to_owned(),
- "each".to_owned(),
- "12".to_owned(),
- "each".to_owned(),
- "dozen".to_owned(),
- ],
- vec![
- "radroots:price".to_owned(),
- "bin-a".to_owned(),
- "6".to_owned(),
- "USD".to_owned(),
- "1".to_owned(),
- "each".to_owned(),
- "6".to_owned(),
- "each".to_owned(),
- ],
- vec!["inventory".to_owned(), "5".to_owned()],
- vec!["status".to_owned(), status.to_owned()],
- ],
- }
- }
}
diff --git a/src/runtime/local_events.rs b/src/runtime/local_events.rs
@@ -5,19 +5,17 @@ use std::time::{SystemTime, UNIX_EPOCH};
use radroots_local_events::{
LocalEventRecord, LocalEventRecordInput, LocalEventsStore, LocalRecordFamily,
- LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryFailure,
- SourceRuntime,
+ LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
};
use radroots_runtime_paths::{
default_shared_local_events_database_path_from_shared_accounts_data_root,
default_shared_local_events_root_from_shared_accounts_data_root,
};
use radroots_sql_core::SqliteExecutor;
-use serde_json::{Value, json};
+use serde_json::Value;
use crate::runtime::RuntimeError;
use crate::runtime::config::{PathsConfig, RuntimeConfig};
-use crate::runtime::direct_relay::{DirectRelayFailure, DirectRelayPublishError};
static RECORD_COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -60,109 +58,6 @@ pub fn append_local_work(
Ok(store.append_record(&input)?)
}
-pub fn append_signed_event(
- config: &RuntimeConfig,
- subject: &str,
- owner_account_id: Option<String>,
- owner_pubkey: Option<String>,
- farm_id: Option<String>,
- listing_addr: Option<String>,
- event: &radroots_nostr::prelude::RadrootsNostrEvent,
-) -> Result<LocalEventRecord, RuntimeError> {
- let timestamp = current_time_ms()?;
- let delivery_evidence = RelayDeliveryEvidence::pending(&config.relay.urls)?;
- let relay_set = delivery_evidence.relay_set_fingerprint();
- let input = LocalEventRecordInput {
- record_id: format!("cli:signed_event:{subject}:{}", event.id.to_hex()),
- family: LocalRecordFamily::SignedEvent,
- status: LocalRecordStatus::PendingPublish,
- source_runtime: SourceRuntime::Cli,
- created_at_ms: timestamp,
- inserted_at_ms: timestamp,
- owner_account_id,
- owner_pubkey,
- farm_id,
- listing_addr,
- local_work_json: None,
- event_id: Some(event.id.to_hex()),
- event_kind: Some(i64::from(u32::from(event.kind.as_u16()))),
- event_pubkey: Some(event.pubkey.to_string()),
- event_created_at: Some(event_created_at_i64(event)?),
- event_tags_json: Some(json!(event_tags(event))),
- event_content: Some(event.content.clone()),
- event_sig: Some(event.sig.to_string()),
- raw_event_json: Some(raw_event_json(event)?),
- outbox_status: PublishOutboxStatus::Pending,
- relay_set_fingerprint: relay_set,
- relay_delivery_json: Some(delivery_evidence.to_json_value()?),
- };
- let store = open_store(config)?;
- Ok(store.append_record(&input)?)
-}
-
-pub fn mark_signed_event_acknowledged(
- config: &RuntimeConfig,
- record_id: &str,
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- acknowledged_relays: Vec<String>,
- failed_relays: Vec<DirectRelayFailure>,
-) -> Result<LocalEventRecord, RuntimeError> {
- let delivery_evidence = acknowledged_delivery_evidence(
- target_relays,
- connected_relays,
- acknowledged_relays,
- failed_relays,
- )?;
- update_signed_event_outbox(
- config,
- record_id,
- LocalRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- delivery_evidence,
- )
-}
-
-pub fn mark_signed_event_failed(
- config: &RuntimeConfig,
- record_id: &str,
- reason: String,
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- failed_relays: Vec<DirectRelayFailure>,
-) -> Result<LocalEventRecord, RuntimeError> {
- let delivery_evidence = failed_delivery_evidence(
- target_relays,
- connected_relays,
- failed_relays,
- reason.as_str(),
- )?;
- update_signed_event_outbox(
- config,
- record_id,
- LocalRecordStatus::Failed,
- PublishOutboxStatus::Failed,
- delivery_evidence,
- )
-}
-
-pub fn mark_signed_event_failed_for_publish_error(
- config: &RuntimeConfig,
- record_id: &str,
- error: &DirectRelayPublishError,
-) -> Result<LocalEventRecord, RuntimeError> {
- let (target_relays, connected_relays, failed_relays) =
- publish_error_delivery_parts(error, &config.relay.urls);
- mark_signed_event_failed(
- config,
- record_id,
- error.to_string(),
- target_relays,
- connected_relays,
- failed_relays,
- )
-}
-
pub fn shared_local_events_db_path(config: &RuntimeConfig) -> Result<PathBuf, RuntimeError> {
shared_local_events_db_path_from_paths(&config.paths)
}
@@ -217,28 +112,6 @@ pub fn get_shared_record(
Ok(store.get_record(record_id)?)
}
-fn update_signed_event_outbox(
- config: &RuntimeConfig,
- record_id: &str,
- status: LocalRecordStatus,
- outbox_status: PublishOutboxStatus,
- delivery_evidence: RelayDeliveryEvidence,
-) -> Result<LocalEventRecord, RuntimeError> {
- let relay_set_fingerprint = delivery_evidence.relay_set_fingerprint();
- let relay_delivery_json = delivery_evidence.to_json_value()?;
- let store = open_store(config)?;
- Ok(
- store.update_outbox(&radroots_local_events::LocalEventRecordUpdate {
- record_id: record_id.to_owned(),
- status,
- outbox_status,
- relay_set_fingerprint,
- relay_delivery_json: Some(relay_delivery_json),
- updated_at_ms: current_time_ms()?,
- })?,
- )
-}
-
fn open_store(config: &RuntimeConfig) -> Result<LocalEventsStore<SqliteExecutor>, RuntimeError> {
let root = shared_local_events_root_from_paths(&config.paths)?;
fs::create_dir_all(&root)?;
@@ -259,14 +132,8 @@ fn shared_local_events_root_from_paths(paths: &PathsConfig) -> Result<PathBuf, R
mod tests {
use std::path::PathBuf;
- use serde_json::json;
-
- use super::{
- acknowledged_delivery_evidence, failed_delivery_evidence,
- shared_local_events_db_path_from_paths, shared_local_events_root_from_paths,
- };
+ use super::{shared_local_events_db_path_from_paths, shared_local_events_root_from_paths};
use crate::runtime::config::PathsConfig;
- use crate::runtime::direct_relay::DirectRelayFailure;
#[test]
fn shared_local_events_paths_use_shared_runtime_contract() {
@@ -284,71 +151,6 @@ mod tests {
);
}
- #[test]
- fn acknowledged_delivery_evidence_uses_actual_target_relays() {
- let evidence = acknowledged_delivery_evidence(
- vec![
- "wss://actual-a.example".to_owned(),
- "wss://actual-b.example".to_owned(),
- ],
- vec!["wss://actual-a.example".to_owned()],
- vec!["wss://actual-a.example".to_owned()],
- vec![DirectRelayFailure {
- relay: "wss://actual-b.example".to_owned(),
- reason: "timeout".to_owned(),
- }],
- )
- .expect("acknowledged evidence");
-
- assert_eq!(
- evidence.relay_set_fingerprint(),
- radroots_local_events::canonical_relay_set_fingerprint([
- "wss://actual-a.example",
- "wss://actual-b.example"
- ])
- );
- assert_eq!(
- evidence.to_json_value().expect("delivery json"),
- json!({
- "state": "acknowledged",
- "target_relays": ["wss://actual-a.example", "wss://actual-b.example"],
- "connected_relays": ["wss://actual-a.example"],
- "acknowledged_relays": ["wss://actual-a.example"],
- "failed_relays": [
- {"relay_url": "wss://actual-b.example", "error": "timeout"}
- ]
- })
- );
- }
-
- #[test]
- fn failed_delivery_evidence_synthesizes_target_failures_when_transport_has_none() {
- let evidence = failed_delivery_evidence(
- vec![
- "wss://actual-a.example".to_owned(),
- "wss://actual-b.example".to_owned(),
- ],
- Vec::new(),
- Vec::new(),
- "publish failed",
- )
- .expect("failed evidence");
-
- assert_eq!(
- evidence.to_json_value().expect("delivery json"),
- json!({
- "state": "failed",
- "target_relays": ["wss://actual-a.example", "wss://actual-b.example"],
- "connected_relays": [],
- "acknowledged_relays": [],
- "failed_relays": [
- {"relay_url": "wss://actual-a.example", "error": "publish failed"},
- {"relay_url": "wss://actual-b.example", "error": "publish failed"}
- ]
- })
- );
- }
-
fn paths_config(shared_accounts_data_root: &str) -> PathsConfig {
PathsConfig {
profile: "repo_local".to_owned(),
@@ -387,118 +189,3 @@ fn current_time_ms() -> Result<i64, RuntimeError> {
i64::try_from(duration.as_millis())
.map_err(|_| RuntimeError::Config("current timestamp exceeds i64 milliseconds".to_owned()))
}
-
-fn publish_error_delivery_parts(
- error: &DirectRelayPublishError,
- relay_urls: &[String],
-) -> (Vec<String>, Vec<String>, Vec<DirectRelayFailure>) {
- match error {
- DirectRelayPublishError::MissingRelays
- | DirectRelayPublishError::Runtime(_)
- | DirectRelayPublishError::Build(_)
- | DirectRelayPublishError::Sign(_) => (relay_urls.to_vec(), Vec::new(), Vec::new()),
- DirectRelayPublishError::RelayConfig { relay, source } => (
- relay_urls.to_vec(),
- Vec::new(),
- vec![DirectRelayFailure {
- relay: relay.clone(),
- reason: source.to_string(),
- }],
- ),
- DirectRelayPublishError::Connect {
- target_relays,
- connected_relays,
- failed_relays,
- ..
- }
- | DirectRelayPublishError::Publish {
- target_relays,
- connected_relays,
- failed_relays,
- ..
- } => (
- target_relays.clone(),
- connected_relays.clone(),
- failed_relays.clone(),
- ),
- }
-}
-
-fn acknowledged_delivery_evidence(
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- acknowledged_relays: Vec<String>,
- failed_relays: Vec<DirectRelayFailure>,
-) -> Result<RelayDeliveryEvidence, RuntimeError> {
- RelayDeliveryEvidence::acknowledged(
- target_relays,
- connected_relays,
- acknowledged_relays,
- relay_delivery_failures(failed_relays)?,
- )
- .map_err(Into::into)
-}
-
-fn failed_delivery_evidence(
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- failed_relays: Vec<DirectRelayFailure>,
- reason: &str,
-) -> Result<RelayDeliveryEvidence, RuntimeError> {
- let delivery_failures = failed_delivery_failures(failed_relays, &target_relays, reason)?;
- RelayDeliveryEvidence::failed(target_relays, connected_relays, delivery_failures)
- .map_err(Into::into)
-}
-
-fn relay_delivery_failures(
- failures: Vec<DirectRelayFailure>,
-) -> Result<Vec<RelayDeliveryFailure>, RuntimeError> {
- failures
- .into_iter()
- .map(|failure| RelayDeliveryFailure::new(failure.relay, failure.reason).map_err(Into::into))
- .collect()
-}
-
-fn failed_delivery_failures(
- failed_relays: Vec<DirectRelayFailure>,
- target_relays: &[String],
- reason: &str,
-) -> Result<Vec<RelayDeliveryFailure>, RuntimeError> {
- let failures = relay_delivery_failures(failed_relays)?;
- if !failures.is_empty() {
- return Ok(failures);
- }
- target_relays
- .iter()
- .map(|relay| RelayDeliveryFailure::new(relay, reason).map_err(Into::into))
- .collect()
-}
-
-fn event_tags(event: &radroots_nostr::prelude::RadrootsNostrEvent) -> Vec<Vec<String>> {
- event
- .tags
- .iter()
- .map(|tag| tag.as_slice().to_vec())
- .collect()
-}
-
-fn event_created_at_i64(
- event: &radroots_nostr::prelude::RadrootsNostrEvent,
-) -> Result<i64, RuntimeError> {
- i64::try_from(event.created_at.as_secs())
- .map_err(|_| RuntimeError::Config("event timestamp exceeds i64 seconds".to_owned()))
-}
-
-fn raw_event_json(
- event: &radroots_nostr::prelude::RadrootsNostrEvent,
-) -> Result<Value, RuntimeError> {
- Ok(json!({
- "id": event.id.to_hex(),
- "pubkey": event.pubkey.to_string(),
- "created_at": event_created_at_i64(event)?,
- "kind": u32::from(event.kind.as_u16()),
- "tags": event_tags(event),
- "content": event.content.clone(),
- "sig": event.sig.to_string(),
- }))
-}
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -919,8 +919,6 @@ pub fn submit(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: None,
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(reason),
job: None,
issues: Vec::new(),
@@ -956,8 +954,6 @@ pub fn submit(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: None,
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(format!("order draft `{}` was not found", args.key)),
job: None,
issues: Vec::new(),
@@ -1004,8 +1000,6 @@ pub fn submit(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: None,
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some("order draft is not ready for submit".to_owned()),
job: None,
issues,
@@ -8772,8 +8766,6 @@ fn order_submit_unconfigured_view(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: None,
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(reason.into()),
job: None,
issues,
@@ -8813,8 +8805,6 @@ fn order_submit_app_signed_evidence_view(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: None,
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(
"matching signed order request evidence already exists; publish skipped".to_owned(),
),
@@ -8850,8 +8840,6 @@ fn order_submit_app_signed_evidence_view(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: None,
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(
"signed order request evidence conflicts with the app-authored local order"
.to_owned(),
@@ -8900,8 +8888,6 @@ fn order_submit_invalid_quantity_view(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: None,
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(reason.into()),
job: None,
issues,
@@ -8972,8 +8958,6 @@ fn order_submit_listing_provenance_preflight_view(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(
"order submit requires at least one configured relay that is known to carry the listing"
.to_owned(),
@@ -9221,8 +9205,6 @@ fn order_submit_deduplicated_view(
failed_relays: relay_failures(failed_relays),
idempotency_key: args.idempotency_key.clone(),
signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(
"an identical order request is already visible on the configured relays; publish skipped"
.to_owned(),
@@ -9265,8 +9247,6 @@ fn order_submit_dry_run_view(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some("dry run requested; SDK enqueue and relay push skipped".to_owned()),
job: None,
issues: Vec::new(),
@@ -9311,8 +9291,6 @@ fn order_submit_invalid_existing_request_view(
failed_relays: relay_failures(failed_relays),
idempotency_key: args.idempotency_key.clone(),
signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(reason.into()),
job: None,
issues,
@@ -9552,8 +9530,6 @@ fn sdk_enqueued_order_submit_view(
failed_relays: push_event.map(sdk_push_failed_relays).unwrap_or_default(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- requested_signer_session_id: None,
reason: sdk_order_submit_reason(&enqueue.workflow, push_event),
job: None,
issues: Vec::new(),
@@ -9730,8 +9706,6 @@ fn order_binding_error_view(
failed_relays: Vec::new(),
idempotency_key: args.idempotency_key.clone(),
signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- requested_signer_session_id: None,
reason: Some(reason),
job: None,
issues: Vec::new(),
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -2,7 +2,7 @@
use crate::runtime::config::{
CapabilityBindingInspection, CapabilityBindingInspectionState, INFERENCE_HYF_STDIO_CAPABILITY,
};
-use crate::runtime::config::{PublishMode, RuntimeConfig};
+use crate::runtime::config::{PublishTransport, RuntimeConfig};
#[cfg(test)]
use crate::runtime::hyf;
use crate::view::runtime::PublishRuntimeView;
@@ -21,7 +21,7 @@ pub enum ProviderProvenance {
DirectConfig,
#[cfg(test)]
Disabled,
- PublishMode,
+ PublishTransport,
#[cfg(test)]
Unavailable,
}
@@ -37,7 +37,7 @@ impl ProviderProvenance {
Self::DirectConfig => "direct_config",
#[cfg(test)]
Self::Disabled => "disabled",
- Self::PublishMode => "publish_mode",
+ Self::PublishTransport => "publish_transport",
#[cfg(test)]
Self::Unavailable => "unavailable",
}
@@ -67,14 +67,12 @@ pub struct WritePlaneProviderView {
pub target_kind: Option<String>,
pub target: Option<String>,
pub detail: String,
- pub bridge_auth_configured: bool,
}
#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedWritePlaneTarget {
pub url: String,
- pub bridge_bearer_token: String,
}
#[cfg(test)]
@@ -97,31 +95,27 @@ pub fn resolve_write_plane_provider(
config: &RuntimeConfig,
publish: &PublishRuntimeView,
) -> WritePlaneProviderView {
- let (provider_runtime_id, binding_model, detail, bridge_auth_configured) =
- match config.publish.mode {
- PublishMode::NostrRelay => (
- "nostr_relay",
- "direct_relay_publish",
- "direct relay publish is selected; readiness is reported under publish",
- false,
- ),
- PublishMode::Radrootsd => (
- "radrootsd",
- "radrootsd_bridge_publish",
- "radrootsd bridge publish is selected; readiness is reported under publish",
- config.rpc.bridge_bearer_token.is_some(),
- ),
- };
+ let (provider_runtime_id, binding_model, detail) = match config.publish.transport {
+ PublishTransport::DirectNostrRelay => (
+ "direct_nostr_relay",
+ "direct_relay_publish",
+ "direct relay publish is selected; readiness is reported under publish",
+ ),
+ PublishTransport::RadrootsdProxy => (
+ "radrootsd_proxy",
+ "daemon_proxy_publish",
+ "radrootsd_proxy publish is selected; readiness is reported under publish",
+ ),
+ };
WritePlaneProviderView {
provider_runtime_id: provider_runtime_id.to_owned(),
binding_model: binding_model.to_owned(),
state: publish.state.clone(),
- provenance: ProviderProvenance::PublishMode.as_str().to_owned(),
+ provenance: ProviderProvenance::PublishTransport.as_str().to_owned(),
source: publish.source.clone(),
target_kind: None,
target: None,
detail: publish.reason.clone().unwrap_or_else(|| detail.to_owned()),
- bridge_auth_configured,
}
}
@@ -268,9 +262,9 @@ mod tests {
AccountConfig, AccountSecretContractConfig, CapabilityBindingConfig,
CapabilityBindingSource, CapabilityBindingTargetKind, HyfConfig, IdentityConfig,
InteractionConfig, LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig,
- OutputFormat, PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig,
- RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
- SignerConfig, Verbosity,
+ OutputFormat, PathsConfig, PublishConfig, PublishTransport, PublishTransportSource,
+ RelayConfig, RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig,
+ SignerBackend, SignerConfig, Verbosity,
};
use crate::view::runtime::{
PublishProviderRuntimeView, PublishRelayRuntimeView, PublishRuntimeView,
@@ -340,8 +334,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -364,7 +359,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
@@ -379,9 +373,9 @@ mod tests {
reason: Option<&str>,
) -> PublishRuntimeView {
PublishRuntimeView {
- mode: config.publish.mode.as_str().to_owned(),
+ transport: config.publish.transport.as_str().to_owned(),
source: config.publish.source.as_str().to_owned(),
- transport_family: config.publish.mode.transport_family().to_owned(),
+ transport_family: config.publish.transport.transport_family().to_owned(),
state: state.to_owned(),
executable: state == "ready",
reason: reason.map(str::to_owned),
@@ -392,7 +386,7 @@ mod tests {
source: config.relay.source.as_str().to_owned(),
},
provider: PublishProviderRuntimeView {
- provider_runtime_id: config.publish.mode.as_str().to_owned(),
+ provider_runtime_id: config.publish.transport.as_str().to_owned(),
state: state.to_owned(),
source: config.publish.source.as_str().to_owned(),
reason: reason.map(str::to_owned),
@@ -406,16 +400,18 @@ mod tests {
let publish = publish_view(
&config,
"unconfigured",
- Some("nostr_relay publish mode requires a configured relay"),
+ Some("direct_nostr_relay publish transport requires a configured relay"),
);
let view = resolve_write_plane_provider(&config, &publish);
- assert_eq!(view.provider_runtime_id, "nostr_relay");
+ assert_eq!(view.provider_runtime_id, "direct_nostr_relay");
assert_eq!(view.binding_model, "direct_relay_publish");
assert_eq!(view.state, "unconfigured");
- assert_eq!(view.provenance, ProviderProvenance::PublishMode.as_str());
+ assert_eq!(
+ view.provenance,
+ ProviderProvenance::PublishTransport.as_str()
+ );
assert!(view.target.is_none());
assert!(view.detail.contains("configured relay"));
- assert!(!view.bridge_auth_configured);
}
#[test]
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -1,20 +1,25 @@
#![allow(dead_code)]
+use std::fs;
use std::future::Future;
use std::path::PathBuf;
use radroots_authority::RadrootsLocalEventSigner;
use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_sdk::{
- RadrootsSdk, RadrootsSdkBuilder, RadrootsSdkError, RadrootsSdkStorageConfig, SdkRelayUrlPolicy,
+ RadrootsSdk, RadrootsSdkBuilder, RadrootsSdkError, RadrootsSdkStorageConfig,
+ SdkPublishTransport, SdkRelayUrlPolicy,
+ adapters::radrootsd::{RadrootsdAuth, RadrootsdProxyConfig as SdkRadrootsdProxyConfig},
};
+use radroots_secret_vault::{RadrootsSecretVault, RadrootsSecretVaultOsKeyring};
use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime};
use crate::runtime::RuntimeError;
use crate::runtime::account;
-use crate::runtime::config::RuntimeConfig;
+use crate::runtime::config::{PublishTransport, RuntimeConfig};
const SDK_STORAGE_DIR_NAME: &str = "sdk";
+const RADROOTSD_PROXY_SECRET_SERVICE: &str = "org.radroots.cli.radrootsd-proxy";
#[derive(Debug, thiserror::Error)]
pub enum CliSdkAdapterError {
@@ -29,15 +34,17 @@ pub struct CliSdkConfig {
pub storage_root: PathBuf,
pub relay_url_policy: SdkRelayUrlPolicy,
pub relay_urls: Vec<String>,
+ pub publish_transport: SdkPublishTransport,
}
impl CliSdkConfig {
- pub fn from_runtime_config(config: &RuntimeConfig) -> Self {
- Self {
+ pub fn from_runtime_config(config: &RuntimeConfig) -> Result<Self, RuntimeError> {
+ Ok(Self {
storage_root: sdk_storage_root(config),
relay_url_policy: sdk_relay_url_policy(config),
relay_urls: config.relay.urls.clone(),
- }
+ publish_transport: sdk_publish_transport(config)?,
+ })
}
pub fn builder(&self) -> RadrootsSdkBuilder {
@@ -46,7 +53,8 @@ impl CliSdkConfig {
.storage(RadrootsSdkStorageConfig::Directory(
self.storage_root.clone(),
))
- .relay_url_policy(self.relay_url_policy),
+ .relay_url_policy(self.relay_url_policy)
+ .publish_transport(self.publish_transport.clone()),
|builder, relay_url| builder.relay_url(relay_url.clone()),
)
}
@@ -60,7 +68,7 @@ pub struct CliSdkSession {
impl CliSdkSession {
pub fn connect(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> {
- let sdk_config = CliSdkConfig::from_runtime_config(config);
+ let sdk_config = CliSdkConfig::from_runtime_config(config)?;
let runtime = sdk_runtime()?;
let sdk = runtime.block_on(sdk_config.builder().build())?;
Ok(Self {
@@ -71,7 +79,7 @@ impl CliSdkSession {
}
pub fn connect_memory(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> {
- let sdk_config = CliSdkConfig::from_runtime_config(config);
+ let sdk_config = CliSdkConfig::from_runtime_config(config)?;
let runtime = sdk_runtime()?;
let sdk = runtime.block_on(memory_builder(&sdk_config).build())?;
Ok(Self {
@@ -151,7 +159,9 @@ pub(crate) fn sdk_runtime() -> Result<Runtime, RuntimeError> {
fn memory_builder(config: &CliSdkConfig) -> RadrootsSdkBuilder {
config.relay_urls.iter().fold(
- RadrootsSdk::builder().relay_url_policy(config.relay_url_policy),
+ RadrootsSdk::builder()
+ .relay_url_policy(config.relay_url_policy)
+ .publish_transport(config.publish_transport.clone()),
|builder, relay_url| builder.relay_url(relay_url.clone()),
)
}
@@ -169,6 +179,66 @@ pub fn sdk_relay_url_policy(config: &RuntimeConfig) -> SdkRelayUrlPolicy {
}
}
+pub fn sdk_relay_target_policy(config: &RuntimeConfig) -> radroots_sdk::SdkRelayTargetPolicy {
+ match config.publish.transport {
+ PublishTransport::DirectNostrRelay => {
+ radroots_sdk::SdkRelayTargetPolicy::UseConfiguredRelays
+ }
+ PublishTransport::RadrootsdProxy => {
+ radroots_sdk::SdkRelayTargetPolicy::use_publish_transport()
+ }
+ }
+}
+
+fn sdk_publish_transport(config: &RuntimeConfig) -> Result<SdkPublishTransport, RuntimeError> {
+ match config.publish.transport {
+ PublishTransport::DirectNostrRelay => Ok(SdkPublishTransport::DirectNostrRelay),
+ PublishTransport::RadrootsdProxy => {
+ let mut proxy_config =
+ SdkRadrootsdProxyConfig::new(config.publish.radrootsd_proxy.url.clone());
+ if let Some(auth) = radrootsd_proxy_auth(config)? {
+ proxy_config = proxy_config.with_auth(auth);
+ }
+ Ok(SdkPublishTransport::RadrootsdProxy(proxy_config))
+ }
+ }
+}
+
+fn radrootsd_proxy_auth(config: &RuntimeConfig) -> Result<Option<RadrootsdAuth>, RuntimeError> {
+ let proxy = &config.publish.radrootsd_proxy;
+ let token = if let Some(path) = proxy.token_file.as_ref() {
+ fs::read_to_string(path).map_err(|error| {
+ RuntimeError::Config(format!(
+ "failed to read radrootsd proxy token file {}: {error}",
+ path.display()
+ ))
+ })?
+ } else if let Some(secret_id) = proxy.token_secret_id.as_ref() {
+ let vault = RadrootsSecretVaultOsKeyring::new(RADROOTSD_PROXY_SECRET_SERVICE);
+ vault
+ .load_secret(secret_id)
+ .map_err(|error| {
+ RuntimeError::Config(format!(
+ "failed to load radrootsd proxy token secret `{secret_id}`: {error}"
+ ))
+ })?
+ .ok_or_else(|| {
+ RuntimeError::Config(format!(
+ "radrootsd proxy token secret `{secret_id}` was not found"
+ ))
+ })?
+ } else {
+ return Ok(None);
+ };
+ let token = token.trim();
+ if token.is_empty() {
+ return Err(RuntimeError::Config(
+ "radrootsd proxy bearer token is empty".to_owned(),
+ ));
+ }
+ Ok(Some(RadrootsdAuth::BearerToken(token.to_owned())))
+}
+
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
@@ -185,8 +255,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RhiConfig, RpcConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RhiConfig, RpcConfig, SignerBackend, SignerConfig,
+ Verbosity,
};
struct DirectRrRsDependency {
@@ -358,23 +429,6 @@ mod tests {
const LEGACY_DIRECT_RELAY_CONSUMERS: &[LegacyDirectRelayConsumer] = &[
LegacyDirectRelayConsumer {
- path: "src/runtime/listing.rs",
- required_tokens: &[
- "mutate_via_direct_relay(",
- "publish_signed_event_with_identity",
- ],
- owner: "listing.nostr_relay.write",
- reason: "non-migrated listing direct relay write mode outside SDK local publish",
- lifecycle: "retain until listing relay publish migrates to SDK-backed write APIs",
- },
- LegacyDirectRelayConsumer {
- path: "src/runtime/local_events.rs",
- required_tokens: &["DirectRelayFailure", "DirectRelayPublishError"],
- owner: "local-event.delivery-evidence",
- reason: "delivery evidence mapping for non-migrated direct relay publish outcomes",
- lifecycle: "retain until delivery evidence moves behind SDK or local-events APIs",
- },
- LegacyDirectRelayConsumer {
path: "src/runtime/order.rs",
required_tokens: &[
"legacy_order_preflight_relay_status",
@@ -559,7 +613,7 @@ mod tests {
vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()],
);
- let sdk_config = CliSdkConfig::from_runtime_config(&config);
+ let sdk_config = CliSdkConfig::from_runtime_config(&config).expect("sdk config");
assert_eq!(sdk_config.storage_root, config.local.root.join("sdk"));
assert_eq!(sdk_config.relay_url_policy, SdkRelayUrlPolicy::Public);
@@ -617,8 +671,7 @@ mod tests {
#[test]
fn sdk_sources_do_not_import_cli_types() {
- let sdk_src = Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("../../../../domains/radroots/sdk/crates/sdk/src");
+ let sdk_src = Path::new(env!("CARGO_MANIFEST_DIR")).join("../sdk/crates/sdk/src");
let mut files = Vec::new();
collect_rs_files(sdk_src.as_path(), &mut files);
let forbidden = [
@@ -724,7 +777,10 @@ mod tests {
.flat_map(move |dependencies| {
dependencies.iter().filter_map(move |(name, value)| {
dependency_path(value)
- .filter(|path| path.contains("domains/radroots/lib/crates"))
+ .filter(|path| {
+ path.contains("../lib/crates")
+ || path.contains("domains/radroots/lib/crates")
+ })
.map(|_| format!("{section}:{name}"))
})
})
@@ -867,8 +923,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: relays,
@@ -891,7 +948,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".to_owned(),
- bridge_bearer_token: None,
},
rhi: RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs
@@ -54,7 +54,7 @@ pub struct ActorWriteSignerAuthority {
pub provider_runtime_id: String,
pub account_identity_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
- pub provider_signer_session_id: Option<String>,
+ pub provider_session_ref: Option<String>,
}
pub fn resolve_signer_status(config: &RuntimeConfig) -> SignerStatusView {
@@ -267,7 +267,7 @@ fn disabled_binding_status() -> SignerBindingStatusView {
target: None,
managed_account_ref: None,
signer_session_ref: None,
- resolved_signer_session_id: None,
+ resolved_session_ref: None,
matched_session_count: None,
reason: Some(
"remote myc signer binding is disabled while cli signer mode is `local`".to_owned(),
@@ -286,7 +286,7 @@ fn deferred_myc_binding_status() -> SignerBindingStatusView {
target: None,
managed_account_ref: None,
signer_session_ref: None,
- resolved_signer_session_id: None,
+ resolved_session_ref: None,
matched_session_count: None,
reason: Some(MYC_DEFERRED_REASON.to_owned()),
}
diff --git a/src/runtime/store.rs b/src/runtime/store.rs
@@ -680,7 +680,7 @@ fn manifest_counts(manifest: &ReplicaDbExportManifestRs) -> LocalReplicaCountsVi
farms: table_row_count(manifest, "farm"),
listings: table_row_count(manifest, "trade_product"),
profiles: table_row_count(manifest, "nostr_profile"),
- relays: table_row_count(manifest, "nostr_relay"),
+ relays: table_row_count(manifest, "direct_nostr_relay"),
event_states: table_row_count(manifest, "nostr_event_state"),
}
}
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -26,7 +26,7 @@ use serde_json::json;
use crate::cli::global::SyncWatchArgs;
use crate::runtime::RuntimeError;
-use crate::runtime::config::{PublishMode, RuntimeConfig};
+use crate::runtime::config::RuntimeConfig;
use crate::runtime::direct_relay::{
DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, fetch_events_from_relays,
};
@@ -45,8 +45,6 @@ const SYNC_PUSH_ACTION: &str = "radroots sync push";
const SYNC_READY_ACTION: &str = "radroots market product search eggs";
const MARKET_READY_ACTION: &str = "radroots market product search eggs";
const INGEST_SOURCE: &str = "direct Nostr relay fetch · local replica ingest";
-const RADROOTSD_SYNC_PUSH_SOURCE: &str = "radrootsd sync push · deferred";
-pub(crate) const RADROOTSD_SYNC_PUSH_UNAVAILABLE_REASON: &str = "sync push is only available in publish mode `nostr_relay`; radrootsd sync push is not implemented";
const RELAY_FETCH_LIMIT: usize = 1_000;
const RELAY_FETCH_MAX_PAGES: usize = 5;
const MARKET_FRESHNESS_STALE_AFTER_SECONDS: u64 = 15 * 60;
@@ -329,10 +327,6 @@ where
}
pub fn push(config: &RuntimeConfig) -> Result<SyncActionView, CliSdkAdapterError> {
- if matches!(config.publish.mode, PublishMode::Radrootsd) {
- return Ok(push_radrootsd_unavailable_view(config));
- }
-
let session = CliSdkSession::connect(config)?;
if config.output.dry_run {
let status = session.block_on(session.sdk().sync().status(SyncStatusRequest::new()))?;
@@ -413,41 +407,6 @@ fn empty_action_from_snapshot(snapshot: SyncSnapshot, direction: &str) -> SyncAc
}
}
-fn push_radrootsd_unavailable_view(config: &RuntimeConfig) -> SyncActionView {
- SyncActionView {
- direction: "push".to_owned(),
- state: "unavailable".to_owned(),
- source: RADROOTSD_SYNC_PUSH_SOURCE.to_owned(),
- local_root: config.local.root.display().to_string(),
- replica_db: "not_checked".to_owned(),
- relay_count: config.relay.urls.len(),
- publish_policy: config.relay.publish_policy.as_str().to_owned(),
- freshness: SyncFreshnessView {
- state: "not_checked".to_owned(),
- display: "not checked".to_owned(),
- age_seconds: None,
- last_event_at: None,
- run: None,
- },
- queue: legacy_sync_queue(0, 0),
- target_relays: config.relay.urls.clone(),
- connected_relays: Vec::new(),
- acknowledged_relays: Vec::new(),
- failed_relays: Vec::new(),
- fetched_count: None,
- ingested_count: None,
- publishable_count: None,
- published_count: None,
- skipped_count: None,
- unsupported_count: None,
- failed_count: None,
- publish_plan: None,
- reason_code: Some("not_implemented".to_owned()),
- reason: Some(RADROOTSD_SYNC_PUSH_UNAVAILABLE_REASON.to_owned()),
- actions: vec!["radroots --publish-mode nostr_relay sync push".to_owned()],
- }
-}
-
fn sdk_sync_status_view(config: &RuntimeConfig, receipt: SyncStatusReceipt) -> SyncStatusView {
let actions = sdk_sync_status_actions(&receipt);
let relay_count = receipt.relay_targets.configured_count;
@@ -1471,7 +1430,7 @@ mod tests {
use super::{
DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, RelayIngestScope,
- freshness_for_scope, market_refresh_with_fetcher, pull_with_fetcher, push,
+ freshness_for_scope, market_refresh_with_fetcher, pull_with_fetcher,
relay_provenance_relays_for_scope, sdk_push_dry_run_view, sdk_push_view,
sdk_sync_status_view,
};
@@ -1479,8 +1438,9 @@ mod tests {
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
- RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
+ PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
+ RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
+ SignerConfig, Verbosity,
};
const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
@@ -1748,28 +1708,6 @@ mod tests {
);
}
- #[test]
- fn sync_push_rejects_radrootsd_before_store_or_sdk_work() {
- let dir = tempdir().expect("tempdir");
- let mut config = sample_config(dir.path(), Vec::new());
- config.publish.mode = PublishMode::Radrootsd;
-
- let view = push(&config).expect("radrootsd sync push view");
-
- assert_eq!(view.state, "unavailable");
- assert_eq!(view.replica_db, "not_checked");
- assert_eq!(view.relay_count, 0);
- assert_eq!(
- view.reason.as_deref(),
- Some(super::RADROOTSD_SYNC_PUSH_UNAVAILABLE_REASON)
- );
- assert_eq!(
- view.actions,
- vec!["radroots --publish-mode nostr_relay sync push"]
- );
- assert!(!config.local.replica_db_path.exists());
- }
-
fn sdk_status_receipt(
total_events: i64,
outbox_total_events: i64,
@@ -2343,8 +2281,9 @@ mod tests {
backend: SignerBackend::Local,
},
publish: PublishConfig {
- mode: PublishMode::NostrRelay,
- source: PublishModeSource::Defaults,
+ transport: PublishTransport::DirectNostrRelay,
+ source: PublishTransportSource::Defaults,
+ radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
},
relay: RelayConfig {
urls: relays,
@@ -2367,7 +2306,6 @@ mod tests {
},
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
- bridge_bearer_token: None,
},
rhi: crate::runtime::config::RhiConfig {
trusted_worker_pubkeys: Vec::new(),
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -299,7 +299,7 @@ pub struct RelayRuntimeView {
#[derive(Debug, Clone, Serialize)]
pub struct PublishRuntimeView {
- pub mode: String,
+ pub transport: String,
pub source: String,
pub transport_family: String,
pub state: String,
@@ -394,13 +394,11 @@ pub struct WritePlaneRuntimeView {
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
pub detail: String,
- pub bridge_auth_configured: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct RpcRuntimeView {
pub url: String,
- pub bridge_auth_configured: bool,
}
#[derive(Debug, Clone, Serialize)]
@@ -860,7 +858,7 @@ pub struct FarmStatusView {
pub config_valid: bool,
pub account_state: String,
pub listing_defaults_state: String,
- pub publish_mode: String,
+ pub publish_transport: String,
pub publish_state: String,
pub publish_executable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -920,8 +918,6 @@ pub struct FarmPublishView {
pub seller_pubkey: String,
pub seller_actor_source: String,
pub farm_d_tag: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub requested_signer_session_id: Option<String>,
pub profile: FarmPublishComponentView,
pub farm: FarmPublishComponentView,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -966,8 +962,6 @@ pub struct FarmPublishComponentView {
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_addr: Option<String>,
@@ -1007,11 +1001,7 @@ pub struct FarmPublishJobView {
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub requested_signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
pub signer_mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
@@ -1412,8 +1402,6 @@ pub struct JobSummaryView {
pub state: String,
pub terminal: bool,
pub signer: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
pub requested_at_unix: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at_unix: Option<u64>,
@@ -1427,8 +1415,6 @@ pub struct JobDetailView {
pub state: String,
pub terminal: bool,
pub signer: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
pub requested_at_unix: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at_unix: Option<u64>,
@@ -1456,8 +1442,6 @@ pub struct JobWatchFrameView {
pub state: String,
pub terminal: bool,
pub signer: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
pub summary: String,
}
@@ -1737,10 +1721,6 @@ pub struct OrderSubmitView {
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub requested_signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub job: Option<OrderJobView>,
@@ -2398,10 +2378,6 @@ pub struct OrderJobView {
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub requested_signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_addr: Option<String>,
@@ -2725,7 +2701,7 @@ pub struct SellMutationView {
#[serde(default)]
pub deduplicated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
- pub publish_mode: Option<String>,
+ pub publish_transport: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -2852,10 +2828,6 @@ pub struct ListingMutationView {
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub requested_signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
pub local_replica: Option<ListingMutationLocalReplicaView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
@@ -2947,12 +2919,8 @@ pub struct ListingMutationJobView {
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub requested_signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
pub signer_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
pub relay_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub acknowledged_relay_count: Option<usize>,
@@ -3558,7 +3526,7 @@ pub struct SignerBindingStatusView {
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_session_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub resolved_signer_session_id: Option<String>,
+ pub resolved_session_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub matched_session_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -1308,6 +1308,8 @@ fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() {
assert!(!output.status.success());
assert_eq!(value["operation_id"], "farm.publish");
assert_eq!(value["result"], serde_json::Value::Null);
+ assert_eq!(value["errors"][0]["code"], "network_unavailable");
+ assert_eq!(value["errors"][0]["detail"]["class"], "network");
let detail = &value["errors"][0]["detail"];
assert_eq!(detail["source"], "SDK farm publish · local key");
assert_eq!(detail["profile"]["state"], "not_submitted");
@@ -1981,11 +1983,16 @@ fn local_seller_publish_commands_attempt_configured_relay() {
listing_file_arg.as_ref(),
]);
assert!(!archive_output.status.success());
- assert_direct_relay_connection_failure(
- &archive_value,
- "listing.archive",
- &["listing", "archive"],
+ assert_eq!(archive_value["operation_id"], "listing.archive");
+ assert_eq!(archive_value["result"], serde_json::Value::Null);
+ assert_eq!(archive_value["errors"][0]["code"], "network_unavailable");
+ assert_eq!(archive_value["errors"][0]["detail"]["class"], "network");
+ assert_contains(
+ &archive_value["errors"][0]["message"],
+ "SDK relay publish did not reach accepted quorum",
);
+ assert_no_removed_command_reference(&archive_value, &["listing", "archive"]);
+ assert_no_daemon_runtime_reference(&archive_value, &["listing", "archive"]);
assert_eq!(
archive_value["errors"][0]["detail"]["target_relays"][0],
relay
@@ -1995,7 +2002,7 @@ fn local_seller_publish_commands_attempt_configured_relay() {
.as_array()
.expect("connected relays")
.len(),
- 0
+ 1
);
assert_eq!(
archive_value["errors"][0]["detail"]["failed_relays"]
@@ -2116,8 +2123,8 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
let submit_args = [
"--format",
"json",
- "--publish-mode",
- "nostr_relay",
+ "--publish-transport",
+ "direct_nostr_relay",
"--dry-run",
"order",
"submit",
@@ -2200,8 +2207,8 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
let accept_args = [
"--format",
"json",
- "--publish-mode",
- "nostr_relay",
+ "--publish-transport",
+ "direct_nostr_relay",
"--dry-run",
"order",
"accept",
@@ -2218,8 +2225,8 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
let decline_args = [
"--format",
"json",
- "--publish-mode",
- "nostr_relay",
+ "--publish-transport",
+ "direct_nostr_relay",
"--dry-run",
"order",
"decline",
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -2,7 +2,7 @@ mod support;
use std::fs;
use std::net::{TcpListener, TcpStream};
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::thread::{self, JoinHandle};
use radroots_events::RadrootsNostrEventPtr;
@@ -29,8 +29,8 @@ use support::{
ORDERABLE_LISTING_RELAY, RadrootsCliSandbox, assert_contains,
assert_no_daemon_runtime_reference, assert_no_removed_command_reference, create_listing_draft,
duplicate_orderable_listing_row, identity_public, identity_secret, json_from_stdout,
- make_listing_publishable, make_listing_publishable_with_seller, ndjson_from_stdout, radroots,
- remove_orderable_listing, replace_latest_listing_event_id, seed_orderable_listing, toml_string,
+ make_listing_publishable, ndjson_from_stdout, radroots, remove_orderable_listing,
+ replace_latest_listing_event_id, seed_orderable_listing, toml_string,
update_orderable_listing_available_amount, update_orderable_listing_primary_bin_id,
write_public_identity_profile, write_secret_identity_profile,
};
@@ -55,6 +55,12 @@ fn test_pubkey(value: &str) -> RadrootsPublicKey {
value.parse().expect("valid public key")
}
+fn radrootsd_proxy_token_file(sandbox: &RadrootsCliSandbox) -> PathBuf {
+ let path = sandbox.root().join("radrootsd_proxy.token");
+ fs::write(&path, "proxy_test_token\n").expect("write proxy token file");
+ path
+}
+
struct RelayFetchServer {
endpoint: String,
handle: JoinHandle<()>,
@@ -770,21 +776,18 @@ fn root_help_exposes_only_target_namespaces() {
}
#[test]
-fn root_help_explains_publish_modes() {
+fn root_help_explains_publish_transports() {
let output = radroots().arg("--help").output().expect("run root help");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
- assert!(stdout.contains("nostr_relay uses direct relay publish"));
- assert!(stdout.contains("radrootsd is reserved and fails closed"));
- assert!(stdout.contains("Relay mode never silently falls back"));
+ assert!(stdout.contains("direct_nostr_relay publishes directly to configured relays"));
+ assert!(stdout.contains("radrootsd_proxy publishes locally signed events"));
assert!(stdout.contains("Inspect local readiness and mode-specific recovery steps"));
- assert!(
- stdout.contains(
- "Select nostr_relay direct relay publish or reserved radrootsd guardrail mode"
- )
- );
+ assert!(stdout.contains(
+ "Select direct_nostr_relay direct relay publish or radrootsd_proxy daemon proxy publish"
+ ));
}
fn help_lists(stdout: &str, command: &str) -> bool {
@@ -794,29 +797,6 @@ fn help_lists(stdout: &str, command: &str) -> bool {
})
}
-fn assert_radrootsd_deferred_message(value: &Value) {
- let message = value["errors"][0]["message"]
- .as_str()
- .expect("error message");
- assert!(message.contains("radrootsd publish mode is deferred"));
- assert!(message.contains("publish mode `nostr_relay`"));
- assert!(
- !message.contains("signer.remote_nip46"),
- "deferred publish-mode message should not suggest signer-session setup: {message}"
- );
-}
-
-fn assert_direct_relay_next_action(actions: &Value, command: &str) {
- let action = actions
- .as_array()
- .expect("next actions")
- .iter()
- .find(|action| action["command"] == command)
- .expect("direct relay next action");
-
- assert_eq!(action["kind"], "cli_command");
-}
-
#[test]
fn removed_global_flags_are_rejected_publicly() {
for args in [
@@ -839,24 +819,27 @@ fn removed_global_flags_are_rejected_publicly() {
}
#[test]
-fn config_get_exposes_resolved_publish_state() {
+fn config_get_exposes_radrootsd_proxy_missing_token_state() {
let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n");
+ sandbox.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
let value = sandbox.json_success(&["--format", "json", "config", "get"]);
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["mode"], "radrootsd");
+ assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
assert_eq!(
value["result"]["publish"]["source"],
"user config · local first"
);
- assert_eq!(value["result"]["publish"]["transport_family"], "radrootsd");
- assert_eq!(value["result"]["publish"]["state"], "unavailable");
+ assert_eq!(
+ value["result"]["publish"]["transport_family"],
+ "radrootsd_proxy"
+ );
+ assert_eq!(value["result"]["publish"]["state"], "unconfigured");
assert_eq!(value["result"]["publish"]["executable"], false);
assert_contains(
&value["result"]["publish"]["reason"],
- "radrootsd publish mode is deferred",
+ "configured token file or token secret id",
);
assert_eq!(
value["result"]["account_resolution"]["status"],
@@ -864,77 +847,76 @@ fn config_get_exposes_resolved_publish_state() {
);
assert_eq!(
value["result"]["publish"]["provider"]["provider_runtime_id"],
- "radrootsd"
+ "radrootsd_proxy"
);
assert_eq!(
value["result"]["write_plane"]["provider_runtime_id"],
- "radrootsd"
+ "radrootsd_proxy"
);
assert_eq!(
value["result"]["write_plane"]["binding_model"],
- "radrootsd_bridge_publish"
+ "daemon_proxy_publish"
+ );
+ assert_eq!(value["result"]["write_plane"]["state"], "unconfigured");
+ assert_eq!(
+ value["result"]["radrootsd_proxy"]["token_file_configured"],
+ false
);
- assert_eq!(value["result"]["write_plane"]["state"], "unavailable");
assert_eq!(
- value["result"]["write_plane"]["bridge_auth_configured"],
+ value["result"]["radrootsd_proxy"]["token_secret_id_configured"],
false
);
- assert_eq!(value["result"]["rpc"]["bridge_auth_configured"], false);
assert_eq!(
value["result"]["actions"][0],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get"
+ "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
);
- assert_direct_relay_next_action(
- &value["next_actions"],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get",
+ assert_eq!(
+ value["next_actions"][0]["env_var"],
+ "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE"
);
}
#[test]
-fn config_get_radrootsd_with_bridge_auth_still_reports_deferred_publish_mode() {
+fn config_get_radrootsd_proxy_with_token_file_reports_ready_transport() {
let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n");
+ sandbox.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
+ let token_file = radrootsd_proxy_token_file(&sandbox);
let mut command = sandbox.command();
command
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE", token_file)
.args(["--format", "json", "config", "get"]);
let output = command.output().expect("run config get");
let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
assert!(output.status.success());
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["mode"], "radrootsd");
- assert_eq!(value["result"]["publish"]["state"], "unavailable");
- assert_eq!(value["result"]["publish"]["executable"], false);
- assert_contains(
- &value["result"]["publish"]["reason"],
- "radrootsd publish mode is deferred",
- );
- assert_eq!(value["result"]["rpc"]["bridge_auth_configured"], true);
+ assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
+ assert_eq!(value["result"]["publish"]["state"], "ready");
+ assert_eq!(value["result"]["publish"]["executable"], true);
+ assert_eq!(value["result"]["publish"]["reason"], Value::Null);
assert_eq!(
- value["result"]["actions"][0],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get"
+ value["result"]["radrootsd_proxy"]["token_file_configured"],
+ true
);
assert_eq!(
- value["next_actions"]
+ value["result"]["actions"]
.as_array()
- .expect("next actions")
+ .expect("actions")
.len(),
- 1
- );
- assert_direct_relay_next_action(
- &value["next_actions"],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get",
+ 0
);
}
#[test]
-fn config_get_marks_radrootsd_deferred_even_with_bridge_auth_and_session_binding() {
+fn config_get_marks_radrootsd_proxy_unavailable_with_myc_signer() {
let sandbox = RadrootsCliSandbox::new();
sandbox.write_app_config(
r#"[publish]
-mode = "radrootsd"
+transport = "radrootsd_proxy"
+
+[signer]
+backend = "myc"
[[capability_binding]]
capability = "signer.remote_nip46"
@@ -944,33 +926,26 @@ target = "http://myc.invalid"
signer_session_ref = "session_ready"
"#,
);
+ let token_file = radrootsd_proxy_token_file(&sandbox);
let mut command = sandbox.command();
command
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE", token_file)
.args(["--format", "json", "config", "get"]);
let output = command.output().expect("run config get");
let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
assert!(output.status.success());
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["mode"], "radrootsd");
- assert_eq!(value["result"]["publish"]["relay"]["ready"], false);
+ assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
assert_eq!(value["result"]["publish"]["state"], "unavailable");
assert_eq!(value["result"]["publish"]["executable"], false);
- assert_contains(
- &value["result"]["publish"]["reason"],
- "radrootsd publish mode is deferred",
- );
+ assert_contains(&value["result"]["publish"]["reason"], "signer mode `local`");
assert_eq!(
value["result"]["publish"]["provider"]["state"],
"unavailable"
);
- assert_eq!(value["result"]["rpc"]["bridge_auth_configured"], true);
- assert_eq!(
- value["result"]["actions"][0],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get"
- );
+ assert_eq!(value["result"]["actions"][0], "radroots signer status get");
}
#[test]
@@ -987,7 +962,10 @@ fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() {
]);
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["mode"], "nostr_relay");
+ assert_eq!(
+ value["result"]["publish"]["transport"],
+ "direct_nostr_relay"
+ );
assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
assert_eq!(value["result"]["publish"]["signed_write_required"], true);
assert_eq!(value["result"]["publish"]["state"], "unconfigured");
@@ -1002,7 +980,7 @@ fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() {
);
assert_eq!(
value["result"]["write_plane"]["provider_runtime_id"],
- "nostr_relay"
+ "direct_nostr_relay"
);
assert_eq!(
value["result"]["write_plane"]["binding_model"],
@@ -1046,7 +1024,10 @@ fn config_get_marks_relay_publish_ready_with_secret_backed_local_account() {
"get",
]);
- assert_eq!(value["result"]["publish"]["mode"], "nostr_relay");
+ assert_eq!(
+ value["result"]["publish"]["transport"],
+ "direct_nostr_relay"
+ );
assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
assert_eq!(value["result"]["publish"]["signed_write_required"], true);
assert_eq!(value["result"]["publish"]["state"], "ready");
@@ -1070,7 +1051,10 @@ fn config_get_marks_relay_publish_unavailable_with_deferred_signer_mode() {
"get",
]);
- assert_eq!(value["result"]["publish"]["mode"], "nostr_relay");
+ assert_eq!(
+ value["result"]["publish"]["transport"],
+ "direct_nostr_relay"
+ );
assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
assert_eq!(value["result"]["publish"]["signed_write_required"], true);
assert_eq!(value["result"]["publish"]["state"], "unavailable");
@@ -1119,8 +1103,10 @@ fn config_get_marks_relay_publish_unconfigured_with_watch_only_account() {
fn health_surfaces_publish_state_under_deferred_signer_mode() {
let sandbox = RadrootsCliSandbox::new();
let missing_myc = sandbox.root().join("bin/missing-myc");
+ let token_file = radrootsd_proxy_token_file(&sandbox);
sandbox.write_app_config(&format!(
- "[publish]\nmode = \"radrootsd\"\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
+ "[publish]\ntransport = \"radrootsd_proxy\"\n\n[publish.radrootsd_proxy]\ntoken_file = \"{}\"\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
+ toml_string(token_file.display().to_string().as_str()),
toml_string(missing_myc.display().to_string().as_str())
));
@@ -1128,16 +1114,13 @@ fn health_surfaces_publish_state_under_deferred_signer_mode() {
assert_eq!(value["operation_id"], "health.status.get");
assert_eq!(value["result"]["state"], "needs_attention");
- assert_eq!(value["result"]["publish"]["mode"], "radrootsd");
+ assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
assert_eq!(value["result"]["publish"]["executable"], false);
assert_eq!(
value["result"]["publish"]["provider"]["state"],
"unavailable"
);
- assert_contains(
- &value["result"]["publish"]["reason"],
- "radrootsd publish mode is deferred",
- );
+ assert_contains(&value["result"]["publish"]["reason"], "signer mode `local`");
assert_eq!(value["result"]["store"]["state"], "ready");
assert_eq!(
value["result"]["store"]["source"],
@@ -1146,17 +1129,14 @@ fn health_surfaces_publish_state_under_deferred_signer_mode() {
assert_eq!(value["result"]["store"]["canonical_store"], "sdk");
assert_eq!(value["result"]["signer"]["state"], "unavailable");
assert_eq!(value["result"]["actions"][0], "radroots account create");
- assert_eq!(
- value["result"]["actions"][1],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get"
- );
+ assert_eq!(value["result"]["actions"][1], "radroots signer status get");
assert_eq!(
value["next_actions"][0]["command"],
"radroots account create"
);
- assert_direct_relay_next_action(
- &value["next_actions"],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get",
+ assert_eq!(
+ value["next_actions"][1]["command"],
+ "radroots signer status get"
);
assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
}
@@ -1198,7 +1178,7 @@ fn health_status_distinguishes_relay_ready_from_missing_signed_write_account() {
#[test]
fn health_check_exposes_publish_readiness() {
let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n");
+ sandbox.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
let value = sandbox.json_success(&["--format", "json", "health", "check", "run"]);
@@ -1209,12 +1189,18 @@ fn health_check_exposes_publish_readiness() {
"unresolved"
);
assert_eq!(value["result"]["account_resolution"]["source"], "none");
- assert_eq!(value["result"]["checks"]["publish"]["mode"], "radrootsd");
- assert_eq!(value["result"]["checks"]["publish"]["state"], "unavailable");
+ assert_eq!(
+ value["result"]["checks"]["publish"]["transport"],
+ "radrootsd_proxy"
+ );
+ assert_eq!(
+ value["result"]["checks"]["publish"]["state"],
+ "unconfigured"
+ );
assert_eq!(value["result"]["checks"]["publish"]["executable"], false);
assert_contains(
&value["result"]["checks"]["publish"]["reason"],
- "radrootsd publish mode is deferred",
+ "configured token file or token secret id",
);
assert_eq!(value["result"]["checks"]["store"]["state"], "ready");
assert_eq!(
@@ -1226,15 +1212,19 @@ fn health_check_exposes_publish_readiness() {
assert_eq!(value["result"]["actions"][0], "radroots account create");
assert_eq!(
value["result"]["actions"][1],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get"
+ "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
);
assert_eq!(
value["next_actions"][0]["command"],
"radroots account create"
);
- assert_direct_relay_next_action(
- &value["next_actions"],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get",
+ assert_eq!(
+ value["next_actions"][1]["description"],
+ "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
+ );
+ assert_eq!(
+ value["next_actions"][1]["env_var"],
+ "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE"
);
assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
}
@@ -1270,7 +1260,10 @@ fn health_check_marks_relay_publish_ready_with_secret_backed_local_account() {
value["result"]["account_resolution"]["resolved_account"]["write_capable"],
true
);
- assert_eq!(value["result"]["checks"]["publish"]["mode"], "nostr_relay");
+ assert_eq!(
+ value["result"]["checks"]["publish"]["transport"],
+ "direct_nostr_relay"
+ );
assert_eq!(value["result"]["checks"]["publish"]["state"], "ready");
assert_eq!(value["result"]["checks"]["publish"]["executable"], true);
assert_eq!(
@@ -1308,7 +1301,7 @@ fn farm_readiness_check_reports_mode_specific_publish_gates() {
} else {
&relay_value["result"]
};
- assert_eq!(relay_detail["publish_mode"], "nostr_relay");
+ assert_eq!(relay_detail["publish_transport"], "direct_nostr_relay");
assert_eq!(relay_detail["publish_state"], "unconfigured");
assert_eq!(relay_detail["publish_executable"], false);
assert_eq!(relay_detail["missing"][0], "Configured relay");
@@ -1324,466 +1317,106 @@ signer_session_ref = "session_test"
);
let output = sandbox
.command()
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
+ .env(
+ "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE",
+ radrootsd_proxy_token_file(&sandbox),
+ )
.args([
"--format",
"json",
- "--publish-mode",
- "radrootsd",
+ "--publish-transport",
+ "radrootsd_proxy",
"farm",
"readiness",
"check",
])
.output()
- .expect("run radrootsd farm readiness");
+ .expect("run radrootsd proxy farm readiness");
let radrootsd_value: Value = serde_json::from_slice(&output.stdout).expect("json output");
assert!(output.status.success());
assert_eq!(radrootsd_value["operation_id"], "farm.readiness.check");
- assert_eq!(radrootsd_value["result"]["publish_mode"], "radrootsd");
- assert_eq!(radrootsd_value["result"]["publish_state"], "unavailable");
- assert_eq!(radrootsd_value["result"]["publish_executable"], false);
assert_contains(
- &radrootsd_value["result"]["reason"],
- "radrootsd publish mode is deferred",
+ &radrootsd_value["result"]["publish_transport"],
+ "radrootsd_proxy",
);
+ assert_eq!(radrootsd_value["result"]["publish_state"], "ready");
+ assert_eq!(radrootsd_value["result"]["publish_executable"], true);
+ assert_eq!(radrootsd_value["result"]["reason"], Value::Null);
assert_eq!(
radrootsd_value["result"]["actions"][0],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com farm publish"
- );
-}
-
-#[test]
-fn radrootsd_listing_publish_fails_closed_without_bridge_or_relay_side_effects() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Router Farm",
- "--location",
- "farmstand",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ]);
- let listing_file = create_listing_draft(&sandbox, "radrootsd-router");
- make_listing_publishable(
- &listing_file,
- farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag"),
- );
-
- let output = sandbox
- .command()
- .env("RADROOTS_CLI_RPC_URL", "http://127.0.0.1:9")
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
- .args([
- "--format",
- "json",
- "--publish-mode",
- "radrootsd",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "idem_listing",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ])
- .output()
- .expect("run radrootsd listing publish");
- let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&value);
- assert_eq!(
- value["errors"][0]["detail"]["actions"][0],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com listing publish <file>"
- );
- assert_direct_relay_next_action(
- &value["next_actions"],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com listing publish <file>",
- );
-}
-
-#[test]
-fn radrootsd_farm_publish_fails_closed_without_bridge_or_relay_side_effects() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Router Farm",
- "--location",
- "farmstand",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ]);
-
- let output = sandbox
- .command()
- .env("RADROOTS_CLI_RPC_URL", "http://127.0.0.1:9")
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
- .args([
- "--format",
- "json",
- "--publish-mode",
- "radrootsd",
- "--approval-token",
- "approve",
- "--idempotency-key",
- "idem_farm",
- "farm",
- "publish",
- ])
- .output()
- .expect("run radrootsd farm publish");
- let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
- assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&value);
- assert_eq!(
- value["errors"][0]["detail"]["actions"][0],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com farm publish"
- );
- assert_direct_relay_next_action(
- &value["next_actions"],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com farm publish",
- );
-
- let persisted = sandbox.json_success(&["--format", "json", "farm", "get"]);
- assert_eq!(
- persisted["result"]["document"]["publication"]["profile_event_id"],
- Value::Null
- );
- assert_eq!(
- persisted["result"]["document"]["publication"]["farm_event_id"],
- Value::Null
+ "radroots farm publish"
);
}
#[test]
-fn radrootsd_farm_publish_ignores_signer_session_binding_and_fails_closed() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Binding Farm",
- "--location",
- "farmstand",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ]);
- sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n");
-
- let dry_run_output = sandbox
- .command()
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
- .args(["--format", "json", "--dry-run", "farm", "publish"])
- .output()
- .expect("run radrootsd farm publish dry-run");
- let dry_run: Value = serde_json::from_slice(&dry_run_output.stdout).expect("json output");
-
- assert!(!dry_run_output.status.success());
- assert_eq!(dry_run_output.status.code(), Some(3));
- assert_eq!(dry_run["operation_id"], "farm.publish");
- assert_eq!(dry_run["errors"][0]["code"], "operation_unavailable");
- assert_eq!(dry_run["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&dry_run);
-
- let live_output = sandbox
- .command()
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
- .args([
+fn radrootsd_proxy_listing_publish_update_and_archive_dry_run_without_direct_relays() {
+ for operation in ["publish", "update", "archive"] {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.json_success(&["--format", "json", "account", "create"]);
+ let farm = sandbox.json_success(&[
"--format",
"json",
- "--approval-token",
- "approve",
"farm",
- "publish",
- ])
- .output()
- .expect("run radrootsd farm publish");
- let live: Value = serde_json::from_slice(&live_output.stdout).expect("json output");
-
- assert!(!live_output.status.success());
- assert_eq!(live_output.status.code(), Some(3));
- assert_eq!(live["operation_id"], "farm.publish");
- assert_eq!(live["errors"][0]["code"], "operation_unavailable");
- assert_eq!(live["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&live);
-}
-
-#[test]
-fn radrootsd_listing_writes_dry_run_fail_closed_before_account_or_bridge_work() {
- for operation in ["publish", "update", "archive"] {
- let sandbox = RadrootsCliSandbox::new();
- let seller = identity_public(42);
- let listing_file = create_listing_draft(
- &sandbox,
- format!("radrootsd-no-account-dry-run-{operation}").as_str(),
- );
- make_listing_publishable_with_seller(
+ "create",
+ "--name",
+ "Proxy Farm",
+ "--location",
+ "farmstand",
+ "--country",
+ "US",
+ "--delivery-method",
+ "pickup",
+ ]);
+ let listing_file =
+ create_listing_draft(&sandbox, format!("radrootsd-proxy-{operation}").as_str());
+ make_listing_publishable(
&listing_file,
- "AAAAAAAAAAAAAAAAAAAAAw",
- seller.public_key_hex.as_str(),
- );
- sandbox.write_app_config(
- r#"[publish]
-mode = "radrootsd"
-
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "explicit_endpoint"
-target = "http://myc.invalid"
-signer_session_ref = "session_test"
-"#,
+ farm["result"]["config"]["farm_d_tag"]
+ .as_str()
+ .expect("farm d tag"),
);
- let mut command = sandbox.command();
- command
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
+ let output = sandbox
+ .command()
+ .env(
+ "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE",
+ radrootsd_proxy_token_file(&sandbox),
+ )
.args([
"--format",
"json",
- "--account-id",
- "missing-local-account",
+ "--publish-transport",
+ "radrootsd_proxy",
"--dry-run",
"listing",
operation,
listing_file.to_string_lossy().as_ref(),
- ]);
- let output = command
+ ])
.output()
- .expect("run radrootsd dry-run listing write");
+ .expect("run proxy listing dry run");
let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
+ assert!(output.status.success());
assert_eq!(value["operation_id"], format!("listing.{operation}"));
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&value);
- }
-}
-
-#[test]
-fn radrootsd_listing_writes_fail_closed_before_account_or_bridge_work() {
- for operation in ["publish", "update", "archive"] {
- let sandbox = RadrootsCliSandbox::new();
- let seller = identity_public(43);
- let listing_file = create_listing_draft(
- &sandbox,
- format!("radrootsd-no-account-{operation}").as_str(),
- );
- make_listing_publishable_with_seller(
- &listing_file,
- "AAAAAAAAAAAAAAAAAAAAAw",
- seller.public_key_hex.as_str(),
+ assert_eq!(value["result"]["state"], "dry_run");
+ assert_eq!(value["result"]["source"], "SDK listing publish · local key");
+ assert_eq!(value["result"]["dry_run"], true);
+ assert_eq!(
+ value["result"]["target_relays"]
+ .as_array()
+ .expect("relays")
+ .len(),
+ 0
);
- sandbox.write_app_config(
- r#"[publish]
-mode = "radrootsd"
-
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "explicit_endpoint"
-target = "http://myc.invalid"
-signer_session_ref = "session_test"
-"#,
+ assert_contains(
+ &value["result"]["reason"],
+ "SDK enqueue and relay push skipped",
);
- let mut command = sandbox.command();
- command
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
- .args([
- "--format",
- "json",
- "--account-id",
- "missing-local-account",
- "--approval-token",
- "approve",
- "listing",
- operation,
- listing_file.to_string_lossy().as_ref(),
- ]);
- let output = command.output().expect("run radrootsd listing write");
- let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
- assert_eq!(value["operation_id"], format!("listing.{operation}"));
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&value);
}
}
#[test]
-fn radrootsd_listing_publish_does_not_surface_bridge_errors_before_guardrail() {
- let sandbox = RadrootsCliSandbox::new();
- let listing_file = create_listing_draft(&sandbox, "radrootsd-bridge-error");
- make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
- sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n");
-
- let mut command = sandbox.command();
- command
- .env("RADROOTS_CLI_RPC_URL", "http://127.0.0.1:9")
- .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
- .args([
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
- let output = command.output().expect("run radrootsd listing publish");
- let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&value);
-}
-
-#[test]
-fn radrootsd_listing_publish_fails_closed_before_relay_or_myc_preflight() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Deferred Farm",
- "--location",
- "farmstand",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ]);
- let listing_file = create_listing_draft(&sandbox, "radrootsd-myc-router");
- make_listing_publishable(
- &listing_file,
- farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag"),
- );
- sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n\n[signer]\nbackend = \"myc\"\n");
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--approval-token",
- "approve",
- "listing",
- "publish",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&value);
- assert!(
- !value["errors"][0]["message"]
- .as_str()
- .expect("error message")
- .contains("signer mode `myc`")
- );
-}
-
-#[test]
-fn radrootsd_publish_mode_routes_listing_update() {
- let sandbox = RadrootsCliSandbox::new();
- sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
- "--format",
- "json",
- "farm",
- "create",
- "--name",
- "Update Farm",
- "--location",
- "farmstand",
- "--country",
- "US",
- "--delivery-method",
- "pickup",
- ]);
- let listing_file = create_listing_draft(&sandbox, "radrootsd-update-router");
- make_listing_publishable(
- &listing_file,
- farm["result"]["config"]["farm_d_tag"]
- .as_str()
- .expect("farm d tag"),
- );
-
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--publish-mode",
- "radrootsd",
- "--approval-token",
- "approve",
- "listing",
- "update",
- listing_file.to_string_lossy().as_ref(),
- ]);
-
- assert!(!output.status.success());
- assert_eq!(output.status.code(), Some(3));
- assert_eq!(value["operation_id"], "listing.update");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["class"], "operation");
- assert_radrootsd_deferred_message(&value);
- assert_eq!(
- value["errors"][0]["detail"]["actions"][0],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com listing update <file>"
- );
-}
-
-#[test]
fn listing_update_publish_attempts_direct_relay_with_approval() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
@@ -1828,7 +1461,7 @@ fn listing_update_publish_attempts_direct_relay_with_approval() {
assert_eq!(value["errors"][0]["detail"]["class"], "network");
assert_contains(
&value["errors"][0]["message"],
- "direct relay connection failed",
+ "SDK relay publish did not reach accepted quorum",
);
assert!(
!value["errors"][0]["message"]
@@ -2252,15 +1885,20 @@ fn next_actions_mirror_result_actions_for_json_and_ndjson() {
&["--format", "ndjson", "health", "check", "run"][..],
] {
let daemon = RadrootsCliSandbox::new();
- daemon.write_app_config("[publish]\nmode = \"radrootsd\"\n");
+ daemon.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
let output = daemon.command().args(args).output().expect("run ndjson");
let frames = ndjson_from_stdout(&output);
let terminal = frames.last().expect("terminal ndjson frame");
assert!(output.status.success(), "{args:?}");
- assert_direct_relay_next_action(
- &terminal["payload"]["next_actions"],
- "radroots --publish-mode nostr_relay --relay wss://relay.example.com config get",
+ assert!(
+ terminal["payload"]["next_actions"]
+ .as_array()
+ .expect("next actions")
+ .iter()
+ .any(|action| action["description"]
+ == "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"),
+ "{args:?}"
);
}
}
@@ -2294,9 +1932,8 @@ fn human_health_status_surfaces_publish_reason_and_actions() {
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
assert!(stdout.starts_with("health.status.get: needs_attention\n"));
- assert!(stdout.contains("publish_mode: nostr_relay"));
assert!(stdout.contains("publish_state: unconfigured"));
- assert!(stdout.contains("reason: nostr_relay publish mode requires a selected or default write-capable local account"));
+ assert!(stdout.contains("reason: direct_nostr_relay publish transport requires a selected or default write-capable local account for signed writes"));
assert!(stdout.contains("- radroots account create"));
assert!(serde_json::from_str::<Value>(&stdout).is_err());
}
@@ -3138,76 +2775,18 @@ fn order_status_get_invalid_order_id_uses_sdk_error_contract() {
}
#[test]
-fn radrootsd_sync_push_failure_exposes_nostr_relay_recovery_action() {
+fn legacy_radrootsd_publish_transport_value_is_rejected() {
let sandbox = RadrootsCliSandbox::new();
- let (json_output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--publish-mode",
- "radrootsd",
- "sync",
- "push",
- ]);
-
- assert_eq!(json_output.status.code(), Some(3));
- assert_eq!(value["operation_id"], "sync.push");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "operation_unavailable");
- assert_eq!(value["errors"][0]["detail"]["state"], "unavailable");
- assert_eq!(value["errors"][0]["detail"]["publish"]["mode"], "radrootsd");
- assert_eq!(
- value["errors"][0]["detail"]["publish"]["state"],
- "unavailable"
- );
- assert_eq!(
- value["errors"][0]["detail"]["actions"][0],
- "radroots --publish-mode nostr_relay sync push"
- );
- assert_eq!(value["next_actions"][0]["kind"], "cli_command");
- assert_eq!(
- value["next_actions"][0]["command"],
- "radroots --publish-mode nostr_relay sync push"
- );
-
- let ndjson_output = sandbox
+ let output = sandbox
.command()
- .args([
- "--format",
- "ndjson",
- "--publish-mode",
- "radrootsd",
- "sync",
- "push",
- ])
+ .args(["--publish-transport", "radrootsd", "sync", "push"])
.output()
- .expect("run sync push ndjson");
- let frames = ndjson_from_stdout(&ndjson_output);
- let terminal = frames.last().expect("terminal frame");
-
- assert_eq!(ndjson_output.status.code(), Some(3));
- assert_eq!(terminal["operation_id"], "sync.push");
- assert_eq!(terminal["frame_type"], "error");
- assert_eq!(
- terminal["payload"]["next_actions"][0]["command"],
- "radroots --publish-mode nostr_relay sync push"
- );
+ .expect("run legacy publish transport");
+ let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
- let human_output = sandbox
- .command()
- .args(["--publish-mode", "radrootsd", "sync", "push"])
- .output()
- .expect("run sync push human");
- let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
-
- assert_eq!(human_output.status.code(), Some(3));
- assert!(stdout.starts_with("sync.push: error\n"));
- assert!(stdout.contains("error: operation_unavailable"));
- assert!(stdout.contains("state: unavailable"));
- assert!(stdout.contains("publish_mode: radrootsd"));
- assert!(stdout.contains("publish_state: unavailable"));
- assert!(stdout.contains("radrootsd publish mode is deferred"));
- assert!(stdout.contains("- radroots --publish-mode nostr_relay sync push"));
- assert!(serde_json::from_str::<Value>(&stdout).is_err());
+ assert!(!output.status.success());
+ assert!(stderr.contains("invalid value"));
+ assert!(stderr.contains("radrootsd_proxy"));
}
#[test]
@@ -7685,11 +7264,11 @@ fn seller_target_flow_acceptance_uses_target_operations() {
assert_eq!(unavailable_archive["operation_id"], "listing.archive");
assert_eq!(
unavailable_archive["errors"][0]["code"],
- "network_unavailable"
+ "empty_target_relays"
);
assert_eq!(
unavailable_archive["errors"][0]["detail"]["class"],
- "network"
+ "configuration"
);
assert_no_removed_command_reference(&unavailable_archive, &["listing", "archive"]);
assert_no_daemon_runtime_reference(&unavailable_archive, &["listing", "archive"]);