commit c83384f32fa144e1ba51160e7b7f63531248aba3
parent 30c47396a172af443694a6e174944ee4ee02d337
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Jul 2026 00:34:15 +0000
cli: add transport and mesh command surfaces
- add transport profile, outbox, mesh scope, and mesh policy target operations
- replace active relay and publish-transport config with transport profile config
- align SDK runtime, status output, validation gates, and sync provenance wording
- update CLI integration coverage for removed surfaces and transport profiles
Diffstat:
47 files changed, 2733 insertions(+), 1528 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3630,7 +3630,6 @@ dependencies = [
"radroots_nostr_connect",
"radroots_nostr_signer",
"radroots_protected_store",
- "radroots_relay_transport",
"radroots_replica_db",
"radroots_replica_db_schema",
"radroots_replica_sync",
@@ -3641,6 +3640,7 @@ dependencies = [
"radroots_sp1_host_trade",
"radroots_sql_core",
"radroots_trade",
+ "radroots_transport_nostr",
"serde",
"serde_json",
"tar",
@@ -3668,6 +3668,7 @@ version = "0.1.0-alpha.2"
dependencies = [
"radroots_events",
"radroots_nostr",
+ "radroots_transport",
"serde",
"serde_json",
"sqlx",
@@ -3813,6 +3814,7 @@ dependencies = [
"hex",
"radroots_event_store",
"radroots_events",
+ "radroots_transport",
"serde",
"serde_json",
"sha2",
@@ -3840,23 +3842,6 @@ dependencies = [
]
[[package]]
-name = "radroots_relay_transport"
-version = "0.1.0-alpha.2"
-dependencies = [
- "futures",
- "nostr",
- "radroots_event_store",
- "radroots_events",
- "radroots_nostr",
- "radroots_outbox",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "tokio",
- "url",
-]
-
-[[package]]
name = "radroots_replica_db"
version = "0.1.0-alpha.2"
dependencies = [
@@ -3945,9 +3930,11 @@ dependencies = [
"radroots_nostr_signer",
"radroots_outbox",
"radroots_publish_proxy_protocol",
- "radroots_relay_transport",
"radroots_runtime_paths",
"radroots_trade",
+ "radroots_transport",
+ "radroots_transport_nostr",
+ "radroots_transport_reticulum",
"reqwest",
"serde",
"serde_json",
@@ -4020,6 +4007,39 @@ dependencies = [
]
[[package]]
+name = "radroots_transport"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "serde",
+ "sha2",
+]
+
+[[package]]
+name = "radroots_transport_nostr"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "futures",
+ "nostr",
+ "radroots_event_store",
+ "radroots_events",
+ "radroots_nostr",
+ "radroots_outbox",
+ "radroots_transport",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "radroots_transport_reticulum"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "radroots_transport",
+]
+
+[[package]]
name = "radroots_types"
version = "0.1.0-alpha.2"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -42,7 +42,9 @@ radroots_protected_store = { path = "../lib/crates/protected_store", features =
radroots_replica_db = { path = "../lib/crates/replica_db" }
radroots_replica_db_schema = { path = "../lib/crates/replica_db_schema" }
radroots_replica_sync = { path = "../lib/crates/replica_sync" }
-radroots_relay_transport = { path = "../lib/crates/relay_transport", default-features = false, features = ["runtime-tokio"] }
+radroots_transport_nostr = { path = "../lib/crates/transport_nostr", default-features = false, features = [
+ "runtime-tokio",
+] }
radroots_runtime = { path = "../lib/crates/runtime" }
radroots_runtime_paths = { path = "../lib/crates/runtime_paths" }
radroots_sdk = { path = "../sdk/crates/sdk", features = ["local-runtime-radrootsd-proxy", "relay-runtime"] }
diff --git a/src/cli/global.rs b/src/cli/global.rs
@@ -38,8 +38,6 @@ pub struct RuntimeInvocationArgs {
pub account: Option<String>,
pub identity_path: Option<PathBuf>,
pub signer: Option<String>,
- pub publish_transport: Option<String>,
- pub relay: Vec<String>,
pub myc_executable: Option<PathBuf>,
pub myc_status_timeout_ms: Option<u64>,
pub hyf_enabled: bool,
diff --git a/src/cli/input.rs b/src/cli/input.rs
@@ -26,8 +26,6 @@ pub fn runtime_invocation_args_from_target(args: &TargetCliArgs) -> RuntimeInvoc
account: args.account_id.clone(),
identity_path: None,
signer: None,
- publish_transport: args.publish_transport.map(|mode| mode.as_str().to_owned()),
- relay: args.relay.clone(),
myc_executable: None,
myc_status_timeout_ms: None,
hyf_enabled: false,
@@ -45,9 +43,10 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
AccountCommand, AccountSelectionCommand, BasketAdjustmentCommand, BasketCommand,
BasketItemCommand, BasketQuoteCommand, FarmCommand, FarmFulfillmentCommand,
FarmLocationCommand, FarmProfileCommand, ListingAppCommand, ListingCommand, MarketCommand,
- MarketListingCommand, MarketProductCommand, StoreBackupCommand, StoreCommand,
- TradeAppCommand, TradeCommand, TradeEventCommand, TradeRevisionCommand, TradeStatusCommand,
- ValidationCommand, ValidationReceiptCommand,
+ MarketListingCommand, MarketProductCommand, MeshCommand, MeshScopeCommand,
+ StoreBackupCommand, StoreCommand, TradeAppCommand, TradeCommand, TradeEventCommand,
+ TradeRevisionCommand, TradeStatusCommand, TransportCommand, TransportOutboxCommand,
+ TransportProfileCommand, ValidationCommand, ValidationReceiptCommand,
};
let mut input = OperationData::new();
@@ -324,6 +323,42 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData {
}
},
},
+ TargetCommand::Transport(args) => match &args.command {
+ TransportCommand::Profile(profile) => match &profile.command {
+ TransportProfileCommand::Set(args) => {
+ input.insert(
+ "kind".to_owned(),
+ Value::String(args.kind.as_str().to_owned()),
+ );
+ insert_string_array(&mut input, "nostr_relays", args.nostr_relay.as_slice());
+ if let Some(behavior) = args.reticulum_preview_behavior {
+ input.insert(
+ "reticulum_preview_behavior".to_owned(),
+ Value::String(behavior.as_str().to_owned()),
+ );
+ }
+ insert_string(&mut input, "proxy_url", &args.proxy_url);
+ }
+ TransportProfileCommand::Get => {}
+ },
+ TransportCommand::Status => {}
+ TransportCommand::Outbox(outbox) => match outbox.command {
+ TransportOutboxCommand::Status | TransportOutboxCommand::Push => {}
+ },
+ },
+ TargetCommand::Mesh(args) => match &args.command {
+ MeshCommand::Scope(scope) => match &scope.command {
+ MeshScopeCommand::Set(args) => {
+ input.insert(
+ "scope".to_owned(),
+ Value::String(args.scope.as_str().to_owned()),
+ );
+ }
+ MeshScopeCommand::Get => {}
+ },
+ MeshCommand::Status => {}
+ MeshCommand::Policy(_) => {}
+ },
_ => {}
}
input
diff --git a/src/cli/mesh.rs b/src/cli/mesh.rs
@@ -0,0 +1,58 @@
+use clap::{Args, Subcommand, ValueEnum};
+
+#[derive(Debug, Clone, Args)]
+pub struct MeshArgs {
+ #[command(subcommand)]
+ pub command: MeshCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum MeshCommand {
+ Scope(MeshScopeArgs),
+ Status,
+ Policy(MeshPolicyArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct MeshScopeArgs {
+ #[command(subcommand)]
+ pub command: MeshScopeCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum MeshScopeCommand {
+ Get,
+ Set(MeshScopeSetArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct MeshScopeSetArgs {
+ #[arg(long = "scope", value_enum)]
+ pub scope: MeshScopeArg,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
+pub enum MeshScopeArg {
+ Disabled,
+ LocalPreview,
+}
+
+impl MeshScopeArg {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Disabled => "disabled",
+ Self::LocalPreview => "local_preview",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct MeshPolicyArgs {
+ #[command(subcommand)]
+ pub command: MeshPolicyCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum MeshPolicyCommand {
+ Check,
+}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
@@ -8,11 +8,12 @@ pub mod health;
pub mod input;
pub mod listing;
pub mod market;
-pub mod relay;
+pub mod mesh;
pub mod signer;
pub mod store;
pub mod sync;
pub mod trade;
+pub mod transport;
pub mod validation;
pub mod workspace;
@@ -23,11 +24,12 @@ pub use farm::*;
pub use health::*;
pub use listing::*;
pub use market::*;
-pub use relay::*;
+pub use mesh::*;
pub use signer::*;
pub use store::*;
pub use sync::*;
pub use trade::*;
+pub use transport::*;
pub use validation::*;
pub use workspace::*;
@@ -40,28 +42,11 @@ pub enum TargetOutputFormat {
Ndjson,
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
-pub enum TargetPublishTransport {
- #[value(name = "direct_nostr_relay")]
- DirectNostrRelay,
- #[value(name = "radrootsd_proxy")]
- RadrootsdProxy,
-}
-
-impl TargetPublishTransport {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::DirectNostrRelay => "direct_nostr_relay",
- Self::RadrootsdProxy => "radrootsd_proxy",
- }
- }
-}
-
#[derive(Debug, Parser, Clone)]
#[command(
name = "radroots",
about = "Operate Radroots local-first trade workflows.",
- 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.",
+ long_about = "Operate Radroots local-first trade workflows.",
disable_help_subcommand = true
)]
pub struct TargetCliArgs {
@@ -69,15 +54,6 @@ pub struct TargetCliArgs {
pub format: Option<TargetOutputFormat>,
#[arg(long = "account-id", global = true)]
pub account_id: Option<String>,
- #[arg(long = "relay", global = true)]
- pub relay: Vec<String>,
- #[arg(
- long = "publish-transport",
- global = true,
- value_enum,
- help = "Select direct_nostr_relay direct relay publish or radrootsd_proxy daemon proxy publish"
- )]
- 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")]
@@ -114,8 +90,10 @@ pub enum TargetCommand {
Account(AccountArgs),
#[command(about = "Inspect signer readiness for local relay writes.")]
Signer(SignerArgs),
- #[command(about = "List configured relay targets for direct relay mode.")]
- Relay(RelayArgs),
+ #[command(about = "Manage transport profiles and outbox delivery.")]
+ Transport(TransportArgs),
+ #[command(about = "Inspect mesh scope and Reticulum preview policy.")]
+ Mesh(MeshArgs),
#[command(about = "Initialize and inspect the local replica store.")]
Store(StoreArgs),
#[command(about = "Read from relay events into the local replica.")]
@@ -170,8 +148,26 @@ impl TargetCommand {
SignerStatusCommand::Get => "signer.status.get",
},
},
- Self::Relay(args) => match args.command {
- RelayCommand::List => "relay.list",
+ Self::Transport(args) => match &args.command {
+ TransportCommand::Profile(profile) => match &profile.command {
+ TransportProfileCommand::Get => "transport.profile.get",
+ TransportProfileCommand::Set(_) => "transport.profile.set",
+ },
+ TransportCommand::Status => "transport.status",
+ TransportCommand::Outbox(outbox) => match outbox.command {
+ TransportOutboxCommand::Status => "transport.outbox.status",
+ TransportOutboxCommand::Push => "transport.outbox.push",
+ },
+ },
+ Self::Mesh(args) => match &args.command {
+ MeshCommand::Scope(scope) => match &scope.command {
+ MeshScopeCommand::Get => "mesh.scope.get",
+ MeshScopeCommand::Set(_) => "mesh.scope.set",
+ },
+ MeshCommand::Status => "mesh.status",
+ MeshCommand::Policy(policy) => match policy.command {
+ MeshPolicyCommand::Check => "mesh.policy.check",
+ },
},
Self::Store(args) => match &args.command {
StoreCommand::Init => "store.init",
@@ -324,7 +320,8 @@ mod tests {
"config",
"account",
"signer",
- "relay",
+ "transport",
+ "mesh",
"store",
"sync",
"farm",
@@ -349,10 +346,6 @@ mod tests {
"ndjson",
"--account-id",
"acct_test",
- "--relay",
- "wss://relay.one",
- "--relay",
- "wss://relay.two",
"--offline",
"--dry-run",
"--idempotency-key",
@@ -370,10 +363,6 @@ mod tests {
assert_eq!(parsed.format, Some(TargetOutputFormat::Ndjson));
assert_eq!(parsed.account_id.as_deref(), Some("acct_test"));
- assert_eq!(
- parsed.relay,
- vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
- );
assert!(parsed.offline);
assert!(parsed.dry_run);
assert_eq!(parsed.idempotency_key.as_deref(), Some("idem_test"));
@@ -393,6 +382,20 @@ mod tests {
}
#[test]
+ fn target_parser_rejects_removed_relay_flag() {
+ let error = TargetCliArgs::try_parse_from([
+ "radroots",
+ "--relay",
+ "wss://relay.example.com",
+ "workspace",
+ "get",
+ ])
+ .expect_err("removed relay flag should be rejected");
+
+ assert_eq!(error.kind(), clap::error::ErrorKind::UnknownArgument);
+ }
+
+ #[test]
fn target_parser_accepts_account_attach_secret_inputs() {
let parsed = TargetCliArgs::try_parse_from([
"radroots",
diff --git a/src/cli/transport.rs b/src/cli/transport.rs
@@ -0,0 +1,84 @@
+use clap::{Args, Subcommand, ValueEnum};
+
+#[derive(Debug, Clone, Args)]
+pub struct TransportArgs {
+ #[command(subcommand)]
+ pub command: TransportCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum TransportCommand {
+ Profile(TransportProfileArgs),
+ Status,
+ Outbox(TransportOutboxArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct TransportProfileArgs {
+ #[command(subcommand)]
+ pub command: TransportProfileCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum TransportProfileCommand {
+ Get,
+ Set(TransportProfileSetArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct TransportProfileSetArgs {
+ #[arg(long = "kind", value_enum)]
+ pub kind: TransportProfileKindArg,
+ #[arg(long = "nostr-relay")]
+ pub nostr_relay: Vec<String>,
+ #[arg(long = "reticulum-preview-behavior", value_enum)]
+ pub reticulum_preview_behavior: Option<ReticulumPreviewBehaviorArg>,
+ #[arg(long = "proxy-url")]
+ pub proxy_url: Option<String>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
+pub enum TransportProfileKindArg {
+ LocalOnly,
+ Nostr,
+ ReticulumPreview,
+ Proxy,
+}
+
+impl TransportProfileKindArg {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::LocalOnly => "local_only",
+ Self::Nostr => "nostr",
+ Self::ReticulumPreview => "reticulum_preview",
+ Self::Proxy => "proxy",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
+pub enum ReticulumPreviewBehaviorArg {
+ RejectDeliveryAttempts,
+ DeferDeliveryPlans,
+}
+
+impl ReticulumPreviewBehaviorArg {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::RejectDeliveryAttempts => "reject_delivery_attempts",
+ Self::DeferDeliveryPlans => "defer_delivery_plans",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct TransportOutboxArgs {
+ #[command(subcommand)]
+ pub command: TransportOutboxCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum TransportOutboxCommand {
+ Status,
+ Push,
+}
diff --git a/src/main.rs b/src/main.rs
@@ -29,10 +29,12 @@ use radroots_cli::out::terminal::renderer::{
TerminalRenderContext, TerminalVerbosity, render_terminal_document,
};
use radroots_cli::registry::{
- NetworkRequirement, OPERATION_REGISTRY, network_requirement, requires_local_signer_mode,
+ ApprovalPolicy, NetworkRequirement, OPERATION_REGISTRY, network_requirement,
+ requires_local_signer_mode,
};
use radroots_cli::runtime::config::{
- OutputFormat as RuntimeOutputFormat, RuntimeConfig, SignerBackend, Verbosity,
+ OutputFormat as RuntimeOutputFormat, RuntimeConfig, SignerBackend, TransportProfileKind,
+ Verbosity,
};
use radroots_cli::runtime::logging::initialize_logging;
@@ -149,7 +151,31 @@ fn execute_request(
TargetOperationRequest::SignerStatusGet(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
- TargetOperationRequest::RelayList(request) => {
+ TargetOperationRequest::TransportProfileGet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::TransportProfileSet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::TransportStatus(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::TransportOutboxStatus(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::TransportOutboxPush(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::MeshScopeGet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::MeshScopeSet(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::MeshStatus(request) => {
+ execute_with(RuntimeOperationService::new(config), request)
+ }
+ TargetOperationRequest::MeshPolicyCheck(request) => {
execute_with(RuntimeOperationService::new(config), request)
}
TargetOperationRequest::SyncStatusGet(request) => {
@@ -414,7 +440,7 @@ fn validate_network_contract(
return Err(OperationAdapterError::OfflineForbidden {
operation_id: spec.operation_id.to_owned(),
message: format!(
- "`{}` requires relay, provider, or workflow network access",
+ "`{}` requires transport, provider, or workflow network access",
spec.cli_path
),
});
@@ -426,13 +452,13 @@ fn validate_network_contract(
dry_run_requires_network,
} = requirement
&& (!request.context().dry_run || dry_run_requires_network)
- && requires_pre_runtime_relay_target(spec.operation_id)
- && config.relay.urls.is_empty()
+ && requires_pre_runtime_transport_target(spec.operation_id)
+ && !transport_profile_is_usable_for_delivery(config)
{
return Err(OperationAdapterError::NetworkUnavailable {
operation_id: spec.operation_id.to_owned(),
message: format!(
- "`{}` requires at least one configured relay for online execution",
+ "`{}` requires a delivery-capable transport profile for online execution",
spec.cli_path
),
});
@@ -450,7 +476,7 @@ fn requires_local_signer_mode_for_publish_transport(
requires_local_signer_mode(operation_id)
}
-fn requires_pre_runtime_relay_target(operation_id: &str) -> bool {
+fn requires_pre_runtime_transport_target(operation_id: &str) -> bool {
!is_publish_transport_routed_operation(operation_id)
}
@@ -462,18 +488,66 @@ fn validate_publish_transport_contract(
request: &TargetOperationRequest,
config: &RuntimeConfig,
) -> Result<(), OperationAdapterError> {
- let _ = request;
- let _ = config;
+ let spec = request.spec();
+ if !is_publish_transport_routed_operation(spec.operation_id) {
+ return Ok(());
+ }
+ if request.context().dry_run
+ || matches!(
+ request.context().network_mode,
+ OperationNetworkMode::Offline
+ )
+ || (spec.approval_policy == ApprovalPolicy::Required
+ && request.context().requires_approval_token())
+ {
+ return Ok(());
+ }
+ if matches!(
+ config.transport.profile,
+ TransportProfileKind::LocalOnly | TransportProfileKind::ReticulumPreview
+ ) {
+ return Err(OperationAdapterError::NetworkUnavailable {
+ operation_id: spec.operation_id.to_owned(),
+ message: format!(
+ "`{}` requires a delivery-capable transport profile; active profile `{}` cannot deliver",
+ spec.cli_path,
+ config.transport.profile.as_str()
+ ),
+ });
+ }
+ if matches!(config.transport.profile, TransportProfileKind::Nostr)
+ && config.transport.nostr_relay_urls.is_empty()
+ {
+ return Err(OperationAdapterError::NetworkUnavailable {
+ operation_id: spec.operation_id.to_owned(),
+ message: format!(
+ "`{}` requires at least one configured Nostr relay in the active transport profile",
+ spec.cli_path
+ ),
+ });
+ }
Ok(())
}
fn is_publish_transport_routed_operation(operation_id: &str) -> bool {
matches!(
operation_id,
- "farm.publish" | "listing.publish" | "listing.update" | "listing.archive"
+ "farm.publish"
+ | "listing.publish"
+ | "listing.update"
+ | "listing.archive"
+ | "transport.outbox.push"
)
}
+fn transport_profile_is_usable_for_delivery(config: &RuntimeConfig) -> bool {
+ match config.transport.profile {
+ TransportProfileKind::Nostr => !config.transport.nostr_relay_urls.is_empty(),
+ TransportProfileKind::Proxy => true,
+ TransportProfileKind::LocalOnly | TransportProfileKind::ReticulumPreview => false,
+ }
+}
+
fn failure_envelope(
request: &TargetOperationRequest,
error: OperationAdapterError,
diff --git a/src/ops/context.rs b/src/ops/context.rs
@@ -64,7 +64,6 @@ impl Default for OperationInputMode {
pub struct OperationContext {
pub output_format: OperationOutputFormat,
pub account_id: Option<String>,
- pub relays: Vec<String>,
pub network_mode: OperationNetworkMode,
pub dry_run: bool,
pub idempotency_key: Option<String>,
@@ -84,7 +83,6 @@ impl OperationContext {
.map(OperationOutputFormat::from)
.unwrap_or_default(),
account_id: args.account_id.clone(),
- relays: args.relay.clone(),
network_mode: if args.offline {
OperationNetworkMode::Offline
} else if args.online {
diff --git a/src/ops/error.rs b/src/ops/error.rs
@@ -587,8 +587,8 @@ fn sdk_recovery_next_actions(
RadrootsSdkRecoveryAction::InspectLocalStores => {
Some("radroots store status get".to_owned())
}
- RadrootsSdkRecoveryAction::ConfigureRelayTargets => {
- Some("radroots relay list".to_owned())
+ RadrootsSdkRecoveryAction::ConfigureTransportTargets => {
+ Some("radroots transport profile get".to_owned())
}
RadrootsSdkRecoveryAction::SelectAuthorizedActor => {
Some("radroots account list".to_owned())
@@ -774,15 +774,7 @@ fn classify_runtime_failure(
}
if contains_any(
&lowered,
- &[
- "provider",
- "write-plane",
- "write plane",
- "radrootsd",
- "bridge",
- "rpc",
- "daemon",
- ],
+ &["provider", "write-plane", "write plane", "rpc", "proxy"],
) {
return match availability {
RuntimeFailureAvailability::Unconfigured => {
@@ -851,8 +843,6 @@ fn looks_like_provider_failure(value: &str) -> bool {
"provider unconfigured",
"provider runtime",
"provider failed",
- "radrootsd unavailable",
- "daemon unavailable",
"proxy provider",
],
)
diff --git a/src/ops/exec/basket.rs b/src/ops/exec/basket.rs
@@ -1906,10 +1906,11 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::local_only(),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -1930,6 +1931,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/ops/exec/core.rs b/src/ops/exec/core.rs
@@ -197,7 +197,7 @@ impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
let write_plane =
crate::runtime::provider::resolve_write_plane_provider(self.config, &publish);
let actions = config_actions(self.config, &account, &publish);
- let mut result = json!({
+ let result = json!({
"output": {
"format": self.config.output.format.as_str(),
"verbosity": self.config.output.verbosity.as_str(),
@@ -225,12 +225,9 @@ impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
"signer": {
"mode": self.config.signer.backend.as_str(),
},
+ "transport": crate::runtime::transport::profile(self.config),
+ "mesh": crate::runtime::mesh::scope(self.config),
"publish": publish,
- "relay": {
- "count": self.config.relay.urls.len(),
- "urls": self.config.relay.urls,
- "source": self.config.relay.source.as_str(),
- },
"myc": {
"executable": self.config.myc.executable.display().to_string(),
"status_timeout_ms": self.config.myc.status_timeout_ms,
@@ -257,16 +254,6 @@ impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
},
"actions": actions,
});
- 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(),
- });
- }
json_operation_result::<ConfigGetResult>(result)
}
}
@@ -816,13 +803,9 @@ fn publish_runtime_view(
};
match config.publish.transport {
- PublishTransport::DirectNostrRelay => {
- let (state, executable, reason) = direct_nostr_relay_publish_readiness(
- config,
- relay_ready,
- signed_write_required,
- account,
- );
+ PublishTransport::Nostr => {
+ let (state, executable, reason) =
+ nostr_publish_readiness(config, relay_ready, signed_write_required, account);
PublishRuntimeView {
transport: config.publish.transport.as_str().to_owned(),
source,
@@ -833,15 +816,15 @@ fn publish_runtime_view(
signed_write_required,
relay,
provider: PublishProviderRuntimeView {
- provider_runtime_id: "direct_nostr_relay".to_owned(),
+ provider_runtime_id: "nostr".to_owned(),
state: state.to_owned(),
source: config.relay.source.as_str().to_owned(),
reason,
},
}
}
- PublishTransport::RadrootsdProxy => {
- let (state, executable, reason) = radrootsd_publish_readiness(config);
+ PublishTransport::Proxy => {
+ let (state, executable, reason) = proxy_publish_readiness(config);
PublishRuntimeView {
transport: config.publish.transport.as_str().to_owned(),
source,
@@ -852,9 +835,9 @@ fn publish_runtime_view(
signed_write_required,
relay,
provider: PublishProviderRuntimeView {
- provider_runtime_id: "radrootsd_proxy".to_owned(),
+ provider_runtime_id: "proxy".to_owned(),
state: state.to_owned(),
- source: "publish transport · local first".to_owned(),
+ source: "publish transport · transport profile".to_owned(),
reason,
},
}
@@ -862,7 +845,7 @@ fn publish_runtime_view(
}
}
-fn direct_nostr_relay_publish_readiness(
+fn nostr_publish_readiness(
config: &RuntimeConfig,
relay_ready: bool,
signed_write_required: bool,
@@ -873,7 +856,7 @@ fn direct_nostr_relay_publish_readiness(
"unconfigured",
false,
Some(
- "direct_nostr_relay publish transport requires at least one configured relay for writes"
+ "Nostr transport profile requires at least one configured Nostr relay for writes"
.to_owned(),
),
);
@@ -897,7 +880,7 @@ fn direct_nostr_relay_publish_readiness(
"unconfigured",
false,
Some(
- "direct_nostr_relay publish transport requires a selected or default write-capable local account for signed writes"
+ "Nostr transport profile requires a selected or default write-capable local account for signed writes"
.to_owned(),
),
);
@@ -916,14 +899,15 @@ fn direct_nostr_relay_publish_readiness(
("ready", true, None)
}
-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()
- {
+fn proxy_publish_readiness(config: &RuntimeConfig) -> (&'static str, bool, Option<String>) {
+ if config.publish.proxy.token_file.is_none() && config.publish.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()),
+ Some(
+ "proxy transport profile requires a configured token file or token secret id"
+ .to_owned(),
+ ),
);
}
@@ -1089,11 +1073,11 @@ fn publish_recovery_actions(
let mut actions = Vec::new();
match config.publish.transport {
- PublishTransport::DirectNostrRelay => {
+ PublishTransport::Nostr => {
if config.relay.urls.is_empty() {
push_unique(
&mut actions,
- "radroots --relay wss://relay.example.com sync pull",
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com",
);
}
if publish.signed_write_required {
@@ -1108,7 +1092,7 @@ fn publish_recovery_actions(
}
}
}
- PublishTransport::RadrootsdProxy => {
+ PublishTransport::Proxy => {
if self::proxy_token_configured(config) {
if publish.signed_write_required
&& matches!(config.signer.backend, SignerBackend::Myc)
@@ -1118,7 +1102,7 @@ fn publish_recovery_actions(
} else {
push_unique(
&mut actions,
- "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID",
+ "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID",
);
}
}
@@ -1127,8 +1111,7 @@ fn publish_recovery_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()
+ config.publish.proxy.token_file.is_some() || config.publish.proxy.token_secret_id.is_some()
}
fn push_unique(actions: &mut Vec<String>, action: impl Into<String>) {
@@ -1464,10 +1447,11 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::local_only(),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -1488,6 +1472,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs
@@ -232,10 +232,7 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
request.operation_id(),
));
}
- if matches!(
- self.config.publish.transport,
- PublishTransport::DirectNostrRelay
- ) {
+ if matches!(self.config.publish.transport, PublishTransport::Nostr) {
require_relay_target(&request, self.config)?;
}
@@ -368,7 +365,7 @@ where
Err(OperationAdapterError::NetworkUnavailable {
operation_id: request.operation_id().to_owned(),
message: format!(
- "`{}` requires at least one configured relay for direct relay publication",
+ "`{}` requires at least one configured Nostr relay in the active transport profile",
request.spec.cli_path
),
})
@@ -879,10 +876,11 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::local_only(),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -903,6 +901,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/ops/exec/listing.rs b/src/ops/exec/listing.rs
@@ -335,10 +335,10 @@ where
fn listing_relay_unavailable(view: &ListingMutationView) -> bool {
matches!(
view.source.as_str(),
- "direct Nostr relay publish · local key" | "SDK listing publish · configured signer"
+ "Nostr transport publish · local key" | "SDK listing publish · configured signer"
) && (view.reason.as_deref().is_some_and(|reason| {
- reason.contains("configured relay")
- || reason.contains("direct relay connection failed")
+ reason.contains("configured Nostr relay")
+ || reason.contains("Nostr transport connection failed")
|| reason.contains("SDK relay publish")
}) || !view.target_relays.is_empty()
|| !view.connected_relays.is_empty()
@@ -606,10 +606,11 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::local_only(),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -630,6 +631,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/ops/exec/market.rs b/src/ops/exec/market.rs
@@ -82,7 +82,7 @@ fn market_refresh_view(mut view: SyncActionView) -> SyncActionView {
actions.push("radroots store init".to_owned());
}
if view.relay_count == 0 {
- actions.push("radroots --relay wss://relay.example.com market refresh".to_owned());
+ actions.push("radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com".to_owned());
}
if actions.is_empty() {
actions.extend(std::mem::take(&mut view.actions));
@@ -361,7 +361,7 @@ mod tests {
assert_eq!(envelope.result["state"], "unconfigured");
assert_eq!(
envelope.result["actions"][0],
- "radroots --relay wss://relay.example.com market refresh"
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
);
}
@@ -670,10 +670,11 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::local_only(),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -694,6 +695,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/ops/exec/runtime.rs b/src/ops/exec/runtime.rs
@@ -3,11 +3,16 @@ use serde_json::Value;
use crate::cli::global::SyncWatchArgs;
use crate::ops::{
+ MeshPolicyCheckRequest, MeshPolicyCheckResult, MeshScopeGetRequest, MeshScopeGetResult,
+ MeshScopeSetRequest, MeshScopeSetResult, MeshStatusRequest, MeshStatusResult,
OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
- OperationResult, OperationResultData, OperationService, RelayListRequest, RelayListResult,
- SignerStatusGetRequest, SignerStatusGetResult, SyncPullRequest, SyncPullResult,
- SyncPushRequest, SyncPushResult, SyncStatusGetRequest, SyncStatusGetResult, SyncWatchRequest,
- SyncWatchResult,
+ OperationResult, OperationResultData, OperationService, SignerStatusGetRequest,
+ SignerStatusGetResult, SyncPullRequest, SyncPullResult, SyncPushRequest, SyncPushResult,
+ SyncStatusGetRequest, SyncStatusGetResult, SyncWatchRequest, SyncWatchResult,
+ TransportOutboxPushRequest, TransportOutboxPushResult, TransportOutboxStatusRequest,
+ TransportOutboxStatusResult, TransportProfileGetRequest, TransportProfileGetResult,
+ TransportProfileSetRequest, TransportProfileSetResult, TransportStatusRequest,
+ TransportStatusResult,
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
@@ -35,15 +40,126 @@ impl OperationService<SignerStatusGetRequest> for RuntimeOperationService<'_> {
}
}
-impl OperationService<RelayListRequest> for RuntimeOperationService<'_> {
- type Result = RelayListResult;
+impl OperationService<TransportProfileGetRequest> for RuntimeOperationService<'_> {
+ type Result = TransportProfileGetResult;
fn execute(
&self,
- _request: OperationRequest<RelayListRequest>,
+ _request: OperationRequest<TransportProfileGetRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::network::relay_list(self.config);
- serialized_operation_result::<RelayListResult, _>(&view)
+ let view = crate::runtime::transport::profile(self.config);
+ serialized_operation_result::<TransportProfileGetResult, _>(&view)
+ }
+}
+
+impl OperationService<TransportProfileSetRequest> for RuntimeOperationService<'_> {
+ type Result = TransportProfileSetResult;
+
+ fn execute(
+ &self,
+ request: OperationRequest<TransportProfileSetRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let view = map_runtime(
+ "transport.profile.set",
+ crate::runtime::transport::set_profile(self.config, request.payload.input()),
+ )?;
+ serialized_operation_result::<TransportProfileSetResult, _>(&view)
+ }
+}
+
+impl OperationService<TransportStatusRequest> for RuntimeOperationService<'_> {
+ type Result = TransportStatusResult;
+
+ fn execute(
+ &self,
+ _request: OperationRequest<TransportStatusRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let view = crate::runtime::transport::status(self.config);
+ serialized_operation_result::<TransportStatusResult, _>(&view)
+ }
+}
+
+impl OperationService<TransportOutboxStatusRequest> for RuntimeOperationService<'_> {
+ type Result = TransportOutboxStatusResult;
+
+ fn execute(
+ &self,
+ _request: OperationRequest<TransportOutboxStatusRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let view = crate::runtime::transport::outbox_status(self.config).map_err(|error| {
+ OperationAdapterError::sdk_adapter_failure("transport.outbox.status", error)
+ })?;
+ serialized_operation_result::<TransportOutboxStatusResult, _>(&view)
+ }
+}
+
+impl OperationService<TransportOutboxPushRequest> for RuntimeOperationService<'_> {
+ type Result = TransportOutboxPushResult;
+
+ fn execute(
+ &self,
+ request: OperationRequest<TransportOutboxPushRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ if request.context.requires_approval_token() {
+ return Err(OperationAdapterError::approval_required(
+ "transport.outbox.push",
+ ));
+ }
+ let view = crate::runtime::transport::outbox_push(self.config).map_err(|error| {
+ OperationAdapterError::sdk_adapter_failure("transport.outbox.push", error)
+ })?;
+ serialized_operation_result::<TransportOutboxPushResult, _>(&view)
+ }
+}
+
+impl OperationService<MeshScopeGetRequest> for RuntimeOperationService<'_> {
+ type Result = MeshScopeGetResult;
+
+ fn execute(
+ &self,
+ _request: OperationRequest<MeshScopeGetRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let view = crate::runtime::mesh::scope(self.config);
+ serialized_operation_result::<MeshScopeGetResult, _>(&view)
+ }
+}
+
+impl OperationService<MeshScopeSetRequest> for RuntimeOperationService<'_> {
+ type Result = MeshScopeSetResult;
+
+ fn execute(
+ &self,
+ request: OperationRequest<MeshScopeSetRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let view = map_runtime(
+ "mesh.scope.set",
+ crate::runtime::mesh::set_scope(self.config, request.payload.input()),
+ )?;
+ serialized_operation_result::<MeshScopeSetResult, _>(&view)
+ }
+}
+
+impl OperationService<MeshStatusRequest> for RuntimeOperationService<'_> {
+ type Result = MeshStatusResult;
+
+ fn execute(
+ &self,
+ _request: OperationRequest<MeshStatusRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let view = crate::runtime::mesh::status(self.config);
+ serialized_operation_result::<MeshStatusResult, _>(&view)
+ }
+}
+
+impl OperationService<MeshPolicyCheckRequest> for RuntimeOperationService<'_> {
+ type Result = MeshPolicyCheckResult;
+
+ fn execute(
+ &self,
+ _request: OperationRequest<MeshPolicyCheckRequest>,
+ ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let view = crate::runtime::mesh::policy_check(self.config);
+ serialized_operation_result::<MeshPolicyCheckResult, _>(&view)
}
}
@@ -220,8 +336,8 @@ mod tests {
use super::RuntimeOperationService;
use crate::ops::{
- OperationAdapter, OperationContext, OperationRequest, RelayListRequest,
- SignerStatusGetRequest, SyncStatusGetRequest,
+ OperationAdapter, OperationContext, OperationRequest, SignerStatusGetRequest,
+ SyncStatusGetRequest, TransportProfileGetRequest,
};
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
@@ -231,7 +347,7 @@ mod tests {
};
#[test]
- fn runtime_service_backs_signer_and_relay_status() {
+ fn runtime_service_backs_signer_and_transport_profile() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path(), vec!["wss://relay.test".into()]);
let service = OperationAdapter::new(RuntimeOperationService::new(&config));
@@ -249,16 +365,19 @@ mod tests {
assert_eq!(signer_envelope.operation_id, "signer.status.get");
assert_eq!(signer_envelope.result["state"], "unconfigured");
- let relay = OperationRequest::new(OperationContext::default(), RelayListRequest::default())
- .expect("relay list request");
- let relay_envelope = service
- .execute(relay)
- .expect("relay list result")
- .to_envelope(OperationContext::default().envelope_context("req_relay"))
- .expect("relay envelope");
- assert_eq!(relay_envelope.operation_id, "relay.list");
- assert_eq!(relay_envelope.result["state"], "configured");
- assert_eq!(relay_envelope.result["count"], 1);
+ let profile = OperationRequest::new(
+ OperationContext::default(),
+ TransportProfileGetRequest::default(),
+ )
+ .expect("transport profile request");
+ let profile_envelope = service
+ .execute(profile)
+ .expect("transport profile result")
+ .to_envelope(OperationContext::default().envelope_context("req_transport"))
+ .expect("transport profile envelope");
+ assert_eq!(profile_envelope.operation_id, "transport.profile.get");
+ assert_eq!(profile_envelope.result["state"], "configured");
+ assert_eq!(profile_envelope.result["profile_id"], "nostr");
}
#[test]
@@ -353,10 +472,13 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::from_nostr_relay_urls(
+ relays.clone(),
+ ),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: relays,
@@ -377,6 +499,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/ops/exec/trade.rs b/src/ops/exec/trade.rs
@@ -1662,14 +1662,20 @@ mod tests {
assert_eq!(envelope.errors[0].code, "operation_unavailable");
assert_eq!(envelope.errors[0].exit_code, 3);
- assert!(envelope.errors[0].message.contains("configured relay"));
+ assert!(
+ envelope.errors[0]
+ .message
+ .contains("configured Nostr transport profile")
+ );
assert_eq!(
envelope.errors[0].detail.as_ref().unwrap()["actions"][0],
- "radroots --relay wss://relay.example.com trade event list"
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
);
assert_eq!(
envelope.next_actions[0].command.as_deref(),
- Some("radroots --relay wss://relay.example.com trade event list")
+ Some(
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ )
);
}
@@ -1806,10 +1812,11 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::local_only(),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -1830,6 +1837,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
@@ -61,8 +61,6 @@ mod tests {
"json",
"--account-id",
"acct_test",
- "--relay",
- "wss://relay.one",
"--online",
"--dry-run",
"--idempotency-key",
@@ -86,7 +84,6 @@ mod tests {
assert_eq!(context.output_format, OperationOutputFormat::Json);
assert_eq!(context.account_id.as_deref(), Some("acct_test"));
- assert_eq!(context.relays, vec!["wss://relay.one".to_owned()]);
assert_eq!(context.network_mode, OperationNetworkMode::Online);
assert!(context.dry_run);
assert_eq!(context.idempotency_key.as_deref(), Some("idem_test"));
@@ -601,7 +598,7 @@ mod tests {
(
OperationAdapterError::unavailable(
"listing.publish",
- "radrootsd bridge is unavailable".to_owned(),
+ "proxy provider is unavailable".to_owned(),
),
"provider_unavailable",
"provider",
@@ -657,7 +654,7 @@ mod tests {
(
OperationAdapterError::runtime_failure(
"farm.publish",
- RuntimeError::Network("direct relay connection failed".to_owned()),
+ RuntimeError::Network("Nostr transport connection failed".to_owned()),
),
"network_unavailable",
"network",
diff --git a/src/ops/target.rs b/src/ops/target.rs
@@ -189,7 +189,15 @@ target_operation_contracts! {
AccountSelectionUpdate => (AccountSelectionUpdateRequest, AccountSelectionUpdateResult, "account.selection.update"),
AccountSelectionClear => (AccountSelectionClearRequest, AccountSelectionClearResult, "account.selection.clear"),
SignerStatusGet => (SignerStatusGetRequest, SignerStatusGetResult, "signer.status.get"),
- RelayList => (RelayListRequest, RelayListResult, "relay.list"),
+ TransportProfileGet => (TransportProfileGetRequest, TransportProfileGetResult, "transport.profile.get"),
+ TransportProfileSet => (TransportProfileSetRequest, TransportProfileSetResult, "transport.profile.set"),
+ TransportStatus => (TransportStatusRequest, TransportStatusResult, "transport.status"),
+ TransportOutboxStatus => (TransportOutboxStatusRequest, TransportOutboxStatusResult, "transport.outbox.status"),
+ TransportOutboxPush => (TransportOutboxPushRequest, TransportOutboxPushResult, "transport.outbox.push"),
+ MeshScopeGet => (MeshScopeGetRequest, MeshScopeGetResult, "mesh.scope.get"),
+ MeshScopeSet => (MeshScopeSetRequest, MeshScopeSetResult, "mesh.scope.set"),
+ MeshStatus => (MeshStatusRequest, MeshStatusResult, "mesh.status"),
+ MeshPolicyCheck => (MeshPolicyCheckRequest, MeshPolicyCheckResult, "mesh.policy.check"),
StoreInit => (StoreInitRequest, StoreInitResult, "store.init"),
StoreStatusGet => (StoreStatusGetRequest, StoreStatusGetResult, "store.status.get"),
StoreExport => (StoreExportRequest, StoreExportResult, "store.export"),
diff --git a/src/out/envelope.rs b/src/out/envelope.rs
@@ -277,14 +277,14 @@ 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_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
+ == "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"
{
return Some(NextAction {
kind: NextActionKind::OperatorConfig,
- label: "configure radrootsd proxy token source".to_owned(),
+ label: "configure proxy token source".to_owned(),
command: None,
description: Some(action.to_owned()),
- env_var: Some("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE".to_owned()),
+ env_var: Some("RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE".to_owned()),
config_key: None,
});
}
@@ -324,8 +324,6 @@ fn next_action_label(command: &str) -> String {
part,
"--format"
| "--account-id"
- | "--relay"
- | "--publish-transport"
| "--idempotency-key"
| "--correlation-id"
| "--approval-token"
@@ -608,9 +606,9 @@ mod tests {
);
error.detail = Some(json!({
"actions": [
- "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID",
+ "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID",
"configure signer.remote_nip46 signer_session_ref",
- "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
+ "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"
]
}));
let envelope = OutputEnvelope::failure(
@@ -627,12 +625,12 @@ mod tests {
);
assert_eq!(
envelope.next_actions[0].label,
- "configure radrootsd proxy token source"
+ "configure proxy token source"
);
assert_eq!(envelope.next_actions[0].command, None);
assert_eq!(
envelope.next_actions[0].env_var.as_deref(),
- Some("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE")
+ Some("RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE")
);
assert_eq!(
envelope.next_actions[1].kind,
diff --git a/src/out/terminal/actions.rs b/src/out/terminal/actions.rs
@@ -316,8 +316,8 @@ mod tests {
kind: NextActionKind::OperatorConfig,
label: "configure token".to_owned(),
command: None,
- description: Some("configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE".to_owned()),
- env_var: Some("RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE".to_owned()),
+ description: Some("configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE".to_owned()),
+ env_var: Some("RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE".to_owned()),
config_key: None,
};
@@ -327,7 +327,7 @@ mod tests {
assert_eq!(terminal[0].kind, TerminalActionKind::Setup);
assert_eq!(
terminal[0].description.as_deref(),
- Some("configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE")
+ Some("configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE")
);
}
}
diff --git a/src/out/terminal/renderer.rs b/src/out/terminal/renderer.rs
@@ -210,7 +210,7 @@ mod tests {
"Listing published",
))
.with_field(TerminalField::new("Listing", "AAAAAAAAAAAAAAAAAAAAAg"))
- .with_field(TerminalField::new("Transport", "direct nostr relay"))
+ .with_field(TerminalField::new("Transport", "nostr"))
.with_field(TerminalField::new("Relays", "2 acknowledged · 0 failed"))
.with_field(TerminalField::new("Event", "9f3a…c12"))
.with_next(TerminalAction::command(
@@ -219,7 +219,7 @@ mod tests {
assert_eq!(
render_terminal_document(&document, &TerminalRenderContext::default()),
- "✓ Listing published\n\n Listing AAAAAAAAAAAAAAAAAAAAAg\n Transport direct nostr relay\n Relays 2 acknowledged · 0 failed\n Event 9f3a…c12\n\nNext\n radroots listing get AAAAAAAAAAAAAAAAAAAAAg"
+ "✓ Listing published\n\n Listing AAAAAAAAAAAAAAAAAAAAAg\n Transport nostr\n Relays 2 acknowledged · 0 failed\n Event 9f3a…c12\n\nNext\n radroots listing get AAAAAAAAAAAAAAAAAAAAAg"
);
}
diff --git a/src/out/terminal/renderers/runtime.rs b/src/out/terminal/renderers/runtime.rs
@@ -11,7 +11,15 @@ use super::common;
pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
registry
.register("signer.status.get", &RUNTIME_RENDERER)
- .register("relay.list", &RUNTIME_RENDERER)
+ .register("transport.profile.get", &RUNTIME_RENDERER)
+ .register("transport.profile.set", &RUNTIME_RENDERER)
+ .register("transport.status", &RUNTIME_RENDERER)
+ .register("transport.outbox.status", &RUNTIME_RENDERER)
+ .register("transport.outbox.push", &RUNTIME_RENDERER)
+ .register("mesh.scope.get", &RUNTIME_RENDERER)
+ .register("mesh.scope.set", &RUNTIME_RENDERER)
+ .register("mesh.status", &RUNTIME_RENDERER)
+ .register("mesh.policy.check", &RUNTIME_RENDERER)
}
struct RuntimeRenderer;
@@ -25,7 +33,14 @@ impl TerminalOperationRenderer for RuntimeRenderer {
}
let result = common::result(envelope);
match envelope.operation_id.as_str() {
- "relay.list" => relay_document(envelope, result),
+ "transport.profile.get" | "transport.profile.set" => {
+ transport_profile_document(envelope, result)
+ }
+ "transport.status" => transport_status_document(envelope, result),
+ "transport.outbox.status" => transport_outbox_status_document(envelope, result),
+ "transport.outbox.push" => transport_outbox_push_document(envelope, result),
+ "mesh.scope.get" | "mesh.scope.set" => mesh_scope_document(envelope, result),
+ "mesh.status" | "mesh.policy.check" => mesh_status_document(envelope, result),
_ => signer_document(envelope, result),
}
}
@@ -77,36 +92,134 @@ fn signer_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocumen
document
}
-fn relay_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
- let title = common::title_for(envelope, "Relays");
+fn transport_profile_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let title = common::title_for(envelope, "Transport Profile");
let mut document = common::document_with_title(envelope, title);
common::push_path_field(&mut document, "State", result, &["state"]);
- common::push_count_field(&mut document, "Count", result, &["count"]);
common::push_path_field(&mut document, "Source", result, &["source"]);
- let rows = common::array(result, &["relays"])
+ common::push_path_field(&mut document, "Profile", result, &["profile_id"]);
+ common::push_path_field(&mut document, "Kind", result, &["transport_kind"]);
+ common::push_path_field(&mut document, "Configured", result, &["configured_state"]);
+ common::push_path_field(
+ &mut document,
+ "Implementation",
+ result,
+ &["implementation_state"],
+ );
+ common::push_bool_field(&mut document, "Usable", result, &["usable_for_delivery"]);
+ common::push_path_field(&mut document, "Message", result, &["message"]);
+ if let Some(relays) = common::array(result, &["nostr_relays"]) {
+ let rows = relays
+ .iter()
+ .map(|relay| TerminalTableRow::new(vec![relay.as_str().unwrap_or_default().to_owned()]))
+ .collect::<Vec<_>>();
+ document.sections.push(common::table_section(
+ "Nostr relays",
+ vec![TerminalTableColumn::new("URL", 12, 48)],
+ rows,
+ "No Nostr relays configured",
+ ));
+ }
+ document
+}
+
+fn transport_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let title = common::title_for(envelope, "Transport Status");
+ let mut document = common::document_with_title(envelope, title);
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Source", result, &["source"]);
+ let rows = common::array(result, &["transports"])
.into_iter()
.flatten()
- .map(|relay| {
+ .map(|transport| {
TerminalTableRow::new(vec![
- common::string(relay, &["url"]).unwrap_or_default(),
- common::bool_path(relay, &["read"])
- .map(|value| if value { "yes" } else { "no" }.to_owned())
- .unwrap_or_default(),
- common::bool_path(relay, &["write"])
+ common::string(transport, &["profile_id"]).unwrap_or_default(),
+ common::string(transport, &["transport_kind"]).unwrap_or_default(),
+ common::string(transport, &["configured_state"]).unwrap_or_default(),
+ common::string(transport, &["implementation_state"]).unwrap_or_default(),
+ common::bool_path(transport, &["usable_for_delivery"])
.map(|value| if value { "yes" } else { "no" }.to_owned())
.unwrap_or_default(),
])
})
.collect::<Vec<_>>();
document.sections.push(common::table_section(
- "Relays",
+ "Transports",
vec![
- TerminalTableColumn::new("URL", 12, 42),
- TerminalTableColumn::new("Read", 4, 4),
- TerminalTableColumn::new("Write", 5, 5),
+ TerminalTableColumn::new("Profile", 7, 18),
+ TerminalTableColumn::new("Kind", 4, 12),
+ TerminalTableColumn::new("Configured", 10, 18),
+ TerminalTableColumn::new("Implementation", 14, 24),
+ TerminalTableColumn::new("Usable", 6, 6),
],
rows,
- "No relays configured",
+ "No transports reported",
));
document
}
+
+fn transport_outbox_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let title = common::title_for(envelope, "Transport Outbox");
+ let mut document = common::document_with_title(envelope, title);
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Profile", result, &["transport_profile"]);
+ common::push_count_field(&mut document, "Total", result, &["total_count"]);
+ common::push_count_field(&mut document, "Pending", result, &["pending_count"]);
+ common::push_count_field(&mut document, "Retryable", result, &["retryable_count"]);
+ common::push_count_field(&mut document, "Terminal", result, &["terminal_count"]);
+ common::push_count_field(
+ &mut document,
+ "Ready signed",
+ result,
+ &["ready_signed_count"],
+ );
+ common::push_path_field(&mut document, "Last error", result, &["last_error"]);
+ document
+}
+
+fn transport_outbox_push_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let title = common::title_for(envelope, "Transport Outbox Push");
+ let mut document = common::document_with_title(envelope, title);
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_count_field(&mut document, "Attempted", result, &["attempted_events"]);
+ common::push_count_field(&mut document, "Published", result, &["published_events"]);
+ common::push_count_field(&mut document, "Retryable", result, &["retryable_events"]);
+ common::push_count_field(&mut document, "Terminal", result, &["terminal_events"]);
+ common::push_count_field(&mut document, "Targets", result, &["target_count"]);
+ common::push_path_field(&mut document, "Reason", result, &["reason"]);
+ document
+}
+
+fn mesh_scope_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let title = common::title_for(envelope, "Mesh Scope");
+ let mut document = common::document_with_title(envelope, title);
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Scope", result, &["scope"]);
+ common::push_path_field(
+ &mut document,
+ "Implementation",
+ result,
+ &["implementation_state"],
+ );
+ common::push_path_field(&mut document, "Message", result, &["message"]);
+ document
+}
+
+fn mesh_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let title = common::title_for(envelope, "Mesh");
+ let mut document = common::document_with_title(envelope, title);
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Scope", result, &["scope"]);
+ common::push_path_field(&mut document, "Kind", result, &["transport_kind"]);
+ common::push_path_field(&mut document, "Configured", result, &["configured_state"]);
+ common::push_path_field(
+ &mut document,
+ "Implementation",
+ result,
+ &["implementation_state"],
+ );
+ common::push_bool_field(&mut document, "Usable", result, &["usable_for_delivery"]);
+ common::push_path_field(&mut document, "Decision", result, &["decision"]);
+ common::push_path_field(&mut document, "Message", result, &["message"]);
+ document
+}
diff --git a/src/out/terminal/renderers/validation.rs b/src/out/terminal/renderers/validation.rs
@@ -253,14 +253,14 @@ mod tests {
"invalid_count": 0,
"receipts": [],
"invalid_receipts": [],
- "actions": ["radroots --relay wss://relay.example.com validation receipt list --trade-id trade_test"]
+ "actions": ["radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"]
}),
);
let document = VALIDATION_RENDERER.render(&envelope, &TerminalRenderContext::default());
let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
assert!(rendered.contains("Validation receipts"));
- assert!(rendered.contains("--trade-id trade_test"));
+ assert!(rendered.contains("trade_test"));
assert!(!rendered.contains("--order-id"));
}
diff --git a/src/out/terminal/values.rs b/src/out/terminal/values.rs
@@ -4,8 +4,8 @@ use serde_json::Value;
pub fn transport_label(value: &str) -> String {
match value {
- "direct_nostr_relay" => "direct nostr relay",
- "radrootsd_proxy" => "radrootsd proxy",
+ "nostr" => "nostr",
+ "proxy" => "proxy",
"local" => "local",
"preview" => "preview",
other => other,
@@ -105,7 +105,7 @@ mod tests {
#[test]
fn formats_domain_values() {
- assert_eq!(transport_label("direct_nostr_relay"), "direct nostr relay");
+ assert_eq!(transport_label("nostr"), "nostr");
assert_eq!(
relay_summary(2, 0, "acknowledged"),
"2 acknowledged · 0 failed"
diff --git a/src/registry/mesh.rs b/src/registry/mesh.rs
@@ -0,0 +1,65 @@
+use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
+
+pub const MESH_SCOPE_GET: OperationSpec = operation!(
+ "mesh.scope.get",
+ "radroots mesh scope get",
+ "mesh",
+ "mesh_scope_get",
+ "MeshScopeGetRequest",
+ "MeshScopeGetResult",
+ "Read the configured mesh scope.",
+ Any,
+ false,
+ None,
+ Low,
+ false,
+ false
+);
+
+pub const MESH_SCOPE_SET: OperationSpec = operation!(
+ "mesh.scope.set",
+ "radroots mesh scope set --scope disabled",
+ "mesh",
+ "mesh_scope_set",
+ "MeshScopeSetRequest",
+ "MeshScopeSetResult",
+ "Write the configured mesh scope.",
+ Any,
+ true,
+ Required,
+ High,
+ false,
+ true
+);
+
+pub const MESH_STATUS: OperationSpec = operation!(
+ "mesh.status",
+ "radroots mesh status",
+ "mesh",
+ "mesh_status",
+ "MeshStatusRequest",
+ "MeshStatusResult",
+ "Read mesh implementation status.",
+ Any,
+ false,
+ None,
+ Low,
+ false,
+ false
+);
+
+pub const MESH_POLICY_CHECK: OperationSpec = operation!(
+ "mesh.policy.check",
+ "radroots mesh policy check",
+ "mesh",
+ "mesh_policy_check",
+ "MeshPolicyCheckRequest",
+ "MeshPolicyCheckResult",
+ "Evaluate mesh delivery policy for the active preview state.",
+ Any,
+ false,
+ None,
+ Low,
+ false,
+ false
+);
diff --git a/src/registry/mod.rs b/src/registry/mod.rs
@@ -88,11 +88,12 @@ mod farm;
mod health;
mod listing;
mod market;
-mod relay;
+mod mesh;
mod signer;
mod store;
mod sync;
mod trade;
+mod transport;
mod validation;
mod workspace;
@@ -112,7 +113,15 @@ pub const OPERATION_REGISTRY: &[OperationSpec] = &[
account::ACCOUNT_SELECTION_UPDATE,
account::ACCOUNT_SELECTION_CLEAR,
signer::SIGNER_STATUS_GET,
- relay::RELAY_LIST,
+ transport::TRANSPORT_PROFILE_GET,
+ transport::TRANSPORT_PROFILE_SET,
+ transport::TRANSPORT_STATUS,
+ transport::TRANSPORT_OUTBOX_STATUS,
+ transport::TRANSPORT_OUTBOX_PUSH,
+ mesh::MESH_SCOPE_GET,
+ mesh::MESH_SCOPE_SET,
+ mesh::MESH_STATUS,
+ mesh::MESH_POLICY_CHECK,
store::STORE_INIT,
store::STORE_STATUS_GET,
store::STORE_EXPORT,
@@ -185,6 +194,7 @@ pub fn network_requirement(operation_id: &str) -> NetworkRequirement {
match operation_id {
"sync.pull"
| "sync.push"
+ | "transport.outbox.push"
| "sync.watch"
| "market.refresh"
| "farm.publish"
@@ -214,6 +224,7 @@ pub fn requires_local_signer_mode(operation_id: &str) -> bool {
matches!(
operation_id,
"sync.push"
+ | "transport.outbox.push"
| "trade.submit"
| "trade.accept"
| "trade.decline"
@@ -224,11 +235,11 @@ pub fn requires_local_signer_mode(operation_id: &str) -> bool {
)
}
-#[cfg(test)]
-pub fn requires_direct_nostr_relay_publish_transport(operation_id: &str) -> bool {
+pub fn requires_nostr_publish_transport(operation_id: &str) -> bool {
matches!(
operation_id,
"sync.push"
+ | "transport.outbox.push"
| "farm.publish"
| "listing.publish"
| "listing.update"
@@ -258,8 +269,8 @@ mod tests {
use super::{
ApprovalPolicy, NetworkRequirement, OPERATION_REGISTRY, OperationRole, RiskLevel,
- get_operation, network_requirement, requires_direct_nostr_relay_publish_transport,
- requires_local_signer_mode,
+ get_operation, network_requirement, requires_local_signer_mode,
+ requires_nostr_publish_transport,
};
const EXPECTED_OPERATION_IDS: &[&str] = &[
@@ -278,7 +289,15 @@ mod tests {
"account.selection.update",
"account.selection.clear",
"signer.status.get",
- "relay.list",
+ "transport.profile.get",
+ "transport.profile.set",
+ "transport.status",
+ "transport.outbox.status",
+ "transport.outbox.push",
+ "mesh.scope.get",
+ "mesh.scope.set",
+ "mesh.status",
+ "mesh.policy.check",
"store.init",
"store.status.get",
"store.export",
@@ -354,6 +373,9 @@ mod tests {
"store.backup.restore",
"sync.pull",
"sync.push",
+ "transport.profile.set",
+ "transport.outbox.push",
+ "mesh.scope.set",
"farm.create",
"farm.rebind",
"farm.profile.update",
@@ -396,7 +418,7 @@ mod tests {
.copied()
.collect::<BTreeSet<_>>();
assert_eq!(actual, expected);
- assert_eq!(OPERATION_REGISTRY.len(), 76);
+ assert_eq!(OPERATION_REGISTRY.len(), 84);
}
#[test]
@@ -448,6 +470,9 @@ mod tests {
"listing.publish",
"listing.update",
"listing.archive",
+ "transport.profile.set",
+ "transport.outbox.push",
+ "mesh.scope.set",
"trade.submit",
"trade.rebind",
"trade.accept",
@@ -513,7 +538,7 @@ mod tests {
"health.check.run",
"config.get",
"account.list",
- "relay.list",
+ "transport.outbox.push",
"farm.location.set",
"sync.pull",
"sync.push",
@@ -560,6 +585,7 @@ mod tests {
"trade.revision.propose",
"trade.revision.accept",
"trade.revision.decline",
+ "transport.outbox.push",
"trade.event.list",
"validation.receipt.get",
"validation.receipt.list",
@@ -587,6 +613,7 @@ mod tests {
"trade.revision.propose",
"trade.revision.accept",
"trade.revision.decline",
+ "transport.outbox.push",
]
.into_iter()
.collect::<BTreeSet<_>>();
@@ -595,16 +622,15 @@ mod tests {
}
#[test]
- fn registry_direct_nostr_relay_publish_requirements_are_explicit() {
+ fn registry_nostr_publish_requirements_are_explicit() {
let publish = OPERATION_REGISTRY
.iter()
- .filter(|operation| {
- requires_direct_nostr_relay_publish_transport(operation.operation_id)
- })
+ .filter(|operation| requires_nostr_publish_transport(operation.operation_id))
.map(|operation| operation.operation_id)
.collect::<BTreeSet<_>>();
let expected = [
"sync.push",
+ "transport.outbox.push",
"farm.publish",
"listing.publish",
"listing.update",
diff --git a/src/registry/relay.rs b/src/registry/relay.rs
@@ -1,17 +0,0 @@
-use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
-
-pub const RELAY_LIST: OperationSpec = operation!(
- "relay.list",
- "radroots relay list",
- "relay",
- "relay_list",
- "RelayListRequest",
- "RelayListResult",
- "List configured relays.",
- Any,
- false,
- None,
- Low,
- true,
- false
-);
diff --git a/src/registry/transport.rs b/src/registry/transport.rs
@@ -0,0 +1,81 @@
+use super::{ApprovalPolicy, OperationRole, OperationSpec, RiskLevel};
+
+pub const TRANSPORT_PROFILE_GET: OperationSpec = operation!(
+ "transport.profile.get",
+ "radroots transport profile get",
+ "transport",
+ "transport_profile_get",
+ "TransportProfileGetRequest",
+ "TransportProfileGetResult",
+ "Read the active transport profile.",
+ Any,
+ false,
+ None,
+ Low,
+ false,
+ false
+);
+
+pub const TRANSPORT_PROFILE_SET: OperationSpec = operation!(
+ "transport.profile.set",
+ "radroots transport profile set --kind local-only",
+ "transport",
+ "transport_profile_set",
+ "TransportProfileSetRequest",
+ "TransportProfileSetResult",
+ "Write the active transport profile.",
+ Any,
+ true,
+ Required,
+ High,
+ false,
+ true
+);
+
+pub const TRANSPORT_STATUS: OperationSpec = operation!(
+ "transport.status",
+ "radroots transport status",
+ "transport",
+ "transport_status",
+ "TransportStatusRequest",
+ "TransportStatusResult",
+ "Read transport implementation readiness.",
+ Any,
+ false,
+ None,
+ Low,
+ false,
+ false
+);
+
+pub const TRANSPORT_OUTBOX_STATUS: OperationSpec = operation!(
+ "transport.outbox.status",
+ "radroots transport outbox status",
+ "transport",
+ "transport_outbox_status",
+ "TransportOutboxStatusRequest",
+ "TransportOutboxStatusResult",
+ "Read SDK transport outbox status.",
+ Any,
+ false,
+ None,
+ Low,
+ false,
+ false
+);
+
+pub const TRANSPORT_OUTBOX_PUSH: OperationSpec = operation!(
+ "transport.outbox.push",
+ "radroots transport outbox push",
+ "transport",
+ "transport_outbox_push",
+ "TransportOutboxPushRequest",
+ "TransportOutboxPushResult",
+ "Push ready SDK outbox events through the active transport profile.",
+ Any,
+ true,
+ Required,
+ High,
+ true,
+ true
+);
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -4,10 +4,12 @@ use std::io::IsTerminal;
use std::path::Path;
use std::path::PathBuf;
-use radroots_local_events::{RelayUrlValidationError, normalize_relay_url};
use radroots_runtime::{parse_bool_value, parse_strict_env_file, parse_u64_value};
use radroots_runtime_paths::RadrootsPathResolver;
use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
+use radroots_transport_nostr::{
+ RadrootsRelayTransportError, RadrootsRelayUrl, RadrootsRelayUrlPolicy,
+};
use serde::Deserialize;
use url::Url;
@@ -39,17 +41,20 @@ const ENV_CLI_ACCOUNT_SELECTOR: &str = "RADROOTS_CLI_ACCOUNT_SELECTOR";
const ENV_CLI_ACCOUNT_SECRET_BACKEND: &str = "RADROOTS_CLI_ACCOUNT_SECRET_BACKEND";
const ENV_CLI_IDENTITY_PATH: &str = "RADROOTS_CLI_IDENTITY_PATH";
const ENV_CLI_SIGNER_BACKEND: &str = "RADROOTS_CLI_SIGNER_BACKEND";
-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_TRANSPORT_PROFILE: &str = "RADROOTS_CLI_TRANSPORT_PROFILE";
+const ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS: &str = "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS";
+const ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR: &str =
+ "RADROOTS_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR";
+const ENV_CLI_TRANSPORT_PROXY_URL: &str = "RADROOTS_CLI_TRANSPORT_PROXY_URL";
+const ENV_CLI_TRANSPORT_PROXY_TOKEN_FILE: &str = "RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE";
+const ENV_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID: &str =
+ "RADROOTS_CLI_TRANSPORT_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_RHI_TRUSTED_WORKER_PUBKEYS: &str = "RADROOTS_CLI_RHI_TRUSTED_WORKER_PUBKEYS";
+const ENV_CLI_MESH_SCOPE: &str = "RADROOTS_CLI_MESH_SCOPE";
const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
ENV_CLI_OUTPUT_FORMAT,
ENV_CLI_LOG_FILTER,
@@ -61,16 +66,18 @@ const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
ENV_CLI_ACCOUNT_SECRET_BACKEND,
ENV_CLI_IDENTITY_PATH,
ENV_CLI_SIGNER_BACKEND,
- 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_TRANSPORT_PROFILE,
+ ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS,
+ ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR,
+ ENV_CLI_TRANSPORT_PROXY_URL,
+ ENV_CLI_TRANSPORT_PROXY_TOKEN_FILE,
+ ENV_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID,
ENV_CLI_MYC_EXECUTABLE,
ENV_CLI_MYC_STATUS_TIMEOUT_MS,
ENV_CLI_HYF_ENABLED,
ENV_CLI_HYF_EXECUTABLE,
ENV_CLI_RHI_TRUSTED_WORKER_PUBKEYS,
+ ENV_CLI_MESH_SCOPE,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -175,23 +182,110 @@ pub struct SignerConfig {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TransportProfileKind {
+ LocalOnly,
+ Nostr,
+ ReticulumPreview,
+ Proxy,
+}
+
+impl TransportProfileKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::LocalOnly => "local_only",
+ Self::Nostr => "nostr",
+ Self::ReticulumPreview => "reticulum_preview",
+ Self::Proxy => "proxy",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ReticulumPreviewBehavior {
+ RejectDeliveryAttempts,
+ DeferDeliveryPlans,
+}
+
+impl ReticulumPreviewBehavior {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::RejectDeliveryAttempts => "reject_delivery_attempts",
+ Self::DeferDeliveryPlans => "defer_delivery_plans",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TransportConfigSource {
+ Environment,
+ UserConfig,
+ WorkspaceConfig,
+ Defaults,
+}
+
+impl TransportConfigSource {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Environment => "environment · transport profile",
+ Self::UserConfig => "user config · transport profile",
+ Self::WorkspaceConfig => "workspace config · transport profile",
+ Self::Defaults => "defaults · transport profile",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct TransportConfig {
+ pub profile: TransportProfileKind,
+ pub source: TransportConfigSource,
+ pub nostr_relay_urls: Vec<String>,
+ pub reticulum_preview_behavior: ReticulumPreviewBehavior,
+ pub proxy: ProxyTransportConfig,
+}
+
+impl TransportConfig {
+ pub fn local_only() -> Self {
+ Self {
+ profile: TransportProfileKind::LocalOnly,
+ source: TransportConfigSource::Defaults,
+ nostr_relay_urls: Vec::new(),
+ reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts,
+ proxy: ProxyTransportConfig::default(),
+ }
+ }
+
+ pub fn from_nostr_relay_urls(nostr_relay_urls: Vec<String>) -> Self {
+ if nostr_relay_urls.is_empty() {
+ return Self::local_only();
+ }
+ Self {
+ profile: TransportProfileKind::Nostr,
+ source: TransportConfigSource::Defaults,
+ nostr_relay_urls,
+ reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts,
+ proxy: ProxyTransportConfig::default(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishTransport {
- DirectNostrRelay,
- RadrootsdProxy,
+ Nostr,
+ Proxy,
}
impl PublishTransport {
pub fn as_str(self) -> &'static str {
match self {
- Self::DirectNostrRelay => "direct_nostr_relay",
- Self::RadrootsdProxy => "radrootsd_proxy",
+ Self::Nostr => "nostr",
+ Self::Proxy => "proxy",
}
}
pub fn transport_family(self) -> &'static str {
match self {
- Self::DirectNostrRelay => "direct_nostr_relay",
- Self::RadrootsdProxy => "radrootsd_proxy",
+ Self::Nostr => "nostr",
+ Self::Proxy => "proxy",
}
}
}
@@ -208,11 +302,11 @@ pub enum PublishTransportSource {
impl PublishTransportSource {
pub fn as_str(self) -> &'static str {
match self {
- Self::Flags => "cli flags · local first",
- Self::Environment => "environment · local first",
- Self::UserConfig => "user config · local first",
- Self::WorkspaceConfig => "workspace config · local first",
- Self::Defaults => "defaults · local first",
+ Self::Flags => "cli flags · transport profile",
+ Self::Environment => "environment · transport profile",
+ Self::UserConfig => "user config · transport profile",
+ Self::WorkspaceConfig => "workspace config · transport profile",
+ Self::Defaults => "defaults · transport profile",
}
}
}
@@ -221,17 +315,17 @@ impl PublishTransportSource {
pub struct PublishConfig {
pub transport: PublishTransport,
pub source: PublishTransportSource,
- pub radrootsd_proxy: RadrootsdProxyConfig,
+ pub proxy: ProxyTransportConfig,
}
#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsdProxyConfig {
+pub struct ProxyTransportConfig {
pub url: String,
pub token_file: Option<PathBuf>,
pub token_secret_id: Option<String>,
}
-impl Default for RadrootsdProxyConfig {
+impl Default for ProxyTransportConfig {
fn default() -> Self {
Self {
url: DEFAULT_RPC_URL.to_owned(),
@@ -303,6 +397,55 @@ pub struct HyfConfig {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MeshScope {
+ Disabled,
+ LocalPreview,
+}
+
+impl MeshScope {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Disabled => "disabled",
+ Self::LocalPreview => "local_preview",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MeshConfig {
+ pub scope: MeshScope,
+ pub source: MeshConfigSource,
+}
+
+impl MeshConfig {
+ pub fn disabled() -> Self {
+ Self {
+ scope: MeshScope::Disabled,
+ source: MeshConfigSource::Defaults,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MeshConfigSource {
+ Environment,
+ UserConfig,
+ WorkspaceConfig,
+ Defaults,
+}
+
+impl MeshConfigSource {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Environment => "environment · local first",
+ Self::UserConfig => "user config · local first",
+ Self::WorkspaceConfig => "workspace config · local first",
+ Self::Defaults => "defaults · local first",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilityBindingTargetKind {
ManagedInstance,
ExplicitEndpoint,
@@ -386,11 +529,13 @@ pub struct RuntimeConfig {
pub account_secret_contract: AccountSecretContractConfig,
pub identity: IdentityConfig,
pub signer: SignerConfig,
+ pub transport: TransportConfig,
pub publish: PublishConfig,
pub relay: RelayConfig,
pub local: LocalConfig,
pub myc: MycConfig,
pub hyf: HyfConfig,
+ pub mesh: MeshConfig,
pub rpc: RpcConfig,
pub rhi: RhiConfig,
pub capability_bindings: Vec<CapabilityBindingConfig>,
@@ -412,11 +557,11 @@ struct CliConfigFile {
logging: Option<LoggingFileConfig>,
account: Option<AccountFileConfig>,
identity: Option<IdentityFileConfig>,
- relays: Option<RelayFileConfig>,
- publish: Option<PublishFileConfig>,
+ transport: Option<TransportFileConfig>,
signer: Option<SignerFileConfig>,
myc: Option<MycFileConfig>,
hyf: Option<HyfFileConfig>,
+ mesh: Option<MeshFileConfig>,
rpc: Option<RpcFileConfig>,
rhi: Option<RhiFileConfig>,
capability_binding: Option<Vec<CapabilityBindingFileConfig>>,
@@ -457,21 +602,28 @@ struct IdentityFileConfig {
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
-struct RelayFileConfig {
- urls: Option<Vec<String>>,
- publish_policy: Option<String>,
+struct TransportFileConfig {
+ profile: Option<String>,
+ nostr: Option<NostrTransportFileConfig>,
+ reticulum_preview: Option<ReticulumPreviewFileConfig>,
+ proxy: Option<TransportProxyFileConfig>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
-struct PublishFileConfig {
- transport: Option<String>,
- radrootsd_proxy: Option<RadrootsdProxyFileConfig>,
+struct NostrTransportFileConfig {
+ relay_urls: Option<Vec<String>>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
-struct RadrootsdProxyFileConfig {
+struct ReticulumPreviewFileConfig {
+ behavior: Option<String>,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct TransportProxyFileConfig {
url: Option<String>,
token_file: Option<PathBuf>,
token_secret_id: Option<String>,
@@ -509,6 +661,12 @@ struct HyfFileConfig {
executable: Option<PathBuf>,
}
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct MeshFileConfig {
+ scope: Option<String>,
+}
+
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct CapabilityBindingFileConfig {
@@ -642,6 +800,12 @@ impl RuntimeConfig {
)?,
};
validate_logging_output_contract(&output, &logging)?;
+ let transport = resolve_transport_config(
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ )?;
Ok(Self {
capability_bindings: resolve_capability_bindings(
app_config.as_ref(),
@@ -711,20 +875,9 @@ impl RuntimeConfig {
app_config.as_ref(),
workspace_config.as_ref(),
)?,
- publish: resolve_publish_config(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- relay: resolve_relay_config(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
+ transport: transport.clone(),
+ publish: publish_config_from_transport(&transport),
+ relay: relay_config_from_transport(&transport),
local: LocalConfig {
root: paths.app_data_root.join(DEFAULT_LOCAL_STATE_DIR),
replica_db_path: paths
@@ -763,6 +916,12 @@ impl RuntimeConfig {
workspace_config.as_ref(),
),
},
+ mesh: resolve_mesh_config(
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ )?,
rpc: resolve_rpc_config(
env,
env_file,
@@ -889,37 +1048,188 @@ fn resolve_rpc_config(
})
}
-fn resolve_radrootsd_proxy_config(
+fn resolve_transport_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()))
+) -> Result<TransportConfig, RuntimeError> {
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_TRANSPORT_PROFILE]) {
+ let profile = parse_transport_profile_kind(key.as_str(), value.as_str())?;
+ let reticulum_preview_behavior = env_value(
+ env,
+ env_file,
+ &[ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR],
+ )
+ .map(|value| {
+ parse_reticulum_preview_behavior(
+ ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR,
+ value.as_str(),
+ )
+ })
+ .transpose()?
+ .unwrap_or(ReticulumPreviewBehavior::RejectDeliveryAttempts);
+ return Ok(TransportConfig {
+ profile,
+ source: TransportConfigSource::Environment,
+ nostr_relay_urls: env_value(env, env_file, &[ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS])
+ .map(|value| {
+ parse_relay_env_value(value.as_str(), ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS)
+ })
+ .transpose()?
+ .unwrap_or_default(),
+ reticulum_preview_behavior,
+ proxy: resolve_transport_proxy_env(env, env_file)?,
+ });
+ }
+
+ if let Some(transport) = user_config.and_then(|config| config.transport.as_ref()) {
+ return resolve_transport_file_config(
+ transport,
+ TransportConfigSource::UserConfig,
+ "user config [transport]",
+ );
+ }
+
+ if let Some(transport) = workspace_config.and_then(|config| config.transport.as_ref()) {
+ return resolve_transport_file_config(
+ transport,
+ TransportConfigSource::WorkspaceConfig,
+ "workspace config [transport]",
+ );
+ }
+
+ Ok(TransportConfig {
+ profile: TransportProfileKind::LocalOnly,
+ source: TransportConfigSource::Defaults,
+ nostr_relay_urls: Vec::new(),
+ reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts,
+ proxy: ProxyTransportConfig::default(),
+ })
+}
+
+fn resolve_transport_file_config(
+ transport: &TransportFileConfig,
+ source: TransportConfigSource,
+ source_label: &str,
+) -> Result<TransportConfig, RuntimeError> {
+ let Some(profile_value) = transport.profile.as_deref() else {
+ if transport.nostr.is_some()
+ || transport.reticulum_preview.is_some()
+ || transport.proxy.is_some()
+ {
+ return Err(RuntimeError::Config(format!(
+ "{source_label}.profile must be set when profile-specific transport tables are present"
+ )));
+ }
+ return Ok(TransportConfig {
+ profile: TransportProfileKind::LocalOnly,
+ source,
+ nostr_relay_urls: Vec::new(),
+ reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts,
+ proxy: ProxyTransportConfig::default(),
+ });
+ };
+ let profile =
+ parse_transport_profile_kind(format!("{source_label}.profile").as_str(), profile_value)?;
+ let nostr_relay_urls = transport
+ .nostr
+ .as_ref()
+ .and_then(|nostr| nostr.relay_urls.clone())
+ .map(|urls| normalize_relay_urls(urls, format!("{source_label}.nostr.relay_urls").as_str()))
+ .transpose()?
+ .unwrap_or_default();
+ let reticulum_preview_behavior = transport
+ .reticulum_preview
+ .as_ref()
+ .and_then(|preview| preview.behavior.as_deref())
+ .map(|value| {
+ parse_reticulum_preview_behavior(
+ format!("{source_label}.reticulum_preview.behavior").as_str(),
+ value,
+ )
+ })
+ .transpose()?
+ .unwrap_or(ReticulumPreviewBehavior::RejectDeliveryAttempts);
+ Ok(TransportConfig {
+ profile,
+ source,
+ nostr_relay_urls,
+ reticulum_preview_behavior,
+ proxy: resolve_transport_proxy_file(transport.proxy.as_ref(), source_label)?,
+ })
+}
+
+fn resolve_transport_proxy_env(
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+) -> Result<ProxyTransportConfig, RuntimeError> {
+ let url = env_value(env, env_file, &[ENV_CLI_TRANSPORT_PROXY_URL])
.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 {
+ let token_file =
+ env_value(env, env_file, &[ENV_CLI_TRANSPORT_PROXY_TOKEN_FILE]).map(PathBuf::from);
+ let token_secret_id = env_value(env, env_file, &[ENV_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID]);
+ Ok(ProxyTransportConfig {
url: validate_rpc_url(url.as_str())?,
token_file,
token_secret_id,
})
}
+fn resolve_transport_proxy_file(
+ proxy: Option<&TransportProxyFileConfig>,
+ source_label: &str,
+) -> Result<ProxyTransportConfig, RuntimeError> {
+ let url = proxy
+ .and_then(|proxy| proxy.url.clone())
+ .unwrap_or_else(|| DEFAULT_RPC_URL.to_owned());
+ Ok(ProxyTransportConfig {
+ url: validate_rpc_url(url.as_str()).map_err(|error| match error {
+ RuntimeError::Config(message) => {
+ RuntimeError::Config(format!("{source_label}.proxy.url is invalid: {message}"))
+ }
+ other => other,
+ })?,
+ token_file: proxy.and_then(|proxy| proxy.token_file.clone()),
+ token_secret_id: proxy.and_then(|proxy| proxy.token_secret_id.clone()),
+ })
+}
+
+fn publish_config_from_transport(transport: &TransportConfig) -> PublishConfig {
+ PublishConfig {
+ transport: match transport.profile {
+ TransportProfileKind::Proxy => PublishTransport::Proxy,
+ TransportProfileKind::LocalOnly
+ | TransportProfileKind::Nostr
+ | TransportProfileKind::ReticulumPreview => PublishTransport::Nostr,
+ },
+ source: match transport.source {
+ TransportConfigSource::Environment => PublishTransportSource::Environment,
+ TransportConfigSource::UserConfig => PublishTransportSource::UserConfig,
+ TransportConfigSource::WorkspaceConfig => PublishTransportSource::WorkspaceConfig,
+ TransportConfigSource::Defaults => PublishTransportSource::Defaults,
+ },
+ proxy: transport.proxy.clone(),
+ }
+}
+
+fn relay_config_from_transport(transport: &TransportConfig) -> RelayConfig {
+ RelayConfig {
+ urls: if matches!(transport.profile, TransportProfileKind::Nostr) {
+ transport.nostr_relay_urls.clone()
+ } else {
+ Vec::new()
+ },
+ publish_policy: RelayPublishPolicy::Any,
+ source: match transport.source {
+ TransportConfigSource::Environment => RelayConfigSource::Environment,
+ TransportConfigSource::UserConfig => RelayConfigSource::UserConfig,
+ TransportConfigSource::WorkspaceConfig => RelayConfigSource::WorkspaceConfig,
+ TransportConfigSource::Defaults => RelayConfigSource::Defaults,
+ },
+ }
+}
+
fn resolve_rhi_config(
env: &dyn Environment,
env_file: &EnvFileValues,
@@ -1105,59 +1415,6 @@ fn normalize_binding_ref(value: Option<&str>) -> Option<String> {
value.map(ToOwned::to_owned)
}
-fn resolve_relay_config(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<RelayConfig, RuntimeError> {
- let publish_policy = resolve_relay_publish_policy(user_config, workspace_config)?
- .unwrap_or(RelayPublishPolicy::Any);
-
- if !args.relay.is_empty() {
- return Ok(RelayConfig {
- urls: normalize_relay_urls(args.relay.clone(), "--relay")?,
- publish_policy,
- source: RelayConfigSource::Flags,
- });
- }
-
- if let Some(value) = env_value(env, env_file, &[ENV_CLI_RELAYS_URLS]) {
- return Ok(RelayConfig {
- urls: parse_relay_env_value(value.as_str(), ENV_CLI_RELAYS_URLS)?,
- publish_policy,
- source: RelayConfigSource::Environment,
- });
- }
-
- if let Some(relay) = user_config.and_then(|config| config.relays.as_ref()) {
- if let Some(urls) = relay.urls.clone() {
- return Ok(RelayConfig {
- urls: normalize_relay_urls(urls, "user config [relays].urls")?,
- publish_policy,
- source: RelayConfigSource::UserConfig,
- });
- }
- }
-
- if let Some(relay) = workspace_config.and_then(|config| config.relays.as_ref()) {
- if let Some(urls) = relay.urls.clone() {
- return Ok(RelayConfig {
- urls: normalize_relay_urls(urls, "workspace config [relays].urls")?,
- publish_policy,
- source: RelayConfigSource::WorkspaceConfig,
- });
- }
- }
-
- Ok(RelayConfig {
- urls: Vec::new(),
- publish_policy,
- source: RelayConfigSource::Defaults,
- })
-}
-
fn resolve_signer_config(
args: &RuntimeInvocationArgs,
env: &dyn Environment,
@@ -1186,60 +1443,6 @@ fn resolve_signer_config(
Ok(SignerConfig { backend })
}
-fn resolve_publish_config(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<PublishConfig, RuntimeError> {
- 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 {
- 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_TRANSPORT]) {
- return Ok(PublishConfig {
- 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.transport.clone())
- {
- return Ok(PublishConfig {
- 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.transport.clone())
- {
- return Ok(PublishConfig {
- transport: parse_publish_transport("workspace config [publish].transport", value)?,
- source: PublishTransportSource::WorkspaceConfig,
- radrootsd_proxy,
- });
- }
-
- Ok(PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
- source: PublishTransportSource::Defaults,
- radrootsd_proxy,
- })
-}
-
fn resolve_myc_config(
args: &RuntimeInvocationArgs,
env: &dyn Environment,
@@ -1380,32 +1583,48 @@ fn resolve_hyf_executable(
.unwrap_or_else(|| PathBuf::from(DEFAULT_HYF_EXECUTABLE))
}
-fn resolve_relay_publish_policy(
+fn resolve_mesh_config(
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
user_config: Option<&CliConfigFile>,
workspace_config: Option<&CliConfigFile>,
-) -> Result<Option<RelayPublishPolicy>, RuntimeError> {
+) -> Result<MeshConfig, RuntimeError> {
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_MESH_SCOPE]) {
+ return Ok(MeshConfig {
+ scope: parse_mesh_scope(key.as_str(), value.as_str())?,
+ source: MeshConfigSource::Environment,
+ });
+ }
if let Some(value) = user_config
- .and_then(|config| config.relays.as_ref())
- .and_then(|relay| relay.publish_policy.as_deref())
+ .and_then(|config| config.mesh.as_ref())
+ .and_then(|mesh| mesh.scope.as_deref())
{
- return parse_relay_publish_policy(value).map(Some);
+ return Ok(MeshConfig {
+ scope: parse_mesh_scope("user config [mesh].scope", value)?,
+ source: MeshConfigSource::UserConfig,
+ });
}
-
if let Some(value) = workspace_config
- .and_then(|config| config.relays.as_ref())
- .and_then(|relay| relay.publish_policy.as_deref())
+ .and_then(|config| config.mesh.as_ref())
+ .and_then(|mesh| mesh.scope.as_deref())
{
- return parse_relay_publish_policy(value).map(Some);
+ return Ok(MeshConfig {
+ scope: parse_mesh_scope("workspace config [mesh].scope", value)?,
+ source: MeshConfigSource::WorkspaceConfig,
+ });
}
-
- Ok(None)
+ Ok(MeshConfig {
+ scope: MeshScope::Disabled,
+ source: MeshConfigSource::Defaults,
+ })
}
-fn parse_relay_publish_policy(value: &str) -> Result<RelayPublishPolicy, RuntimeError> {
+fn parse_mesh_scope(source: &str, value: &str) -> Result<MeshScope, RuntimeError> {
match value.trim().to_ascii_lowercase().as_str() {
- "any" => Ok(RelayPublishPolicy::Any),
+ "disabled" => Ok(MeshScope::Disabled),
+ "local_preview" => Ok(MeshScope::LocalPreview),
other => Err(RuntimeError::Config(format!(
- "[relays].publish_policy must be `any`, got `{other}`"
+ "{source} must be `disabled` or `local_preview`, got `{other}`"
))),
}
}
@@ -1459,16 +1678,25 @@ fn validate_relay_url(value: &str, source: &str) -> Result<String, RuntimeError>
"{source} contains an empty relay url"
)));
}
- normalize_relay_url(trimmed).map_err(|error| match error {
- RelayUrlValidationError::UnsupportedScheme(_) => RuntimeError::Config(format!(
- "{source} must use websocket relay urls, got `{trimmed}`"
- )),
- _ => RuntimeError::Config(format!(
- "{source} contains invalid relay url `{trimmed}`: {error}"
+ RadrootsRelayUrl::parse(trimmed, nostr_relay_url_policy_for_url(trimmed)).map(|relay| relay.into_string()).map_err(|error| match error {
+ RadrootsRelayTransportError::UnsupportedRelayScheme { .. }
+ | RadrootsRelayTransportError::WsRequiresLocalhostPolicy { .. } => {
+ RuntimeError::Config(format!("{source} must use websocket relay urls allowed by the active Nostr policy, got `{trimmed}`"))
+ }
+ other => RuntimeError::Config(format!(
+ "{source} contains invalid relay url `{trimmed}`: {other}"
)),
})
}
+fn nostr_relay_url_policy_for_url(value: &str) -> RadrootsRelayUrlPolicy {
+ if value.trim_start().starts_with("ws://") {
+ RadrootsRelayUrlPolicy::Localhost
+ } else {
+ RadrootsRelayUrlPolicy::Public
+ }
+}
+
fn resolve_env_file_path(args: &RuntimeInvocationArgs, env: &dyn Environment) -> Option<PathBuf> {
args.env_file
.clone()
@@ -1735,14 +1963,30 @@ fn parse_signer_mode(source: &str, value: String) -> Result<SignerBackend, Runti
}
}
-fn parse_publish_transport(source: &str, value: String) -> Result<PublishTransport, RuntimeError> {
+fn parse_transport_profile_kind(
+ source: &str,
+ value: &str,
+) -> Result<TransportProfileKind, RuntimeError> {
match value.trim().to_ascii_lowercase().as_str() {
- "direct_nostr_relay" => Ok(PublishTransport::DirectNostrRelay),
- "radrootsd_proxy" => Ok(PublishTransport::RadrootsdProxy),
+ "local_only" => Ok(TransportProfileKind::LocalOnly),
+ "nostr" => Ok(TransportProfileKind::Nostr),
+ "reticulum_preview" => Ok(TransportProfileKind::ReticulumPreview),
+ "proxy" => Ok(TransportProfileKind::Proxy),
other => Err(RuntimeError::Config(format!(
- "{source} must be `{}` or `{}`, got `{other}`",
- PublishTransport::DirectNostrRelay.as_str(),
- PublishTransport::RadrootsdProxy.as_str()
+ "{source} must be `local_only`, `nostr`, `reticulum_preview`, or `proxy`, got `{other}`"
+ ))),
+ }
+}
+
+fn parse_reticulum_preview_behavior(
+ source: &str,
+ value: &str,
+) -> Result<ReticulumPreviewBehavior, RuntimeError> {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "reject_delivery_attempts" => Ok(ReticulumPreviewBehavior::RejectDeliveryAttempts),
+ "defer_delivery_plans" => Ok(ReticulumPreviewBehavior::DeferDeliveryPlans),
+ other => Err(RuntimeError::Config(format!(
+ "{source} must be `reject_delivery_attempts` or `defer_delivery_plans`, got `{other}`"
))),
}
}
@@ -1803,8 +2047,8 @@ mod tests {
DEFAULT_LOG_FILTER, DEFAULT_MYC_STATUS_TIMEOUT_MS, DEFAULT_RPC_URL, EnvFileValues,
Environment, HyfConfig, INFERENCE_HYF_STDIO_CAPABILITY, InteractionConfig, OutputConfig,
OutputFormat, PathsConfig, PublishConfig, PublishTransport, PublishTransportSource,
- RelayConfigSource, RelayPublishPolicy, RuntimeConfig, SignerBackend, Verbosity,
- parse_env_file_values,
+ RelayConfigSource, RelayPublishPolicy, RuntimeConfig, SignerBackend, TransportConfigSource,
+ TransportProfileKind, Verbosity, parse_env_file_values,
};
use crate::cli::global::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
@@ -1912,8 +2156,6 @@ mod tests {
log_stdout: false,
identity_path: Some(PathBuf::from("custom-identity.json")),
signer: Some("local".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),
hyf_enabled: true,
@@ -1933,12 +2175,12 @@ mod tests {
),
("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
(
- "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
- "radrootsd_proxy".to_owned(),
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
+ "nostr".to_owned(),
),
(
- "RADROOTS_CLI_RELAYS_URLS".to_owned(),
- "wss://relay.env".to_owned(),
+ "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
+ "wss://relay.one,wss://relay.two".to_owned(),
),
(
"RADROOTS_CLI_MYC_EXECUTABLE".to_owned(),
@@ -2034,19 +2276,24 @@ mod tests {
}
);
assert_eq!(resolved.signer.backend, SignerBackend::Local);
+ assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
+ assert_eq!(
+ resolved.transport.source,
+ TransportConfigSource::Environment
+ );
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
- source: PublishTransportSource::Flags,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ transport: PublishTransport::Nostr,
+ source: PublishTransportSource::Environment,
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
assert_eq!(
resolved.relay.urls,
vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
);
- assert_eq!(resolved.relay.source, RelayConfigSource::Flags);
+ assert_eq!(resolved.relay.source, RelayConfigSource::Environment);
assert_eq!(resolved.relay.publish_policy, RelayPublishPolicy::Any);
assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc-cli"));
assert_eq!(resolved.myc.status_timeout_ms, 2500);
@@ -2083,11 +2330,11 @@ mod tests {
),
("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
(
- "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
- "radrootsd_proxy".to_owned(),
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
+ "nostr".to_owned(),
),
(
- "RADROOTS_CLI_RELAYS_URLS".to_owned(),
+ "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
"wss://relay.one,wss://relay.two".to_owned(),
),
(
@@ -2139,12 +2386,17 @@ mod tests {
);
assert_eq!(resolved.identity.path, PathBuf::from("state/identity.json"));
assert_eq!(resolved.signer.backend, SignerBackend::Myc);
+ assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
+ assert_eq!(
+ resolved.transport.source,
+ TransportConfigSource::Environment
+ );
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::RadrootsdProxy,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Environment,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
assert_eq!(
@@ -2181,7 +2433,7 @@ mod tests {
"old-identity.json".to_owned(),
),
("RADROOTS_SIGNER".to_owned(), "myc".to_owned()),
- ("RADROOTS_PUBLISH_MODE".to_owned(), "radrootsd".to_owned()),
+ ("RADROOTS_PUBLISH_MODE".to_owned(), "proxy".to_owned()),
(
"RADROOTS_RELAYS".to_owned(),
"wss://old-relay.example".to_owned(),
@@ -2219,10 +2471,7 @@ mod tests {
RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop())
);
assert_eq!(resolved.signer.backend, SignerBackend::Local);
- assert_eq!(
- resolved.publish.transport,
- PublishTransport::DirectNostrRelay
- );
+ assert_eq!(resolved.publish.transport, PublishTransport::Nostr);
assert_eq!(resolved.relay.urls, Vec::<String>::new());
assert_eq!(resolved.myc.executable, PathBuf::from("myc"));
assert_eq!(
@@ -2486,14 +2735,14 @@ path = "identity/from-toml.json"
);
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
"relay".to_owned(),
)]));
let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .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"));
+ .expect_err("invalid transport profile");
+ assert!(error.to_string().contains("RADROOTS_CLI_TRANSPORT_PROFILE"));
+ assert!(error.to_string().contains("local_only"));
+ assert!(error.to_string().contains("reticulum_preview"));
let args = RuntimeInvocationArgs {
myc_status_timeout_ms: Some(0),
@@ -2518,8 +2767,8 @@ 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_TRANSPORT=radrootsd_proxy
-RADROOTS_CLI_RELAYS_URLS=wss://relay.env-file
+RADROOTS_CLI_TRANSPORT_PROFILE=nostr
+RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS=wss://relay.env-file
RADROOTS_CLI_MYC_EXECUTABLE=bin/myc
RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS=4500
RADROOTS_CLI_HYF_ENABLED=true
@@ -2541,12 +2790,17 @@ RADROOTS_CLI_HYF_EXECUTABLE=bin/hyfd
assert_eq!(resolved.account.selector.as_deref(), Some("acct_env_file"));
assert_eq!(resolved.identity.path, PathBuf::from("state/identity.json"));
assert_eq!(resolved.signer.backend, SignerBackend::Myc);
+ assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
+ assert_eq!(
+ resolved.transport.source,
+ TransportConfigSource::Environment
+ );
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::RadrootsdProxy,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Environment,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
assert_eq!(resolved.relay.urls, vec!["wss://relay.env-file".to_owned()]);
@@ -2645,7 +2899,7 @@ RADROOTS_CLI_LOGGING_STDOUT=true
}
#[test]
- fn user_relay_config_overrides_workspace_relay_config() {
+ fn user_transport_config_overrides_workspace_transport_config() {
let temp = tempdir().expect("tempdir");
let workspace_root = temp.path().join("workspace");
let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
@@ -2655,12 +2909,12 @@ RADROOTS_CLI_LOGGING_STDOUT=true
fs::create_dir_all(&app_config_dir).expect("app config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[relays]\nurls = [\"wss://relay.workspace\"]\npublish_policy = \"any\"\n",
+ "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.workspace\"]\n",
)
.expect("write workspace config");
fs::write(
app_config_dir.join("config.toml"),
- "[relays]\nurls = [\"wss://relay.user\", \"wss://relay.workspace\"]\n",
+ "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.user\", \"wss://relay.workspace\"]\n",
)
.expect("write user config");
@@ -2690,6 +2944,8 @@ RADROOTS_CLI_LOGGING_STDOUT=true
let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect("resolve config");
+ assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
+ assert_eq!(resolved.transport.source, TransportConfigSource::UserConfig);
assert_eq!(
resolved.relay.urls,
vec![
@@ -2702,7 +2958,7 @@ RADROOTS_CLI_LOGGING_STDOUT=true
}
#[test]
- fn publish_transport_precedence_tracks_source() {
+ fn transport_profile_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");
@@ -2712,12 +2968,12 @@ RADROOTS_CLI_LOGGING_STDOUT=true
fs::create_dir_all(&app_config_dir).expect("app config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[publish]\ntransport = \"radrootsd_proxy\"\n",
+ "[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\nurl = \"http://127.0.0.1:7070\"\n",
)
.expect("write workspace config");
fs::write(
app_config_dir.join("config.toml"),
- "[publish]\ntransport = \"direct_nostr_relay\"\n",
+ "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.user\"]\n",
)
.expect("write user config");
@@ -2726,22 +2982,24 @@ RADROOTS_CLI_LOGGING_STDOUT=true
repo_local_root.clone(),
user_home.clone(),
BTreeMap::from([(
- "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
- "radrootsd_proxy".to_owned(),
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
+ "proxy".to_owned(),
)]),
);
- let args = RuntimeInvocationArgs {
- 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 transport");
+ let resolved =
+ RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
+ .expect("resolve environment transport profile");
+ assert_eq!(resolved.transport.profile, TransportProfileKind::Proxy);
+ assert_eq!(
+ resolved.transport.source,
+ TransportConfigSource::Environment
+ );
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
- source: PublishTransportSource::Flags,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ transport: PublishTransport::Proxy,
+ source: PublishTransportSource::Environment,
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
@@ -2750,8 +3008,8 @@ RADROOTS_CLI_LOGGING_STDOUT=true
repo_local_root.clone(),
user_home.clone(),
BTreeMap::from([(
- "RADROOTS_CLI_PUBLISH_TRANSPORT".to_owned(),
- "radrootsd_proxy".to_owned(),
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
+ "proxy".to_owned(),
)]),
);
let resolved =
@@ -2760,9 +3018,9 @@ RADROOTS_CLI_LOGGING_STDOUT=true
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::RadrootsdProxy,
+ transport: PublishTransport::Proxy,
source: PublishTransportSource::Environment,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
@@ -2774,13 +3032,15 @@ RADROOTS_CLI_LOGGING_STDOUT=true
);
let resolved =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve user publish transport");
+ .expect("resolve user transport profile");
+ assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
+ assert_eq!(resolved.transport.source, TransportConfigSource::UserConfig);
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::UserConfig,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
@@ -2793,13 +3053,18 @@ RADROOTS_CLI_LOGGING_STDOUT=true
);
let resolved =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve workspace publish transport");
+ .expect("resolve workspace transport profile");
+ assert_eq!(resolved.transport.profile, TransportProfileKind::Proxy);
+ assert_eq!(
+ resolved.transport.source,
+ TransportConfigSource::WorkspaceConfig
+ );
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::RadrootsdProxy,
+ transport: PublishTransport::Proxy,
source: PublishTransportSource::WorkspaceConfig,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
@@ -2807,13 +3072,15 @@ RADROOTS_CLI_LOGGING_STDOUT=true
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 transport");
+ .expect("resolve default transport profile");
+ assert_eq!(resolved.transport.profile, TransportProfileKind::LocalOnly);
+ assert_eq!(resolved.transport.source, TransportConfigSource::Defaults);
assert_eq!(
resolved.publish,
PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
}
);
}
@@ -3001,7 +3268,7 @@ RADROOTS_CLI_LOGGING_STDOUT=true
}
#[test]
- fn invalid_publish_config_reports_config_source() {
+ fn invalid_transport_config_reports_config_source() {
let temp = tempdir().expect("tempdir");
let workspace_root = temp.path().join("workspace");
let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
@@ -3009,18 +3276,18 @@ RADROOTS_CLI_LOGGING_STDOUT=true
fs::create_dir_all(&repo_local_root).expect("workspace config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[publish]\ntransport = \"nostr\"\n",
+ "[transport]\nprofile = \"relay\"\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 transport");
+ .expect_err("invalid transport profile");
let message = error.to_string();
- assert!(message.contains("workspace config [publish].transport"));
- assert!(message.contains("direct_nostr_relay"));
- assert!(message.contains("radrootsd_proxy"));
+ assert!(message.contains("workspace config [transport].profile"));
+ assert!(message.contains("local_only"));
+ assert!(message.contains("reticulum_preview"));
}
#[test]
@@ -3110,7 +3377,7 @@ target = "bin/user-hyfd"
r#"
[[capability_binding]]
capability = "write_plane.trade_jsonrpc"
-provider = "radrootsd"
+ provider = "legacy_proxy"
target_kind = "explicit_endpoint"
target = "https://rpc.workspace.test/jsonrpc"
"#,
@@ -3179,11 +3446,17 @@ target = "workflow-default"
"wss://relay.example:abc",
" ",
] {
- let args = RuntimeInvocationArgs {
- relay: vec![relay.to_owned()],
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::new());
+ let args = runtime_args();
+ let env = MapEnvironment::new(BTreeMap::from([
+ (
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
+ "nostr".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
+ relay.to_owned(),
+ ),
+ ]));
let error =
RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect_err("invalid relay url");
@@ -3197,10 +3470,16 @@ target = "workflow-default"
#[test]
fn relay_env_value_rejects_empty_entries() {
- let env = MapEnvironment::new(BTreeMap::from([(
- super::ENV_CLI_RELAYS_URLS.to_owned(),
- "wss://relay.example,,wss://relay-two.example".to_owned(),
- )]));
+ let env = MapEnvironment::new(BTreeMap::from([
+ (
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
+ "nostr".to_owned(),
+ ),
+ (
+ super::ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS.to_owned(),
+ "wss://relay.example,,wss://relay-two.example".to_owned(),
+ ),
+ ]));
let error =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
.expect_err("empty relay entry");
@@ -3210,15 +3489,21 @@ target = "workflow-default"
#[test]
fn valid_ipv6_relay_url_resolves() {
- let args = RuntimeInvocationArgs {
- relay: vec![" wss://[2001:db8::1]:443/relay ".to_owned()],
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::new());
+ let args = runtime_args();
+ let env = MapEnvironment::new(BTreeMap::from([
+ (
+ "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
+ "nostr".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
+ " ws://[::1]:443/relay ".to_owned(),
+ ),
+ ]));
let config = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect("valid relay url");
- assert_eq!(config.relay.urls, vec!["wss://[2001:db8::1]:443/relay"]);
+ assert_eq!(config.relay.urls, vec!["ws://[::1]:443/relay"]);
}
#[test]
@@ -3479,7 +3764,7 @@ RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT=.local/radroots/dev
("[signer]\nmode = \"local\"\n", "unknown field `mode`"),
(
"[relays]\nurls = [\"wss://relay.example\"]\nextra = true\n",
- "unknown field `extra`",
+ "unknown field `relays`",
),
] {
fs::write(repo_local_root.join("config.toml"), raw).expect("write config");
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -15,7 +15,7 @@ use radroots_sdk::{
FarmPrivateLocationClearRequest, FarmPrivateLocationInput, FarmPrivateLocationLookupCandidate,
FarmPrivateLocationReceipt, FarmPrivateLocationSetRequest, FarmPrivateLocationSetResult,
FarmPublishPlan, GeocoderLocalityQuery, PushOutboxEventReceipt, PushOutboxEventState,
- PushOutboxReceipt, PushOutboxRelayOutcomeKind, PushOutboxRequest, SdkExactLocation,
+ PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind, SdkExactLocation,
SdkMutationState,
};
use serde_json::json;
@@ -34,7 +34,7 @@ use crate::runtime::farm_config::{
};
use crate::runtime::local_events::append_local_work;
use crate::runtime::sdk::{
- CliSdkAdapterError, CliSdkSession, sdk_relay_target_policy, sdk_relay_url_policy,
+ CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_policy,
validate_configured_signer_for_actor,
};
use crate::runtime::signer::ActorWriteBindingError;
@@ -621,17 +621,16 @@ fn relay_farm_publish_readiness(
config: &RuntimeConfig,
account: &AccountRecordView,
) -> FarmPublishReadiness {
- if matches!(config.publish.transport, PublishTransport::DirectNostrRelay)
- && config.relay.urls.is_empty()
- {
+ if matches!(config.publish.transport, PublishTransport::Nostr) && config.relay.urls.is_empty() {
return FarmPublishReadiness {
state: "unconfigured",
executable: false,
- reason: Some(
- "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()],
+ reason: Some("farm publish requires a configured Nostr transport profile".to_owned()),
+ missing: vec!["Configured Nostr transport profile".to_owned()],
+ actions: vec![
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ .to_owned(),
+ ],
};
}
@@ -816,7 +815,7 @@ fn publish_via_sdk(
"farm seller",
)?;
let mut request =
- FarmEnqueuePublishRequest::new(input.actor, input.farm, sdk_relay_target_policy(config));
+ FarmEnqueuePublishRequest::new(input.actor, input.farm, sdk_target_policy(config));
if let Some(idempotency_key) = farm_idempotency_key.as_deref() {
request = request.try_with_idempotency_key(idempotency_key)?;
}
@@ -825,7 +824,7 @@ fn publish_via_sdk(
session.sdk().sync().push_outbox(
PushOutboxRequest::new()
.with_limit(1)
- .with_relay_url_policy(sdk_relay_url_policy(config)),
+ .with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)),
),
)?;
let view = sdk_enqueued_publish_view(
@@ -1233,71 +1232,72 @@ fn sdk_publish_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<Strin
fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .map(|relay| relay.relay_url.clone())
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| relay.attempted)
- .map(|relay| relay.relay_url.clone())
+ .filter(|target| target.attempted)
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| {
+ .filter(|target| {
matches!(
- relay.outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ target.outcome_kind,
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)
})
- .map(|relay| relay.relay_url.clone())
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| {
+ .filter(|target| {
!matches!(
- relay.outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ target.outcome_kind,
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)
})
- .map(|relay| RelayFailureView {
- relay: relay.relay_url.clone(),
- reason: relay
+ .map(|target| RelayFailureView {
+ relay: target.endpoint_uri.clone(),
+ reason: target
.message
.clone()
- .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()),
+ .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()),
})
.collect()
}
-fn sdk_relay_outcome_kind(kind: PushOutboxRelayOutcomeKind) -> &'static str {
+fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str {
match kind {
- PushOutboxRelayOutcomeKind::Accepted => "accepted",
- PushOutboxRelayOutcomeKind::DuplicateAccepted => "duplicate_accepted",
- PushOutboxRelayOutcomeKind::Blocked => "blocked",
- PushOutboxRelayOutcomeKind::RateLimited => "rate_limited",
- PushOutboxRelayOutcomeKind::Invalid => "invalid",
- PushOutboxRelayOutcomeKind::PowRequired => "pow_required",
- PushOutboxRelayOutcomeKind::Restricted => "restricted",
- PushOutboxRelayOutcomeKind::AuthRequired => "auth_required",
- PushOutboxRelayOutcomeKind::Error => "error",
- PushOutboxRelayOutcomeKind::Timeout => "timeout",
- PushOutboxRelayOutcomeKind::ConnectionFailed => "connection_failed",
- PushOutboxRelayOutcomeKind::Unknown => "unknown",
+ PushOutboxTargetOutcomeKind::Accepted => "accepted",
+ PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted",
+ PushOutboxTargetOutcomeKind::Blocked => "blocked",
+ PushOutboxTargetOutcomeKind::RateLimited => "rate_limited",
+ PushOutboxTargetOutcomeKind::Invalid => "invalid",
+ PushOutboxTargetOutcomeKind::PowRequired => "pow_required",
+ PushOutboxTargetOutcomeKind::Restricted => "restricted",
+ PushOutboxTargetOutcomeKind::AuthRequired => "auth_required",
+ PushOutboxTargetOutcomeKind::Error => "error",
+ PushOutboxTargetOutcomeKind::Timeout => "timeout",
+ PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed",
+ PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected",
+ PushOutboxTargetOutcomeKind::Unknown => "unknown",
_ => "unknown",
}
}
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -28,7 +28,7 @@ use radroots_replica_db::ReplicaSql;
use radroots_sdk::{
ListingEnqueuePublishRequest, ListingEnqueueReceipt, ListingPreparePublishRequest,
ListingPublishPlan, PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt,
- PushOutboxRelayOutcomeKind, PushOutboxRequest, SdkMutationState,
+ PushOutboxRequest, PushOutboxTargetOutcomeKind, SdkMutationState,
};
use radroots_sql_core::SqliteExecutor;
use radroots_trade::listing::{RadrootsListingDraftDocumentV1, validation::validate_listing_event};
@@ -48,7 +48,7 @@ use crate::runtime::local_events::{
shared_local_events_db_path,
};
use crate::runtime::sdk::{
- CliSdkAdapterError, CliSdkSession, sdk_relay_target_policy, sdk_relay_url_policy,
+ CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_policy,
validate_configured_signer_for_actor,
};
use crate::runtime::sync::{
@@ -1769,7 +1769,7 @@ pub fn publish_via_sdk(
let mut request = ListingEnqueuePublishRequest::from_document(
input.actor,
input.document,
- sdk_relay_target_policy(config),
+ sdk_target_policy(config),
);
if let Some(idempotency_key) = args.idempotency_key.as_deref() {
request = request.try_with_idempotency_key(idempotency_key)?;
@@ -1783,7 +1783,7 @@ pub fn publish_via_sdk(
session.sdk().sync().push_outbox(
PushOutboxRequest::new()
.with_limit(1)
- .with_relay_url_policy(sdk_relay_url_policy(config)),
+ .with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)),
),
)?,
)
@@ -2020,71 +2020,72 @@ fn sdk_publish_actions(
fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .map(|relay| relay.relay_url.clone())
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| relay.attempted)
- .map(|relay| relay.relay_url.clone())
+ .filter(|target| target.attempted)
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| {
+ .filter(|target| {
matches!(
- relay.outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ target.outcome_kind,
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)
})
- .map(|relay| relay.relay_url.clone())
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| {
+ .filter(|target| {
!matches!(
- relay.outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ target.outcome_kind,
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)
})
- .map(|relay| RelayFailureView {
- relay: relay.relay_url.clone(),
- reason: relay
+ .map(|target| RelayFailureView {
+ relay: target.endpoint_uri.clone(),
+ reason: target
.message
.clone()
- .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()),
+ .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()),
})
.collect()
}
-fn sdk_relay_outcome_kind(kind: PushOutboxRelayOutcomeKind) -> &'static str {
+fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str {
match kind {
- PushOutboxRelayOutcomeKind::Accepted => "accepted",
- PushOutboxRelayOutcomeKind::DuplicateAccepted => "duplicate_accepted",
- PushOutboxRelayOutcomeKind::Blocked => "blocked",
- PushOutboxRelayOutcomeKind::RateLimited => "rate_limited",
- PushOutboxRelayOutcomeKind::Invalid => "invalid",
- PushOutboxRelayOutcomeKind::PowRequired => "pow_required",
- PushOutboxRelayOutcomeKind::Restricted => "restricted",
- PushOutboxRelayOutcomeKind::AuthRequired => "auth_required",
- PushOutboxRelayOutcomeKind::Error => "error",
- PushOutboxRelayOutcomeKind::Timeout => "timeout",
- PushOutboxRelayOutcomeKind::ConnectionFailed => "connection_failed",
- PushOutboxRelayOutcomeKind::Unknown => "unknown",
+ PushOutboxTargetOutcomeKind::Accepted => "accepted",
+ PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted",
+ PushOutboxTargetOutcomeKind::Blocked => "blocked",
+ PushOutboxTargetOutcomeKind::RateLimited => "rate_limited",
+ PushOutboxTargetOutcomeKind::Invalid => "invalid",
+ PushOutboxTargetOutcomeKind::PowRequired => "pow_required",
+ PushOutboxTargetOutcomeKind::Restricted => "restricted",
+ PushOutboxTargetOutcomeKind::AuthRequired => "auth_required",
+ PushOutboxTargetOutcomeKind::Error => "error",
+ PushOutboxTargetOutcomeKind::Timeout => "timeout",
+ PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed",
+ PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected",
+ PushOutboxTargetOutcomeKind::Unknown => "unknown",
_ => "unknown",
}
}
@@ -2185,11 +2186,8 @@ fn mutate_via_sdk_from_canonical(
canonical.seller_pubkey.as_str(),
"listing seller",
)?;
- let mut request = ListingEnqueuePublishRequest::from_document(
- actor,
- document,
- sdk_relay_target_policy(config),
- );
+ let mut request =
+ ListingEnqueuePublishRequest::from_document(actor, document, sdk_target_policy(config));
if let Some(idempotency_key) = args.idempotency_key.as_deref() {
request = request.try_with_idempotency_key(idempotency_key)?;
}
@@ -2202,7 +2200,7 @@ fn mutate_via_sdk_from_canonical(
session.sdk().sync().push_outbox(
PushOutboxRequest::new()
.with_limit(1)
- .with_relay_url_policy(sdk_relay_url_policy(config)),
+ .with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)),
),
)?,
)
@@ -3151,8 +3149,8 @@ mod tests {
use radroots_events::ids::RadrootsEventId;
use radroots_events_codec::d_tag::is_d_tag_base64url;
use radroots_sdk::{
- PushOutboxEventReceipt, PushOutboxEventState, PushOutboxRelayOutcomeKind,
- PushOutboxRelayReceipt,
+ PushOutboxEventReceipt, PushOutboxEventState, PushOutboxTargetOutcomeKind,
+ PushOutboxTargetReceipt,
};
#[test]
@@ -3172,7 +3170,7 @@ mod tests {
fn sdk_push_receipt_helpers_map_published_and_auth_required_states() {
let accepted = sdk_push_event(
PushOutboxEventState::Published,
- PushOutboxRelayOutcomeKind::Accepted,
+ PushOutboxTargetOutcomeKind::Accepted,
Some("accepted".to_owned()),
);
let args = listing_mutation_args(false);
@@ -3188,7 +3186,7 @@ mod tests {
let auth_required = sdk_push_event(
PushOutboxEventState::PublishRetryable,
- PushOutboxRelayOutcomeKind::AuthRequired,
+ PushOutboxTargetOutcomeKind::AuthRequired,
Some("auth required".to_owned()),
);
let failed = sdk_push_failed_relays(&auth_required);
@@ -3349,7 +3347,7 @@ mod tests {
fn sdk_push_event(
final_state: PushOutboxEventState,
- outcome_kind: PushOutboxRelayOutcomeKind,
+ outcome_kind: PushOutboxTargetOutcomeKind,
message: Option<String>,
) -> PushOutboxEventReceipt {
PushOutboxEventReceipt {
@@ -3359,24 +3357,25 @@ mod tests {
attempted_count: 1,
accepted_count: usize::from(matches!(
outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)),
retryable_count: usize::from(matches!(
outcome_kind,
- PushOutboxRelayOutcomeKind::AuthRequired
- | PushOutboxRelayOutcomeKind::Timeout
- | PushOutboxRelayOutcomeKind::ConnectionFailed
+ PushOutboxTargetOutcomeKind::AuthRequired
+ | PushOutboxTargetOutcomeKind::Timeout
+ | PushOutboxTargetOutcomeKind::ConnectionFailed
)),
terminal_count: 0,
quorum: 1,
quorum_met: matches!(
outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
),
- relays: vec![PushOutboxRelayReceipt {
- relay_url: "ws://127.0.0.1:19000".to_owned(),
+ targets: vec![PushOutboxTargetReceipt {
+ transport_kind: "nostr".to_owned(),
+ endpoint_uri: "ws://127.0.0.1:19000".to_owned(),
outcome_kind,
attempted: true,
message,
diff --git a/src/runtime/local_events.rs b/src/runtime/local_events.rs
@@ -51,8 +51,6 @@ pub fn append_local_work(
event_sig: None,
raw_event_json: None,
outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
};
let store = open_store(config)?;
Ok(store.append_record(&input)?)
diff --git a/src/runtime/mesh.rs b/src/runtime/mesh.rs
@@ -0,0 +1,76 @@
+use serde_json::Value as JsonValue;
+use toml::{Value, map::Map};
+
+use crate::ops::OperationData;
+use crate::runtime::RuntimeError;
+use crate::runtime::config::RuntimeConfig;
+use crate::runtime::transport::update_app_config_table;
+use crate::view::runtime::{MeshPolicyCheckView, MeshScopeView, MeshStatusView};
+
+const MESH_SOURCE: &str = "mesh config";
+
+pub fn scope(config: &RuntimeConfig) -> MeshScopeView {
+ scope_view(config.mesh.scope.as_str(), "ready")
+}
+
+pub fn set_scope(
+ config: &RuntimeConfig,
+ input: &OperationData,
+) -> Result<MeshScopeView, RuntimeError> {
+ let scope = input
+ .get("scope")
+ .and_then(JsonValue::as_str)
+ .unwrap_or("disabled");
+ match scope {
+ "disabled" | "local_preview" => {}
+ other => {
+ return Err(RuntimeError::Config(format!(
+ "mesh scope `{other}` is not supported"
+ )));
+ }
+ }
+ let mut mesh = Map::new();
+ mesh.insert("scope".to_owned(), Value::String(scope.to_owned()));
+ update_app_config_table(config, "mesh", Value::Table(mesh))?;
+ Ok(scope_view(scope, "configured"))
+}
+
+pub fn status(config: &RuntimeConfig) -> MeshStatusView {
+ let scope = config.mesh.scope.as_str();
+ MeshStatusView {
+ state: "ready".to_owned(),
+ source: MESH_SOURCE.to_owned(),
+ scope: scope.to_owned(),
+ transport_kind: "reticulum".to_owned(),
+ configured_state: scope.to_owned(),
+ implementation_state: "preview_unavailable".to_owned(),
+ usable_for_delivery: false,
+ message: "Reticulum mesh preview is explicit and unavailable for real delivery".to_owned(),
+ }
+}
+
+pub fn policy_check(config: &RuntimeConfig) -> MeshPolicyCheckView {
+ MeshPolicyCheckView {
+ state: "ready".to_owned(),
+ source: MESH_SOURCE.to_owned(),
+ scope: config.mesh.scope.as_str().to_owned(),
+ policy: "reticulum_preview_delivery".to_owned(),
+ transport_kind: "reticulum".to_owned(),
+ usable_for_delivery: false,
+ decision: "reject_delivery_attempt".to_owned(),
+ message: "Reticulum preview never falls back to Nostr and cannot deliver real events"
+ .to_owned(),
+ }
+}
+
+fn scope_view(scope: &str, state: &str) -> MeshScopeView {
+ MeshScopeView {
+ state: state.to_owned(),
+ source: MESH_SOURCE.to_owned(),
+ scope: scope.to_owned(),
+ implementation_state: "preview_unavailable".to_owned(),
+ message: "Mesh delivery is disabled unless a preview scope is explicitly configured"
+ .to_owned(),
+ actions: vec!["radroots mesh policy check".to_owned()],
+ }
+}
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
@@ -7,7 +7,7 @@ pub mod hyf;
pub mod listing;
pub mod local_events;
pub mod logging;
-pub mod network;
+pub mod mesh;
pub mod order;
pub mod paths;
pub mod provider;
@@ -15,6 +15,7 @@ pub mod sdk;
pub mod signer;
pub mod store;
pub mod sync;
+pub mod transport;
pub mod validation_receipt;
use std::process::ExitCode;
diff --git a/src/runtime/network.rs b/src/runtime/network.rs
@@ -1,42 +0,0 @@
-use crate::runtime::config::RuntimeConfig;
-use crate::view::runtime::{RelayEntryView, RelayListView};
-
-pub fn relay_list(config: &RuntimeConfig) -> RelayListView {
- let relays = config
- .relay
- .urls
- .iter()
- .cloned()
- .map(|url| RelayEntryView {
- url,
- read: true,
- write: true,
- })
- .collect::<Vec<_>>();
-
- let state = if relays.is_empty() {
- "unconfigured"
- } else {
- "configured"
- };
-
- RelayListView {
- state: state.to_owned(),
- source: config.relay.source.as_str().to_owned(),
- publish_policy: config.relay.publish_policy.as_str().to_owned(),
- count: relays.len(),
- reason: relays
- .is_empty()
- .then_some("no relays are configured for this operator session".to_owned()),
- relays,
- actions: relay_actions(config),
- }
-}
-
-fn relay_actions(config: &RuntimeConfig) -> Vec<String> {
- if config.relay.urls.is_empty() {
- vec!["radroots --relay wss://relay.example.com relay list".to_owned()]
- } else {
- Vec::new()
- }
-}
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -32,16 +32,13 @@ use radroots_events_codec::d_tag::is_d_tag_base64url;
use radroots_events_codec::order::{order_event_context_from_tags, order_request_from_event};
use radroots_local_events::{
BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalRecordFamily,
- LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryState,
- SourceRuntime, normalize_relay_urls, validate_supported_buyer_order_request_local_work_payload,
+ LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
+ validate_supported_buyer_order_request_local_work_payload,
};
use radroots_nostr::prelude::{
RadrootsNostrEvent, RadrootsNostrFilter, radroots_event_from_nostr, radroots_nostr_filter_tag,
radroots_nostr_kind,
};
-use radroots_relay_transport::{
- RadrootsRelayFetchFailure, RadrootsRelayFetchedEventsReceipt, RadrootsRelayTransportError,
-};
use radroots_replica_db::{
ReplicaSql, ReplicaTradeProductSummaryRow, nostr_event_head, trade_product,
};
@@ -52,19 +49,23 @@ use radroots_replica_db_schema::trade_product::{
ITradeProductFieldsFilter, ITradeProductFindMany, TradeProduct,
};
use radroots_sdk::{
- AckPolicy, PrivacyPreflightConfirmation, ProductSensitivityField, PublishMode,
- PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRelayOutcomeKind,
- RelayResolutionPolicy, SdkMutationState, SdkTradeStatusSource, TradeAcceptRequest,
- TradeCancelRequest, TradeCancellationPlan, TradeCancellationReceipt, TradeDecisionPlan,
- TradeDecisionReceipt, TradeDeclineRequest, TradeEvidenceMode, TradeMutationOutcome,
- TradeProposeRequest, TradeRevisionDecisionPlan, TradeRevisionDecisionReceipt,
- TradeRevisionDecisionRequest, TradeRevisionProposalPlan, TradeRevisionProposalReceipt,
- TradeRevisionProposalRequest, TradeStatusReceipt, TradeStatusRequest, TradeSubmitPlan,
- TradeSubmitReceipt, TradeWorkflowEnqueueReceipt,
+ PrivacyPreflightConfirmation, ProductSensitivityField, PublishMode, PushOutboxEventReceipt,
+ PushOutboxEventState, PushOutboxReceipt, PushOutboxTargetOutcomeKind, SatisfactionPolicy,
+ SdkMutationState, SdkTradeStatusSource, TargetPolicy, TradeAcceptRequest, TradeCancelRequest,
+ TradeCancellationPlan, TradeCancellationReceipt, TradeDecisionPlan, TradeDecisionReceipt,
+ TradeDeclineRequest, TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest,
+ TradeRevisionDecisionPlan, TradeRevisionDecisionReceipt, TradeRevisionDecisionRequest,
+ TradeRevisionProposalPlan, TradeRevisionProposalReceipt, TradeRevisionProposalRequest,
+ TradeStatusReceipt, TradeStatusRequest, TradeSubmitPlan, TradeSubmitReceipt,
+ TradeWorkflowEnqueueReceipt,
};
use radroots_sql_core::SqliteExecutor;
use radroots_trade::identity::RadrootsTradeLocator;
use radroots_trade::order::canonicalize_order_request_for_signer;
+use radroots_transport_nostr::{
+ RadrootsRelayFetchFailure, RadrootsRelayFetchedEventsReceipt, RadrootsRelayTransportError,
+ RadrootsRelayUrl, RadrootsRelayUrlPolicy,
+};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -105,7 +106,7 @@ const ORDER_CANCELLATION_SOURCE: &str = "SDK trade cancellation · local key";
const ORDER_EVENT_LIST_SOURCE: &str = "shared relay transport fetch · selected seller identity";
const ORDER_STATUS_SDK_SOURCE: &str = "SDK local trade projection";
const ORDER_EVENT_LIST_RELAY_ACTION: &str =
- "radroots --relay wss://relay.example.com trade event list";
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com";
const ORDER_BUYER_ACTOR_SOURCE_RESOLVED_ACCOUNT: &str = "resolved_account";
const ORDER_BUYER_ACTOR_SOURCE_REBIND: &str = "order_rebind";
const ORDER_APP_RECORD_LIST_LIMIT: u32 = 500;
@@ -161,10 +162,10 @@ fn trade_publish_mode(config: &RuntimeConfig) -> PublishMode {
}
}
-fn trade_ack_policy(mode: PublishMode) -> Result<AckPolicy, RuntimeError> {
+fn trade_satisfaction_policy(mode: PublishMode) -> Result<SatisfactionPolicy, RuntimeError> {
Ok(match mode {
- PublishMode::DryRun | PublishMode::EnqueueOnly => AckPolicy::NoWait,
- PublishMode::EnqueueAndPublish => AckPolicy::AtLeastOneRelay,
+ PublishMode::DryRun | PublishMode::EnqueueOnly => SatisfactionPolicy::NoWait,
+ PublishMode::EnqueueAndPublish => SatisfactionPolicy::AtLeastOneTarget,
_ => {
return Err(RuntimeError::Config(
"unsupported SDK publish mode for CLI trade workflow".to_owned(),
@@ -173,8 +174,8 @@ fn trade_ack_policy(mode: PublishMode) -> Result<AckPolicy, RuntimeError> {
})
}
-fn trade_relay_resolution_policy() -> RelayResolutionPolicy {
- RelayResolutionPolicy::configured_relays()
+fn trade_target_policy() -> TargetPolicy {
+ TargetPolicy::use_transport_profile()
}
fn trade_privacy_confirmation(confirm_public_note: bool) -> PrivacyPreflightConfirmation {
@@ -860,7 +861,7 @@ pub fn app_record_export(
app_order.loaded.document.order.order_id
),
format!(
- "radroots --relay wss://relay.example.com trade submit {}",
+ "radroots trade submit {}",
app_order.loaded.document.order.order_id
),
],
@@ -1167,7 +1168,7 @@ pub fn event_list(
return Ok(order_event_list_unconfigured(
None,
ORDER_ACTOR_CONTEXT_NETWORK_ONLY,
- "trade event list requires at least one configured relay".to_owned(),
+ "trade event list requires a configured Nostr transport profile".to_owned(),
Vec::new(),
vec![ORDER_EVENT_LIST_RELAY_ACTION.to_owned()],
));
@@ -1314,7 +1315,7 @@ fn decide_trade_via_sdk(
let status = trade_status_for_locator(config, &session, locator.clone())?;
let status_view = sdk_order_status_view(status.clone());
let publish_mode = trade_publish_mode(config);
- let ack_policy = trade_ack_policy(publish_mode)?;
+ let ack_policy = trade_satisfaction_policy(publish_mode)?;
let outcome = match args.decision {
TradeDecisionArg::Accept => {
let commitments = inventory_commitments_from_status(&status)?;
@@ -1322,7 +1323,7 @@ fn decide_trade_via_sdk(
actor,
locator,
commitments,
- trade_relay_resolution_policy(),
+ trade_target_policy(),
publish_mode,
ack_policy,
TradeEvidenceMode::ResyncBeforeMutation,
@@ -1346,7 +1347,7 @@ fn decide_trade_via_sdk(
actor,
locator,
reason,
- trade_relay_resolution_policy(),
+ trade_target_policy(),
publish_mode,
ack_policy,
TradeEvidenceMode::ResyncBeforeMutation,
@@ -1378,7 +1379,7 @@ fn propose_revision_via_sdk(
let status_view = sdk_order_status_view(status);
let revision = revision_request_parts_from_status(args, &status_view)?;
let publish_mode = trade_publish_mode(config);
- let ack_policy = trade_ack_policy(publish_mode)?;
+ let ack_policy = trade_satisfaction_policy(publish_mode)?;
let mut request = TradeRevisionProposalRequest::new(
actor,
locator,
@@ -1386,7 +1387,7 @@ fn propose_revision_via_sdk(
revision.items.clone(),
revision.economics.clone(),
args.reason.trim(),
- trade_relay_resolution_policy(),
+ trade_target_policy(),
publish_mode,
ack_policy,
TradeEvidenceMode::ResyncBeforeMutation,
@@ -1441,13 +1442,13 @@ fn decide_revision_via_sdk(
},
};
let publish_mode = trade_publish_mode(config);
- let ack_policy = trade_ack_policy(publish_mode)?;
+ let ack_policy = trade_satisfaction_policy(publish_mode)?;
let mut request = TradeRevisionDecisionRequest::new(
actor,
locator,
revision_id,
decision,
- trade_relay_resolution_policy(),
+ trade_target_policy(),
publish_mode,
ack_policy,
TradeEvidenceMode::ResyncBeforeMutation,
@@ -1489,12 +1490,12 @@ fn cancel_trade_via_sdk(
let status = trade_status_for_locator(config, &session, locator.clone())?;
let status_view = sdk_order_status_view(status);
let publish_mode = trade_publish_mode(config);
- let ack_policy = trade_ack_policy(publish_mode)?;
+ let ack_policy = trade_satisfaction_policy(publish_mode)?;
let mut request = TradeCancelRequest::new(
actor,
locator,
args.reason.trim(),
- trade_relay_resolution_policy(),
+ trade_target_policy(),
publish_mode,
ack_policy,
TradeEvidenceMode::ResyncBeforeMutation,
@@ -2955,48 +2956,8 @@ fn resolve_shared_signed_listing_provenance(
listing_addr: &str,
listing_event_id: Option<&str>,
) -> Result<Option<SharedListingProvenance>, RuntimeError> {
- let mut candidates = list_shared_records_latest(config, ORDER_APP_RECORD_LIST_LIMIT)?
- .into_iter()
- .filter(|record| record.family == LocalRecordFamily::SignedEvent)
- .filter(|record| record.status == LocalRecordStatus::Published)
- .filter(|record| record.event_kind == Some(i64::from(KIND_LISTING)))
- .filter(|record| record.listing_addr.as_deref() == Some(listing_addr))
- .filter(|record| {
- listing_event_id.is_none() || record.event_id.as_deref() == listing_event_id
- })
- .filter_map(|record| {
- let event_id = record.event_id?;
- if !is_valid_event_id(event_id.as_str()) {
- return None;
- }
- let delivery = record.relay_delivery_json.as_ref()?;
- let evidence = RelayDeliveryEvidence::from_json_value(delivery).ok()?;
- let relays = listing_provenance_relays_from_delivery_evidence(evidence).ok()?;
- if relays.is_empty() {
- return None;
- }
- Some(SharedListingProvenance { event_id, relays })
- })
- .collect::<Vec<_>>();
- candidates.sort_by(|left, right| left.event_id.cmp(&right.event_id));
- candidates.dedup_by(|left, right| left.event_id == right.event_id);
- if candidates.len() > 1 && listing_event_id.is_none() {
- return Err(RuntimeError::Config(format!(
- "listing address `{listing_addr}` has multiple published shared local listing events; run `radroots market refresh` or pass a current listing event id source"
- )));
- }
- Ok(candidates.pop())
-}
-
-fn listing_provenance_relays_from_delivery_evidence(
- evidence: RelayDeliveryEvidence,
-) -> Result<Vec<String>, String> {
- let relays = match evidence.state {
- RelayDeliveryState::Acknowledged => evidence.acknowledged_relays,
- RelayDeliveryState::Observed => evidence.observed_relays,
- RelayDeliveryState::Pending | RelayDeliveryState::Failed => Vec::new(),
- };
- normalize_listing_relay_set(relays)
+ let _ = (config, listing_addr, listing_event_id);
+ Ok(None)
}
fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsFilter {
@@ -3948,10 +3909,7 @@ fn app_order_record_summary(
vec![
format!("radroots trade get {}", document.order.order_id),
format!("radroots trade app export {}", record.record_id),
- format!(
- "radroots --relay wss://relay.example.com trade submit {}",
- document.order.order_id
- ),
+ format!("radroots trade submit {}", document.order.order_id),
]
} else if app_order_issue_present(&issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
vec![format!(
@@ -4894,7 +4852,7 @@ fn propose_trade_via_sdk(
) -> Result<OrderSubmitView, CliSdkAdapterError> {
let actor = sdk_trade_actor(account, RadrootsActorRole::Buyer, "propose")?;
let publish_mode = trade_publish_mode(config);
- let ack_policy = trade_ack_policy(publish_mode)?;
+ let ack_policy = trade_satisfaction_policy(publish_mode)?;
let economics =
loaded.document.order.economics.clone().ok_or_else(|| {
RuntimeError::Config("trade draft is missing quote economics".to_owned())
@@ -4922,7 +4880,7 @@ fn propose_trade_via_sdk(
)?,
items,
economics,
- trade_relay_resolution_policy(),
+ trade_target_policy(),
publish_mode,
ack_policy,
)
@@ -5063,71 +5021,72 @@ fn sdk_order_submit_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<
fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .map(|relay| relay.relay_url.clone())
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| relay.attempted)
- .map(|relay| relay.relay_url.clone())
+ .filter(|target| target.attempted)
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| {
+ .filter(|target| {
matches!(
- relay.outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ target.outcome_kind,
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)
})
- .map(|relay| relay.relay_url.clone())
+ .map(|target| target.endpoint_uri.clone())
.collect()
}
fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> {
event
- .relays
+ .targets
.iter()
- .filter(|relay| {
+ .filter(|target| {
!matches!(
- relay.outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ target.outcome_kind,
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)
})
- .map(|relay| RelayFailureView {
- relay: relay.relay_url.clone(),
- reason: relay
+ .map(|target| RelayFailureView {
+ relay: target.endpoint_uri.clone(),
+ reason: target
.message
.clone()
- .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()),
+ .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()),
})
.collect()
}
-fn sdk_relay_outcome_kind(kind: PushOutboxRelayOutcomeKind) -> &'static str {
+fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str {
match kind {
- PushOutboxRelayOutcomeKind::Accepted => "accepted",
- PushOutboxRelayOutcomeKind::DuplicateAccepted => "duplicate_accepted",
- PushOutboxRelayOutcomeKind::Blocked => "blocked",
- PushOutboxRelayOutcomeKind::RateLimited => "rate_limited",
- PushOutboxRelayOutcomeKind::Invalid => "invalid",
- PushOutboxRelayOutcomeKind::PowRequired => "pow_required",
- PushOutboxRelayOutcomeKind::Restricted => "restricted",
- PushOutboxRelayOutcomeKind::AuthRequired => "auth_required",
- PushOutboxRelayOutcomeKind::Error => "error",
- PushOutboxRelayOutcomeKind::Timeout => "timeout",
- PushOutboxRelayOutcomeKind::ConnectionFailed => "connection_failed",
- PushOutboxRelayOutcomeKind::Unknown => "unknown",
+ PushOutboxTargetOutcomeKind::Accepted => "accepted",
+ PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted",
+ PushOutboxTargetOutcomeKind::Blocked => "blocked",
+ PushOutboxTargetOutcomeKind::RateLimited => "rate_limited",
+ PushOutboxTargetOutcomeKind::Invalid => "invalid",
+ PushOutboxTargetOutcomeKind::PowRequired => "pow_required",
+ PushOutboxTargetOutcomeKind::Restricted => "restricted",
+ PushOutboxTargetOutcomeKind::AuthRequired => "auth_required",
+ PushOutboxTargetOutcomeKind::Error => "error",
+ PushOutboxTargetOutcomeKind::Timeout => "timeout",
+ PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed",
+ PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected",
+ PushOutboxTargetOutcomeKind::Unknown => "unknown",
_ => "unknown",
}
}
@@ -5437,7 +5396,16 @@ where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
- normalize_relay_urls(values).map_err(|error| error.to_string())
+ let mut normalized = Vec::new();
+ for value in values {
+ let relay = RadrootsRelayUrl::parse(value.as_ref(), RadrootsRelayUrlPolicy::Public)
+ .map_err(|error| error.to_string())?
+ .into_string();
+ if !normalized.contains(&relay) {
+ normalized.push(relay);
+ }
+ }
+ Ok(normalized)
}
fn order_listing_relays(document: &OrderDraftDocument) -> Vec<String> {
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -96,15 +96,15 @@ pub fn resolve_write_plane_provider(
publish: &PublishRuntimeView,
) -> WritePlaneProviderView {
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::Nostr => (
+ "nostr",
+ "nostr_transport",
+ "Nostr transport profile is selected; readiness is reported under publish",
),
- PublishTransport::RadrootsdProxy => (
- "radrootsd_proxy",
- "daemon_proxy_publish",
- "radrootsd_proxy publish is selected; readiness is reported under publish",
+ PublishTransport::Proxy => (
+ "proxy",
+ "proxy_transport",
+ "proxy transport profile is selected; readiness is reported under publish",
),
};
WritePlaneProviderView {
@@ -326,10 +326,11 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::local_only(),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: Vec::new(),
@@ -350,6 +351,7 @@ mod tests {
enabled: hyf_enabled,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
@@ -388,23 +390,23 @@ mod tests {
}
#[test]
- fn write_plane_provider_tracks_direct_relay_publish() {
+ fn write_plane_provider_tracks_nostr_transport_publish() {
let config = sample_config(Vec::new(), false);
let publish = publish_view(
&config,
"unconfigured",
- Some("direct_nostr_relay publish transport requires a configured relay"),
+ Some("Nostr transport profile requires a configured Nostr relay"),
);
let view = resolve_write_plane_provider(&config, &publish);
- assert_eq!(view.provider_runtime_id, "direct_nostr_relay");
- assert_eq!(view.binding_model, "direct_relay_publish");
+ assert_eq!(view.provider_runtime_id, "nostr");
+ assert_eq!(view.binding_model, "nostr_transport");
assert_eq!(view.state, "unconfigured");
assert_eq!(
view.provenance,
ProviderProvenance::PublishTransport.as_str()
);
assert!(view.target.is_none());
- assert!(view.detail.contains("configured relay"));
+ assert!(view.detail.contains("configured Nostr relay"));
}
#[test]
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -1,4 +1,3 @@
-use std::fs;
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
@@ -15,18 +14,18 @@ use radroots_nostr_connect::prelude::{
RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectBunkerUri,
RadrootsNostrConnectClientTarget, RadrootsNostrConnectError, RadrootsNostrConnectUri,
};
-use radroots_relay_transport::{
+use radroots_sdk::{
+ NostrProfile, NostrRelayUrlPolicy, ProxyProfile, RadrootsClient, RadrootsClientBuilder,
+ RadrootsSdkError, RadrootsSdkLocalKeySigner, RadrootsSdkMycNip46RequestPolicy,
+ RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture,
+ RadrootsSdkSignerProvider, RadrootsSdkStorageConfig,
+ ReticulumPreviewBehavior as SdkReticulumPreviewBehavior, ReticulumPreviewProfile, TargetPolicy,
+ TransportProfile,
+};
+use radroots_transport_nostr::{
RadrootsNostrClientFetchAdapter, RadrootsRelayFetchRequest, RadrootsRelayFetchedEventsReceipt,
RadrootsRelayTransportError, fetch_relay_events_blocking,
};
-use radroots_sdk::{
- RadrootsClient, RadrootsClientBuilder, RadrootsSdkError, RadrootsSdkLocalKeySigner,
- RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport,
- RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, RadrootsSdkStorageConfig,
- SdkPublishTransport, SdkRelayUrlPolicy,
- adapters::radrootsd::{RadrootsdAuth, RadrootsdProxyConfig as SdkRadrootsdProxyConfig},
-};
-use radroots_secret_vault::{RadrootsSecretVault, RadrootsSecretVaultOsKeyring};
use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime};
use tokio::sync::{Mutex, broadcast};
use tokio::time::{Instant, timeout};
@@ -35,12 +34,11 @@ use url::Url;
use crate::runtime::RuntimeError;
use crate::runtime::account;
use crate::runtime::config::{
- CapabilityBindingTargetKind, PublishTransport, RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY,
- SignerBackend,
+ CapabilityBindingTargetKind, ReticulumPreviewBehavior, RuntimeConfig,
+ SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend, TransportProfileKind,
};
const SDK_STORAGE_DIR_NAME: &str = "sdk";
-const RADROOTSD_PROXY_SECRET_SERVICE: &str = "org.radroots.cli.radrootsd-proxy";
const CLI_RELAY_FETCH_TIMEOUT_MS: u64 = 10_000;
pub(crate) const MYC_NIP46_SESSION_SECRET_SERVICE: &str = "org.radroots.cli.myc-nip46-session";
@@ -56,9 +54,8 @@ pub enum CliSdkAdapterError {
pub struct CliSdkConfig {
pub storage_root: PathBuf,
pub geonames_cache_root: PathBuf,
- pub relay_url_policy: SdkRelayUrlPolicy,
- pub relay_urls: Vec<String>,
- pub publish_transport: SdkPublishTransport,
+ pub nostr_relay_url_policy: NostrRelayUrlPolicy,
+ pub transport_profile: TransportProfile,
}
impl CliSdkConfig {
@@ -66,23 +63,18 @@ impl CliSdkConfig {
Ok(Self {
storage_root: sdk_storage_root(config),
geonames_cache_root: config.paths.shared_cache_root.clone(),
- relay_url_policy: sdk_relay_url_policy(config),
- relay_urls: config.relay.urls.clone(),
- publish_transport: sdk_publish_transport(config)?,
+ nostr_relay_url_policy: sdk_nostr_relay_url_policy(config),
+ transport_profile: sdk_transport_profile(config)?,
})
}
pub fn builder(&self) -> RadrootsClientBuilder {
- self.relay_urls.iter().fold(
- RadrootsClient::builder()
- .storage(RadrootsSdkStorageConfig::Directory(
- self.storage_root.clone(),
- ))
- .geonames_cache_root(self.geonames_cache_root.clone())
- .relay_url_policy(self.relay_url_policy)
- .publish_transport(self.publish_transport.clone()),
- |builder, relay_url| builder.relay_url(relay_url.clone()),
- )
+ RadrootsClient::builder()
+ .storage(RadrootsSdkStorageConfig::Directory(
+ self.storage_root.clone(),
+ ))
+ .geonames_cache_root(self.geonames_cache_root.clone())
+ .transport_profile(self.transport_profile.clone())
}
}
@@ -606,86 +598,56 @@ pub(crate) fn fetch_relay_events_via_shared_transport(
}
fn memory_builder(config: &CliSdkConfig) -> RadrootsClientBuilder {
- config.relay_urls.iter().fold(
- RadrootsClient::builder()
- .geonames_cache_root(config.geonames_cache_root.clone())
- .relay_url_policy(config.relay_url_policy)
- .publish_transport(config.publish_transport.clone()),
- |builder, relay_url| builder.relay_url(relay_url.clone()),
- )
+ RadrootsClient::builder()
+ .geonames_cache_root(config.geonames_cache_root.clone())
+ .transport_profile(config.transport_profile.clone())
}
-pub fn sdk_relay_url_policy(config: &RuntimeConfig) -> SdkRelayUrlPolicy {
+pub fn sdk_nostr_relay_url_policy(config: &RuntimeConfig) -> NostrRelayUrlPolicy {
if config
- .relay
- .urls
+ .transport
+ .nostr_relay_urls
.iter()
.any(|relay_url| relay_url.starts_with("ws://"))
{
- SdkRelayUrlPolicy::Localhost
+ NostrRelayUrlPolicy::Localhost
} else {
- SdkRelayUrlPolicy::Public
+ NostrRelayUrlPolicy::Public
}
}
-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()
- }
- }
+pub fn sdk_target_policy(_config: &RuntimeConfig) -> TargetPolicy {
+ TargetPolicy::use_transport_profile()
}
-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 sdk_transport_profile(config: &RuntimeConfig) -> Result<TransportProfile, RuntimeError> {
+ match config.transport.profile {
+ TransportProfileKind::LocalOnly => Ok(TransportProfile::local_only()),
+ TransportProfileKind::Nostr => {
+ let profile = NostrProfile::new(
+ config.transport.nostr_relay_urls.iter().map(String::as_str),
+ sdk_nostr_relay_url_policy(config),
+ )
+ .map_err(|error| RuntimeError::Config(error.to_string()))?;
+ Ok(TransportProfile::nostr(profile))
}
- }
-}
-
-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()
+ TransportProfileKind::ReticulumPreview => {
+ let behavior = match config.transport.reticulum_preview_behavior {
+ ReticulumPreviewBehavior::RejectDeliveryAttempts => {
+ SdkReticulumPreviewBehavior::RejectDeliveryAttempts
+ }
+ ReticulumPreviewBehavior::DeferDeliveryPlans => {
+ SdkReticulumPreviewBehavior::DeferDeliveryPlans
+ }
+ };
+ Ok(TransportProfile::reticulum_preview(
+ ReticulumPreviewProfile::preview_unavailable().with_behavior(behavior),
))
- })?
- } 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(),
- ));
+ }
+ TransportProfileKind::Proxy => Ok(TransportProfile::proxy(ProxyProfile::new(
+ config.transport.proxy.url.clone(),
+ ))),
}
- Ok(Some(RadrootsdAuth::BearerToken(token.to_owned())))
}
#[cfg(test)]
@@ -783,9 +745,9 @@ mod tests {
},
DirectRrRsDependency {
section: "dependencies",
- name: "radroots_relay_transport",
- owner: "cli-shared-relay-read-boundary",
- reason: "shared fail-closed relay fetch receipts for trade event list, sync pull, and market refresh",
+ name: "radroots_transport_nostr",
+ owner: "cli-nostr-transport-read-boundary",
+ reason: "shared fail-closed Nostr relay fetch receipts for trade event list, sync pull, and market refresh",
lifecycle: "retain until those read surfaces are fully SDK-owned",
},
DirectRrRsDependency {
@@ -881,7 +843,7 @@ mod tests {
},
];
- const DIRECT_RELAY_FETCH_DISALLOWED_TOKENS: &[&str] = &[
+ const NOSTR_RELAY_FETCH_DISALLOWED_TOKENS: &[&str] = &[
"pub mod direct_relay",
"use crate::runtime::direct_relay",
"fetch_events_from_relays",
@@ -1150,9 +1112,15 @@ mod tests {
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);
assert_eq!(
- sdk_config.relay_urls,
+ sdk_config.nostr_relay_url_policy,
+ NostrRelayUrlPolicy::Public
+ );
+ let TransportProfile::Nostr { profile } = sdk_config.transport_profile else {
+ panic!("expected Nostr transport profile");
+ };
+ assert_eq!(
+ profile.relay_urls(),
vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
);
}
@@ -1162,7 +1130,10 @@ mod tests {
let root = tempdir().expect("tempdir");
let config = sample_config(root.path(), vec!["ws://127.0.0.1:8080".to_owned()]);
- assert_eq!(sdk_relay_url_policy(&config), SdkRelayUrlPolicy::Localhost);
+ assert_eq!(
+ sdk_nostr_relay_url_policy(&config),
+ NostrRelayUrlPolicy::Localhost
+ );
}
#[test]
@@ -1298,7 +1269,7 @@ mod tests {
}
#[test]
- fn cli_production_sources_reject_direct_relay_fetch_helpers() {
+ fn cli_production_sources_reject_unowned_relay_fetch_helpers() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut files = Vec::new();
collect_rs_files(manifest_dir.join("src").as_path(), &mut files);
@@ -1311,7 +1282,7 @@ mod tests {
let relative_path = relative_source_path(manifest_dir, file.as_path());
match production_source_without_tests(&relative_path, &source) {
Ok(production_source) => {
- direct_relay_fetch_findings(&relative_path, production_source.as_str())
+ nostr_relay_fetch_findings(&relative_path, production_source.as_str())
}
Err(error) => vec![error],
}
@@ -1320,7 +1291,7 @@ mod tests {
assert!(
findings.is_empty(),
- "CLI production sources contain direct relay fetch helpers:\n{}",
+ "CLI production sources contain unowned Nostr relay fetch helpers:\n{}",
findings.join("\n")
);
}
@@ -1723,13 +1694,13 @@ mod tests {
.collect()
}
- fn direct_relay_fetch_findings(label: &str, source: &str) -> Vec<String> {
- DIRECT_RELAY_FETCH_DISALLOWED_TOKENS
+ fn nostr_relay_fetch_findings(label: &str, source: &str) -> Vec<String> {
+ NOSTR_RELAY_FETCH_DISALLOWED_TOKENS
.iter()
.flat_map(|token| {
source.match_indices(token).map(move |(index, _)| {
format!(
- "{label}:{} uses direct relay fetch token `{token}`",
+ "{label}:{} uses unowned Nostr relay fetch token `{token}`",
line_number(source, index)
)
})
@@ -2151,10 +2122,13 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::from_nostr_relay_urls(
+ relays.clone(),
+ ),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: relays,
@@ -2175,6 +2149,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".to_owned(),
},
diff --git a/src/runtime/store.rs b/src/runtime/store.rs
@@ -412,7 +412,7 @@ fn sdk_event_store_status_view(
store: sdk_sqlite_status_view(status.store),
total_events: status.total_events,
projection_eligible_events: status.projection_eligible_events,
- relay_observations: status.relay_observations,
+ transport_observations: status.transport_observations,
last_event_seq: status.last_event_seq,
last_event_updated_at_ms: status.last_event_updated_at_ms,
}
@@ -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, "direct_nostr_relay"),
+ relays: table_row_count(manifest, "nostr"),
event_states: table_row_count(manifest, "nostr_event_state"),
}
}
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -11,19 +11,19 @@ use radroots_events::kinds::{
use radroots_nostr::prelude::{
RadrootsNostrFilter, RadrootsNostrTimestamp, radroots_event_from_nostr, radroots_nostr_kind,
};
-use radroots_relay_transport::{
- RadrootsRelayFetchFailure, RadrootsRelayFetchedEventsReceipt, RadrootsRelayTransportError,
-};
use radroots_replica_db::{ReplicaSql, migrations};
use radroots_replica_sync::{
RadrootsReplicaEventsError, RadrootsReplicaIngestOutcome, radroots_replica_ingest_event,
radroots_replica_sync_status,
};
use radroots_sdk::{
- PushOutboxEventReceipt, PushOutboxReceipt, PushOutboxRelayOutcomeKind, PushOutboxRequest,
- SyncStatusReceipt, SyncStatusRequest,
+ PushOutboxEventReceipt, PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind,
+ PushOutboxTargetReceipt, SyncStatusReceipt, SyncStatusRequest,
};
use radroots_sql_core::{SqlExecutor, SqliteExecutor};
+use radroots_transport_nostr::{
+ RadrootsRelayFetchFailure, RadrootsRelayFetchedEventsReceipt, RadrootsRelayTransportError,
+};
use serde::Deserialize;
use serde_json::json;
@@ -32,7 +32,7 @@ use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
use crate::runtime::sdk::{
CliSdkAdapterError, CliSdkSession, fetch_relay_events_via_shared_transport,
- sdk_relay_url_policy,
+ sdk_nostr_relay_url_policy,
};
use crate::view::runtime::{
RelayFailureView, SyncActionView, SyncFreshnessView, SyncQueueView, SyncRunFreshnessView,
@@ -42,7 +42,8 @@ use crate::view::runtime::{
const SYNC_SOURCE: &str = "local replica · local first";
const SDK_SYNC_SOURCE: &str = "SDK canonical event store and outbox";
const SDK_PUSH_SOURCE: &str = "SDK outbox push";
-const RELAY_PULL_SETUP_ACTION: &str = "radroots --relay wss://relay.example.com sync pull";
+const RELAY_PULL_SETUP_ACTION: &str =
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com";
const SYNC_PULL_ACTION: &str = "radroots sync pull";
const SYNC_PUSH_ACTION: &str = "radroots sync push";
const SYNC_READY_ACTION: &str = "radroots market product search eggs";
@@ -340,7 +341,7 @@ pub fn push(config: &RuntimeConfig) -> Result<SyncActionView, CliSdkAdapterError
}
let receipt = session.block_on(session.sdk().sync().push_outbox(
- PushOutboxRequest::new().with_relay_url_policy(sdk_relay_url_policy(config)),
+ PushOutboxRequest::new().with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)),
))?;
let status = session.block_on(session.sdk().sync().status(SyncStatusRequest::new()))?;
Ok(sdk_push_view(config, receipt, status))
@@ -415,7 +416,7 @@ fn empty_action_from_snapshot(snapshot: SyncSnapshot, direction: &str) -> SyncAc
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;
+ let relay_count = receipt.transport_profile.configured_nostr_relay_count;
SyncStatusView {
state: "ready".to_owned(),
source: SDK_SYNC_SOURCE.to_owned(),
@@ -449,7 +450,7 @@ fn sdk_push_dry_run_view(config: &RuntimeConfig, status: SyncStatusReceipt) -> S
state,
sdk_sync_queue(&status),
sdk_sync_freshness(&status),
- status.relay_targets.configured_relays,
+ status.transport_profile.configured_nostr_relays,
Vec::new(),
Vec::new(),
Vec::new(),
@@ -646,78 +647,81 @@ fn sdk_sync_freshness(receipt: &SyncStatusReceipt) -> SyncFreshnessView {
}
fn sdk_push_target_relays(receipt: &PushOutboxReceipt, status: &SyncStatusReceipt) -> Vec<String> {
- let mut relays = Vec::new();
- for relay in receipt.events.iter().flat_map(|event| event.relays.iter()) {
- if !relays.contains(&relay.relay_url) {
- relays.push(relay.relay_url.clone());
+ let mut targets = Vec::new();
+ for target in receipt.events.iter().flat_map(|event| event.targets.iter()) {
+ if !targets.contains(&target.endpoint_uri) {
+ targets.push(target.endpoint_uri.clone());
}
}
- if relays.is_empty() {
- relays.extend(status.relay_targets.configured_relays.clone());
+ if targets.is_empty() {
+ targets.extend(status.transport_profile.configured_nostr_relays.clone());
}
- relays
+ targets
}
fn sdk_push_connected_relays(receipt: &PushOutboxReceipt) -> Vec<String> {
- sdk_push_relays_matching(receipt, |_, relay| relay.attempted)
+ sdk_push_targets_matching(receipt, |_, target| target.attempted)
}
fn sdk_push_acknowledged_relays(receipt: &PushOutboxReceipt) -> Vec<String> {
- sdk_push_relays_matching(receipt, |_, relay| sdk_relay_accepted(relay.outcome_kind))
+ sdk_push_targets_matching(receipt, |_, target| {
+ sdk_target_accepted(target.outcome_kind)
+ })
}
-fn sdk_push_relays_matching(
+fn sdk_push_targets_matching(
receipt: &PushOutboxReceipt,
- predicate: impl Fn(&PushOutboxEventReceipt, &radroots_sdk::PushOutboxRelayReceipt) -> bool,
+ predicate: impl Fn(&PushOutboxEventReceipt, &PushOutboxTargetReceipt) -> bool,
) -> Vec<String> {
- let mut relays = Vec::new();
+ let mut targets = Vec::new();
for event in &receipt.events {
- for relay in &event.relays {
- if predicate(event, relay) && !relays.contains(&relay.relay_url) {
- relays.push(relay.relay_url.clone());
+ for target in &event.targets {
+ if predicate(event, target) && !targets.contains(&target.endpoint_uri) {
+ targets.push(target.endpoint_uri.clone());
}
}
}
- relays
+ targets
}
fn sdk_push_failed_relays(receipt: &PushOutboxReceipt) -> Vec<RelayFailureView> {
receipt
.events
.iter()
- .flat_map(|event| event.relays.iter())
- .filter(|relay| !sdk_relay_accepted(relay.outcome_kind))
- .map(|relay| RelayFailureView {
- relay: relay.relay_url.clone(),
- reason: relay
+ .flat_map(|event| event.targets.iter())
+ .filter(|target| !sdk_target_accepted(target.outcome_kind))
+ .map(|target| RelayFailureView {
+ relay: target.endpoint_uri.clone(),
+ reason: target
.message
.clone()
- .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()),
+ .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()),
})
.collect()
}
-fn sdk_relay_accepted(kind: PushOutboxRelayOutcomeKind) -> bool {
+fn sdk_target_accepted(kind: PushOutboxTargetOutcomeKind) -> bool {
matches!(
kind,
- PushOutboxRelayOutcomeKind::Accepted | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ PushOutboxTargetOutcomeKind::Accepted | PushOutboxTargetOutcomeKind::DuplicateAccepted
)
}
-fn sdk_relay_outcome_kind(kind: PushOutboxRelayOutcomeKind) -> &'static str {
+fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str {
match kind {
- PushOutboxRelayOutcomeKind::Accepted => "accepted",
- PushOutboxRelayOutcomeKind::DuplicateAccepted => "duplicate_accepted",
- PushOutboxRelayOutcomeKind::Blocked => "blocked",
- PushOutboxRelayOutcomeKind::RateLimited => "rate_limited",
- PushOutboxRelayOutcomeKind::Invalid => "invalid",
- PushOutboxRelayOutcomeKind::PowRequired => "pow_required",
- PushOutboxRelayOutcomeKind::Restricted => "restricted",
- PushOutboxRelayOutcomeKind::AuthRequired => "auth_required",
- PushOutboxRelayOutcomeKind::Error => "error",
- PushOutboxRelayOutcomeKind::Timeout => "timeout",
- PushOutboxRelayOutcomeKind::ConnectionFailed => "connection_failed",
- PushOutboxRelayOutcomeKind::Unknown => "unknown",
+ PushOutboxTargetOutcomeKind::Accepted => "accepted",
+ PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted",
+ PushOutboxTargetOutcomeKind::Blocked => "blocked",
+ PushOutboxTargetOutcomeKind::RateLimited => "rate_limited",
+ PushOutboxTargetOutcomeKind::Invalid => "invalid",
+ PushOutboxTargetOutcomeKind::PowRequired => "pow_required",
+ PushOutboxTargetOutcomeKind::Restricted => "restricted",
+ PushOutboxTargetOutcomeKind::AuthRequired => "auth_required",
+ PushOutboxTargetOutcomeKind::Error => "error",
+ PushOutboxTargetOutcomeKind::Timeout => "timeout",
+ PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed",
+ PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected",
+ PushOutboxTargetOutcomeKind::Unknown => "unknown",
_ => "unknown",
}
}
@@ -1441,16 +1445,16 @@ mod tests {
use radroots_nostr::prelude::{
RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrTimestamp, radroots_nostr_build_event,
};
- use radroots_relay_transport::{
- RadrootsRelayFetchFailure, RadrootsRelayFetchedEvent, RadrootsRelayFetchedEventsReceipt,
- RadrootsRelayTransportError,
- };
use radroots_sdk::{
PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt,
- PushOutboxRelayOutcomeKind, PushOutboxRelayReceipt, SyncEventStoreStatus, SyncOutboxStatus,
- SyncRelayTargetSummary, SyncStatusReceipt, SyncStatusSource,
+ PushOutboxTargetOutcomeKind, PushOutboxTargetReceipt, SyncEventStoreStatus,
+ SyncOutboxStatus, SyncStatusReceipt, SyncStatusSource, SyncTransportProfileSummary,
};
use radroots_secret_vault::RadrootsSecretBackend;
+ use radroots_transport_nostr::{
+ RadrootsRelayFetchFailure, RadrootsRelayFetchedEvent, RadrootsRelayFetchedEventsReceipt,
+ RadrootsRelayTransportError,
+ };
use tempfile::tempdir;
use super::{
@@ -1503,7 +1507,9 @@ mod tests {
assert_eq!(view.state, "unconfigured");
assert_eq!(
view.actions,
- vec!["radroots --relay wss://relay.example.com sync pull"]
+ vec![
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ ]
);
}
@@ -1667,14 +1673,14 @@ mod tests {
sdk_push_event(
"a",
PushOutboxEventState::Published,
- PushOutboxRelayOutcomeKind::Accepted,
+ PushOutboxTargetOutcomeKind::Accepted,
"wss://relay-a.example.com",
Some("accepted".to_owned()),
),
sdk_push_event(
"b",
PushOutboxEventState::PublishRetryable,
- PushOutboxRelayOutcomeKind::AuthRequired,
+ PushOutboxTargetOutcomeKind::AuthRequired,
"wss://relay-b.example.com",
Some("auth-required: login".to_owned()),
),
@@ -1750,7 +1756,7 @@ mod tests {
event_store: SyncEventStoreStatus {
total_events,
projection_eligible_events: total_events,
- relay_observations: 0,
+ transport_observations: 0,
last_event_seq: (total_events > 0).then_some(total_events),
last_event_updated_at_ms: (total_events > 0).then_some(1_700_000_000_000),
},
@@ -1765,9 +1771,10 @@ mod tests {
last_attempt_at_ms,
last_error,
},
- relay_targets: SyncRelayTargetSummary {
- configured_count: relays.len(),
- configured_relays: relays.iter().map(|relay| (*relay).to_owned()).collect(),
+ transport_profile: SyncTransportProfileSummary {
+ transport_profile_id: "nostr".to_owned(),
+ configured_nostr_relay_count: relays.len(),
+ configured_nostr_relays: relays.iter().map(|relay| (*relay).to_owned()).collect(),
},
}
}
@@ -1775,7 +1782,7 @@ mod tests {
fn sdk_push_event(
event_id_prefix: &str,
final_state: PushOutboxEventState,
- outcome_kind: PushOutboxRelayOutcomeKind,
+ outcome_kind: PushOutboxTargetOutcomeKind,
relay_url: &str,
message: Option<String>,
) -> PushOutboxEventReceipt {
@@ -1787,33 +1794,34 @@ mod tests {
attempted_count: 1,
accepted_count: usize::from(matches!(
outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
)),
retryable_count: usize::from(matches!(
outcome_kind,
- PushOutboxRelayOutcomeKind::AuthRequired
- | PushOutboxRelayOutcomeKind::Timeout
- | PushOutboxRelayOutcomeKind::ConnectionFailed
+ PushOutboxTargetOutcomeKind::AuthRequired
+ | PushOutboxTargetOutcomeKind::Timeout
+ | PushOutboxTargetOutcomeKind::ConnectionFailed
)),
terminal_count: usize::from(matches!(
outcome_kind,
- PushOutboxRelayOutcomeKind::Blocked
- | PushOutboxRelayOutcomeKind::RateLimited
- | PushOutboxRelayOutcomeKind::Invalid
- | PushOutboxRelayOutcomeKind::PowRequired
- | PushOutboxRelayOutcomeKind::Restricted
- | PushOutboxRelayOutcomeKind::Error
- | PushOutboxRelayOutcomeKind::Unknown
+ PushOutboxTargetOutcomeKind::Blocked
+ | PushOutboxTargetOutcomeKind::RateLimited
+ | PushOutboxTargetOutcomeKind::Invalid
+ | PushOutboxTargetOutcomeKind::PowRequired
+ | PushOutboxTargetOutcomeKind::Restricted
+ | PushOutboxTargetOutcomeKind::Error
+ | PushOutboxTargetOutcomeKind::Unknown
)),
quorum: 1,
quorum_met: matches!(
outcome_kind,
- PushOutboxRelayOutcomeKind::Accepted
- | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ PushOutboxTargetOutcomeKind::Accepted
+ | PushOutboxTargetOutcomeKind::DuplicateAccepted
),
- relays: vec![PushOutboxRelayReceipt {
- relay_url: relay_url.to_owned(),
+ targets: vec![PushOutboxTargetReceipt {
+ transport_kind: "nostr".to_owned(),
+ endpoint_uri: relay_url.to_owned(),
outcome_kind,
attempted: true,
message,
@@ -2337,10 +2345,13 @@ mod tests {
signer: SignerConfig {
backend: SignerBackend::Local,
},
+ transport: crate::runtime::config::TransportConfig::from_nostr_relay_urls(
+ relays.clone(),
+ ),
publish: PublishConfig {
- transport: PublishTransport::DirectNostrRelay,
+ transport: PublishTransport::Nostr,
source: PublishTransportSource::Defaults,
- radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
+ proxy: crate::runtime::config::ProxyTransportConfig::default(),
},
relay: RelayConfig {
urls: relays,
@@ -2361,6 +2372,7 @@ mod tests {
enabled: false,
executable: PathBuf::from("hyfd"),
},
+ mesh: crate::runtime::config::MeshConfig::disabled(),
rpc: RpcConfig {
url: "http://127.0.0.1:7070".into(),
},
diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs
@@ -0,0 +1,299 @@
+use std::fs;
+
+use radroots_sdk::{PushOutboxRequest, SyncStatusRequest};
+use serde_json::Value as JsonValue;
+use toml::{Value, map::Map};
+
+use crate::ops::OperationData;
+use crate::runtime::RuntimeError;
+use crate::runtime::config::{RuntimeConfig, TransportProfileKind};
+use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy};
+use crate::view::runtime::{
+ TransportOutboxPushView, TransportOutboxStatusView, TransportProfileView, TransportStatusView,
+};
+
+const TRANSPORT_SOURCE: &str = "transport profile config";
+
+pub fn profile(config: &RuntimeConfig) -> TransportProfileView {
+ active_profile_view(config)
+}
+
+pub fn set_profile(
+ config: &RuntimeConfig,
+ input: &OperationData,
+) -> Result<TransportProfileView, RuntimeError> {
+ let kind = string_input(input, "kind").unwrap_or("local_only");
+ let mut transport = Map::new();
+ transport.insert("profile".to_owned(), Value::String(kind.to_owned()));
+ match kind {
+ "local_only" => {}
+ "nostr" => {
+ let relays = string_array_input(input, "nostr_relays");
+ if relays.is_empty() {
+ return Err(RuntimeError::Config(
+ "transport profile `nostr` requires at least one --nostr-relay".to_owned(),
+ ));
+ }
+ let mut nostr = Map::new();
+ nostr.insert(
+ "relay_urls".to_owned(),
+ Value::Array(relays.into_iter().map(Value::String).collect()),
+ );
+ transport.insert("nostr".to_owned(), Value::Table(nostr));
+ }
+ "reticulum_preview" => {
+ let behavior = string_input(input, "reticulum_preview_behavior")
+ .unwrap_or("reject_delivery_attempts");
+ let mut preview = Map::new();
+ preview.insert("behavior".to_owned(), Value::String(behavior.to_owned()));
+ transport.insert("reticulum_preview".to_owned(), Value::Table(preview));
+ }
+ "proxy" => {
+ let Some(url) = string_input(input, "proxy_url") else {
+ return Err(RuntimeError::Config(
+ "transport profile `proxy` requires --proxy-url".to_owned(),
+ ));
+ };
+ let mut proxy = Map::new();
+ proxy.insert("url".to_owned(), Value::String(url.to_owned()));
+ transport.insert("proxy".to_owned(), Value::Table(proxy));
+ }
+ other => {
+ return Err(RuntimeError::Config(format!(
+ "transport profile kind `{other}` is not supported"
+ )));
+ }
+ }
+ update_app_config_table(config, "transport", Value::Table(transport))?;
+ Ok(profile_view_from_parts(
+ kind,
+ string_array_input(input, "nostr_relays"),
+ string_input(input, "reticulum_preview_behavior").map(str::to_owned),
+ string_input(input, "proxy_url").map(str::to_owned),
+ "configured",
+ ))
+}
+
+pub fn status(config: &RuntimeConfig) -> TransportStatusView {
+ TransportStatusView {
+ state: "ready".to_owned(),
+ source: TRANSPORT_SOURCE.to_owned(),
+ transports: vec![
+ active_profile_view(config),
+ profile_view_from_parts(
+ "reticulum_preview",
+ Vec::new(),
+ Some("reject_delivery_attempts".to_owned()),
+ None,
+ "preview_unavailable",
+ ),
+ ],
+ }
+}
+
+pub fn outbox_status(
+ config: &RuntimeConfig,
+) -> Result<TransportOutboxStatusView, CliSdkAdapterError> {
+ let session = CliSdkSession::connect(config)?;
+ let receipt = session.block_on(session.sdk().sync().status(SyncStatusRequest::new()))?;
+ Ok(TransportOutboxStatusView {
+ state: "ready".to_owned(),
+ source: "SDK transport outbox".to_owned(),
+ transport_profile: receipt.transport_profile.transport_profile_id,
+ total_count: receipt.outbox.total_events,
+ pending_count: receipt.outbox.pending_events,
+ retryable_count: receipt.outbox.retryable_events,
+ terminal_count: receipt.outbox.terminal_events,
+ ready_signed_count: receipt.outbox.ready_signed_events,
+ publishing_count: receipt.outbox.publishing_events,
+ last_attempt_at_ms: receipt.outbox.last_attempt_at_ms,
+ last_error: receipt.outbox.last_error,
+ actions: vec!["radroots transport outbox push".to_owned()],
+ })
+}
+
+pub fn outbox_push(config: &RuntimeConfig) -> Result<TransportOutboxPushView, CliSdkAdapterError> {
+ if config.output.dry_run {
+ let status = outbox_status(config)?;
+ return Ok(TransportOutboxPushView {
+ state: "dry_run".to_owned(),
+ source: "SDK transport outbox".to_owned(),
+ attempted_events: 0,
+ published_events: 0,
+ retryable_events: 0,
+ terminal_events: 0,
+ target_count: usize::try_from(status.ready_signed_count).unwrap_or_default(),
+ reason: Some("dry run requested; transport outbox push skipped".to_owned()),
+ actions: vec!["radroots transport outbox status".to_owned()],
+ });
+ }
+ let session = CliSdkSession::connect(config)?;
+ let receipt = session.block_on(session.sdk().sync().push_outbox(
+ PushOutboxRequest::new().with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)),
+ ))?;
+ let target_count = receipt
+ .events
+ .iter()
+ .flat_map(|event| event.targets.iter())
+ .count();
+ let failed_count = receipt.retryable_events + receipt.terminal_events;
+ let state = if receipt.attempted_events == 0 {
+ "ready"
+ } else if receipt.published_events > 0 && failed_count > 0 {
+ "partial"
+ } else if failed_count > 0 {
+ "unavailable"
+ } else if receipt.published_events > 0 {
+ "published"
+ } else {
+ "ready"
+ }
+ .to_owned();
+ Ok(TransportOutboxPushView {
+ state,
+ source: "SDK transport outbox".to_owned(),
+ attempted_events: receipt.attempted_events,
+ published_events: receipt.published_events,
+ retryable_events: receipt.retryable_events,
+ terminal_events: receipt.terminal_events,
+ target_count,
+ reason: (receipt.attempted_events == 0)
+ .then_some("SDK outbox had no ready signed events to push".to_owned()),
+ actions: vec!["radroots transport outbox status".to_owned()],
+ })
+}
+
+fn active_profile_view(config: &RuntimeConfig) -> TransportProfileView {
+ match config.transport.profile {
+ TransportProfileKind::LocalOnly => {
+ profile_view_from_parts("local_only", Vec::new(), None, None, "configured")
+ }
+ TransportProfileKind::Nostr => profile_view_from_parts(
+ "nostr",
+ config.transport.nostr_relay_urls.clone(),
+ None,
+ None,
+ if config.transport.nostr_relay_urls.is_empty() {
+ "unconfigured"
+ } else {
+ "configured"
+ },
+ ),
+ TransportProfileKind::ReticulumPreview => profile_view_from_parts(
+ "reticulum_preview",
+ Vec::new(),
+ Some(
+ config
+ .transport
+ .reticulum_preview_behavior
+ .as_str()
+ .to_owned(),
+ ),
+ None,
+ "preview_unavailable",
+ ),
+ TransportProfileKind::Proxy => profile_view_from_parts(
+ "proxy",
+ Vec::new(),
+ None,
+ Some(config.transport.proxy.url.clone()),
+ "configured",
+ ),
+ }
+}
+
+fn profile_view_from_parts(
+ profile_id: &str,
+ nostr_relays: Vec<String>,
+ reticulum_preview_behavior: Option<String>,
+ proxy_url: Option<String>,
+ configured_state: &str,
+) -> TransportProfileView {
+ let transport_kind = match profile_id {
+ "nostr" => "nostr",
+ "reticulum_preview" => "reticulum",
+ "proxy" => "proxy",
+ _ => "local",
+ };
+ let implementation_state = match profile_id {
+ "nostr" => "available",
+ "reticulum_preview" => "preview_unavailable",
+ "proxy" => "delegated",
+ _ => "local_only",
+ };
+ let usable_for_delivery =
+ matches!(profile_id, "nostr" | "proxy") && configured_state == "configured";
+ let message = match profile_id {
+ "nostr" if usable_for_delivery => "Nostr relay transport is configured for delivery",
+ "nostr" => "Nostr transport requires configured Nostr relay targets",
+ "reticulum_preview" => "Reticulum preview is explicit and unavailable for real delivery",
+ "proxy" => "Proxy transport delegates delivery to the configured endpoint",
+ _ => "Local-only profile does not deliver to network transports",
+ };
+ TransportProfileView {
+ state: configured_state.to_owned(),
+ source: TRANSPORT_SOURCE.to_owned(),
+ profile_id: profile_id.to_owned(),
+ transport_kind: transport_kind.to_owned(),
+ configured_state: configured_state.to_owned(),
+ implementation_state: implementation_state.to_owned(),
+ usable_for_delivery,
+ message: message.to_owned(),
+ nostr_relays,
+ reticulum_preview_behavior,
+ proxy_url,
+ actions: if usable_for_delivery {
+ Vec::new()
+ } else {
+ vec![
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ .to_owned(),
+ ]
+ },
+ }
+}
+
+pub(crate) fn update_app_config_table(
+ config: &RuntimeConfig,
+ key: &str,
+ value: Value,
+) -> Result<(), RuntimeError> {
+ let path = &config.paths.app_config_path;
+ let mut document = if path.exists() {
+ let raw = fs::read_to_string(path)?;
+ toml::from_str::<Value>(&raw)
+ .map_err(|error| RuntimeError::Config(format!("failed to parse app config: {error}")))?
+ } else {
+ Value::Table(Map::new())
+ };
+ let Some(table) = document.as_table_mut() else {
+ return Err(RuntimeError::Config(
+ "app config root must be a TOML table".to_owned(),
+ ));
+ };
+ table.remove("publish");
+ table.remove("relays");
+ table.insert(key.to_owned(), value);
+ if let Some(parent) = path.parent() {
+ fs::create_dir_all(parent)?;
+ }
+ let rendered = toml::to_string_pretty(&document)
+ .map_err(|error| RuntimeError::Config(format!("failed to render app config: {error}")))?;
+ fs::write(path, rendered)?;
+ Ok(())
+}
+
+fn string_input<'a>(input: &'a OperationData, key: &str) -> Option<&'a str> {
+ input.get(key).and_then(JsonValue::as_str)
+}
+
+fn string_array_input(input: &OperationData, key: &str) -> Vec<String> {
+ input
+ .get(key)
+ .and_then(JsonValue::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(JsonValue::as_str)
+ .map(str::to_owned)
+ .collect()
+}
diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs
@@ -362,7 +362,7 @@ fn inspection_from_sdk_receipt(
failed_relays,
reason_code: Some("validation_receipt_not_found".to_owned()),
reason: Some(format!(
- "validation receipt event `{receipt_event_id}` was not found on configured relays"
+ "validation receipt event `{receipt_event_id}` was not found on configured Nostr relays"
)),
sdk_error: None,
actions: Vec::new(),
@@ -638,8 +638,8 @@ fn sdk_error_parts(error: RadrootsSdkError) -> ValidationReceiptSdkErrorParts {
};
let actions = if error.code() == "empty_target_relays" {
vec![
- "radroots --relay wss://relay.example.com validation receipt list --trade-id <trade-id>"
- .to_owned(),
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ .to_owned(),
]
} else {
Vec::new()
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -617,7 +617,7 @@ pub struct SdkEventStoreStatusView {
pub store: SdkSqliteStatusView,
pub total_events: i64,
pub projection_eligible_events: i64,
- pub relay_observations: i64,
+ pub transport_observations: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_event_seq: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -3262,56 +3262,99 @@ impl LocalExportView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct RelayListView {
+pub struct TransportProfileView {
pub state: String,
pub source: String,
- pub publish_policy: String,
- pub count: usize,
+ pub profile_id: String,
+ pub transport_kind: String,
+ pub configured_state: String,
+ pub implementation_state: String,
+ pub usable_for_delivery: bool,
+ pub message: String,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub nostr_relays: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- pub relays: Vec<RelayEntryView>,
+ pub reticulum_preview_behavior: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub proxy_url: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
}
-impl RelayListView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
+#[derive(Debug, Clone, Serialize)]
+pub struct TransportStatusView {
+ pub state: String,
+ pub source: String,
+ pub transports: Vec<TransportProfileView>,
}
#[derive(Debug, Clone, Serialize)]
-pub struct RelayEntryView {
- pub url: String,
- pub read: bool,
- pub write: bool,
+pub struct TransportOutboxStatusView {
+ pub state: String,
+ pub source: String,
+ pub transport_profile: String,
+ pub total_count: i64,
+ pub pending_count: i64,
+ pub retryable_count: i64,
+ pub terminal_count: i64,
+ pub ready_signed_count: i64,
+ pub publishing_count: i64,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub last_attempt_at_ms: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub last_error: Option<String>,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub actions: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
-pub struct NetStatusView {
+pub struct TransportOutboxPushView {
pub state: String,
pub source: String,
- pub session: String,
- pub relay_count: usize,
- pub publish_policy: String,
- pub signer_mode: String,
- pub account_resolution: AccountResolutionView,
+ pub attempted_events: usize,
+ pub published_events: usize,
+ pub retryable_events: usize,
+ pub terminal_events: usize,
+ pub target_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
}
-impl NetStatusView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
+#[derive(Debug, Clone, Serialize)]
+pub struct MeshScopeView {
+ pub state: String,
+ pub source: String,
+ pub scope: String,
+ pub implementation_state: String,
+ pub message: String,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub actions: Vec<String>,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct MeshStatusView {
+ pub state: String,
+ pub source: String,
+ pub scope: String,
+ pub transport_kind: String,
+ pub configured_state: String,
+ pub implementation_state: String,
+ pub usable_for_delivery: bool,
+ pub message: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct MeshPolicyCheckView {
+ pub state: String,
+ pub source: String,
+ pub scope: String,
+ pub policy: String,
+ pub transport_kind: String,
+ pub usable_for_delivery: bool,
+ pub decision: String,
+ pub message: String,
}
#[derive(Debug, Clone, Serialize)]
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -889,7 +889,7 @@ fn myc_mode_allows_read_inspection_commands() {
&["--format", "json", "workspace", "get"][..],
&["--format", "json", "config", "get"][..],
&["--format", "json", "account", "list"][..],
- &["--format", "json", "relay", "list"][..],
+ &["--format", "json", "transport", "profile", "get"][..],
] {
let (output, value) = sandbox.json_output(args);
@@ -920,11 +920,10 @@ fn local_listing_publish_fails_without_local_account_authority() {
account_id,
]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"listing",
@@ -963,6 +962,7 @@ fn local_listing_publish_dry_run_validates_local_account_authority() {
account_id,
]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
@@ -999,6 +999,7 @@ fn local_listing_update_dry_run_validates_local_account_authority() {
account_id,
]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
@@ -1027,6 +1028,7 @@ fn local_listing_update_dry_run_rejects_mismatched_local_account() {
.as_str()
.expect("second account id");
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
@@ -1068,9 +1070,12 @@ fn local_listing_publish_fails_without_configured_relay() {
assert!(!output.status.success());
assert_eq!(value["operation_id"], "listing.publish");
assert_eq!(value["result"], serde_json::Value::Null);
- assert_eq!(value["errors"][0]["code"], "empty_target_relays");
- assert_eq!(value["errors"][0]["detail"]["class"], "configuration");
- assert_contains(&value["errors"][0]["message"], "sdk empty target relays");
+ assert_eq!(value["errors"][0]["code"], "network_unavailable");
+ assert_eq!(value["errors"][0]["detail"]["class"], "network");
+ assert_contains(
+ &value["errors"][0]["message"],
+ "requires a delivery-capable transport profile",
+ );
assert_no_removed_command_reference(&value, &["listing", "publish"]);
assert_no_daemon_runtime_reference(&value, &["listing", "publish"]);
}
@@ -1082,6 +1087,7 @@ fn local_listing_publish_dry_run_does_not_sign_matching_listing() {
let listing_file = create_listing_draft(&sandbox, "local-dry-run");
make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let value = sandbox.json_success(&[
"--format",
"json",
@@ -1121,6 +1127,7 @@ fn local_listing_archive_dry_run_validates_local_account_authority() {
.as_str()
.expect("second account id");
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
@@ -1153,11 +1160,10 @@ fn local_listing_publish_fails_when_selected_account_does_not_match_seller() {
.expect("second account id");
assert_ne!(first_account_id, second_account_id);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--account-id",
second_account_id,
"--approval-token",
@@ -1203,15 +1209,8 @@ fn local_farm_publish_dry_run_validates_secret_backed_account() {
"pickup",
]);
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "farm",
- "publish",
- ]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
+ let value = sandbox.json_success(&["--format", "json", "--dry-run", "farm", "publish"]);
assert_eq!(value["operation_id"], "farm.publish");
assert_eq!(value["dry_run"], true);
@@ -1254,7 +1253,7 @@ fn local_farm_publish_dry_run_fails_without_configured_relay() {
assert_eq!(value["errors"][0]["detail"]["class"], "network");
assert_contains(
&value["errors"][0]["message"],
- "requires at least one configured relay",
+ "requires at least one configured Nostr relay in the active transport profile",
);
assert_no_removed_command_reference(&value, &["farm", "publish", "--dry-run"]);
assert_no_daemon_runtime_reference(&value, &["farm", "publish", "--dry-run"]);
@@ -1299,7 +1298,7 @@ fn local_farm_publish_fails_without_configured_relay() {
assert_eq!(value["errors"][0]["detail"]["class"], "network");
assert_contains(
&value["errors"][0]["message"],
- "requires at least one configured relay",
+ "requires a delivery-capable transport profile",
);
assert_no_removed_command_reference(&value, &["farm", "publish"]);
assert_no_daemon_runtime_reference(&value, &["farm", "publish"]);
@@ -1332,11 +1331,10 @@ fn farm_setup_actions_offer_publish_only_when_relay_publish_executable() {
assert_action_present(&unconfigured, "radroots farm readiness check");
assert_action_absent(&unconfigured, "radroots farm publish");
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let configured = sandbox.json_success(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"farm",
"profile",
"update",
@@ -1367,11 +1365,10 @@ fn farm_setup_actions_withhold_publish_for_watch_only_account() {
public_identity_file.to_string_lossy().as_ref(),
]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let created = sandbox.json_success(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"farm",
"create",
"--name",
@@ -1394,8 +1391,6 @@ fn farm_setup_actions_withhold_publish_for_watch_only_account() {
let updated = sandbox.json_success(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"farm",
"profile",
"update",
@@ -1433,11 +1428,10 @@ fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() {
]);
let relay_url = "ws://127.0.0.1:9";
+ sandbox.write_nostr_transport_profile(&[relay_url]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay_url,
"--approval-token",
"approve",
"--idempotency-key",
@@ -1519,11 +1513,10 @@ fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() {
]);
let relay_url = "ws://127.0.0.1:9";
+ sandbox.write_nostr_transport_profile(&[relay_url]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay_url,
"--approval-token",
"approve",
"--idempotency-key",
@@ -1784,15 +1777,9 @@ fn farm_rebind_is_explicit_and_publish_defaults_ignore_ambient_selection() {
assert_next_action_present(&missing_rebind, "radroots account import <path>");
assert_next_action_present(&missing_rebind, "radroots account create");
- let publish_dry_run = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "farm",
- "publish",
- ]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
+ let publish_dry_run =
+ sandbox.json_success(&["--format", "json", "--dry-run", "farm", "publish"]);
assert_eq!(publish_dry_run["operation_id"], "farm.publish");
assert_eq!(publish_dry_run["result"]["state"], "dry_run");
assert_eq!(
@@ -2098,15 +2085,8 @@ fn farm_rebind_allows_watch_only_target_and_attach_secret_recovers_publish() {
watch_account_id
);
- let readiness = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "farm",
- "readiness",
- "check",
- ]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
+ let readiness = sandbox.json_success(&["--format", "json", "farm", "readiness", "check"]);
assert_eq!(readiness["operation_id"], "farm.readiness.check");
assert_eq!(readiness["result"]["publish_state"], "unconfigured");
assert_eq!(
@@ -2118,15 +2098,8 @@ fn farm_rebind_allows_watch_only_target_and_attach_secret_recovers_publish() {
format!("radroots account attach-secret {watch_account_id} <path>").as_str(),
);
- let (publish_output, publish) = sandbox.json_output(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "farm",
- "publish",
- ]);
+ let (publish_output, publish) =
+ sandbox.json_output(&["--format", "json", "--dry-run", "farm", "publish"]);
assert!(!publish_output.status.success());
assert_eq!(publish["operation_id"], "farm.publish");
assert_eq!(publish["errors"][0]["code"], "account_watch_only");
@@ -2141,15 +2114,7 @@ fn farm_rebind_allows_watch_only_target_and_attach_secret_recovers_publish() {
watch_account_id,
secret_identity_file.to_string_lossy().as_ref(),
]);
- let recovered = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "farm",
- "publish",
- ]);
+ let recovered = sandbox.json_success(&["--format", "json", "--dry-run", "farm", "publish"]);
assert_eq!(recovered["operation_id"], "farm.publish");
assert_eq!(recovered["result"]["state"], "dry_run");
assert_eq!(recovered["result"]["seller_account_id"], watch_account_id);
@@ -2186,11 +2151,10 @@ fn local_seller_publish_commands_attempt_configured_relay() {
.expect("farm d tag");
let relay = "ws://127.0.0.1:9";
+ sandbox.write_nostr_transport_profile(&[relay]);
let (farm_output, farm_value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay,
"--approval-token",
"approve",
"farm",
@@ -2227,8 +2191,6 @@ fn local_seller_publish_commands_attempt_configured_relay() {
let (publish_output, publish_value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay,
"--approval-token",
"approve",
"listing",
@@ -2268,8 +2230,6 @@ fn local_seller_publish_commands_attempt_configured_relay() {
let (archive_output, archive_value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay,
"--approval-token",
"approve",
"listing",
@@ -2336,8 +2296,6 @@ fn local_seller_publish_commands_attempt_configured_relay() {
let (order_output, order_value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay,
"--approval-token",
"approve",
"trade",
@@ -2347,15 +2305,19 @@ fn local_seller_publish_commands_attempt_configured_relay() {
assert!(!order_output.status.success());
assert_eq!(order_value["operation_id"], "trade.submit");
assert_eq!(order_value["result"], serde_json::Value::Null);
- assert_eq!(order_value["errors"][0]["code"], "invalid_relay_url");
- assert_eq!(order_value["errors"][0]["detail"]["class"], "configuration");
+ assert_eq!(order_value["errors"][0]["code"], "operation_unavailable");
+ assert_eq!(order_value["errors"][0]["detail"]["class"], "operation");
assert_eq!(
order_value["errors"][0]["detail"]["operation_id"],
"trade.submit"
);
assert_contains(
&order_value["errors"][0]["message"],
- "loopback IPv4 address",
+ "trade draft is not ready for submit",
+ );
+ assert_eq!(
+ order_value["errors"][0]["detail"]["issues"][0]["code"],
+ "listing_provenance_missing"
);
assert_no_removed_command_reference(&order_value, &["trade", "submit"]);
assert_no_daemon_runtime_reference(&order_value, &["trade", "submit"]);
@@ -2366,10 +2328,9 @@ fn local_order_event_list_attempts_configured_shared_relay_transport() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
let relay = "ws://127.0.0.1:9";
+ sandbox.write_nostr_transport_profile(&[relay]);
- let (output, value) = sandbox.json_output(&[
- "--format", "json", "--relay", relay, "trade", "event", "list",
- ]);
+ let (output, value) = sandbox.json_output(&["--format", "json", "trade", "event", "list"]);
assert!(!output.status.success());
assert_relay_transport_fetch_failure(&value, "trade.event.list", &["trade", "event", "list"]);
@@ -2415,11 +2376,10 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
);
assert_no_daemon_runtime_reference(&watch, &watch_args);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let submit_args = [
"--format",
"json",
- "--publish-transport",
- "direct_nostr_relay",
"--dry-run",
"trade",
"submit",
@@ -2455,6 +2415,7 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
);
assert_no_daemon_runtime_reference(&status, &status_args);
+ sandbox.write_app_config("[transport]\nprofile = \"local_only\"\n");
let event_list_no_relay_args = ["--format", "json", "trade", "event", "list"];
let (event_list_no_relay_output, event_list_no_relay) =
sandbox.json_output(&event_list_no_relay_args);
@@ -2469,19 +2430,12 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
);
assert_eq!(
event_list_no_relay["next_actions"][0]["command"],
- "radroots --relay wss://relay.example.com trade event list"
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
);
assert_no_daemon_runtime_reference(&event_list_no_relay, &event_list_no_relay_args);
- let event_list_no_account_args = [
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "trade",
- "event",
- "list",
- ];
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
+ let event_list_no_account_args = ["--format", "json", "trade", "event", "list"];
let (event_list_no_account_output, event_list_no_account) =
sandbox.json_output(&event_list_no_account_args);
assert!(!event_list_no_account_output.status.success());
@@ -2502,8 +2456,6 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
let accept_args = [
"--format",
"json",
- "--publish-transport",
- "direct_nostr_relay",
"--dry-run",
"trade",
"accept",
@@ -2520,8 +2472,6 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
let decline_args = [
"--format",
"json",
- "--publish-transport",
- "direct_nostr_relay",
"--dry-run",
"trade",
"decline",
@@ -2573,15 +2523,9 @@ fn watch_only_farm_publish_dry_run_fails_as_account_watch_only() {
"pickup",
]);
- let (output, value) = sandbox.json_output(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "farm",
- "publish",
- ]);
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
+ let (output, value) =
+ sandbox.json_output(&["--format", "json", "--dry-run", "farm", "publish"]);
assert!(!output.status.success());
assert_eq!(value["operation_id"], "farm.publish");
@@ -2608,11 +2552,10 @@ fn watch_only_listing_publish_fails_as_account_watch_only() {
let listing_file = create_listing_draft(&sandbox, "watch-only-publish");
make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"listing",
@@ -2649,6 +2592,7 @@ fn watch_only_listing_update_dry_run_fails_as_account_watch_only() {
let listing_file = create_listing_draft(&sandbox, "watch-only-update");
make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
+ sandbox.write_nostr_transport_profile(&["ws://127.0.0.1:9"]);
let (output, value) = sandbox.json_output(&[
"--format",
"json",
@@ -2683,7 +2627,10 @@ fn myc_listing_publish_does_not_fallback_to_local_account() {
)
.as_str(),
);
- configure_myc_mode(&sandbox, &myc);
+ sandbox.write_app_config(&format!(
+ "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"ws://127.0.0.1:9\"]\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
+ toml_string(myc.display().to_string().as_str())
+ ));
let (output, value) = sandbox.json_output(&[
"--format",
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
@@ -10,8 +10,7 @@ use radroots_events::kinds::{KIND_FARM, KIND_LISTING};
use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
use radroots_local_events::{
LocalEventRecord, LocalEventRecordInput, LocalEventsStore, LocalRecordFamily,
- LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime,
- canonical_relay_set_fingerprint,
+ LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
};
use radroots_protected_store::RadrootsProtectedFileSecretVault;
use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
@@ -24,7 +23,6 @@ use tempfile::TempDir;
use std::os::unix::fs::PermissionsExt;
static COMMAND_LOCK: Mutex<()> = Mutex::new(());
-pub const ORDERABLE_LISTING_RELAY: &str = "ws://127.0.0.1:9";
pub fn radroots() -> Command {
Command::cargo_bin("radroots").expect("binary")
@@ -111,6 +109,20 @@ impl RadrootsCliSandbox {
path
}
+ pub fn write_nostr_transport_profile(&self, relay_urls: &[&str]) -> PathBuf {
+ let relays = relay_urls
+ .iter()
+ .map(|relay| format!("\"{}\"", relay.replace('"', "\\\"")))
+ .collect::<Vec<_>>()
+ .join(", ");
+ self.write_app_config(
+ format!(
+ "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [{relays}]\n"
+ )
+ .as_str(),
+ )
+ }
+
pub fn replica_db_path(&self) -> PathBuf {
self.root
.path()
@@ -159,6 +171,8 @@ impl RadrootsCliSandbox {
const _: () = {
let _ = ndjson_from_stdout as fn(&Output) -> Vec<Value>;
let _ = RadrootsCliSandbox::write_workspace_config as fn(&RadrootsCliSandbox, &str) -> PathBuf;
+ let _ = RadrootsCliSandbox::write_nostr_transport_profile
+ as fn(&RadrootsCliSandbox, &[&str]) -> PathBuf;
let _ = RadrootsCliSandbox::replica_db_path as fn(&RadrootsCliSandbox) -> PathBuf;
let _ =
RadrootsCliSandbox::local_event_records as fn(&RadrootsCliSandbox) -> Vec<LocalEventRecord>;
@@ -318,6 +332,98 @@ pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str)
event_id
}
+#[allow(dead_code)]
+pub fn seed_market_refresh_provenance(sandbox: &RadrootsCliSandbox, relay_urls: &[&str]) {
+ let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
+ executor
+ .exec(
+ "CREATE TABLE IF NOT EXISTS radroots_cli_sync_run (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ scope TEXT NOT NULL,
+ relay_set_fingerprint TEXT NOT NULL,
+ target_relays_json TEXT NOT NULL,
+ connected_relays_json TEXT NOT NULL,
+ failed_relays_json TEXT NOT NULL,
+ started_at INTEGER NOT NULL,
+ completed_at INTEGER,
+ state TEXT NOT NULL,
+ fetched_count INTEGER NOT NULL,
+ ingested_count INTEGER NOT NULL,
+ skipped_count INTEGER NOT NULL,
+ unsupported_count INTEGER NOT NULL,
+ failed_count INTEGER NOT NULL,
+ failure_reason TEXT
+ );
+ CREATE INDEX IF NOT EXISTS idx_radroots_cli_sync_run_scope_started
+ ON radroots_cli_sync_run(scope, started_at DESC);",
+ "[]",
+ )
+ .expect("create sync run table");
+ let relays = relay_urls
+ .iter()
+ .map(|relay| relay.to_string())
+ .collect::<Vec<_>>();
+ executor
+ .exec(
+ "INSERT INTO radroots_cli_sync_run (
+ scope,
+ relay_set_fingerprint,
+ target_relays_json,
+ connected_relays_json,
+ failed_relays_json,
+ started_at,
+ completed_at,
+ state,
+ fetched_count,
+ ingested_count,
+ skipped_count,
+ unsupported_count,
+ failed_count,
+ failure_reason
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14);",
+ json!([
+ "market_refresh",
+ relay_set_fingerprint(&relays),
+ serde_json::to_string(&relays).expect("target relays json"),
+ serde_json::to_string(&relays).expect("connected relays json"),
+ serde_json::to_string(&Vec::<String>::new()).expect("failed relays json"),
+ 1_779_000_001_000_i64,
+ 1_779_000_001_001_i64,
+ "success",
+ 1_i64,
+ 1_i64,
+ 0_i64,
+ 0_i64,
+ 0_i64,
+ Value::Null
+ ])
+ .to_string()
+ .as_str(),
+ )
+ .expect("insert market refresh provenance");
+}
+
+#[allow(dead_code)]
+fn relay_set_fingerprint(relays: &[String]) -> String {
+ let mut normalized = relays
+ .iter()
+ .map(|relay| relay.trim().to_ascii_lowercase())
+ .filter(|relay| !relay.is_empty())
+ .collect::<Vec<_>>();
+ normalized.sort();
+ normalized.dedup();
+ let mut hash = 0xcbf29ce484222325_u64;
+ for relay in normalized {
+ for byte in relay.as_bytes() {
+ hash ^= u64::from(*byte);
+ hash = hash.wrapping_mul(0x100000001b3);
+ }
+ hash ^= 0xff;
+ hash = hash.wrapping_mul(0x100000001b3);
+ }
+ format!("relayset_{hash:016x}")
+}
+
fn seed_orderable_listing_signed_event(
sandbox: &RadrootsCliSandbox,
event: &RadrootsNostrEvent,
@@ -329,13 +435,6 @@ fn seed_orderable_listing_signed_event(
let executor = SqliteExecutor::open(database_path).expect("open local events");
let store = LocalEventsStore::new(executor);
store.migrate_up().expect("migrate local events");
- let delivery = RelayDeliveryEvidence::acknowledged(
- [ORDERABLE_LISTING_RELAY],
- [ORDERABLE_LISTING_RELAY],
- [ORDERABLE_LISTING_RELAY],
- Vec::new(),
- )
- .expect("listing relay delivery evidence");
store
.append_record(&LocalEventRecordInput {
record_id: format!("test:signed_listing:{}", event.id),
@@ -358,8 +457,6 @@ fn seed_orderable_listing_signed_event(
event_sig: Some(event.sig.clone()),
raw_event_json: Some(json!(event)),
outbox_status: PublishOutboxStatus::Acknowledged,
- relay_set_fingerprint: canonical_relay_set_fingerprint([ORDERABLE_LISTING_RELAY]),
- relay_delivery_json: Some(delivery.to_json_value().expect("delivery json")),
})
.expect("append listing signed event record");
}
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -23,8 +23,7 @@ use radroots_events_codec::order::order_request_event_build;
use radroots_identity::RadrootsIdentity;
use radroots_local_events::{
BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecordInput, LocalEventsStore,
- LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence,
- SourceRuntime, canonical_relay_set_fingerprint,
+ LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
};
use radroots_nostr::prelude::{RadrootsNostrEvent, radroots_nostr_build_event};
use radroots_nostr_connect::prelude::{
@@ -39,16 +38,17 @@ use serde_json::Value;
use serde_json::json;
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, ndjson_from_stdout, radroots, remove_orderable_listing,
- replace_latest_listing_event_id, seed_orderable_listing, store_test_session_secret,
- toml_string, update_orderable_listing_available_amount,
- update_orderable_listing_primary_bin_id, write_public_identity_profile,
- write_secret_identity_profile,
+ 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,
+ ndjson_from_stdout, radroots, remove_orderable_listing, replace_latest_listing_event_id,
+ seed_market_refresh_provenance, seed_orderable_listing, store_test_session_secret, toml_string,
+ update_orderable_listing_available_amount, update_orderable_listing_primary_bin_id,
+ write_public_identity_profile, write_secret_identity_profile,
};
+const ORDERABLE_LISTING_RELAY: &str = "wss://relay.example.com";
+
const LISTING_ADDR: &str =
"30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
const DERIVED_PROJECTION_SYNC_PUSH_FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
@@ -69,12 +69,22 @@ 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");
+fn proxy_token_file(sandbox: &RadrootsCliSandbox) -> PathBuf {
+ let path = sandbox.root().join("proxy.token");
fs::write(&path, "proxy_test_token\n").expect("write proxy token file");
path
}
+fn configure_nostr_transport(sandbox: &RadrootsCliSandbox, relay_url: &str) {
+ sandbox.write_nostr_transport_profile(&[relay_url]);
+}
+
+fn write_proxy_transport_config(sandbox: &RadrootsCliSandbox, body: &str) -> PathBuf {
+ sandbox.write_app_config(
+ format!("[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\n{body}").as_str(),
+ )
+}
+
struct RelayFetchServer {
endpoint: String,
handle: JoinHandle<()>,
@@ -100,33 +110,31 @@ impl RelayFetchServer {
}
}
-struct RadrootsdProxyJsonRpcServer {
+struct ProxyJsonRpcServer {
endpoint: String,
handle: JoinHandle<Value>,
}
-impl RadrootsdProxyJsonRpcServer {
- fn once(expected_token: &'static str) -> Self {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind radrootsd proxy");
- listener
- .set_nonblocking(true)
- .expect("radrootsd proxy nonblocking");
+impl ProxyJsonRpcServer {
+ fn once(expected_token: Option<&'static str>) -> Self {
+ let listener = TcpListener::bind("127.0.0.1:0").expect("bind proxy");
+ listener.set_nonblocking(true).expect("proxy nonblocking");
let endpoint = format!("http://{}", listener.local_addr().expect("proxy addr"));
let handle = thread::spawn(move || {
let deadline = Instant::now() + Duration::from_secs(30);
loop {
match listener.accept() {
Ok((stream, _)) => {
- return handle_radrootsd_proxy_connection(stream, expected_token);
+ return handle_proxy_connection(stream, expected_token);
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
assert!(
Instant::now() < deadline,
- "timed out waiting for radrootsd proxy request"
+ "timed out waiting for proxy request"
);
thread::sleep(Duration::from_millis(10));
}
- Err(error) => panic!("accept radrootsd proxy connection: {error}"),
+ Err(error) => panic!("accept proxy connection: {error}"),
}
}
});
@@ -138,7 +146,7 @@ impl RadrootsdProxyJsonRpcServer {
}
fn join(self) -> Value {
- self.handle.join().expect("radrootsd proxy server join")
+ self.handle.join().expect("proxy server join")
}
}
@@ -481,12 +489,12 @@ signer_session_ref = "{}"
)
}
-fn handle_radrootsd_proxy_connection(mut stream: TcpStream, expected_token: &str) -> Value {
+fn handle_proxy_connection(mut stream: TcpStream, expected_token: Option<&str>) -> Value {
let mut bytes = Vec::new();
let mut buffer = [0_u8; 1024];
let body_start = loop {
- let read = stream.read(&mut buffer).expect("read radrootsd proxy");
- assert!(read > 0, "radrootsd proxy request closed before headers");
+ let read = stream.read(&mut buffer).expect("read proxy");
+ assert!(read > 0, "proxy request closed before headers");
bytes.extend_from_slice(&buffer[..read]);
if let Some(index) = http_body_start(&bytes) {
break index;
@@ -495,19 +503,21 @@ fn handle_radrootsd_proxy_connection(mut stream: TcpStream, expected_token: &str
let headers = String::from_utf8(bytes[..body_start].to_vec()).expect("headers utf8");
let content_length = http_content_length(headers.as_str());
while bytes.len() < body_start + content_length {
- let read = stream.read(&mut buffer).expect("read radrootsd proxy body");
- assert!(read > 0, "radrootsd proxy request closed before body");
+ let read = stream.read(&mut buffer).expect("read proxy body");
+ assert!(read > 0, "proxy request closed before body");
bytes.extend_from_slice(&buffer[..read]);
}
let body = String::from_utf8(bytes[body_start..body_start + content_length].to_vec())
.expect("body utf8");
- assert!(
- headers
- .to_ascii_lowercase()
- .contains(format!("authorization: bearer {expected_token}").as_str()),
- "radrootsd proxy request missing expected bearer auth: {headers}"
- );
- let request: Value = serde_json::from_str(body.as_str()).expect("radrootsd proxy json");
+ if let Some(expected_token) = expected_token {
+ assert!(
+ headers
+ .to_ascii_lowercase()
+ .contains(format!("authorization: bearer {expected_token}").as_str()),
+ "proxy request missing expected bearer auth: {headers}"
+ );
+ }
+ let request: Value = serde_json::from_str(body.as_str()).expect("proxy json");
assert_eq!(request["jsonrpc"], "2.0");
assert_eq!(request["method"], "publish.event");
let event = &request["params"]["event"];
@@ -558,7 +568,7 @@ fn handle_radrootsd_proxy_connection(mut stream: TcpStream, expected_token: &str
);
stream
.write_all(raw_response.as_bytes())
- .expect("write radrootsd proxy response");
+ .expect("write proxy response");
request
}
@@ -702,8 +712,6 @@ fn seed_app_farm_record(
event_sig: None,
raw_event_json: None,
outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
},
sandbox,
);
@@ -889,8 +897,6 @@ fn seed_app_listing_record_identity_variant(
event_sig: None,
raw_event_json: None,
outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
},
sandbox,
);
@@ -1098,8 +1104,6 @@ fn seed_app_order_record_variant_with_record_id(
event_sig: None,
raw_event_json: None,
outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
},
sandbox,
);
@@ -1194,13 +1198,6 @@ fn append_app_signed_order_request_record(
.iter()
.map(|tag| tag.as_slice().to_vec())
.collect::<Vec<_>>();
- let delivery = RelayDeliveryEvidence::acknowledged(
- [ORDERABLE_LISTING_RELAY],
- [ORDERABLE_LISTING_RELAY],
- [ORDERABLE_LISTING_RELAY],
- Vec::new(),
- )
- .expect("order request delivery evidence");
let record_id = format!("app:signed_event:{event_id}");
append_app_local_record(
LocalEventRecordInput {
@@ -1235,8 +1232,6 @@ fn append_app_signed_order_request_record(
"sig": event.sig.to_string(),
})),
outbox_status: PublishOutboxStatus::Acknowledged,
- relay_set_fingerprint: canonical_relay_set_fingerprint([ORDERABLE_LISTING_RELAY]),
- relay_delivery_json: Some(delivery.to_json_value().expect("delivery json")),
},
sandbox,
);
@@ -1255,7 +1250,8 @@ fn root_help_exposes_only_target_namespaces() {
"config",
"account",
"signer",
- "relay",
+ "transport",
+ "mesh",
"store",
"sync",
"farm",
@@ -1263,6 +1259,7 @@ fn root_help_exposes_only_target_namespaces() {
"market",
"basket",
"trade",
+ "validation",
] {
assert!(
help_lists(&stdout, namespace),
@@ -1271,8 +1268,8 @@ fn root_help_exposes_only_target_namespaces() {
}
for removed in [
- "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "product",
- "runtime", "job", "message", "approval", "agent",
+ "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc", "relay",
+ "product", "runtime", "job", "message", "approval", "agent",
] {
assert!(
!help_lists(&stdout, removed),
@@ -1282,18 +1279,16 @@ fn root_help_exposes_only_target_namespaces() {
}
#[test]
-fn root_help_explains_publish_transports() {
+fn root_help_explains_transport_and_mesh_surfaces() {
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("direct_nostr_relay publishes directly to configured relays"));
- assert!(stdout.contains("radrootsd_proxy publishes locally signed events"));
+ assert!(stdout.contains("Manage transport profiles and outbox delivery."));
+ assert!(stdout.contains("Inspect mesh scope and Reticulum preview policy."));
assert!(stdout.contains("Inspect local readiness and mode-specific recovery steps"));
- assert!(stdout.contains(
- "Select direct_nostr_relay direct relay publish or radrootsd_proxy daemon proxy publish"
- ));
+ assert!(stdout.contains("Show effective configuration and publish-plane readiness."));
}
fn help_lists(stdout: &str, command: &str) -> bool {
@@ -1325,22 +1320,19 @@ fn removed_global_flags_are_rejected_publicly() {
}
#[test]
-fn config_get_exposes_radrootsd_proxy_missing_token_state() {
+fn config_get_exposes_proxy_missing_token_state() {
let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
+ write_proxy_transport_config(&sandbox, "");
let value = sandbox.json_success(&["--format", "json", "config", "get"]);
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
+ assert_eq!(value["result"]["publish"]["transport"], "proxy");
assert_eq!(
value["result"]["publish"]["source"],
- "user config · local first"
- );
- assert_eq!(
- value["result"]["publish"]["transport_family"],
- "radrootsd_proxy"
+ "user config · transport profile"
);
+ assert_eq!(value["result"]["publish"]["transport_family"], "proxy");
assert_eq!(value["result"]["publish"]["state"], "unconfigured");
assert_eq!(value["result"]["publish"]["executable"], false);
assert_contains(
@@ -1353,58 +1345,52 @@ fn config_get_exposes_radrootsd_proxy_missing_token_state() {
);
assert_eq!(
value["result"]["publish"]["provider"]["provider_runtime_id"],
- "radrootsd_proxy"
+ "proxy"
);
assert_eq!(
value["result"]["write_plane"]["provider_runtime_id"],
- "radrootsd_proxy"
+ "proxy"
);
assert_eq!(
value["result"]["write_plane"]["binding_model"],
- "daemon_proxy_publish"
+ "proxy_transport"
);
assert_eq!(value["result"]["write_plane"]["state"], "unconfigured");
+ assert_eq!(value["result"]["transport"]["profile_id"], "proxy");
assert_eq!(
- value["result"]["radrootsd_proxy"]["token_file_configured"],
- false
- );
- assert_eq!(
- value["result"]["radrootsd_proxy"]["token_secret_id_configured"],
- false
+ value["result"]["transport"]["proxy_url"],
+ "http://127.0.0.1:7070"
);
assert_eq!(
value["result"]["actions"][0],
- "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
+ "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"
);
assert_eq!(
value["next_actions"][0]["env_var"],
- "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE"
+ "RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE"
);
}
#[test]
-fn config_get_radrootsd_proxy_with_token_file_reports_ready_transport() {
+fn config_get_proxy_with_token_file_reports_ready_transport() {
let sandbox = RadrootsCliSandbox::new();
- 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_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");
+ let token_file = proxy_token_file(&sandbox);
+ write_proxy_transport_config(
+ &sandbox,
+ format!(
+ "token_file = \"{}\"\n",
+ toml_string(token_file.display().to_string().as_str())
+ )
+ .as_str(),
+ );
- assert!(output.status.success());
+ let value = sandbox.json_success(&["--format", "json", "config", "get"]);
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
+ assert_eq!(value["result"]["publish"]["transport"], "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"]["radrootsd_proxy"]["token_file_configured"],
- true
- );
+ assert_eq!(value["result"]["transport"]["profile_id"], "proxy");
assert_eq!(
value["result"]["actions"]
.as_array()
@@ -1415,11 +1401,15 @@ fn config_get_radrootsd_proxy_with_token_file_reports_ready_transport() {
}
#[test]
-fn config_get_marks_radrootsd_proxy_unconfigured_with_incomplete_myc_signer() {
+fn config_get_marks_proxy_unconfigured_with_incomplete_myc_signer() {
let sandbox = RadrootsCliSandbox::new();
- sandbox.write_app_config(
- r#"[publish]
-transport = "radrootsd_proxy"
+ let token_file = proxy_token_file(&sandbox);
+ sandbox.write_app_config(&format!(
+ r#"[transport]
+profile = "proxy"
+
+[transport.proxy]
+token_file = "{}"
[signer]
backend = "myc"
@@ -1431,19 +1421,12 @@ target_kind = "explicit_endpoint"
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_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");
+ toml_string(token_file.display().to_string().as_str())
+ ));
- assert!(output.status.success());
+ let value = sandbox.json_success(&["--format", "json", "config", "get"]);
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
+ assert_eq!(value["result"]["publish"]["transport"], "proxy");
assert_eq!(value["result"]["publish"]["state"], "unconfigured");
assert_eq!(value["result"]["publish"]["executable"], false);
assert_contains(&value["result"]["publish"]["reason"], "signer.remote_nip46");
@@ -1457,21 +1440,12 @@ signer_session_ref = "session_ready"
#[test]
fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() {
let sandbox = RadrootsCliSandbox::new();
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:19001");
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:19001",
- "config",
- "get",
- ]);
+ let value = sandbox.json_success(&["--format", "json", "config", "get"]);
assert_eq!(value["operation_id"], "config.get");
- assert_eq!(
- value["result"]["publish"]["transport"],
- "direct_nostr_relay"
- );
+ assert_eq!(value["result"]["publish"]["transport"], "nostr");
assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
assert_eq!(value["result"]["publish"]["signed_write_required"], true);
assert_eq!(value["result"]["publish"]["state"], "unconfigured");
@@ -1486,11 +1460,11 @@ fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() {
);
assert_eq!(
value["result"]["write_plane"]["provider_runtime_id"],
- "direct_nostr_relay"
+ "nostr"
);
assert_eq!(
value["result"]["write_plane"]["binding_model"],
- "direct_relay_publish"
+ "nostr_transport"
);
assert_eq!(value["result"]["write_plane"]["state"], "unconfigured");
assert_eq!(value["result"]["rpc"], Value::Null);
@@ -1503,37 +1477,18 @@ fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() {
value["next_actions"][0]["command"],
"radroots account create"
);
- assert_no_daemon_runtime_reference(
- &value,
- &[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:19001",
- "config",
- "get",
- ],
- );
+ assert_no_daemon_runtime_reference(&value, &["--format", "json", "config", "get"]);
}
#[test]
fn config_get_marks_relay_publish_ready_with_secret_backed_local_account() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:19002");
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:19002",
- "config",
- "get",
- ]);
+ let value = sandbox.json_success(&["--format", "json", "config", "get"]);
- assert_eq!(
- value["result"]["publish"]["transport"],
- "direct_nostr_relay"
- );
+ assert_eq!(value["result"]["publish"]["transport"], "nostr");
assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
assert_eq!(value["result"]["publish"]["signed_write_required"], true);
assert_eq!(value["result"]["publish"]["state"], "ready");
@@ -1546,21 +1501,11 @@ fn config_get_marks_relay_publish_ready_with_secret_backed_local_account() {
fn config_get_marks_relay_publish_unconfigured_with_missing_myc_binding() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.write_app_config("[signer]\nbackend = \"myc\"\n");
+ sandbox.write_app_config("[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"ws://127.0.0.1:19003\"]\n\n[signer]\nbackend = \"myc\"\n");
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:19003",
- "config",
- "get",
- ]);
+ let value = sandbox.json_success(&["--format", "json", "config", "get"]);
- assert_eq!(
- value["result"]["publish"]["transport"],
- "direct_nostr_relay"
- );
+ assert_eq!(value["result"]["publish"]["transport"], "nostr");
assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
assert_eq!(value["result"]["publish"]["signed_write_required"], true);
assert_eq!(value["result"]["publish"]["state"], "unconfigured");
@@ -1592,14 +1537,8 @@ fn config_get_marks_relay_publish_unconfigured_with_watch_only_account() {
public_identity_file.to_string_lossy().as_ref(),
]);
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:19004",
- "config",
- "get",
- ]);
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:19004");
+ let value = sandbox.json_success(&["--format", "json", "config", "get"]);
assert_eq!(value["result"]["publish"]["relay"]["ready"], true);
assert_eq!(value["result"]["publish"]["signed_write_required"], true);
@@ -1612,9 +1551,9 @@ fn config_get_marks_relay_publish_unconfigured_with_watch_only_account() {
fn health_surfaces_publish_state_under_missing_myc_binding() {
let sandbox = RadrootsCliSandbox::new();
let missing_myc = sandbox.root().join("bin/missing-myc");
- let token_file = radrootsd_proxy_token_file(&sandbox);
+ let token_file = proxy_token_file(&sandbox);
sandbox.write_app_config(&format!(
- "[publish]\ntransport = \"radrootsd_proxy\"\n\n[publish.radrootsd_proxy]\ntoken_file = \"{}\"\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
+ "[transport]\nprofile = \"proxy\"\n\n[transport.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())
));
@@ -1623,7 +1562,7 @@ fn health_surfaces_publish_state_under_missing_myc_binding() {
assert_eq!(value["operation_id"], "health.status.get");
assert_eq!(value["result"]["state"], "needs_attention");
- assert_eq!(value["result"]["publish"]["transport"], "radrootsd_proxy");
+ assert_eq!(value["result"]["publish"]["transport"], "proxy");
assert_eq!(value["result"]["publish"]["executable"], false);
assert_eq!(
value["result"]["publish"]["provider"]["state"],
@@ -1656,16 +1595,9 @@ fn health_surfaces_publish_state_under_missing_myc_binding() {
#[test]
fn health_status_distinguishes_relay_ready_from_missing_signed_write_account() {
let sandbox = RadrootsCliSandbox::new();
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:19005");
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:19005",
- "health",
- "status",
- "get",
- ]);
+ let value = sandbox.json_success(&["--format", "json", "health", "status", "get"]);
assert_eq!(value["operation_id"], "health.status.get");
assert_eq!(value["result"]["state"], "needs_attention");
@@ -1690,7 +1622,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]\ntransport = \"radrootsd_proxy\"\n");
+ write_proxy_transport_config(&sandbox, "");
let value = sandbox.json_success(&["--format", "json", "health", "check", "run"]);
@@ -1701,10 +1633,7 @@ fn health_check_exposes_publish_readiness() {
"unresolved"
);
assert_eq!(value["result"]["account_resolution"]["source"], "none");
- assert_eq!(
- value["result"]["checks"]["publish"]["transport"],
- "radrootsd_proxy"
- );
+ assert_eq!(value["result"]["checks"]["publish"]["transport"], "proxy");
assert_eq!(
value["result"]["checks"]["publish"]["state"],
"unconfigured"
@@ -1724,7 +1653,7 @@ fn health_check_exposes_publish_readiness() {
assert_eq!(value["result"]["actions"][0], "radroots account create");
assert_eq!(
value["result"]["actions"][1],
- "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
+ "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"
);
assert_eq!(
value["next_actions"][0]["command"],
@@ -1732,11 +1661,11 @@ fn health_check_exposes_publish_readiness() {
);
assert_eq!(
value["next_actions"][1]["description"],
- "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"
+ "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"
);
assert_eq!(
value["next_actions"][1]["env_var"],
- "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE"
+ "RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE"
);
assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
}
@@ -1746,16 +1675,9 @@ fn health_check_marks_relay_publish_ready_with_secret_backed_local_account() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "workspace", "init"]);
sandbox.json_success(&["--format", "json", "account", "create"]);
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:19006");
- let value = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:19006",
- "health",
- "check",
- "run",
- ]);
+ let value = sandbox.json_success(&["--format", "json", "health", "check", "run"]);
assert_eq!(value["operation_id"], "health.check.run");
assert_eq!(value["result"]["state"], "ready");
@@ -1772,10 +1694,7 @@ 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"]["transport"],
- "direct_nostr_relay"
- );
+ assert_eq!(value["result"]["checks"]["publish"]["transport"], "nostr");
assert_eq!(value["result"]["checks"]["publish"]["state"], "ready");
assert_eq!(value["result"]["checks"]["publish"]["executable"], true);
assert_eq!(
@@ -1817,56 +1736,49 @@ fn farm_readiness_check_reports_mode_specific_publish_gates() {
} else {
&relay_value["result"]
};
- assert_eq!(relay_detail["publish_transport"], "direct_nostr_relay");
+ assert_eq!(relay_detail["publish_transport"], "nostr");
assert_eq!(relay_detail["publish_state"], "unconfigured");
assert_eq!(relay_detail["publish_executable"], false);
- assert_eq!(relay_detail["missing"][0], "Configured relay");
+ assert_eq!(
+ relay_detail["missing"][0],
+ "Configured Nostr transport profile"
+ );
- sandbox.write_app_config(
- r#"[[capability_binding]]
+ let proxy_token_path = proxy_token_file(&sandbox);
+ sandbox.write_app_config(&format!(
+ r#"[transport]
+profile = "proxy"
+
+[transport.proxy]
+token_file = "{}"
+
+[[capability_binding]]
capability = "signer.remote_nip46"
provider = "myc"
target_kind = "explicit_endpoint"
target = "http://myc.invalid"
signer_session_ref = "session_test"
"#,
- );
+ toml_string(proxy_token_path.display().to_string().as_str())
+ ));
let output = sandbox
.command()
- .env(
- "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE",
- radrootsd_proxy_token_file(&sandbox),
- )
- .args([
- "--format",
- "json",
- "--publish-transport",
- "radrootsd_proxy",
- "farm",
- "readiness",
- "check",
- ])
+ .args(["--format", "json", "farm", "readiness", "check"])
.output()
- .expect("run radrootsd proxy farm readiness");
- let radrootsd_value: Value = serde_json::from_slice(&output.stdout).expect("json output");
+ .expect("run proxy farm readiness");
+ let proxy_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_contains(
- &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 farm publish"
- );
+ assert_eq!(proxy_value["operation_id"], "farm.readiness.check");
+ assert_contains(&proxy_value["result"]["publish_transport"], "proxy");
+ assert_eq!(proxy_value["result"]["publish_state"], "ready");
+ assert_eq!(proxy_value["result"]["publish_executable"], true);
+ assert_eq!(proxy_value["result"]["reason"], Value::Null);
+ assert_eq!(proxy_value["result"]["actions"][0], "radroots farm publish");
}
#[test]
-fn radrootsd_proxy_listing_publish_update_and_archive_dry_run_without_direct_publish_relays() {
+fn proxy_listing_publish_update_and_archive_dry_run_without_direct_publish_relays() {
for operation in ["publish", "update", "archive"] {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
@@ -1888,8 +1800,7 @@ fn radrootsd_proxy_listing_publish_update_and_archive_dry_run_without_direct_pub
"--delivery-method",
"pickup",
]);
- let listing_file =
- create_listing_draft(&sandbox, format!("radrootsd-proxy-{operation}").as_str());
+ let listing_file = create_listing_draft(&sandbox, format!("proxy-{operation}").as_str());
make_listing_publishable(
&listing_file,
farm["result"]["config"]["farm_d_tag"]
@@ -1897,17 +1808,17 @@ fn radrootsd_proxy_listing_publish_update_and_archive_dry_run_without_direct_pub
.expect("farm d tag"),
);
+ let token_file = proxy_token_file(&sandbox);
+ sandbox.write_app_config(&format!(
+ "[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\ntoken_file = \"{}\"\n",
+ toml_string(token_file.display().to_string().as_str())
+ ));
+
let output = sandbox
.command()
- .env(
- "RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE",
- radrootsd_proxy_token_file(&sandbox),
- )
.args([
"--format",
"json",
- "--publish-transport",
- "radrootsd_proxy",
"--dry-run",
"listing",
operation,
@@ -1940,16 +1851,16 @@ fn radrootsd_proxy_listing_publish_update_and_archive_dry_run_without_direct_pub
}
#[test]
-fn radrootsd_proxy_listing_publish_non_dry_run_uses_local_jsonrpc_server() {
+fn proxy_listing_publish_non_dry_run_uses_local_jsonrpc_server() {
let sandbox = RadrootsCliSandbox::new();
- let proxy = RadrootsdProxyJsonRpcServer::once("proxy_test_token");
- let token_file = radrootsd_proxy_token_file(&sandbox);
+ let proxy = ProxyJsonRpcServer::once(None);
+ let token_file = proxy_token_file(&sandbox);
sandbox.write_app_config(
format!(
- r#"[publish]
-transport = "radrootsd_proxy"
+ r#"[transport]
+profile = "proxy"
-[publish.radrootsd_proxy]
+[transport.proxy]
url = "{}"
token_file = "{}"
"#,
@@ -1977,7 +1888,7 @@ token_file = "{}"
"--delivery-method",
"pickup",
]);
- let listing_file = create_listing_draft(&sandbox, "radrootsd-proxy-live");
+ let listing_file = create_listing_draft(&sandbox, "proxy-live");
make_listing_publishable(
&listing_file,
farm["result"]["config"]["farm_d_tag"]
@@ -2118,6 +2029,25 @@ fn direct_publish_listing_uses_myc_nip46_sdk_signer() {
.expect("farm d tag"),
);
+ sandbox.write_app_config(
+ format!(
+ r#"[transport]
+profile = "nostr"
+
+[transport.nostr]
+relay_urls = ["{}"]
+
+{}"#,
+ toml_string(relay_endpoint.as_str()),
+ myc_nip46_config(
+ remote_pubkey.as_str(),
+ relay_endpoint.as_str(),
+ account_id,
+ "session_ready",
+ )
+ )
+ .as_str(),
+ );
let output = sandbox
.command()
.args([
@@ -2125,8 +2055,6 @@ fn direct_publish_listing_uses_myc_nip46_sdk_signer() {
"json",
"--approval-token",
"approve",
- "--relay",
- relay_endpoint.as_str(),
"listing",
"publish",
listing_file.to_string_lossy().as_ref(),
@@ -2163,7 +2091,7 @@ fn direct_publish_listing_uses_myc_nip46_sdk_signer() {
}
#[test]
-fn radrootsd_proxy_listing_publish_uses_myc_nip46_sdk_signer() {
+fn proxy_listing_publish_uses_myc_nip46_sdk_signer() {
let sandbox = RadrootsCliSandbox::new();
let user_identity = identity_secret(93);
let client_identity = identity_secret(94);
@@ -2177,8 +2105,8 @@ fn radrootsd_proxy_listing_publish_uses_myc_nip46_sdk_signer() {
user_keys,
Nip46RelayFinish::SignResponse,
);
- let proxy = RadrootsdProxyJsonRpcServer::once("proxy_test_token");
- let token_file = radrootsd_proxy_token_file(&sandbox);
+ let proxy = ProxyJsonRpcServer::once(None);
+ let token_file = proxy_token_file(&sandbox);
let public_identity_file =
write_public_identity_profile(&sandbox, "myc-proxy-user", &user_public);
let imported = sandbox.json_success(&[
@@ -2200,10 +2128,10 @@ fn radrootsd_proxy_listing_publish_uses_myc_nip46_sdk_signer() {
client_identity.secret_key_hex().as_str(),
);
let config = format!(
- r#"[publish]
-transport = "radrootsd_proxy"
+ r#"[transport]
+profile = "proxy"
-[publish.radrootsd_proxy]
+[transport.proxy]
url = "{}"
token_file = "{}"
@@ -2236,7 +2164,7 @@ token_file = "{}"
"--delivery-method",
"pickup",
]);
- let listing_file = create_listing_draft(&sandbox, "myc-radrootsd-proxy-live");
+ let listing_file = create_listing_draft(&sandbox, "myc-proxy-live");
make_listing_publishable(
&listing_file,
farm["result"]["config"]["farm_d_tag"]
@@ -2318,11 +2246,10 @@ fn listing_update_publish_attempts_direct_publish_with_approval() {
.expect("farm d tag"),
);
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"listing",
@@ -2764,9 +2691,9 @@ fn next_actions_mirror_result_actions_for_json_and_ndjson() {
&["--format", "ndjson", "health", "status", "get"][..],
&["--format", "ndjson", "health", "check", "run"][..],
] {
- let daemon = RadrootsCliSandbox::new();
- daemon.write_app_config("[publish]\ntransport = \"radrootsd_proxy\"\n");
- let output = daemon.command().args(args).output().expect("run ndjson");
+ let proxy = RadrootsCliSandbox::new();
+ write_proxy_transport_config(&proxy, "");
+ let output = proxy.command().args(args).output().expect("run ndjson");
let frames = ndjson_from_stdout(&output);
let terminal = frames.last().expect("terminal ndjson frame");
@@ -2777,7 +2704,7 @@ fn next_actions_mirror_result_actions_for_json_and_ndjson() {
.expect("next actions")
.iter()
.any(|action| action["description"]
- == "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID"),
+ == "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"),
"{args:?}"
);
}
@@ -2959,10 +2886,11 @@ fn environment_human_output_format_is_rejected() {
#[test]
fn terminal_health_status_surfaces_publish_reason_and_actions() {
let sandbox = RadrootsCliSandbox::new();
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:19007");
let output = sandbox
.command()
- .args(["--relay", "ws://127.0.0.1:19007", "health", "status", "get"])
+ .args(["health", "status", "get"])
.output()
.expect("run terminal health status");
@@ -2971,7 +2899,7 @@ fn terminal_health_status_surfaces_publish_reason_and_actions() {
assert!(stdout.starts_with("! Health needs attention\n"));
assert!(stdout.contains("Publish unconfigured"));
- assert!(stdout.contains("Reason direct_nostr_relay publish transport requires a selected or default write-capable local account for signed writes"));
+ assert!(stdout.contains("Reason Nostr transport profile requires a selected or default write-capable local account for signed writes"));
assert!(stdout.contains("Next\n radroots account create"));
assert!(serde_json::from_str::<Value>(&stdout).is_err());
}
@@ -3348,16 +3276,9 @@ fn offline_allows_supported_external_dry_run() {
assert_eq!(publish["result"]["state"], "dry_run");
sandbox.json_success(&["--format", "json", "store", "init"]);
- let sync_push = sandbox.json_success(&[
- "--format",
- "json",
- "--offline",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "sync",
- "push",
- ]);
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
+ let sync_push =
+ sandbox.json_success(&["--format", "json", "--offline", "--dry-run", "sync", "push"]);
assert_eq!(sync_push["operation_id"], "sync.push");
assert_eq!(sync_push["result"]["state"], "ready");
@@ -3392,13 +3313,12 @@ fn offline_listing_publish_enqueues_sdk_outbox_without_direct_publish_push() {
make_listing_publishable(&listing_file, farm_d_tag);
let relay = "ws://127.0.0.1:9";
let local_event_records_before_publish = sandbox.local_event_records().len();
+ configure_nostr_transport(&sandbox, relay);
let publish = sandbox.json_success(&[
"--format",
"json",
"--offline",
- "--relay",
- relay,
"--approval-token",
"approve",
"listing",
@@ -3462,13 +3382,12 @@ fn listing_publish_idempotency_conflict_maps_sdk_partial_mutation_recovery() {
make_listing_publishable(&listing_file, farm_d_tag);
let relay = "ws://127.0.0.1:9";
let idempotency_key = "listing-idem-conflict";
+ configure_nostr_transport(&sandbox, relay);
sandbox.json_success(&[
"--format",
"json",
"--offline",
- "--relay",
- relay,
"--approval-token",
"approve",
"--idempotency-key",
@@ -3488,8 +3407,6 @@ fn listing_publish_idempotency_conflict_maps_sdk_partial_mutation_recovery() {
"--format",
"json",
"--offline",
- "--relay",
- relay,
"--approval-token",
"approve",
"--idempotency-key",
@@ -3776,7 +3693,7 @@ fn online_requires_relay_for_external_network_operations() {
value["errors"][0]["message"]
.as_str()
.expect("message")
- .contains("requires at least one configured relay")
+ .contains("requires a delivery-capable transport profile")
);
}
}
@@ -3807,16 +3724,9 @@ fn order_status_get_uses_sdk_local_projection_without_relay_fetch() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind closed relay");
let closed_relay = format!("ws://{}", listener.local_addr().expect("relay addr"));
drop(listener);
- let with_closed_relay = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- closed_relay.as_str(),
- "trade",
- "status",
- "get",
- "ord_missing",
- ]);
+ configure_nostr_transport(&sandbox, closed_relay.as_str());
+ let with_closed_relay =
+ sandbox.json_success(&["--format", "json", "trade", "status", "get", "ord_missing"]);
assert_eq!(with_closed_relay["operation_id"], "trade.status.get");
assert_eq!(with_closed_relay["result"]["state"], "missing");
@@ -3848,18 +3758,17 @@ fn order_status_get_invalid_order_id_uses_sdk_error_contract() {
}
#[test]
-fn legacy_radrootsd_publish_transport_value_is_rejected() {
+fn removed_publish_transport_flag_is_rejected() {
let sandbox = RadrootsCliSandbox::new();
let output = sandbox
.command()
- .args(["--publish-transport", "radrootsd", "sync", "push"])
+ .args(["--publish-transport", "proxy", "sync", "push"])
.output()
- .expect("run legacy publish transport");
+ .expect("run removed publish transport flag");
let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
assert!(!output.status.success());
- assert!(stderr.contains("invalid value"));
- assert!(stderr.contains("radrootsd_proxy"));
+ assert!(stderr.contains("unexpected argument") || stderr.contains("unrecognized"));
}
#[test]
@@ -3890,7 +3799,7 @@ fn online_order_event_watch_returns_deferred_without_relay_preflight() {
!value["errors"][0]["message"]
.as_str()
.expect("message")
- .contains("configured relay")
+ .contains("configured Nostr relay")
);
assert_no_daemon_runtime_reference(&value, &["trade", "event", "watch"]);
}
@@ -5855,12 +5764,11 @@ fn farm_publish_uses_sdk_outbox_without_legacy_signed_event_records() {
]);
let relay_url = "ws://127.0.0.1:9";
let local_event_records_before_publish = sandbox.local_event_records().len();
+ configure_nostr_transport(&sandbox, relay_url);
let (output, publish) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay_url,
"--approval-token",
"approve",
"farm",
@@ -5925,12 +5833,11 @@ fn listing_publish_failure_uses_sdk_outbox_without_legacy_local_event_record() {
make_listing_publishable(&listing_file, farm_d_tag);
let relay_url = "ws://127.0.0.1:9";
let local_event_records_before_publish = sandbox.local_event_records().len();
+ configure_nostr_transport(&sandbox, relay_url);
let (output, publish) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay_url,
"--approval-token",
"approve",
"listing",
@@ -5999,12 +5906,11 @@ fn sync_push_sdk_outbox_failure_reports_network_unavailable() {
let listing_file = create_listing_draft(&sandbox, "sync-sdk-push-eggs");
make_listing_publishable(&listing_file, farm_d_tag);
let relay = "ws://127.0.0.1:9";
+ configure_nostr_transport(&sandbox, relay);
let publish = sandbox.json_success(&[
"--format",
"json",
"--offline",
- "--relay",
- relay,
"--approval-token",
"approve",
"listing",
@@ -6029,8 +5935,6 @@ fn sync_push_sdk_outbox_failure_reports_network_unavailable() {
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- relay,
"--approval-token",
"approve",
"sync",
@@ -6089,8 +5993,6 @@ fn sync_push_ignores_derived_projection_pending_queue_for_sdk_canonical_push() {
let value = sandbox.json_success(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"sync",
@@ -6152,15 +6054,9 @@ fn buyer_market_sync_basket_dry_runs_preflight_without_mutating_local_state() {
assert_eq!(sync_push["result"]["published_count"], 0);
sandbox.json_success(&["--format", "json", "store", "init"]);
- let relay_refresh = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "market",
- "refresh",
- ]);
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
+ let relay_refresh =
+ sandbox.json_success(&["--format", "json", "--dry-run", "market", "refresh"]);
assert_eq!(relay_refresh["operation_id"], "market.refresh");
assert_eq!(relay_refresh["dry_run"], true);
assert_eq!(relay_refresh["result"]["state"], "ready");
@@ -6171,15 +6067,7 @@ fn buyer_market_sync_basket_dry_runs_preflight_without_mutating_local_state() {
assert_eq!(relay_refresh["result"]["fetched_count"], 0);
assert_eq!(relay_refresh["result"]["ingested_count"], 0);
- let sync_push_ready = sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- "ws://127.0.0.1:9",
- "--dry-run",
- "sync",
- "push",
- ]);
+ let sync_push_ready = sandbox.json_success(&["--format", "json", "--dry-run", "sync", "push"]);
assert_eq!(sync_push_ready["operation_id"], "sync.push");
assert_eq!(sync_push_ready["dry_run"], true);
assert_eq!(sync_push_ready["result"]["state"], "ready");
@@ -6288,7 +6176,9 @@ fn buyer_market_sync_basket_dry_runs_preflight_without_mutating_local_state() {
#[test]
fn market_order_request_readiness_gates_buyer_intent_actions() {
let sandbox = RadrootsCliSandbox::new();
+ configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
seed_orderable_listing(&sandbox, LISTING_ADDR);
+ seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
let search = sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
assert_eq!(search["operation_id"], "market.product.search");
@@ -6533,11 +6423,7 @@ fn required_approval_token_rejects_absent_empty_and_whitespace_values() {
"listing.archive",
&["listing", "archive", "missing-listing.toml"],
);
- assert_required_approval_token_rejected(
- &sandbox,
- "sync.push",
- &["--relay", "ws://127.0.0.1:9", "sync", "push"],
- );
+ assert_required_approval_token_rejected(&sandbox, "sync.push", &["sync", "push"]);
assert_required_approval_token_rejected(&sandbox, "trade.submit", &["trade", "submit"]);
assert_required_approval_token_rejected(
&sandbox,
@@ -6642,7 +6528,9 @@ fn order_submit_missing_order_returns_not_found_while_read_view_stays_successful
fn create_ready_order(sandbox: &RadrootsCliSandbox, basket_id: &str) -> String {
sandbox.json_success(&["--format", "json", "account", "create"]);
+ configure_nostr_transport(sandbox, ORDERABLE_LISTING_RELAY);
seed_orderable_listing(sandbox, LISTING_ADDR);
+ seed_market_refresh_provenance(sandbox, &[ORDERABLE_LISTING_RELAY]);
sandbox.json_success(&["--format", "json", "basket", "create", basket_id]);
sandbox.json_success(&[
"--format",
@@ -6659,6 +6547,7 @@ fn create_ready_order(sandbox: &RadrootsCliSandbox, basket_id: &str) -> String {
"2",
]);
let quote = sandbox.json_success(&["--format", "json", "basket", "quote", "create", basket_id]);
+ sandbox.write_app_config("[transport]\nprofile = \"local_only\"\n");
quote["result"]["quote"]["trade_id"]
.as_str()
.expect("order id")
@@ -6766,7 +6655,9 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
assert_eq!(signer["result"]["signer_account_id"], account_id);
assert_no_removed_command_reference(&signer, &["signer", "status", "get"]);
+ configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
+ seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
let search = sandbox.json_success(&["--format", "json", "market", "product", "search", "eggs"]);
assert_eq!(search["operation_id"], "market.product.search");
@@ -6879,7 +6770,10 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
assert_eq!(submit["result"]["state"], "dry_run");
assert_eq!(submit["result"]["source"], "SDK trade submit · local key");
assert_eq!(submit["result"]["event_kind"], 3422);
- assert!(submit["result"]["target_relays"].is_null());
+ assert_eq!(
+ submit["result"]["target_relays"][0],
+ ORDERABLE_LISTING_RELAY
+ );
assert_eq!(
submit["result"]["event_id"]
.as_str()
@@ -6890,6 +6784,7 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
assert_no_removed_command_reference(&submit, &["trade", "submit", "--dry-run"]);
assert_no_daemon_runtime_reference(&submit, &["trade", "submit", "--dry-run"]);
+ sandbox.write_app_config("[transport]\nprofile = \"local_only\"\n");
let (output, unavailable_submit) = sandbox.json_output(&[
"--format",
"json",
@@ -6905,7 +6800,7 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
assert_eq!(unavailable_submit["result"], Value::Null);
assert_eq!(
unavailable_submit["errors"][0]["code"],
- "empty_target_relays"
+ "empty_transport_targets"
);
assert_eq!(
unavailable_submit["errors"][0]["detail"]["class"],
@@ -6913,13 +6808,13 @@ fn buyer_target_flow_acceptance_uses_target_operations() {
);
assert_eq!(
unavailable_submit["errors"][0]["detail"]["detail"]["operation"],
- "sdk relay target set"
+ "publish transport profile"
);
assert!(
unavailable_submit["errors"][0]["message"]
.as_str()
.expect("message")
- .contains("empty target relays")
+ .contains("empty transport targets")
);
assert_no_removed_command_reference(&unavailable_submit, &["trade", "submit"]);
assert_no_daemon_runtime_reference(&unavailable_submit, &["trade", "submit"]);
@@ -7056,7 +6951,9 @@ fn order_get_marks_watch_only_bound_buyer_unready() {
.expect("watch account id");
assert_eq!(imported["result"]["account"]["custody"], "watch_only");
+ configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
seed_orderable_listing(&sandbox, LISTING_ADDR);
+ seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
sandbox.json_success(&["--format", "json", "basket", "create", "watch_buyer"]);
sandbox.json_success(&[
"--format",
@@ -7303,13 +7200,12 @@ fn order_rebind_refuses_visible_published_request() {
.as_str()
.expect("target account id");
let relay = RelayFetchServer::with_events(vec![event]);
+ configure_nostr_transport(&sandbox, relay.endpoint());
let (output, value) = sandbox.json_output(&[
"--format",
"json",
"--dry-run",
- "--relay",
- relay.endpoint(),
"trade",
"rebind",
order_id,
@@ -7413,13 +7309,12 @@ fn order_status_and_event_list_use_draft_context_after_account_override_drift()
assert_eq!(status["result"]["decoded_count"], 0);
let event_list_relay = RelayFetchServer::with_events(vec![event]);
+ configure_nostr_transport(&sandbox, event_list_relay.endpoint());
let events = sandbox.json_success(&[
"--format",
"json",
"--account-id",
drift_account_id,
- "--relay",
- event_list_relay.endpoint(),
"trade",
"event",
"list",
@@ -7518,12 +7413,9 @@ fn order_cancel_uses_bound_buyer_after_default_account_drift() {
assert!(!cancel_output.status.success());
assert_eq!(cancel["operation_id"], "trade.cancel");
assert_eq!(cancel["result"], Value::Null);
- assert_eq!(cancel["errors"][0]["code"], "invalid_request");
- assert_eq!(cancel["errors"][0]["detail"]["class"], "request");
- assert_contains(
- &cancel["errors"][0]["message"],
- "requires a locally projected trade",
- );
+ assert_eq!(cancel["errors"][0]["code"], "empty_transport_targets");
+ assert_eq!(cancel["errors"][0]["detail"]["class"], "configuration");
+ assert_contains(&cancel["errors"][0]["message"], "empty transport targets");
}
#[test]
@@ -7534,6 +7426,7 @@ fn buyer_side_order_writes_reject_conflicting_account_override_for_local_draft()
let drift_account_id = drift_account["result"]["account"]["id"]
.as_str()
.expect("drift account id");
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
for (operation_id, command) in [
(
@@ -7544,8 +7437,6 @@ fn buyer_side_order_writes_reject_conflicting_account_override_for_local_draft()
"--dry-run",
"--account-id",
drift_account_id,
- "--relay",
- "ws://127.0.0.1:9",
"trade",
"revision",
"accept",
@@ -7562,8 +7453,6 @@ fn buyer_side_order_writes_reject_conflicting_account_override_for_local_draft()
"--dry-run",
"--account-id",
drift_account_id,
- "--relay",
- "ws://127.0.0.1:9",
"trade",
"cancel",
order_id.as_str(),
@@ -7592,12 +7481,11 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_replica_freshness_
let sandbox = RadrootsCliSandbox::new();
let order_id = create_ready_order(&sandbox, "freshness_missing_db");
fs::remove_file(sandbox.replica_db_path()).expect("remove replica db");
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"trade",
@@ -7639,12 +7527,11 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_listing_state_gate
let sandbox = RadrootsCliSandbox::new();
let order_id = create_ready_order(&sandbox, "freshness_missing_listing");
remove_orderable_listing(&sandbox, LISTING_ADDR);
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"trade",
@@ -7666,12 +7553,11 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_listing_event_gate
let order_id = create_ready_order(&sandbox, "freshness_superseded_listing");
let replacement_event_id = "3".repeat(64);
replace_latest_listing_event_id(&sandbox, LISTING_ADDR, replacement_event_id.as_str());
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"trade",
@@ -7691,7 +7577,9 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_listing_event_gate
fn order_submit_non_dry_run_uses_sdk_relay_validation_without_quantity_gate() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
+ configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
seed_orderable_listing(&sandbox, LISTING_ADDR);
+ seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
sandbox.json_success(&["--format", "json", "basket", "create", "over_quantity"]);
sandbox.json_success(&[
"--format",
@@ -7718,12 +7606,11 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_quantity_gate() {
let order_id = quote["result"]["quote"]["trade_id"]
.as_str()
.expect("order id");
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"trade",
@@ -7746,12 +7633,11 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_bin_gate() {
let sandbox = RadrootsCliSandbox::new();
let order_id = create_ready_order(&sandbox, "unknown_bin");
rewrite_order_bin(&sandbox, order_id.as_str(), "unknown-bin");
+ configure_nostr_transport(&sandbox, "ws://127.0.0.1:9");
let (output, value) = sandbox.json_output(&[
"--format",
"json",
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"trade",
@@ -7989,7 +7875,9 @@ fn order_submit_dry_run_does_not_use_local_primary_bin_preflight() {
fn order_submit_dry_run_does_not_use_local_quantity_preflight() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
+ configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
seed_orderable_listing(&sandbox, LISTING_ADDR);
+ seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
sandbox.json_success(&["--format", "json", "basket", "create", "dry_over_quantity"]);
sandbox.json_success(&[
"--format",
@@ -8033,7 +7921,9 @@ fn ready_order_submit_dry_run_validates_local_buyer_authority() {
let first_account_id = first["result"]["account"]["id"]
.as_str()
.expect("first account id");
+ configure_nostr_transport(&sandbox, ORDERABLE_LISTING_RELAY);
let listing_event_id = seed_orderable_listing(&sandbox, LISTING_ADDR);
+ seed_market_refresh_provenance(&sandbox, &[ORDERABLE_LISTING_RELAY]);
sandbox.json_success(&["--format", "json", "basket", "create", "ready_order"]);
sandbox.json_success(&[
"--format",
@@ -8077,7 +7967,10 @@ fn ready_order_submit_dry_run_validates_local_buyer_authority() {
assert_eq!(dry_run["result"]["state"], "dry_run");
assert_eq!(dry_run["result"]["source"], "SDK trade submit · local key");
assert_eq!(dry_run["result"]["event_kind"], 3422);
- assert!(dry_run["result"]["target_relays"].is_null());
+ assert_eq!(
+ dry_run["result"]["target_relays"][0],
+ ORDERABLE_LISTING_RELAY
+ );
assert_no_daemon_runtime_reference(&dry_run, &["trade", "submit", "--dry-run"]);
let second = sandbox.json_success(&["--format", "json", "account", "create"]);
@@ -8122,8 +8015,6 @@ fn ready_order_submit_dry_run_validates_local_buyer_authority() {
"json",
"--account-id",
second_account_id,
- "--relay",
- "ws://127.0.0.1:9",
"--approval-token",
"approve",
"trade",
@@ -8275,11 +8166,11 @@ fn seller_target_flow_acceptance_uses_target_operations() {
assert_eq!(unavailable_publish["operation_id"], "listing.publish");
assert_eq!(
unavailable_publish["errors"][0]["code"],
- "empty_target_relays"
+ "network_unavailable"
);
assert_eq!(
unavailable_publish["errors"][0]["detail"]["class"],
- "configuration"
+ "network"
);
assert_no_removed_command_reference(&unavailable_publish, &["listing", "publish"]);
assert_no_daemon_runtime_reference(&unavailable_publish, &["listing", "publish"]);
@@ -8297,11 +8188,11 @@ fn seller_target_flow_acceptance_uses_target_operations() {
assert_eq!(unavailable_archive["operation_id"], "listing.archive");
assert_eq!(
unavailable_archive["errors"][0]["code"],
- "empty_target_relays"
+ "network_unavailable"
);
assert_eq!(
unavailable_archive["errors"][0]["detail"]["class"],
- "configuration"
+ "network"
);
assert_no_removed_command_reference(&unavailable_archive, &["listing", "archive"]);
assert_no_daemon_runtime_reference(&unavailable_archive, &["listing", "archive"]);