cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 0d11a1a0733a7c6ff7b05bed28ebecb718a2fd1f
parent 5512a8d74905fa805539457e67d5cd72b54d6810
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Apr 2026 20:59:13 +0000

cli: remove order daemon workflow paths

- stop order submit from creating daemon jobs
- keep order dry-runs on local draft and account checks
- remove RHI workflow verification from order reads and watch
- cover order outputs without daemon job references

Diffstat:
Msrc/operation_order.rs | 1-
Msrc/runtime/order.rs | 1289+++++++------------------------------------------------------------------------
Msrc/runtime_args.rs | 1-
Mtests/target_cli.rs | 48++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 149 insertions(+), 1190 deletions(-)

diff --git a/src/operation_order.rs b/src/operation_order.rs @@ -43,7 +43,6 @@ impl OperationService<OrderSubmitRequest> for OrderOperationService<'_> { .idempotency_key .clone() .or_else(|| string_input(&request, "idempotency_key")), - signer_session_id: string_input(&request, "signer_session_id"), }; let mut config = self.config.clone(); if request.context.dry_run { diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -1,58 +1,34 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; -use std::thread; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; use radroots_events::kinds::KIND_LISTING; -use radroots_events::trade::{ - RadrootsTradeMessageType, RadrootsTradeOrder, RadrootsTradeOrderItem, -}; use radroots_events_codec::d_tag::is_d_tag_base64url; use radroots_events_codec::trade::RadrootsTradeListingAddress; use radroots_replica_db::ReplicaSql; -use radroots_runtime::BackoffConfig; -use radroots_runtime_paths::{ - RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace, -}; -use radroots_sdk::config::RadrootsSdkConfig; use radroots_sql_core::SqliteExecutor; -use rhi::features::trade_listing::state::{TradeListingRuntime, TradeListingRuntimeConfig}; -use rhi::identity_storage::load_service_identity; -use rhi::rhi::{Rhi, start_subscriber}; use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; use crate::domain::runtime::{ - OrderCancelView, OrderDraftItemView, OrderGetView, OrderHistoryEntryView, OrderHistoryView, - OrderIssueView, OrderJobView, OrderListView, OrderNewView, OrderSubmitView, OrderSummaryView, - OrderWatchFrameView, OrderWatchView, OrderWorkflowView, + OrderCancelView, OrderDraftItemView, OrderGetView, OrderHistoryView, OrderIssueView, + OrderListView, OrderNewView, OrderSubmitView, OrderSummaryView, OrderWatchView, }; use crate::runtime::RuntimeError; use crate::runtime::accounts; -use crate::runtime::config::{ - CapabilityBindingTargetKind, RuntimeConfig, SignerBackend, WORKFLOW_TRADE_CAPABILITY, -}; -use crate::runtime::daemon::{self, DaemonRpcError}; -use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority}; +use crate::runtime::config::{RuntimeConfig, SignerBackend}; +use crate::runtime::signer::ActorWriteBindingError; use crate::runtime_args::{ OrderDraftCreateArgs, OrderSubmitArgs, OrderWatchArgs, RecordLookupArgs, }; const ORDER_DRAFT_KIND: &str = "order_draft_v1"; const ORDER_SOURCE: &str = "local order drafts · local first"; -const ORDER_LIFECYCLE_SOURCE: &str = "local order drafts · durable job lifecycle"; -const ORDER_WORKFLOW_SOURCE: &str = "local order drafts · substrate-authoritative workflow state"; +const ORDER_SUBMIT_UNAVAILABLE_REASON: &str = + "direct Nostr relay order publication is not implemented"; +const ORDER_EVENT_STATE_UNAVAILABLE_REASON: &str = + "relay-backed order event state is not implemented"; const ORDERS_DIR: &str = "orders/drafts"; -const WORKFLOW_PROVIDER_RUNTIME_ID: &str = "rhi"; -const WORKFLOW_TARGET: &str = "workflow-default"; -const WORKFLOW_STATE_DIR_NAME: &str = "trade-listing"; -const WORKFLOW_STATE_FILE_NAME: &str = "state.json"; -const WORKFLOW_IDENTITY_FILE_NAME: &str = "identity.secret.json"; -const WORKFLOW_FETCH_TIMEOUT: Duration = Duration::from_secs(60); -const WORKFLOW_POLL_INTERVAL: Duration = Duration::from_millis(250); -const WORKFLOW_REPLAY_WINDOW_SECS: u64 = 24 * 60 * 60; -const WORKFLOW_REPLAY_OVERLAP_SECS: u64 = 5 * 60; static ORDER_COUNTER: AtomicU64 = AtomicU64::new(0); @@ -66,8 +42,6 @@ struct OrderDraftDocument { listing_lookup: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] buyer_account_id: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - submission: Option<OrderDraftSubmission>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -91,26 +65,6 @@ struct OrderDraftItem { bin_count: u32, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct OrderDraftSubmission { - job_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - state: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - signer_mode: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - signer_session_id: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - command: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - event_id: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - event_addr: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - submitted_at_unix: Option<u64>, -} - #[derive(Debug, Clone)] struct LoadedOrderDraft { file: PathBuf, @@ -119,36 +73,6 @@ struct LoadedOrderDraft { } #[derive(Debug, Clone)] -struct WorkflowContext { - relay_url: String, - identity_path: PathBuf, - state_path: PathBuf, -} - -#[derive(Debug, Clone)] -enum WorkflowResolutionError { - Unconfigured(String), - Unavailable(String), - Error(String), -} - -impl WorkflowResolutionError { - fn state(&self) -> &'static str { - match self { - Self::Unconfigured(_) => "unconfigured", - Self::Unavailable(_) => "unavailable", - Self::Error(_) => "error", - } - } - - fn reason(self) -> String { - match self { - Self::Unconfigured(reason) | Self::Unavailable(reason) | Self::Error(reason) => reason, - } - } -} - -#[derive(Debug, Clone)] struct ResolvedOrderListing { listing_addr: String, seller_pubkey: String, @@ -211,19 +135,14 @@ pub fn scaffold( }, listing_lookup, buyer_account_id, - submission: None, }; save_draft(file.as_path(), &document)?; - let mut view: OrderNewView = view_from_loaded( - config, - LoadedOrderDraft { - file, - updated_at_unix: now_unix(), - document, - }, - false, - ) + let mut view: OrderNewView = view_from_loaded(LoadedOrderDraft { + file, + updated_at_unix: now_unix(), + document, + }) .into(); view.actions .insert(0, format!("radroots order get {}", view.order_id)); @@ -285,18 +204,13 @@ pub fn scaffold_preflight( }, listing_lookup, buyer_account_id, - submission: None, }; - let mut view: OrderNewView = view_from_loaded( - config, - LoadedOrderDraft { - file, - updated_at_unix: now_unix(), - document, - }, - false, - ) + let mut view: OrderNewView = view_from_loaded(LoadedOrderDraft { + file, + updated_at_unix: now_unix(), + document, + }) .into(); view.state = "dry_run".to_owned(); view.actions @@ -335,7 +249,7 @@ pub fn get(config: &RuntimeConfig, args: &RecordLookupArgs) -> Result<OrderGetVi } match load_draft(file.as_path()) { - Ok(loaded) => Ok(view_from_loaded(config, loaded, true)), + Ok(loaded) => Ok(view_from_loaded(loaded)), Err(reason) => Ok(OrderGetView { state: "error".to_owned(), source: ORDER_SOURCE.to_owned(), @@ -379,7 +293,7 @@ pub fn list(config: &RuntimeConfig) -> Result<OrderListView, RuntimeError> { continue; } match load_draft(path.as_path()) { - Ok(loaded) => orders.push(summary_from_loaded(config, &loaded)), + Ok(loaded) => orders.push(summary_from_loaded(&loaded)), Err(reason) => orders.push(summary_for_invalid_file(path.as_path(), reason)), } } @@ -421,7 +335,7 @@ pub fn submit( if !file.exists() { return Ok(OrderSubmitView { state: "missing".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: args.key.clone(), file: file.display().to_string(), listing_lookup: None, @@ -434,7 +348,7 @@ pub fn submit( idempotency_key: args.idempotency_key.clone(), signer_mode: None, signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), + requested_signer_session_id: None, reason: Some(format!("order draft `{}` was not found", args.key)), job: None, issues: Vec::new(), @@ -450,7 +364,7 @@ pub fn submit( Err(reason) => { return Ok(OrderSubmitView { state: "error".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: args.key.clone(), file: file.display().to_string(), listing_lookup: None, @@ -463,7 +377,7 @@ pub fn submit( idempotency_key: args.idempotency_key.clone(), signer_mode: None, signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), + requested_signer_session_id: None, reason: Some(reason), job: None, issues: Vec::new(), @@ -472,38 +386,6 @@ pub fn submit( } }; - if let Some(job) = submission_job_view(config, &loaded.document, true) { - let mut actions = vec![ - format!( - "radroots order event watch {}", - loaded.document.order.order_id - ), - "radroots order event list".to_owned(), - ]; - actions.push(format!("radroots job get {}", job.job_id)); - return Ok(OrderSubmitView { - state: "already_submitted".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), - order_id: loaded.document.order.order_id.clone(), - file: loaded.file.display().to_string(), - listing_lookup: loaded.document.listing_lookup.clone(), - listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()), - buyer_account_id: loaded.document.buyer_account_id.clone(), - buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()), - seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()), - dry_run: config.output.dry_run, - deduplicated: false, - idempotency_key: args.idempotency_key.clone(), - signer_mode: job.signer_mode.clone(), - signer_session_id: job.signer_session_id.clone(), - requested_signer_session_id: args.signer_session_id.clone(), - reason: Some("order draft already has a recorded submission job".to_owned()), - job: Some(job), - issues: Vec::new(), - actions, - }); - } - let issues = collect_issues(&loaded.document); if !issues.is_empty() { let mut actions = actions_for_document(&loaded.document, loaded.file.as_path(), &issues); @@ -513,7 +395,7 @@ pub fn submit( )); return Ok(OrderSubmitView { state: "unconfigured".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: loaded.document.order.order_id.clone(), file: loaded.file.display().to_string(), listing_lookup: loaded.document.listing_lookup.clone(), @@ -526,8 +408,8 @@ pub fn submit( idempotency_key: args.idempotency_key.clone(), signer_mode: None, signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), - reason: Some("order draft is not ready for durable submit".to_owned()), + requested_signer_session_id: None, + reason: Some("order draft is not ready for submit".to_owned()), job: None, issues, actions, @@ -543,7 +425,7 @@ pub fn submit( } return Ok(OrderSubmitView { state: "dry_run".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: loaded.document.order.order_id.clone(), file: loaded.file.display().to_string(), listing_lookup: loaded.document.listing_lookup.clone(), @@ -556,19 +438,9 @@ pub fn submit( idempotency_key: args.idempotency_key.clone(), signer_mode: None, signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), - reason: Some("dry run requested; daemon order submission skipped".to_owned()), - job: Some(OrderJobView { - job_id: "not_submitted".to_owned(), - state: "not_submitted".to_owned(), - command: Some("order.submit".to_owned()), - signer_mode: Some(config.signer.backend.as_str().to_owned()), - signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), - event_id: None, - event_addr: None, - reason: None, - }), + requested_signer_session_id: None, + reason: Some("dry run requested; relay order publication skipped".to_owned()), + job: None, issues: Vec::new(), actions: vec![format!( "radroots order submit {}", @@ -577,106 +449,15 @@ pub fn submit( }); } - let signer_authority = match resolve_actor_write_authority( - config, - "buyer", - loaded.document.order.buyer_pubkey.as_str(), - ) { - Ok(authority) => authority, - Err(error) => return Ok(order_binding_error_view(config, &loaded, args, error)), - }; - - let signer_session_id = match daemon::resolve_signer_session_id( - config, - "buyer", - loaded.document.order.buyer_pubkey.as_str(), - u32::from(RadrootsTradeMessageType::OrderRequest.kind()), - args.signer_session_id.as_deref(), - signer_authority.as_ref(), - ) { - Ok(session_id) => session_id, - Err(error) => return Ok(order_submit_error_view(&loaded, args, error)), - }; - - let order = trade_order_from_document(&loaded.document); - match daemon::bridge_order_request( - config, - &order, - args.idempotency_key.as_deref(), - Some(signer_session_id.as_str()), - signer_authority.as_ref(), - ) { - Ok(result) => { - let mut updated = loaded.document.clone(); - updated.submission = Some(OrderDraftSubmission { - job_id: result.job_id.clone(), - state: Some(result.status.clone()), - signer_mode: Some(result.signer_mode.clone()), - signer_session_id: result.signer_session_id.clone(), - command: Some("order.submit".to_owned()), - event_id: result.event_id.clone(), - event_addr: result.event_addr.clone(), - submitted_at_unix: Some(now_unix()), - }); - save_draft(loaded.file.as_path(), &updated)?; - - let failed = result.status == "failed"; - let mut actions = Vec::new(); - if failed { - actions.push(format!("radroots job get {}", result.job_id)); - actions.push("radroots runtime status get".to_owned()); - actions.push("radroots order event list".to_owned()); - } else { - actions.push(format!( - "radroots order event watch {}", - updated.order.order_id - )); - actions.push(format!("radroots job get {}", result.job_id)); - actions.push("radroots order event list".to_owned()); - } - - Ok(OrderSubmitView { - state: if failed { - "unavailable".to_owned() - } else if result.deduplicated { - "deduplicated".to_owned() - } else { - result.status.clone() - }, - source: daemon::bridge_source().to_owned(), - order_id: updated.order.order_id.clone(), - file: loaded.file.display().to_string(), - listing_lookup: updated.listing_lookup.clone(), - listing_addr: non_empty_string(updated.order.listing_addr.clone()), - buyer_account_id: updated.buyer_account_id.clone(), - buyer_pubkey: non_empty_string(updated.order.buyer_pubkey.clone()), - seller_pubkey: non_empty_string(updated.order.seller_pubkey.clone()), - dry_run: false, - deduplicated: result.deduplicated, - idempotency_key: result.idempotency_key.clone(), - signer_mode: Some(result.signer_mode.clone()), - signer_session_id: result.signer_session_id.clone(), - requested_signer_session_id: args.signer_session_id.clone(), - reason: failed.then(|| { - "daemon order request failed before relay delivery completed".to_owned() - }), - job: Some(OrderJobView { - job_id: result.job_id, - state: result.status, - command: Some("order.submit".to_owned()), - signer_mode: Some(result.signer_mode), - signer_session_id: result.signer_session_id, - requested_signer_session_id: args.signer_session_id.clone(), - event_id: result.event_id, - event_addr: result.event_addr, - reason: None, - }), - issues: Vec::new(), - actions, - }) - } - Err(error) => Ok(order_submit_error_view(&loaded, args, error)), + if let Err(error) = + validate_local_order_write_authority(config, loaded.document.order.buyer_pubkey.as_str()) + { + return Ok(order_binding_error_view(config, &loaded, args, error)); } + + Ok(direct_relay_unavailable_order_submit_view( + config, &loaded, args, + )) } pub fn watch( @@ -693,7 +474,7 @@ pub fn watch( if !file.exists() { return Ok(OrderWatchView { state: "missing".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: args.key.clone(), job_id: None, interval_ms: args.interval_ms, @@ -709,7 +490,7 @@ pub fn watch( Err(reason) => { return Ok(OrderWatchView { state: "error".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: args.key.clone(), job_id: None, interval_ms: args.interval_ms, @@ -721,109 +502,20 @@ pub fn watch( } }; - let Some(submission) = loaded.document.submission.as_ref() else { - return Ok(OrderWatchView { - state: "not_submitted".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), - order_id: loaded.document.order.order_id.clone(), - job_id: None, - interval_ms: args.interval_ms, - reason: Some("order draft does not have a recorded submission job yet".to_owned()), - workflow: None, - frames: Vec::new(), - actions: vec![format!( - "radroots order submit {}", - loaded.document.order.order_id - )], - }); - }; - - let job_id = submission.job_id.clone(); - let max_frames = args.frames.unwrap_or(usize::MAX); - let mut frames = Vec::new(); - loop { - match daemon::bridge_job(config, job_id.as_str()) { - Ok(Some(job)) => { - frames.push(OrderWatchFrameView { - sequence: frames.len() + 1, - observed_at_unix: job.completed_at_unix.unwrap_or(job.requested_at_unix), - state: job.state.clone(), - terminal: job.terminal, - signer_mode: job.signer.clone(), - signer_session_id: job.signer_session_id.clone(), - summary: job.relay_outcome_summary.clone(), - }); - if job.terminal || frames.len() >= max_frames { - let workflow = if job.terminal - && job_state_allows_workflow_verification(job.state.as_str()) - { - match wait_for_order_workflow_truth(config, &loaded.document) { - Ok(workflow) => workflow, - Err(error) => { - let state = error.state().to_owned(); - let reason = error.reason(); - let workflow = workflow_error_view( - &loaded.document, - state.as_str(), - reason.clone(), - ); - return Ok(OrderWatchView { - state, - source: ORDER_WORKFLOW_SOURCE.to_owned(), - order_id: loaded.document.order.order_id.clone(), - job_id: Some(job_id.clone()), - interval_ms: args.interval_ms, - reason: Some(reason.clone()), - workflow: Some(workflow), - frames, - actions: vec!["radroots order event list".to_owned()], - }); - } - } - } else { - None - }; - return Ok(OrderWatchView { - state: if let Some(workflow) = workflow.as_ref() { - workflow.state.clone() - } else if job.terminal { - job.state - } else { - "watching".to_owned() - }, - source: if workflow.is_some() { - ORDER_WORKFLOW_SOURCE.to_owned() - } else { - ORDER_LIFECYCLE_SOURCE.to_owned() - }, - order_id: loaded.document.order.order_id.clone(), - job_id: Some(job_id.clone()), - interval_ms: args.interval_ms, - reason: None, - workflow, - frames, - actions: vec!["radroots order event list".to_owned()], - }); - } - } - Ok(None) => { - return Ok(OrderWatchView { - state: "missing".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), - order_id: loaded.document.order.order_id.clone(), - job_id: Some(job_id.clone()), - interval_ms: args.interval_ms, - reason: Some("recorded job id was not found in radrootsd".to_owned()), - workflow: None, - frames, - actions: vec!["radroots order event list".to_owned()], - }); - } - Err(error) => return Ok(order_watch_error_view(&loaded, args, job_id, frames, error)), - } - - thread::sleep(Duration::from_millis(args.interval_ms)); - } + Ok(OrderWatchView { + state: "unavailable".to_owned(), + source: ORDER_SOURCE.to_owned(), + order_id: loaded.document.order.order_id.clone(), + job_id: None, + interval_ms: args.interval_ms, + reason: Some(ORDER_EVENT_STATE_UNAVAILABLE_REASON.to_owned()), + workflow: None, + frames: Vec::new(), + actions: vec![format!( + "radroots order get {}", + loaded.document.order.order_id + )], + }) } pub fn history(config: &RuntimeConfig) -> Result<OrderHistoryView, RuntimeError> { @@ -831,15 +523,14 @@ pub fn history(config: &RuntimeConfig) -> Result<OrderHistoryView, RuntimeError> if !dir.exists() { return Ok(OrderHistoryView { state: "empty".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), count: 0, - reason: Some("no submitted order drafts recorded yet".to_owned()), + reason: Some("no relay-backed order events recorded yet".to_owned()), orders: Vec::new(), actions: vec!["radroots order list".to_owned()], }); } - let mut orders = Vec::new(); let mut invalid_count = 0usize; for entry in fs::read_dir(&dir)? { let entry = entry?; @@ -847,60 +538,33 @@ pub fn history(config: &RuntimeConfig) -> Result<OrderHistoryView, RuntimeError> if path.extension().and_then(|value| value.to_str()) != Some("toml") { continue; } - match load_draft(path.as_path()) { - Ok(loaded) => { - if loaded.document.submission.is_some() { - orders.push(history_entry_from_loaded(config, &loaded)); - } - } - Err(_) => { - invalid_count += 1; - } + if load_draft(path.as_path()).is_err() { + invalid_count += 1; } } - orders.sort_by(|left, right| { - right - .submitted_at_unix - .unwrap_or(right.updated_at_unix) - .cmp(&left.submitted_at_unix.unwrap_or(left.updated_at_unix)) - .then_with(|| left.id.cmp(&right.id)) - }); - - let state = if orders.is_empty() { - "empty" - } else if invalid_count > 0 - || orders - .iter() - .any(|order| matches!(order.state.as_str(), "error" | "unavailable")) - { + let state = if invalid_count > 0 { "degraded" } else { - "ready" + "empty" }; - let reason = if orders.is_empty() { - Some("no submitted order drafts recorded yet".to_owned()) - } else if invalid_count > 0 { + let reason = if invalid_count > 0 { Some(format!( - "{invalid_count} invalid order draft file{} skipped while building history", + "{invalid_count} invalid order draft file{} skipped while reading local order event state", if invalid_count == 1 { "" } else { "s" } )) } else { - None + Some("no relay-backed order events recorded yet".to_owned()) }; Ok(OrderHistoryView { state: state.to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), - count: orders.len(), + source: ORDER_SOURCE.to_owned(), + count: 0, reason, - orders, - actions: if state == "empty" { - vec!["radroots basket create".to_owned()] - } else { - Vec::new() - }, + orders: Vec::new(), + actions: vec!["radroots order list".to_owned()], }) } @@ -912,7 +576,7 @@ pub fn cancel( if !file.exists() { return Ok(OrderCancelView { state: "missing".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), lookup: args.key.clone(), order_id: None, reason: Some(format!("order draft `{}` was not found", args.key)), @@ -926,7 +590,7 @@ pub fn cancel( Err(reason) => { return Ok(OrderCancelView { state: "error".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + source: ORDER_SOURCE.to_owned(), lookup: args.key.clone(), order_id: None, reason: Some(reason), @@ -936,39 +600,17 @@ pub fn cancel( } }; - let Some(job) = submission_job_view(config, &loaded.document, false) else { - return Ok(OrderCancelView { - state: "not_submitted".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), - lookup: args.key.clone(), - order_id: Some(loaded.document.order.order_id.clone()), - reason: Some("order draft has not been submitted yet".to_owned()), - job: None, - actions: vec![format!( - "radroots order submit {}", - loaded.document.order.order_id - )], - }); - }; - - let job_id = loaded - .document - .submission - .as_ref() - .map(|submission| submission.job_id.clone()); Ok(OrderCancelView { - state: "unconfigured".to_owned(), - source: ORDER_LIFECYCLE_SOURCE.to_owned(), + state: "unavailable".to_owned(), + source: ORDER_SOURCE.to_owned(), lookup: args.key.clone(), order_id: Some(loaded.document.order.order_id.clone()), - reason: Some( - "durable order cancel needs trade-chain root and previous event refs that the current local order read plane does not persist yet".to_owned(), - ), - job: Some(job), - actions: vec![ - "radroots order event list".to_owned(), - format!("radroots job get {}", job_id.unwrap_or_default()), - ], + reason: Some(ORDER_EVENT_STATE_UNAVAILABLE_REASON.to_owned()), + job: None, + actions: vec![format!( + "radroots order get {}", + loaded.document.order.order_id + )], }) } @@ -1044,30 +686,16 @@ fn resolve_order_listing( } } -fn view_from_loaded( - config: &RuntimeConfig, - loaded: LoadedOrderDraft, - enrich_job: bool, -) -> OrderGetView { +fn view_from_loaded(loaded: LoadedOrderDraft) -> OrderGetView { let OrderInspection { state, ready_for_submit, listing_addr, seller_pubkey, issues, - job, - } = inspect_document(config, &loaded.document, enrich_job); - let workflow = resolve_order_workflow_snapshot(config, &loaded.document) - .ok() - .flatten(); - let state = preferred_order_state(state, workflow.as_ref()); - - let mut actions = - actions_for_document(&loaded.document, loaded.file.as_path(), issues.as_slice()); - if let Some(job) = &job { - actions.push(format!("radroots job get {}", job.job_id)); - actions.push("radroots order event list".to_owned()); - } + } = inspect_document(&loaded.document); + + let actions = actions_for_document(&loaded.document, loaded.file.as_path(), issues.as_slice()); OrderGetView { state, @@ -1092,23 +720,22 @@ fn view_from_loaded( }) .collect(), updated_at_unix: Some(loaded.updated_at_unix), - job, - workflow, + job: None, + workflow: None, reason: None, issues, actions, } } -fn summary_from_loaded(config: &RuntimeConfig, loaded: &LoadedOrderDraft) -> OrderSummaryView { +fn summary_from_loaded(loaded: &LoadedOrderDraft) -> OrderSummaryView { let OrderInspection { state, ready_for_submit, listing_addr, seller_pubkey: _, issues, - job, - } = inspect_document(config, &loaded.document, false); + } = inspect_document(&loaded.document); OrderSummaryView { id: loaded.document.order.order_id.clone(), @@ -1120,43 +747,11 @@ fn summary_from_loaded(config: &RuntimeConfig, loaded: &LoadedOrderDraft) -> Ord buyer_account_id: loaded.document.buyer_account_id.clone(), item_count: loaded.document.order.items.len(), updated_at_unix: loaded.updated_at_unix, - job, + job: None, issues, } } -fn history_entry_from_loaded( - config: &RuntimeConfig, - loaded: &LoadedOrderDraft, -) -> OrderHistoryEntryView { - let job = submission_job_view(config, &loaded.document, true); - let workflow = resolve_order_workflow_snapshot(config, &loaded.document) - .ok() - .flatten(); - let submitted_at_unix = loaded - .document - .submission - .as_ref() - .and_then(|submission| submission.submitted_at_unix); - OrderHistoryEntryView { - id: loaded.document.order.order_id.clone(), - state: preferred_order_state( - job.as_ref() - .map(|job| job.state.clone()) - .unwrap_or_else(|| "recorded".to_owned()), - workflow.as_ref(), - ), - listing_lookup: loaded.document.listing_lookup.clone(), - listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()), - buyer_account_id: loaded.document.buyer_account_id.clone(), - submitted_at_unix, - updated_at_unix: loaded.updated_at_unix, - job, - workflow, - issues: Vec::new(), - } -} - fn summary_for_invalid_file(path: &Path, reason: String) -> OrderSummaryView { let id = path .file_stem() @@ -1181,11 +776,7 @@ fn summary_for_invalid_file(path: &Path, reason: String) -> OrderSummaryView { } } -fn inspect_document( - config: &RuntimeConfig, - document: &OrderDraftDocument, - enrich_job: bool, -) -> OrderInspection { +fn inspect_document(document: &OrderDraftDocument) -> OrderInspection { let listing_addr = non_empty_string(document.order.listing_addr.clone()); let parsed_listing_addr = listing_addr .as_deref() @@ -1196,11 +787,8 @@ fn inspect_document( .map(|listing| listing.seller_pubkey.clone()) }); let issues = collect_issues(document); - let job = submission_job_view(config, document, enrich_job); - let ready_for_submit = issues.is_empty() && job.is_none(); - let state = if job.is_some() { - "submitted".to_owned() - } else if ready_for_submit { + let ready_for_submit = issues.is_empty(); + let state = if ready_for_submit { "ready".to_owned() } else { "draft".to_owned() @@ -1212,7 +800,6 @@ fn inspect_document( listing_addr, seller_pubkey, issues, - job, } } @@ -1320,136 +907,14 @@ fn actions_for_document( actions } -fn submission_job_view( +fn direct_relay_unavailable_order_submit_view( config: &RuntimeConfig, - document: &OrderDraftDocument, - enrich: bool, -) -> Option<OrderJobView> { - let submission = document.submission.as_ref()?; - let job_id = normalize_optional(Some(submission.job_id.as_str()))?; - if !enrich || config.rpc.bridge_bearer_token.is_none() { - return Some(recorded_job_view(submission, job_id)); - } - - match daemon::bridge_job(config, job_id.as_str()) { - Ok(Some(job)) => Some(OrderJobView { - job_id, - state: job.state, - command: Some(job.command), - signer_mode: Some(job.signer.clone()), - signer_session_id: job.signer_session_id, - requested_signer_session_id: None, - event_id: job.event_id, - event_addr: job.event_addr, - reason: None, - }), - Ok(None) => Some(OrderJobView { - job_id, - state: "missing".to_owned(), - command: submission.command.clone(), - signer_mode: submission.signer_mode.clone(), - signer_session_id: submission.signer_session_id.clone(), - requested_signer_session_id: None, - event_id: submission.event_id.clone(), - event_addr: submission.event_addr.clone(), - reason: Some("recorded job id was not found in radrootsd".to_owned()), - }), - Err(error) => Some(job_view_from_error(job_id, error)), - } -} - -fn recorded_job_view(submission: &OrderDraftSubmission, job_id: String) -> OrderJobView { - OrderJobView { - job_id, - state: submission - .state - .clone() - .unwrap_or_else(|| "recorded".to_owned()), - command: submission.command.clone(), - signer_mode: submission.signer_mode.clone(), - signer_session_id: submission.signer_session_id.clone(), - requested_signer_session_id: None, - event_id: submission.event_id.clone(), - event_addr: submission.event_addr.clone(), - reason: None, - } -} - -fn job_view_from_error(job_id: String, error: DaemonRpcError) -> OrderJobView { - match error { - DaemonRpcError::Unconfigured(reason) - | DaemonRpcError::Unauthorized(reason) - | DaemonRpcError::MethodUnavailable(reason) => OrderJobView { - job_id, - state: "unconfigured".to_owned(), - command: None, - signer_mode: None, - signer_session_id: None, - requested_signer_session_id: None, - event_id: None, - event_addr: None, - reason: Some(reason), - }, - DaemonRpcError::External(reason) => OrderJobView { - job_id, - state: "unavailable".to_owned(), - command: None, - signer_mode: None, - signer_session_id: None, - requested_signer_session_id: None, - event_id: None, - event_addr: None, - reason: Some(reason), - }, - DaemonRpcError::InvalidResponse(reason) - | DaemonRpcError::Remote(reason) - | DaemonRpcError::UnknownJob(reason) => OrderJobView { - job_id, - state: "error".to_owned(), - command: None, - signer_mode: None, - signer_session_id: None, - requested_signer_session_id: None, - event_id: None, - event_addr: None, - reason: Some(reason), - }, - } -} - -fn order_submit_error_view( loaded: &LoadedOrderDraft, args: &OrderSubmitArgs, - error: DaemonRpcError, ) -> OrderSubmitView { - let (state, reason, mut actions) = match error { - DaemonRpcError::Unconfigured(reason) - | DaemonRpcError::Unauthorized(reason) - | DaemonRpcError::MethodUnavailable(reason) => ( - "unconfigured".to_owned(), - reason, - vec![ - "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(), - "start radrootsd with bridge ingress enabled".to_owned(), - ], - ), - DaemonRpcError::External(reason) => ( - "unavailable".to_owned(), - reason, - vec!["start radrootsd and verify the rpc url".to_owned()], - ), - DaemonRpcError::InvalidResponse(reason) - | DaemonRpcError::Remote(reason) - | DaemonRpcError::UnknownJob(reason) => ("error".to_owned(), reason, Vec::new()), - }; - actions.push(format!( - "radroots order get {}", - loaded.document.order.order_id - )); - OrderSubmitView { - state, - source: daemon::bridge_source().to_owned(), + state: "unavailable".to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: loaded.document.order.order_id.clone(), file: loaded.file.display().to_string(), listing_lookup: loaded.document.listing_lookup.clone(), @@ -1460,13 +925,13 @@ fn order_submit_error_view( dry_run: false, deduplicated: false, idempotency_key: args.idempotency_key.clone(), - signer_mode: None, + signer_mode: Some(config.signer.backend.as_str().to_owned()), signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), - reason: Some(reason), + requested_signer_session_id: None, + reason: Some(ORDER_SUBMIT_UNAVAILABLE_REASON.to_owned()), job: None, issues: Vec::new(), - actions, + actions: Vec::new(), } } @@ -1492,7 +957,7 @@ fn order_binding_error_view( OrderSubmitView { state: state.clone(), - source: daemon::bridge_source().to_owned(), + source: ORDER_SOURCE.to_owned(), order_id: loaded.document.order.order_id.clone(), file: loaded.file.display().to_string(), listing_lookup: loaded.document.listing_lookup.clone(), @@ -1505,7 +970,7 @@ fn order_binding_error_view( idempotency_key: args.idempotency_key.clone(), signer_mode: Some(config.signer.backend.as_str().to_owned()), signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), + requested_signer_session_id: None, reason: Some(reason), job: None, issues: Vec::new(), @@ -1536,468 +1001,6 @@ fn validate_local_order_write_authority( Ok(()) } -fn order_watch_error_view( - loaded: &LoadedOrderDraft, - args: &OrderWatchArgs, - job_id: String, - frames: Vec<OrderWatchFrameView>, - error: DaemonRpcError, -) -> OrderWatchView { - let (state, reason, actions) = match error { - DaemonRpcError::Unconfigured(reason) - | DaemonRpcError::Unauthorized(reason) - | DaemonRpcError::MethodUnavailable(reason) => ( - "unconfigured".to_owned(), - reason, - vec![ - "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(), - "start radrootsd with bridge ingress enabled".to_owned(), - ], - ), - DaemonRpcError::External(reason) => ( - "unavailable".to_owned(), - reason, - vec!["start radrootsd and verify the rpc url".to_owned()], - ), - DaemonRpcError::InvalidResponse(reason) - | DaemonRpcError::Remote(reason) - | DaemonRpcError::UnknownJob(reason) => ("error".to_owned(), reason, Vec::new()), - }; - - OrderWatchView { - state, - source: ORDER_LIFECYCLE_SOURCE.to_owned(), - order_id: loaded.document.order.order_id.clone(), - job_id: Some(job_id), - interval_ms: args.interval_ms, - reason: Some(reason), - workflow: None, - frames, - actions: if actions.is_empty() { - Vec::new() - } else { - actions - }, - } -} - -fn resolve_order_workflow_snapshot( - config: &RuntimeConfig, - document: &OrderDraftDocument, -) -> Result<Option<OrderWorkflowView>, WorkflowResolutionError> { - let Some(context) = resolve_workflow_context(config)? else { - return Ok(None); - }; - load_order_workflow_view( - context.state_path.as_path(), - document.order.order_id.as_str(), - ) - .map_err(WorkflowResolutionError::Error) -} - -fn wait_for_order_workflow_truth( - config: &RuntimeConfig, - document: &OrderDraftDocument, -) -> Result<Option<OrderWorkflowView>, WorkflowResolutionError> { - let Some(context) = resolve_workflow_context(config)? else { - return Ok(None); - }; - - if let Some(workflow) = load_order_workflow_view( - context.state_path.as_path(), - document.order.order_id.as_str(), - ) - .map_err(WorkflowResolutionError::Error)? - { - if workflow_state_is_terminal(workflow.state.as_str()) { - return Ok(Some(workflow)); - } - } - - let identity = - load_service_identity(Some(context.identity_path.as_path()), false).map_err(|error| { - WorkflowResolutionError::Unconfigured(format!( - "workflow verification requires repo-local rhi identity at {}: {error}", - context.identity_path.display() - )) - })?; - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| { - WorkflowResolutionError::Error(format!( - "build localhost workflow verifier runtime: {error}" - )) - })?; - - runtime.block_on(async move { - let replay_window_secs = workflow_replay_window_secs(document); - let trade_listing_runtime = TradeListingRuntime::load(TradeListingRuntimeConfig { - state_path: context.state_path.clone(), - replay_window_secs, - replay_overlap_secs: WORKFLOW_REPLAY_OVERLAP_SECS, - }) - .await - .map_err(|error| { - WorkflowResolutionError::Unavailable(format!( - "load repo-local rhi workflow state {}: {error}", - context.state_path.display() - )) - })?; - - let rhi = Rhi::with_trade_listing_runtime( - identity.keys().clone(), - trade_listing_runtime.clone(), - ); - rhi.client - .add_relay(context.relay_url.as_str()) - .await - .map_err(|error| { - WorkflowResolutionError::Unavailable(format!( - "attach localhost relay `{}` to workflow verifier: {error}", - context.relay_url - )) - })?; - - let handle = start_subscriber( - rhi.client.clone(), - identity.keys().clone(), - trade_listing_runtime, - BackoffConfig::default(), - ) - .await; - - let mut last_observed = load_order_workflow_view( - context.state_path.as_path(), - document.order.order_id.as_str(), - ) - .map_err(WorkflowResolutionError::Error)?; - let deadline = Instant::now() + WORKFLOW_FETCH_TIMEOUT; - - loop { - if let Some(workflow) = load_order_workflow_view( - context.state_path.as_path(), - document.order.order_id.as_str(), - ) - .map_err(WorkflowResolutionError::Error)? - { - if workflow_state_is_terminal(workflow.state.as_str()) { - handle.stop(); - handle.stopped().await; - return Ok(Some(workflow)); - } - last_observed = Some(workflow); - } - - if Instant::now() >= deadline { - handle.stop(); - handle.stopped().await; - let detail = match last_observed { - Some(view) => format!( - "workflow state did not reach a terminal value within {:?}; last observed workflow state was `{}`", - WORKFLOW_FETCH_TIMEOUT, view.state - ), - None => format!( - "workflow state did not appear within {:?} at {}", - WORKFLOW_FETCH_TIMEOUT, - context.state_path.display() - ), - }; - return Err(WorkflowResolutionError::Unavailable(detail)); - } - - tokio::time::sleep(WORKFLOW_POLL_INTERVAL).await; - } - }) -} - -fn workflow_replay_window_secs(document: &OrderDraftDocument) -> u64 { - let Some(submitted_at_unix) = document - .submission - .as_ref() - .and_then(|submission| submission.submitted_at_unix) - else { - return WORKFLOW_REPLAY_WINDOW_SECS; - }; - - let now = now_unix(); - if submitted_at_unix >= now { - return WORKFLOW_REPLAY_OVERLAP_SECS.max(1); - } - - let recent_window = now - .saturating_sub(submitted_at_unix) - .saturating_add(WORKFLOW_REPLAY_OVERLAP_SECS); - recent_window.clamp(1, WORKFLOW_REPLAY_WINDOW_SECS) -} - -fn resolve_workflow_context( - config: &RuntimeConfig, -) -> Result<Option<WorkflowContext>, WorkflowResolutionError> { - let Some(binding) = config.capability_binding(WORKFLOW_TRADE_CAPABILITY) else { - return Ok(None); - }; - - if binding.provider_runtime_id != WORKFLOW_PROVIDER_RUNTIME_ID { - return Err(WorkflowResolutionError::Unconfigured(format!( - "workflow.trade binding must use provider `{WORKFLOW_PROVIDER_RUNTIME_ID}`, got `{}`", - binding.provider_runtime_id - ))); - } - if binding.target_kind != CapabilityBindingTargetKind::ManagedInstance { - return Err(WorkflowResolutionError::Unconfigured(format!( - "workflow.trade binding must use target_kind `managed_instance`, got `{}`", - binding.target_kind.as_str() - ))); - } - if binding.target != WORKFLOW_TARGET { - return Err(WorkflowResolutionError::Unconfigured(format!( - "workflow.trade binding must target `{WORKFLOW_TARGET}`, got `{}`", - binding.target - ))); - } - if config.paths.profile != "repo_local" { - return Err(WorkflowResolutionError::Unconfigured( - "workflow.trade progression requires RADROOTS_CLI_PATHS_PROFILE=repo_local".to_owned(), - )); - } - let repo_local_root = config.paths.repo_local_root.as_ref().ok_or_else(|| { - WorkflowResolutionError::Unconfigured( - "workflow.trade progression requires a repo-local cli root".to_owned(), - ) - })?; - let canonical_relay_url = - canonical_local_relay_url().map_err(WorkflowResolutionError::Error)?; - if !config - .relay - .urls - .iter() - .any(|configured| loopback_endpoint_matches(configured, canonical_relay_url.as_str())) - { - return Err(WorkflowResolutionError::Unconfigured(format!( - "workflow.trade progression requires canonical localhost relay `{canonical_relay_url}`" - ))); - } - - let base_paths = RadrootsPathResolver::current() - .resolve( - RadrootsPathProfile::RepoLocal, - &RadrootsPathOverrides::repo_local(repo_local_root), - ) - .map_err(|error| { - WorkflowResolutionError::Error(format!( - "resolve repo-local workflow verifier roots from {}: {error}", - repo_local_root.display() - )) - })?; - let worker_namespace = - RadrootsRuntimeNamespace::worker(WORKFLOW_PROVIDER_RUNTIME_ID).map_err(|error| { - WorkflowResolutionError::Error(format!( - "resolve worker namespace `{WORKFLOW_PROVIDER_RUNTIME_ID}`: {error}" - )) - })?; - let worker_paths = base_paths.namespaced(&worker_namespace); - - Ok(Some(WorkflowContext { - relay_url: canonical_relay_url, - identity_path: worker_paths.secrets.join(WORKFLOW_IDENTITY_FILE_NAME), - state_path: worker_paths - .data - .join(WORKFLOW_STATE_DIR_NAME) - .join(WORKFLOW_STATE_FILE_NAME), - })) -} - -fn load_order_workflow_view( - state_path: &Path, - order_id: &str, -) -> Result<Option<OrderWorkflowView>, String> { - if !state_path.exists() { - return Ok(None); - } - - let raw = fs::read_to_string(state_path) - .map_err(|error| format!("read workflow state {}: {error}", state_path.display()))?; - let snapshot: JsonValue = serde_json::from_str(raw.as_str()) - .map_err(|error| format!("parse workflow state {}: {error}", state_path.display()))?; - let state = snapshot - .get("state") - .and_then(JsonValue::as_object) - .ok_or_else(|| { - format!( - "workflow state {} did not include top-level `state` object", - state_path.display() - ) - })?; - let orders = state - .get("orders") - .and_then(JsonValue::as_object) - .ok_or_else(|| { - format!( - "workflow state {} did not include `state.orders`", - state_path.display() - ) - })?; - let Some(order) = orders.get(order_id) else { - return Ok(None); - }; - let order_object = order.as_object().ok_or_else(|| { - format!( - "workflow state {} stored `state.orders.{order_id}` as a non-object", - state_path.display() - ) - })?; - let workflow_state = order_object - .get("status") - .and_then(JsonValue::as_str) - .ok_or_else(|| { - format!( - "workflow state {} did not include `status` for order `{order_id}`", - state_path.display() - ) - })?; - let listing_addr = order_object - .get("listing_addr") - .and_then(JsonValue::as_str) - .map(str::to_owned); - let validated_listing_event_id = listing_addr - .as_ref() - .and_then(|listing_addr| { - state - .get("validated_listing_events") - .and_then(JsonValue::as_object) - .and_then(|events| events.get(listing_addr)) - .and_then(JsonValue::as_object) - .and_then(|entry| entry.get("event_id")) - .and_then(JsonValue::as_str) - }) - .map(str::to_owned); - - Ok(Some(OrderWorkflowView { - state: workflow_state.to_owned(), - source: ORDER_WORKFLOW_SOURCE.to_owned(), - order_id: order_id.to_owned(), - listing_addr, - validated_listing_event_id, - root_event_id: order_object - .get("root_event_id") - .and_then(JsonValue::as_str) - .map(str::to_owned), - last_event_id: order_object - .get("last_event_id") - .and_then(JsonValue::as_str) - .map(str::to_owned), - reason: None, - })) -} - -fn preferred_order_state(base_state: String, workflow: Option<&OrderWorkflowView>) -> String { - match workflow { - Some(workflow) if workflow_state_is_business_truth(workflow.state.as_str()) => { - workflow.state.clone() - } - _ => base_state, - } -} - -fn workflow_state_is_business_truth(state: &str) -> bool { - matches!( - state, - "draft" - | "validated" - | "requested" - | "questioned" - | "revised" - | "accepted" - | "declined" - | "cancelled" - | "fulfilled" - | "completed" - ) -} - -fn workflow_state_is_terminal(state: &str) -> bool { - matches!(state, "declined" | "cancelled" | "completed") -} - -fn job_state_allows_workflow_verification(state: &str) -> bool { - !matches!( - state, - "failed" | "error" | "missing" | "unavailable" | "unconfigured" - ) -} - -fn workflow_error_view( - document: &OrderDraftDocument, - state: &str, - reason: String, -) -> OrderWorkflowView { - OrderWorkflowView { - state: state.to_owned(), - source: ORDER_WORKFLOW_SOURCE.to_owned(), - order_id: document.order.order_id.clone(), - listing_addr: non_empty_string(document.order.listing_addr.clone()), - validated_listing_event_id: None, - root_event_id: None, - last_event_id: None, - reason: Some(reason), - } -} - -fn canonical_local_relay_url() -> Result<String, String> { - let config = RadrootsSdkConfig::local(); - config - .resolved_relay_urls() - .map_err(|error| format!("resolve canonical localhost relay url: {error}"))? - .into_iter() - .next() - .ok_or_else(|| "missing canonical localhost relay url".to_owned()) -} - -fn loopback_endpoint_matches(left: &str, right: &str) -> bool { - let Ok(left_url) = url::Url::parse(left) else { - return false; - }; - let Ok(right_url) = url::Url::parse(right) else { - return false; - }; - - if left_url.scheme() != right_url.scheme() - || left_url.port_or_known_default() != right_url.port_or_known_default() - { - return false; - } - - match (left_url.host_str(), right_url.host_str()) { - (Some(left_host), Some(right_host)) if left_host == right_host => true, - (Some(left_host), Some(right_host)) => matches!( - (left_host, right_host), - ("127.0.0.1", "localhost") | ("localhost", "127.0.0.1") - ), - _ => false, - } -} - -fn trade_order_from_document(document: &OrderDraftDocument) -> RadrootsTradeOrder { - RadrootsTradeOrder { - order_id: document.order.order_id.clone(), - listing_addr: document.order.listing_addr.clone(), - buyer_pubkey: document.order.buyer_pubkey.clone(), - seller_pubkey: document.order.seller_pubkey.clone(), - items: document - .order - .items - .iter() - .map(|item| RadrootsTradeOrderItem { - bin_id: item.bin_id.clone(), - bin_count: item.bin_count, - }) - .collect(), - discounts: None, - } -} - fn load_draft(path: &Path) -> Result<LoadedOrderDraft, String> { let contents = fs::read_to_string(path) .map_err(|error| format!("read order draft {}: {error}", path.display()))?; @@ -2141,7 +1144,6 @@ struct OrderInspection { listing_addr: Option<String>, seller_pubkey: Option<String>, issues: Vec<OrderIssueView>, - job: Option<OrderJobView>, } impl From<OrderGetView> for OrderNewView { @@ -2166,11 +1168,7 @@ impl From<OrderGetView> for OrderNewView { #[cfg(test)] mod tests { - use super::{ - ORDER_DRAFT_KIND, OrderDraft, OrderDraftDocument, OrderDraftItem, OrderDraftSubmission, - WORKFLOW_REPLAY_OVERLAP_SECS, WORKFLOW_REPLAY_WINDOW_SECS, next_order_id, now_unix, - workflow_replay_window_secs, - }; + use super::{ORDER_DRAFT_KIND, OrderDraft, OrderDraftDocument, OrderDraftItem, next_order_id}; #[test] fn generated_order_id_uses_stable_prefix() { @@ -2196,95 +1194,10 @@ mod tests { }, listing_lookup: Some("fresh-eggs".to_owned()), buyer_account_id: Some("acct_demo".to_owned()), - submission: Some(OrderDraftSubmission { - job_id: "job_01".to_owned(), - state: Some("accepted".to_owned()), - signer_mode: Some("embedded_service_identity".to_owned()), - signer_session_id: None, - command: Some("order.submit".to_owned()), - event_id: None, - event_addr: None, - submitted_at_unix: Some(1), - }), }; let rendered = toml::to_string_pretty(&document).expect("render draft"); assert!(rendered.contains("kind = \"order_draft_v1\"")); assert!(rendered.contains("order_id = \"ord_AAAAAAAAAAAAAAAAAAAAAg\"")); - assert!(rendered.contains("job_id = \"job_01\"")); - } - - #[test] - fn workflow_replay_window_prefers_recent_submission_age_plus_overlap() { - let now = now_unix(); - let document = OrderDraftDocument { - version: 1, - kind: ORDER_DRAFT_KIND.to_owned(), - order: OrderDraft { - order_id: "ord_AAAAAAAAAAAAAAAAAAAAAg".to_owned(), - listing_addr: "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAg".to_owned(), - buyer_pubkey: "a".repeat(64), - seller_pubkey: "b".repeat(64), - items: vec![OrderDraftItem { - bin_id: "bin-1".to_owned(), - bin_count: 2, - }], - }, - listing_lookup: Some("fresh-eggs".to_owned()), - buyer_account_id: Some("acct_demo".to_owned()), - submission: Some(OrderDraftSubmission { - job_id: "job_01".to_owned(), - state: Some("accepted".to_owned()), - signer_mode: Some("embedded_service_identity".to_owned()), - signer_session_id: None, - command: Some("order.submit".to_owned()), - event_id: None, - event_addr: None, - submitted_at_unix: Some(now.saturating_sub(42)), - }), - }; - - assert_eq!( - workflow_replay_window_secs(&document), - 42 + WORKFLOW_REPLAY_OVERLAP_SECS - ); - } - - #[test] - fn workflow_replay_window_caps_at_default_window_for_old_orders() { - let now = now_unix(); - let document = OrderDraftDocument { - version: 1, - kind: ORDER_DRAFT_KIND.to_owned(), - order: OrderDraft { - order_id: "ord_AAAAAAAAAAAAAAAAAAAAAg".to_owned(), - listing_addr: "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAg".to_owned(), - buyer_pubkey: "a".repeat(64), - seller_pubkey: "b".repeat(64), - items: vec![OrderDraftItem { - bin_id: "bin-1".to_owned(), - bin_count: 2, - }], - }, - listing_lookup: Some("fresh-eggs".to_owned()), - buyer_account_id: Some("acct_demo".to_owned()), - submission: Some(OrderDraftSubmission { - job_id: "job_01".to_owned(), - state: Some("accepted".to_owned()), - signer_mode: Some("embedded_service_identity".to_owned()), - signer_session_id: None, - command: Some("order.submit".to_owned()), - event_id: None, - event_addr: None, - submitted_at_unix: Some( - now.saturating_sub(WORKFLOW_REPLAY_WINDOW_SECS + WORKFLOW_REPLAY_OVERLAP_SECS), - ), - }), - }; - - assert_eq!( - workflow_replay_window_secs(&document), - WORKFLOW_REPLAY_WINDOW_SECS - ); } } diff --git a/src/runtime_args.rs b/src/runtime_args.rs @@ -174,7 +174,6 @@ pub struct OrderDraftCreateArgs { pub struct OrderSubmitArgs { pub key: String, pub idempotency_key: Option<String>, - pub signer_session_id: Option<String>, } #[derive(Debug, Clone)] diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -946,6 +946,7 @@ fn order_submit_missing_order_returns_not_found_while_read_view_stays_successful assert_eq!(submit["errors"][0]["exit_code"], 4); assert_eq!(submit["errors"][0]["detail"]["class"], "resource"); assert_no_removed_command_reference(&submit, &["order", "submit"]); + assert_no_daemon_runtime_reference(&submit, &["order", "submit"]); } #[test] @@ -1024,6 +1025,7 @@ fn buyer_target_flow_acceptance_uses_target_operations() { ); assert_eq!(orders["result"]["orders"][0]["issues"], Value::Null); assert_no_removed_command_reference(&orders, &["order", "list"]); + assert_no_daemon_runtime_reference(&orders, &["order", "list"]); let submit = sandbox.json_success(&["--format", "json", "--dry-run", "order", "submit", order_id]); @@ -1035,6 +1037,50 @@ fn buyer_target_flow_acceptance_uses_target_operations() { assert_eq!(submit["result"]["buyer_account_id"], account_id); assert_eq!(submit["errors"].as_array().expect("errors").len(), 0); assert_no_removed_command_reference(&submit, &["order", "submit", "--dry-run"]); + assert_no_daemon_runtime_reference(&submit, &["order", "submit", "--dry-run"]); + + let (output, unavailable_submit) = sandbox.json_output(&[ + "--format", + "json", + "--approval-token", + "approve", + "order", + "submit", + order_id, + ]); + assert!(!output.status.success()); + assert_eq!(unavailable_submit["operation_id"], "order.submit"); + assert_eq!(unavailable_submit["result"], Value::Null); + assert_eq!( + unavailable_submit["errors"][0]["code"], + "operation_unavailable" + ); + assert_eq!( + unavailable_submit["errors"][0]["detail"]["class"], + "operation" + ); + assert!( + unavailable_submit["errors"][0]["message"] + .as_str() + .expect("message") + .contains("direct Nostr relay order publication is not implemented") + ); + assert_no_removed_command_reference(&unavailable_submit, &["order", "submit"]); + assert_no_daemon_runtime_reference(&unavailable_submit, &["order", "submit"]); + + let order_after_submit = sandbox.json_success(&["--format", "json", "order", "get", order_id]); + assert_eq!(order_after_submit["operation_id"], "order.get"); + assert_eq!(order_after_submit["result"]["state"], "ready"); + assert_eq!(order_after_submit["result"]["job"], Value::Null); + assert_eq!(order_after_submit["result"]["workflow"], Value::Null); + assert_no_daemon_runtime_reference(&order_after_submit, &["order", "get"]); + + let watch = sandbox.json_success(&["--format", "json", "order", "event", "watch", order_id]); + assert_eq!(watch["operation_id"], "order.event.watch"); + assert_eq!(watch["result"]["state"], "unavailable"); + assert_eq!(watch["result"]["job_id"], Value::Null); + assert_eq!(watch["result"]["workflow"], Value::Null); + assert_no_daemon_runtime_reference(&watch, &["order", "event", "watch"]); } #[test] @@ -1084,6 +1130,7 @@ fn ready_order_submit_dry_run_validates_local_buyer_authority() { assert_eq!(dry_run["result"]["state"], "dry_run"); assert_eq!(dry_run["result"]["dry_run"], true); assert_eq!(dry_run["result"]["buyer_account_id"], first_account_id); + assert_no_daemon_runtime_reference(&dry_run, &["order", "submit", "--dry-run"]); let second = sandbox.json_success(&["--format", "json", "account", "create"]); let second_account_id = second["result"]["account"]["id"] @@ -1106,6 +1153,7 @@ fn ready_order_submit_dry_run_validates_local_buyer_authority() { assert_eq!(mismatch["errors"][0]["code"], "account_mismatch"); assert_eq!(mismatch["errors"][0]["detail"]["class"], "account"); assert_no_removed_command_reference(&mismatch, &["order", "submit", "--dry-run"]); + assert_no_daemon_runtime_reference(&mismatch, &["order", "submit", "--dry-run"]); } #[test]