commit 5512a8d74905fa805539457e67d5cd72b54d6810
parent 42903a0ae254a778a15240da98babcdad47a4fd9
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Apr 2026 20:44:32 +0000
cli: fail seller relay writes closed
- remove daemon bridge publish paths from farm and listing mutations
- keep seller dry-runs validating local draft and account authority
- return operation_unavailable until direct relay publishing exists
- cover publish and archive outputs without daemon job references
Diffstat:
8 files changed, 182 insertions(+), 833 deletions(-)
diff --git a/src/operation_farm.rs b/src/operation_farm.rs
@@ -136,7 +136,6 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
.clone()
.or_else(|| string_input(&request, "idempotency_key")),
signer_session_id: string_input(&request, "signer_session_id"),
- print_job: bool_input(&request, "print_job").unwrap_or(false),
print_event: bool_input(&request, "print_event").unwrap_or(false),
};
if request.context.requires_approval_token() {
diff --git a/src/operation_listing.rs b/src/operation_listing.rs
@@ -198,7 +198,6 @@ where
.clone()
.or_else(|| string_input(request, "idempotency_key")),
signer_session_id: string_input(request, "signer_session_id"),
- print_job: bool_input(request, "print_job").unwrap_or(false),
print_event: bool_input(request, "print_event").unwrap_or(false),
})
}
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -11,13 +11,12 @@ use radroots_events_codec::profile::encode::to_wire_parts_with_profile_type;
use crate::domain::runtime::{
FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView,
- FarmPublicationView, FarmPublishComponentView, FarmPublishEventView, FarmPublishJobView,
- FarmPublishView, FarmSelectionView, FarmSetView, FarmSetupView, FarmStatusView,
+ FarmPublicationView, FarmPublishComponentView, FarmPublishEventView, FarmPublishView,
+ FarmSelectionView, FarmSetView, FarmSetupView, FarmStatusView,
};
use crate::runtime::RuntimeError;
use crate::runtime::accounts::{self, AccountRecordView};
use crate::runtime::config::RuntimeConfig;
-use crate::runtime::daemon::{self, BridgeEventPublishResult, DaemonRpcError};
use crate::runtime::farm_config::{
self, FarmConfigDocument, FarmConfigScope, FarmConfigSelection, FarmListingDefaults,
FarmMissingField, FarmPublicationStatus, ResolvedFarmConfig, SUPPORTED_FARM_CONFIG_VERSION,
@@ -28,6 +27,9 @@ use crate::runtime_args::{
};
const FARM_CONFIG_SOURCE: &str = "farm config · local first";
+const FARM_WRITE_SOURCE: &str = "direct Nostr relay publish · pending implementation";
+const DIRECT_RELAY_UNAVAILABLE_REASON: &str =
+ "direct Nostr relay publishing is not implemented for farm publish";
static D_TAG_COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -345,21 +347,18 @@ pub fn publish(
let profile_idempotency_key = component_idempotency_key(args, "profile")?;
let farm_idempotency_key = component_idempotency_key(args, "farm")?;
- let signer_authority = match resolve_farm_write_authority(config, account_pubkey.as_str()) {
- Ok(authority) => authority,
- Err(error) => {
- return Ok(binding_error_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- error,
- ));
- }
- };
+ if let Err(error) = resolve_farm_write_authority(config, account_pubkey.as_str()) {
+ return Ok(binding_error_publish_view(
+ config,
+ args,
+ &resolved,
+ &account_pubkey,
+ previews,
+ profile_idempotency_key,
+ farm_idempotency_key,
+ error,
+ ));
+ }
if config.output.dry_run {
return Ok(base_publish_view(
@@ -369,163 +368,35 @@ pub fn publish(
&resolved,
&account_pubkey,
preview_component(
- "bridge.profile.publish",
+ "relay.profile.publish",
KIND_PROFILE,
profile_idempotency_key,
args,
Some(previews.profile),
),
preview_component(
- "bridge.farm.publish",
+ "relay.farm.publish",
KIND_FARM,
farm_idempotency_key,
args,
Some(previews.farm),
),
- Some("dry run requested; daemon farm publish skipped".to_owned()),
+ Some("dry run requested; relay publish skipped".to_owned()),
vec![format!(
"radroots farm publish --scope {}",
resolved.scope.as_str()
)],
));
}
- let profile_signer_session_id = match daemon::resolve_signer_session_id(
- config,
- "farm profile",
- account_pubkey.as_str(),
- KIND_PROFILE,
- args.signer_session_id.as_deref(),
- signer_authority.as_ref(),
- ) {
- Ok(session_id) => session_id,
- Err(error) => {
- return Ok(daemon_error_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- error,
- FarmPublishFailureStage::Profile,
- ));
- }
- };
- let farm_signer_session_id = match daemon::resolve_signer_session_id(
- config,
- "farm",
- account_pubkey.as_str(),
- KIND_FARM,
- Some(profile_signer_session_id.as_str()),
- signer_authority.as_ref(),
- ) {
- Ok(session_id) => session_id,
- Err(error) => {
- return Ok(daemon_error_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- error,
- FarmPublishFailureStage::Farm,
- ));
- }
- };
-
- let profile_result = match daemon::bridge_profile_publish(
- config,
- &resolved.document.profile,
- Some(RadrootsProfileType::Farm),
- profile_idempotency_key.as_deref(),
- Some(profile_signer_session_id.as_str()),
- signer_authority.as_ref(),
- ) {
- Ok(result) => result_component(
- "bridge.profile.publish",
- KIND_PROFILE,
- result,
- args,
- Some(previews.profile.clone()),
- ),
- Err(error) => {
- return Ok(daemon_error_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- error,
- FarmPublishFailureStage::Profile,
- ));
- }
- };
-
- if component_failed(&profile_result) {
- return Ok(persist_publication_and_view(
- config,
- args,
- resolved,
- &account_pubkey,
- profile_result,
- preview_component(
- "bridge.farm.publish",
- KIND_FARM,
- farm_idempotency_key,
- args,
- Some(previews.farm),
- ),
- )?);
- }
-
- let farm_result = match daemon::bridge_farm_publish(
- config,
- &resolved.document.farm,
- farm_idempotency_key.as_deref(),
- Some(farm_signer_session_id.as_str()),
- signer_authority.as_ref(),
- ) {
- Ok(result) => result_component(
- "bridge.farm.publish",
- KIND_FARM,
- result,
- args,
- Some(previews.farm),
- ),
- Err(error) => {
- let profile_for_error = profile_result.clone();
- let farm_for_error = daemon_error_component(
- "bridge.farm.publish",
- KIND_FARM,
- args,
- farm_idempotency_key,
- error,
- Some(previews.farm),
- );
- return Ok(persist_publication_and_view(
- config,
- args,
- resolved,
- &account_pubkey,
- profile_for_error,
- farm_for_error,
- )?);
- }
- };
-
- persist_publication_and_view(
+ Ok(direct_relay_unavailable_publish_view(
config,
args,
- resolved,
+ &resolved,
&account_pubkey,
- profile_result,
- farm_result,
- )
+ previews,
+ profile_idempotency_key,
+ farm_idempotency_key,
+ ))
}
#[derive(Debug, Clone)]
@@ -534,12 +405,6 @@ struct FarmPublishPreviews {
farm: FarmPublishEventView,
}
-#[derive(Debug, Clone, Copy)]
-enum FarmPublishFailureStage {
- Profile,
- Farm,
-}
-
fn missing_publish_view(
scope: FarmConfigScope,
path: String,
@@ -555,7 +420,7 @@ fn missing_publish_view(
) -> FarmPublishView {
FarmPublishView {
state: "unconfigured".to_owned(),
- source: daemon::bridge_source().to_owned(),
+ source: FARM_WRITE_SOURCE.to_owned(),
scope: scope.as_str().to_owned(),
path,
config_present,
@@ -564,8 +429,8 @@ fn missing_publish_view(
selected_account_pubkey,
farm_d_tag,
requested_signer_session_id: args.signer_session_id.clone(),
- profile: not_submitted_component("bridge.profile.publish", KIND_PROFILE, args, None, None),
- farm: not_submitted_component("bridge.farm.publish", KIND_FARM, args, None, None),
+ profile: not_submitted_component("relay.profile.publish", KIND_PROFILE, args, None, None),
+ farm: not_submitted_component("relay.farm.publish", KIND_FARM, args, None, None),
missing,
reason: Some(reason),
actions,
@@ -611,7 +476,7 @@ fn base_publish_view(
) -> FarmPublishView {
FarmPublishView {
state: state.to_owned(),
- source: daemon::bridge_source().to_owned(),
+ source: FARM_WRITE_SOURCE.to_owned(),
scope: resolved.scope.as_str().to_owned(),
path: resolved.path.display().to_string(),
config_present: true,
@@ -698,15 +563,7 @@ fn preview_component(
event_addr: event.as_ref().and_then(|event| event.event_addr.clone()),
idempotency_key: idempotency_key.clone(),
reason: Some("not submitted".to_owned()),
- job: args.print_job.then(|| FarmPublishJobView {
- rpc_method: rpc_method.to_owned(),
- state: "not_submitted".to_owned(),
- job_id: None,
- idempotency_key,
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: None,
- signer_session_id: None,
- }),
+ job: None,
event: args.print_event.then_some(event).flatten(),
}
}
@@ -721,111 +578,6 @@ fn not_submitted_component(
preview_component(rpc_method, event_kind, idempotency_key, args, event)
}
-fn result_component(
- rpc_method: &str,
- fallback_event_kind: u32,
- result: BridgeEventPublishResult,
- args: &FarmPublishArgs,
- preview: Option<FarmPublishEventView>,
-) -> FarmPublishComponentView {
- let state = if result.status == "failed" {
- "failed".to_owned()
- } else if result.deduplicated {
- "deduplicated".to_owned()
- } else {
- result.status.clone()
- };
- let event_kind = result.event_kind.unwrap_or(fallback_event_kind);
- let event_addr = result
- .event_addr
- .clone()
- .or_else(|| preview.as_ref().and_then(|event| event.event_addr.clone()));
- let event = args.print_event.then(|| FarmPublishEventView {
- event_id: result.event_id.clone(),
- event_addr: event_addr.clone(),
- ..preview.unwrap_or_else(|| FarmPublishEventView {
- kind: event_kind,
- author: String::new(),
- content: String::new(),
- tags: Vec::new(),
- event_id: None,
- event_addr: None,
- })
- });
- FarmPublishComponentView {
- state: state.clone(),
- rpc_method: rpc_method.to_owned(),
- event_kind,
- deduplicated: result.deduplicated,
- job_id: Some(result.job_id.clone()),
- job_status: Some(result.status.clone()),
- signer_mode: Some(result.signer_mode.clone()),
- signer_session_id: result.signer_session_id.clone(),
- event_id: result.event_id.clone(),
- event_addr,
- idempotency_key: result.idempotency_key.clone(),
- reason: (result.status == "failed")
- .then(|| "daemon publish job failed before relay delivery completed".to_owned()),
- job: args.print_job.then(|| FarmPublishJobView {
- rpc_method: rpc_method.to_owned(),
- state,
- job_id: Some(result.job_id),
- idempotency_key: result.idempotency_key,
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(result.signer_mode),
- signer_session_id: result.signer_session_id,
- }),
- event,
- }
-}
-
-fn daemon_error_component(
- rpc_method: &str,
- event_kind: u32,
- args: &FarmPublishArgs,
- idempotency_key: Option<String>,
- error: DaemonRpcError,
- preview: Option<FarmPublishEventView>,
-) -> FarmPublishComponentView {
- let (state, reason) = daemon_error_state_reason(error);
- FarmPublishComponentView {
- state: state.clone(),
- rpc_method: rpc_method.to_owned(),
- event_kind,
- deduplicated: false,
- job_id: None,
- job_status: None,
- signer_mode: None,
- signer_session_id: None,
- event_id: None,
- event_addr: preview.as_ref().and_then(|event| event.event_addr.clone()),
- idempotency_key: idempotency_key.clone(),
- reason: Some(reason),
- job: args.print_job.then(|| FarmPublishJobView {
- rpc_method: rpc_method.to_owned(),
- state,
- job_id: None,
- idempotency_key,
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: None,
- signer_session_id: None,
- }),
- event: args.print_event.then_some(preview).flatten(),
- }
-}
-
-fn daemon_error_state_reason(error: DaemonRpcError) -> (String, String) {
- match error {
- DaemonRpcError::Unconfigured(reason)
- | DaemonRpcError::Unauthorized(reason)
- | DaemonRpcError::MethodUnavailable(reason) => ("unconfigured".to_owned(), reason),
- DaemonRpcError::External(reason) => ("unavailable".to_owned(), reason),
- DaemonRpcError::InvalidResponse(reason)
- | DaemonRpcError::Remote(reason)
- | DaemonRpcError::UnknownJob(reason) => ("error".to_owned(), reason),
- }
-}
-
fn binding_error_publish_view(
config: &RuntimeConfig,
args: &FarmPublishArgs,
@@ -853,7 +605,7 @@ fn binding_error_publish_view(
state: state.clone(),
reason: Some(reason.clone()),
..preview_component(
- "bridge.profile.publish",
+ "relay.profile.publish",
KIND_PROFILE,
profile_idempotency_key,
args,
@@ -864,7 +616,7 @@ fn binding_error_publish_view(
state: state.clone(),
reason: Some(reason.clone()),
..preview_component(
- "bridge.farm.publish",
+ "relay.farm.publish",
KIND_FARM,
farm_idempotency_key,
args,
@@ -876,7 +628,7 @@ fn binding_error_publish_view(
)
}
-fn daemon_error_publish_view(
+fn direct_relay_unavailable_publish_view(
config: &RuntimeConfig,
args: &FarmPublishArgs,
resolved: &ResolvedFarmConfig,
@@ -884,170 +636,40 @@ fn daemon_error_publish_view(
previews: FarmPublishPreviews,
profile_idempotency_key: Option<String>,
farm_idempotency_key: Option<String>,
- error: DaemonRpcError,
- stage: FarmPublishFailureStage,
) -> FarmPublishView {
- let (state, reason) = daemon_error_state_reason(error);
- let profile = match stage {
- FarmPublishFailureStage::Profile => FarmPublishComponentView {
- state: state.clone(),
- reason: Some(reason.clone()),
+ base_publish_view(
+ "unavailable",
+ config,
+ args,
+ resolved,
+ account_pubkey,
+ FarmPublishComponentView {
+ state: "unavailable".to_owned(),
+ reason: Some(DIRECT_RELAY_UNAVAILABLE_REASON.to_owned()),
..preview_component(
- "bridge.profile.publish",
+ "relay.profile.publish",
KIND_PROFILE,
profile_idempotency_key,
args,
Some(previews.profile),
)
},
- FarmPublishFailureStage::Farm => preview_component(
- "bridge.profile.publish",
- KIND_PROFILE,
- profile_idempotency_key,
- args,
- Some(previews.profile),
- ),
- };
- let farm = match stage {
- FarmPublishFailureStage::Profile => preview_component(
- "bridge.farm.publish",
- KIND_FARM,
- farm_idempotency_key,
- args,
- Some(previews.farm),
- ),
- FarmPublishFailureStage::Farm => FarmPublishComponentView {
- state: state.clone(),
- reason: Some(reason.clone()),
+ FarmPublishComponentView {
+ state: "unavailable".to_owned(),
+ reason: Some(DIRECT_RELAY_UNAVAILABLE_REASON.to_owned()),
..preview_component(
- "bridge.farm.publish",
+ "relay.farm.publish",
KIND_FARM,
farm_idempotency_key,
args,
Some(previews.farm),
)
},
- };
- base_publish_view(
- state.as_str(),
- config,
- args,
- resolved,
- account_pubkey,
- profile,
- farm,
- Some(reason),
- daemon_error_actions(state.as_str()),
+ Some(DIRECT_RELAY_UNAVAILABLE_REASON.to_owned()),
+ Vec::new(),
)
}
-fn persist_publication_and_view(
- config: &RuntimeConfig,
- args: &FarmPublishArgs,
- mut resolved: ResolvedFarmConfig,
- account_pubkey: &str,
- profile: FarmPublishComponentView,
- farm: FarmPublishComponentView,
-) -> Result<FarmPublishView, RuntimeError> {
- let now = unix_timestamp_now();
- if component_published(&profile) {
- if let Some(event_id) = &profile.event_id {
- resolved.document.publication.profile_event_id = Some(event_id.clone());
- }
- resolved.document.publication.profile_published_at = Some(now);
- }
- if component_published(&farm) {
- if let Some(event_id) = &farm.event_id {
- resolved.document.publication.farm_event_id = Some(event_id.clone());
- }
- resolved.document.publication.farm_published_at = Some(now);
- }
- if component_published(&profile) || component_published(&farm) {
- farm_config::write(&config.paths, resolved.scope, &resolved.document)?;
- }
-
- let state = publish_view_state(&profile, &farm);
- let mut actions = Vec::new();
- if let Some(job_id) = &profile.job_id {
- actions.push(format!("radroots job get {job_id}"));
- }
- if let Some(job_id) = &farm.job_id {
- actions.push(format!("radroots job get {job_id}"));
- actions.push(format!("radroots job watch {job_id}"));
- }
- if actions.is_empty() {
- actions.push("radroots runtime status get".to_owned());
- }
- let reason = (state == "partial" || state == "unavailable" || state == "error")
- .then(|| "farm publish did not complete for both profile and farm record".to_owned());
-
- Ok(base_publish_view(
- state,
- config,
- args,
- &resolved,
- account_pubkey,
- profile,
- farm,
- reason,
- actions,
- ))
-}
-
-fn publish_view_state(
- profile: &FarmPublishComponentView,
- farm: &FarmPublishComponentView,
-) -> &'static str {
- if component_error(profile) || component_error(farm) {
- return "error";
- }
- if component_unconfigured(profile) || component_unconfigured(farm) {
- return "unconfigured";
- }
- if component_failed(profile) || component_failed(farm) {
- return if component_published(profile) || component_published(farm) {
- "partial"
- } else {
- "unavailable"
- };
- }
- if profile.state == "deduplicated" || farm.state == "deduplicated" {
- return "deduplicated";
- }
- "published"
-}
-
-fn component_published(component: &FarmPublishComponentView) -> bool {
- matches!(component.state.as_str(), "published" | "deduplicated")
- || component
- .job_status
- .as_deref()
- .is_some_and(|status| status == "published")
-}
-
-fn component_failed(component: &FarmPublishComponentView) -> bool {
- matches!(component.state.as_str(), "failed" | "unavailable")
-}
-
-fn component_unconfigured(component: &FarmPublishComponentView) -> bool {
- component.state == "unconfigured"
-}
-
-fn component_error(component: &FarmPublishComponentView) -> bool {
- component.state == "error"
-}
-
-fn daemon_error_actions(state: &str) -> Vec<String> {
- match state {
- "unconfigured" => vec![
- "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(),
- "start radrootsd with bridge ingress enabled".to_owned(),
- ],
- "unavailable" => vec!["start radrootsd and verify the rpc url".to_owned()],
- _ => vec!["inspect the daemon rpc response contract".to_owned()],
- }
-}
-
fn selected_account_for_draft(
config: &RuntimeConfig,
) -> Result<Option<AccountRecordView>, RuntimeError> {
@@ -1579,13 +1201,6 @@ fn non_empty(value: &str) -> Option<String> {
}
}
-fn unix_timestamp_now() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_secs())
- .unwrap_or_default()
-}
-
fn generate_d_tag() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -18,7 +18,6 @@ use radroots_events::listing::{
use radroots_events::trade::RadrootsTradeListingValidationError;
use radroots_events_codec::d_tag::is_d_tag_base64url;
use radroots_events_codec::listing::encode::to_wire_parts_with_kind;
-use radroots_nostr::prelude::radroots_nostr_build_event;
use radroots_replica_db::ReplicaSql;
use radroots_sql_core::SqliteExecutor;
use radroots_trade::listing::publish::validate_listing_for_seller;
@@ -27,14 +26,12 @@ use serde::{Deserialize, Serialize};
use crate::domain::runtime::{
FindPriceView, FindQuantityView, FindResultProvenanceView, ListingGetView, ListingListView,
- ListingMutationEventView, ListingMutationJobView, ListingMutationView, ListingNewView,
- ListingSummaryView, ListingValidateView, ListingValidationIssueView, SyncFreshnessView,
+ ListingMutationEventView, ListingMutationView, ListingNewView, ListingSummaryView,
+ ListingValidateView, ListingValidationIssueView, SyncFreshnessView,
};
use crate::runtime::RuntimeError;
use crate::runtime::accounts;
use crate::runtime::config::{RuntimeConfig, SignerBackend};
-use crate::runtime::daemon;
-use crate::runtime::daemon::DaemonRpcError;
use crate::runtime::farm_config;
use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority};
use crate::runtime::sync::freshness_from_executor;
@@ -45,8 +42,9 @@ use crate::runtime_args::{
const DRAFT_KIND: &str = "listing_draft_v1";
const LISTING_SOURCE: &str = "local draft · local first";
const LISTING_READ_SOURCE: &str = "local replica · local first";
-const LISTING_WRITE_SOURCE: &str = "daemon bridge · durable write plane";
-const LISTING_LOCAL_SIGNED_SOURCE: &str = "local account signer · signed event artifact";
+const LISTING_WRITE_SOURCE: &str = "direct Nostr relay publish · pending implementation";
+const DIRECT_RELAY_UNAVAILABLE_REASON: &str =
+ "direct Nostr relay publishing is not implemented for listing mutations";
const LISTING_DRAFTS_DIR: &str = "listings/drafts";
static D_TAG_COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -806,16 +804,8 @@ fn mutate(
idempotency_key: args.idempotency_key.clone(),
signer_session_id: None,
requested_signer_session_id: args.signer_session_id.clone(),
- reason: Some("dry run requested; daemon publish skipped".to_owned()),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "bridge.listing.publish".to_owned(),
- state: "not_submitted".to_owned(),
- job_id: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- }),
+ reason: Some("dry run requested; relay publish skipped".to_owned()),
+ job: None,
event: args.print_event.then_some(event_preview),
actions: vec![format!(
"radroots listing {} {}",
@@ -825,22 +815,11 @@ fn mutate(
});
}
- if matches!(operation, ListingMutationOperation::Publish)
- && matches!(config.signer.backend, SignerBackend::Local)
- {
- return local_signed_view(
- config,
- args,
- operation,
- &canonical,
- listing_addr,
- event_preview,
- );
- }
-
- let signer_authority =
+ if matches!(config.signer.backend, SignerBackend::Local) {
+ validate_local_listing_signer(config, &canonical)?;
+ } else {
match resolve_actor_write_authority(config, "seller", canonical.seller_pubkey.as_str()) {
- Ok(authority) => authority,
+ Ok(_) => {}
Err(error) => {
return Ok(binding_error_view(
config,
@@ -852,109 +831,17 @@ fn mutate(
error,
));
}
- };
-
- let signer_session_id = match daemon::resolve_signer_session_id(
- config,
- "seller",
- canonical.seller_pubkey.as_str(),
- KIND_LISTING,
- args.signer_session_id.as_deref(),
- signer_authority.as_ref(),
- ) {
- Ok(session_id) => session_id,
- Err(error) => {
- return Ok(daemon_error_view(
- config,
- args,
- operation,
- &canonical,
- listing_addr,
- event_preview,
- error,
- ));
}
- };
+ }
- match daemon::bridge_listing_publish(
+ Ok(direct_relay_unavailable_view(
config,
- &canonical.listing,
- KIND_LISTING,
- args.idempotency_key.as_deref(),
- Some(signer_session_id.as_str()),
- signer_authority.as_ref(),
- ) {
- Ok(result) => {
- let failed = result.status == "failed";
- let mut actions = Vec::new();
- if failed {
- if let Some(job_id) = &Some(result.job_id.clone()) {
- actions.push(format!("radroots job get {job_id}"));
- }
- actions.push("radroots runtime status get".to_owned());
- } else {
- actions.push(format!("radroots job get {}", result.job_id));
- actions.push(format!("radroots job watch {}", result.job_id));
- }
-
- Ok(ListingMutationView {
- state: if failed {
- "unavailable".to_owned()
- } else if result.deduplicated {
- "deduplicated".to_owned()
- } else {
- result.status.clone()
- },
- operation: operation.as_str().to_owned(),
- source: LISTING_WRITE_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id,
- listing_addr: listing_addr.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- event_kind: result.event_kind.unwrap_or(KIND_LISTING),
- dry_run: false,
- deduplicated: result.deduplicated,
- job_id: Some(result.job_id.clone()),
- job_status: Some(result.status.clone()),
- signer_mode: Some(result.signer_mode.clone()),
- signer_session_id: result.signer_session_id.clone(),
- event_id: result.event_id.clone(),
- event_addr: result
- .event_addr
- .clone()
- .or_else(|| Some(listing_addr.clone())),
- idempotency_key: result.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- reason: failed.then(|| {
- "daemon publish job failed before relay delivery completed".to_owned()
- }),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "bridge.listing.publish".to_owned(),
- state: result.status,
- job_id: Some(result.job_id),
- idempotency_key: result.idempotency_key,
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(result.signer_mode),
- signer_session_id: result.signer_session_id,
- }),
- event: args.print_event.then(|| ListingMutationEventView {
- event_id: result.event_id,
- event_addr: result.event_addr.unwrap_or(listing_addr),
- ..event_preview
- }),
- actions,
- })
- }
- Err(error) => Ok(daemon_error_view(
- config,
- args,
- operation,
- &canonical,
- listing_addr,
- event_preview,
- error,
- )),
- }
+ args,
+ operation,
+ &canonical,
+ listing_addr,
+ event_preview,
+ ))
}
fn scaffold_contents(draft: &ListingDraftDocument) -> Result<String, RuntimeError> {
@@ -1289,172 +1176,18 @@ fn build_listing_event_preview(
))
}
-fn daemon_error_view(
+fn direct_relay_unavailable_view(
config: &RuntimeConfig,
args: &ListingMutationArgs,
operation: ListingMutationOperation,
canonical: &CanonicalListingDraft,
listing_addr: String,
event_preview: ListingMutationEventView,
- error: DaemonRpcError,
) -> ListingMutationView {
- match error {
- DaemonRpcError::Unconfigured(reason)
- | DaemonRpcError::Unauthorized(reason)
- | DaemonRpcError::MethodUnavailable(reason) => ListingMutationView {
- state: "unconfigured".to_owned(),
- operation: operation.as_str().to_owned(),
- source: LISTING_WRITE_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr,
- seller_pubkey: canonical.seller_pubkey.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- job_id: None,
- job_status: None,
- signer_mode: None,
- signer_session_id: None,
- event_id: None,
- event_addr: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- reason: Some(reason),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "bridge.listing.publish".to_owned(),
- state: "unconfigured".to_owned(),
- job_id: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- }),
- event: args.print_event.then_some(event_preview),
- actions: vec![
- "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(),
- "start radrootsd with bridge ingress enabled".to_owned(),
- ],
- },
- DaemonRpcError::External(reason) => ListingMutationView {
- state: "unavailable".to_owned(),
- operation: operation.as_str().to_owned(),
- source: LISTING_WRITE_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr,
- seller_pubkey: canonical.seller_pubkey.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- job_id: None,
- job_status: None,
- signer_mode: None,
- signer_session_id: None,
- event_id: None,
- event_addr: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- reason: Some(reason),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "bridge.listing.publish".to_owned(),
- state: "unavailable".to_owned(),
- job_id: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- }),
- event: args.print_event.then_some(event_preview),
- actions: vec!["start radrootsd and verify the rpc url".to_owned()],
- },
- DaemonRpcError::InvalidResponse(reason)
- | DaemonRpcError::Remote(reason)
- | DaemonRpcError::UnknownJob(reason) => ListingMutationView {
- state: "error".to_owned(),
- operation: operation.as_str().to_owned(),
- source: LISTING_WRITE_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr,
- seller_pubkey: canonical.seller_pubkey.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- job_id: None,
- job_status: None,
- signer_mode: None,
- signer_session_id: None,
- event_id: None,
- event_addr: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- reason: Some(reason),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "bridge.listing.publish".to_owned(),
- state: "error".to_owned(),
- job_id: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- }),
- event: args.print_event.then_some(event_preview),
- actions: vec!["inspect the daemon rpc response contract".to_owned()],
- },
- }
-}
-
-fn local_signed_view(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
- canonical: &CanonicalListingDraft,
- listing_addr: String,
- event_preview: ListingMutationEventView,
-) -> Result<ListingMutationView, RuntimeError> {
- let signed_event = match sign_listing_event(config, canonical) {
- Ok(event) => event,
- Err(error) => {
- return Ok(ListingMutationView {
- state: "unconfigured".to_owned(),
- operation: operation.as_str().to_owned(),
- source: LISTING_LOCAL_SIGNED_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr,
- seller_pubkey: canonical.seller_pubkey.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
- job_id: None,
- job_status: None,
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- event_id: None,
- event_addr: None,
- idempotency_key: args.idempotency_key.clone(),
- signer_session_id: None,
- requested_signer_session_id: args.signer_session_id.clone(),
- reason: Some(error.to_string()),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "local.listing.sign".to_owned(),
- state: "unconfigured".to_owned(),
- job_id: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- }),
- event: args.print_event.then_some(event_preview),
- actions: vec!["radroots signer status get".to_owned()],
- });
- }
- };
- let event_view = signed_listing_event_view(&signed_event, listing_addr.as_str());
- Ok(ListingMutationView {
- state: "signed".to_owned(),
+ ListingMutationView {
+ state: "unavailable".to_owned(),
operation: operation.as_str().to_owned(),
- source: LISTING_LOCAL_SIGNED_SOURCE.to_owned(),
+ source: LISTING_WRITE_SOURCE.to_owned(),
file: args.file.display().to_string(),
listing_id: canonical.listing_id.clone(),
listing_addr: listing_addr.clone(),
@@ -1465,38 +1198,16 @@ fn local_signed_view(
job_id: None,
job_status: None,
signer_mode: Some(config.signer.backend.as_str().to_owned()),
- event_id: event_view.event_id.clone(),
+ event_id: None,
event_addr: Some(listing_addr),
idempotency_key: args.idempotency_key.clone(),
signer_session_id: None,
requested_signer_session_id: args.signer_session_id.clone(),
- reason: Some("signed locally; relay delivery was not attempted".to_owned()),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "local.listing.sign".to_owned(),
- state: "not_submitted".to_owned(),
- job_id: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- }),
- event: Some(event_view),
+ reason: Some(DIRECT_RELAY_UNAVAILABLE_REASON.to_owned()),
+ job: None,
+ event: args.print_event.then_some(event_preview),
actions: Vec::new(),
- })
-}
-
-fn sign_listing_event(
- config: &RuntimeConfig,
- canonical: &CanonicalListingDraft,
-) -> Result<radroots_nostr::prelude::RadrootsNostrEvent, RuntimeError> {
- let signing = resolve_listing_signing_identity(config, canonical)?;
- let parts = to_wire_parts_with_kind(&canonical.listing, KIND_LISTING)
- .map_err(|error| RuntimeError::Config(format!("invalid listing contract: {error}")))?;
- let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .map_err(|error| RuntimeError::Config(format!("build local listing event: {error}")))?
- .sign_with_keys(signing.identity.keys())
- .map_err(|error| RuntimeError::Config(format!("sign local listing event: {error}")))?;
- Ok(event)
+ }
}
fn validate_local_listing_signer(
@@ -1526,26 +1237,6 @@ fn resolve_listing_signing_identity(
Ok(signing)
}
-fn signed_listing_event_view(
- event: &radroots_nostr::prelude::RadrootsNostrEvent,
- listing_addr: &str,
-) -> ListingMutationEventView {
- ListingMutationEventView {
- kind: event.kind.as_u16() as u32,
- author: event.pubkey.to_string(),
- created_at: Some(u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX)),
- content: event.content.clone(),
- tags: event
- .tags
- .iter()
- .map(|tag| tag.as_slice().to_vec())
- .collect(),
- event_id: Some(event.id.to_string()),
- signature: Some(event.sig.to_string()),
- event_addr: listing_addr.to_owned(),
- }
-}
-
fn binding_error_view(
config: &RuntimeConfig,
args: &ListingMutationArgs,
@@ -1583,15 +1274,7 @@ fn binding_error_view(
idempotency_key: args.idempotency_key.clone(),
requested_signer_session_id: args.signer_session_id.clone(),
reason: Some(reason),
- job: args.print_job.then(|| ListingMutationJobView {
- rpc_method: "bridge.listing.publish".to_owned(),
- state,
- job_id: None,
- idempotency_key: args.idempotency_key.clone(),
- requested_signer_session_id: args.signer_session_id.clone(),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- signer_session_id: None,
- }),
+ job: None,
event: args.print_event.then_some(event_preview),
actions,
}
diff --git a/src/runtime_args.rs b/src/runtime_args.rs
@@ -128,7 +128,6 @@ pub struct FarmPublishArgs {
pub scope: Option<FarmScopeArg>,
pub idempotency_key: Option<String>,
pub signer_session_id: Option<String>,
- pub print_job: bool,
pub print_event: bool,
}
@@ -160,7 +159,6 @@ pub struct ListingMutationArgs {
pub file: PathBuf,
pub idempotency_key: Option<String>,
pub signer_session_id: Option<String>,
- pub print_job: bool,
pub print_event: bool,
}
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -2,12 +2,10 @@ mod support;
use std::path::Path;
-use radroots_events::kinds::KIND_LISTING;
-use serde_json::json;
use support::{
- RadrootsCliSandbox, assert_contains, assert_hex_len, assert_no_removed_command_reference,
- create_listing_draft, identity_public, make_listing_publishable, shell_single_quoted,
- toml_string, write_public_identity_profile,
+ RadrootsCliSandbox, assert_contains, assert_no_daemon_runtime_reference,
+ assert_no_removed_command_reference, create_listing_draft, identity_public,
+ make_listing_publishable, shell_single_quoted, toml_string, write_public_identity_profile,
};
#[test]
@@ -447,10 +445,10 @@ fn local_listing_publish_dry_run_validates_local_account_authority() {
}
#[test]
-fn local_listing_publish_signs_with_selected_account_without_remote_fallback() {
+fn local_listing_publish_fails_until_direct_relay_publish_exists() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
- let listing_file = create_listing_draft(&sandbox, "local-signed");
+ let listing_file = create_listing_draft(&sandbox, "local-unavailable");
make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
let (output, value) = sandbox.json_output(&[
@@ -463,41 +461,17 @@ fn local_listing_publish_signs_with_selected_account_without_remote_fallback() {
listing_file.to_string_lossy().as_ref(),
]);
- assert!(output.status.success());
+ assert!(!output.status.success());
assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["result"]["state"], "signed");
- assert_eq!(value["result"]["signer_mode"], "local");
- assert_eq!(
- value["result"]["signer_session_id"],
- serde_json::Value::Null
- );
- assert_eq!(value["result"]["job_id"], serde_json::Value::Null);
- assert_eq!(value["result"]["event"]["kind"], KIND_LISTING);
- assert_eq!(
- value["result"]["event"]["author"],
- value["result"]["seller_pubkey"]
- );
- assert_eq!(
- value["result"]["event"]["event_id"],
- value["result"]["event_id"]
- );
- assert_hex_len(&value["result"]["event_id"], 64);
- assert_hex_len(&value["result"]["event"]["signature"], 128);
+ assert_eq!(value["result"], serde_json::Value::Null);
+ assert_eq!(value["errors"][0]["code"], "operation_unavailable");
+ assert_eq!(value["errors"][0]["detail"]["class"], "operation");
assert_contains(
- &value["result"]["reason"],
- "relay delivery was not attempted",
- );
- assert!(
- value["result"]["event"]["tags"]
- .as_array()
- .expect("event tags")
- .iter()
- .any(|tag| tag
- .as_array()
- .is_some_and(|items| items.first() == Some(&json!("d"))
- && items.get(1) == Some(&value["result"]["listing_id"])))
+ &value["errors"][0]["message"],
+ "direct Nostr relay publishing is not implemented",
);
assert_no_removed_command_reference(&value, &["listing", "publish"]);
+ assert_no_daemon_runtime_reference(&value, &["listing", "publish"]);
}
#[test]
@@ -522,6 +496,7 @@ fn local_listing_publish_dry_run_does_not_sign_matching_listing() {
assert_eq!(value["result"]["dry_run"], true);
assert_eq!(value["result"]["event_id"], serde_json::Value::Null);
assert_no_removed_command_reference(&value, &["listing", "publish", "--dry-run"]);
+ assert_no_daemon_runtime_reference(&value, &["listing", "publish", "--dry-run"]);
}
#[test]
@@ -617,6 +592,48 @@ fn local_farm_publish_dry_run_validates_secret_backed_account() {
assert_eq!(value["dry_run"], true);
assert_eq!(value["result"]["state"], "dry_run");
assert_eq!(value["result"]["dry_run"], true);
+ assert_no_daemon_runtime_reference(&value, &["farm", "publish", "--dry-run"]);
+}
+
+#[test]
+fn local_farm_publish_fails_until_direct_relay_publish_exists() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.json_success(&["--format", "json", "account", "create"]);
+ sandbox.json_success(&[
+ "--format",
+ "json",
+ "farm",
+ "create",
+ "--name",
+ "Green Farm",
+ "--location",
+ "farmstand",
+ "--country",
+ "US",
+ "--delivery-method",
+ "pickup",
+ ]);
+
+ let (output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--approval-token",
+ "approve",
+ "farm",
+ "publish",
+ ]);
+
+ assert!(!output.status.success());
+ assert_eq!(value["operation_id"], "farm.publish");
+ assert_eq!(value["result"], serde_json::Value::Null);
+ assert_eq!(value["errors"][0]["code"], "operation_unavailable");
+ assert_eq!(value["errors"][0]["detail"]["class"], "operation");
+ assert_contains(
+ &value["errors"][0]["message"],
+ "direct Nostr relay publishing is not implemented",
+ );
+ assert_no_removed_command_reference(&value, &["farm", "publish"]);
+ assert_no_daemon_runtime_reference(&value, &["farm", "publish"]);
}
#[test]
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
@@ -166,6 +166,16 @@ pub fn assert_no_removed_command_reference(value: &Value, args: &[&str]) {
}
}
+pub fn assert_no_daemon_runtime_reference(value: &Value, args: &[&str]) {
+ let raw = serde_json::to_string(value).expect("json value");
+ for removed in ["radrootsd", "daemon", "bridge", "radroots job"] {
+ assert!(
+ !raw.contains(removed),
+ "`{args:?}` output should not contain daemon runtime reference `{removed}`: {raw}"
+ );
+ }
+}
+
pub fn assert_contains(value: &Value, needle: &str) {
let value = value.as_str().expect("string value");
assert!(
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -6,8 +6,9 @@ use std::path::Path;
use serde_json::Value;
use support::{
- RadrootsCliSandbox, assert_no_removed_command_reference, create_listing_draft, identity_public,
- make_listing_publishable, ndjson_from_stdout, radroots, write_public_identity_profile,
+ RadrootsCliSandbox, assert_no_daemon_runtime_reference, assert_no_removed_command_reference,
+ create_listing_draft, identity_public, make_listing_publishable, ndjson_from_stdout, radroots,
+ write_public_identity_profile,
};
const LISTING_ADDR: &str =
@@ -1207,6 +1208,7 @@ fn seller_target_flow_acceptance_uses_target_operations() {
assert_eq!(publish["operation_id"], "listing.publish");
assert_eq!(publish["result"]["state"], "dry_run");
assert_no_removed_command_reference(&publish, &["listing", "publish", "--dry-run"]);
+ assert_no_daemon_runtime_reference(&publish, &["listing", "publish", "--dry-run"]);
let archive = sandbox.json_success(&[
"--format",
@@ -1220,8 +1222,9 @@ fn seller_target_flow_acceptance_uses_target_operations() {
assert_eq!(archive["result"]["state"], "dry_run");
assert_eq!(archive["result"]["operation"], "archive");
assert_no_removed_command_reference(&archive, &["listing", "archive", "--dry-run"]);
+ assert_no_daemon_runtime_reference(&archive, &["listing", "archive", "--dry-run"]);
- let signed = sandbox.json_success(&[
+ let (publish_output, unavailable_publish) = sandbox.json_output(&[
"--format",
"json",
"--approval-token",
@@ -1230,13 +1233,38 @@ fn seller_target_flow_acceptance_uses_target_operations() {
"publish",
listing_file,
]);
- assert_eq!(signed["operation_id"], "listing.publish");
- assert_eq!(signed["result"]["state"], "signed");
- assert_eq!(signed["result"]["signer_mode"], "local");
+ assert!(!publish_output.status.success());
+ assert_eq!(unavailable_publish["operation_id"], "listing.publish");
assert_eq!(
- signed["result"]["event"]["author"],
- signed["result"]["seller_pubkey"]
+ unavailable_publish["errors"][0]["code"],
+ "operation_unavailable"
);
- assert!(signed["result"]["event"]["signature"].is_string());
- assert_no_removed_command_reference(&signed, &["listing", "publish"]);
+ assert_eq!(
+ unavailable_publish["errors"][0]["detail"]["class"],
+ "operation"
+ );
+ assert_no_removed_command_reference(&unavailable_publish, &["listing", "publish"]);
+ assert_no_daemon_runtime_reference(&unavailable_publish, &["listing", "publish"]);
+
+ let (archive_output, unavailable_archive) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--approval-token",
+ "approve",
+ "listing",
+ "archive",
+ listing_file,
+ ]);
+ assert!(!archive_output.status.success());
+ assert_eq!(unavailable_archive["operation_id"], "listing.archive");
+ assert_eq!(
+ unavailable_archive["errors"][0]["code"],
+ "operation_unavailable"
+ );
+ assert_eq!(
+ unavailable_archive["errors"][0]["detail"]["class"],
+ "operation"
+ );
+ assert_no_removed_command_reference(&unavailable_archive, &["listing", "archive"]);
+ assert_no_daemon_runtime_reference(&unavailable_archive, &["listing", "archive"]);
}