cli

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

order.rs (381814B)


      1 #![allow(dead_code)]
      2 
      3 mod sdk_status;
      4 
      5 use std::collections::{HashMap, HashSet};
      6 use std::fs;
      7 use std::path::{Path, PathBuf};
      8 use std::sync::atomic::{AtomicU64, Ordering};
      9 use std::time::{SystemTime, UNIX_EPOCH};
     10 
     11 use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner};
     12 use radroots_core::{
     13     RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountScope,
     14     RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCoreUnit,
     15     convert_unit_decimal,
     16 };
     17 use radroots_events::contract::RadrootsActorRole;
     18 use radroots_events::ids::{
     19     RadrootsEconomicsDigest, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress,
     20     RadrootsOrderId, RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey,
     21 };
     22 use radroots_events::kinds::{
     23     KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST,
     24     KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL,
     25 };
     26 use radroots_events::listing::{
     27     RadrootsListing, RadrootsListingAvailability, RadrootsListingStatus,
     28 };
     29 use radroots_events::order::{
     30     RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
     31     RadrootsOrderEconomicActor, RadrootsOrderEconomicEffect, RadrootsOrderEconomicItem,
     32     RadrootsOrderEconomicLine, RadrootsOrderEconomicLineKind, RadrootsOrderEconomics,
     33     RadrootsOrderEventType, RadrootsOrderInventoryCommitment, RadrootsOrderItem,
     34     RadrootsOrderPricingBasis, RadrootsOrderRequest, RadrootsOrderRevisionDecision,
     35     RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal,
     36 };
     37 use radroots_events::{RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr};
     38 use radroots_events_codec::d_tag::is_d_tag_base64url;
     39 use radroots_events_codec::listing::decode::listing_from_event;
     40 use radroots_events_codec::order::{
     41     order_cancellation_from_event, order_envelope_from_event, order_event_context_from_tags,
     42     order_request_from_event, order_revision_decision_event_build,
     43     order_revision_decision_from_event, order_revision_proposal_event_build,
     44     order_revision_proposal_from_event,
     45 };
     46 use radroots_events_codec::wire::WireEventParts;
     47 use radroots_local_events::{
     48     BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalRecordFamily,
     49     LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryState,
     50     SourceRuntime, normalize_relay_urls, validate_supported_buyer_order_request_local_work_payload,
     51 };
     52 use radroots_nostr::prelude::{
     53     RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKeys, radroots_event_from_nostr,
     54     radroots_nostr_filter_tag, radroots_nostr_kind,
     55 };
     56 use radroots_replica_db::{
     57     ReplicaSql, ReplicaTradeProductSummaryRow, nostr_event_head, trade_product,
     58 };
     59 use radroots_replica_db_schema::nostr_event_head::{
     60     INostrEventHeadFindOne, INostrEventHeadFindOneArgs, NostrEventHeadQueryBindValues,
     61 };
     62 use radroots_replica_db_schema::trade_product::{
     63     ITradeProductFieldsFilter, ITradeProductFindMany, TradeProduct,
     64 };
     65 use radroots_sdk::{
     66     OrderCancellationEnqueueRequest, OrderCancellationPrepareRequest, OrderCancellationReceipt,
     67     OrderDecisionEnqueueRequest, OrderDecisionReceipt, OrderEvidenceIngestRequest,
     68     OrderRequestEvidenceIngestRequest, OrderRevisionDecisionEnqueueRequest,
     69     OrderRevisionDecisionPrepareRequest, OrderRevisionDecisionReceipt,
     70     OrderRevisionProposalEnqueueRequest, OrderRevisionProposalPrepareRequest,
     71     OrderRevisionProposalReceipt, OrderStatusRequest, OrderSubmitEnqueueRequest, OrderSubmitPlan,
     72     OrderSubmitPrepareRequest, OrderSubmitReceipt, OrderWorkflowEnqueueReceipt,
     73     PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRelayOutcomeKind,
     74     PushOutboxRequest, SdkMutationState, SdkRelayTargetPolicy, SdkRelayUrlPolicy,
     75 };
     76 use radroots_sql_core::SqliteExecutor;
     77 use radroots_trade::order::{
     78     RadrootsListingInventoryAccountingInputs, RadrootsListingInventoryAccountingIssue,
     79     RadrootsListingInventoryAccountingProjection, RadrootsListingInventoryBinAvailability,
     80     RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderIssue,
     81     RadrootsOrderReductionInputs, RadrootsOrderRequestRecord, RadrootsOrderRevisionDecisionRecord,
     82     RadrootsOrderRevisionProposalRecord, RadrootsOrderStatus,
     83     canonicalize_order_decision_for_signer, canonicalize_order_request_for_signer,
     84     reduce_listing_inventory_accounting, reduce_order_events,
     85 };
     86 use serde::{Deserialize, Serialize};
     87 use serde_json::{Value, json};
     88 
     89 use crate::cli::global::{
     90     OrderAppRecordExportArgs, OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs,
     91     OrderDraftCreateArgs, OrderRebindArgs, OrderRevisionDecisionArg, OrderRevisionDecisionArgs,
     92     OrderRevisionProposeArgs, OrderStatusArgs, OrderSubmitArgs, RecordLookupArgs,
     93 };
     94 use crate::runtime::RuntimeError;
     95 use crate::runtime::account;
     96 use crate::runtime::config::{RuntimeConfig, SignerBackend};
     97 use crate::runtime::direct_relay::{
     98     DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, fetch_events_from_relays,
     99 };
    100 use crate::runtime::local_events::{
    101     get_shared_record, list_shared_records_before, list_shared_records_latest,
    102     shared_local_events_db_path,
    103 };
    104 use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession};
    105 use crate::runtime::signer::ActorWriteBindingError;
    106 use crate::runtime::sync::{
    107     RelayIngestScope, freshness_for_scope, freshness_requires_refresh, market_refresh,
    108     relay_provenance_relays_for_scope,
    109 };
    110 use crate::view::runtime::{
    111     OrderAppRecordExportView, OrderAppRecordListView, OrderAppRecordSummaryView,
    112     OrderCancellationView, OrderDecisionView, OrderDraftItemView, OrderEventListEntryView,
    113     OrderEventListView, OrderGetView, OrderInventoryBinView, OrderInventoryView, OrderIssueView,
    114     OrderListView, OrderNewView, OrderRebindView, OrderRevisionDecisionView,
    115     OrderRevisionProposalView, OrderStatusLifecycleCancellationView, OrderStatusLifecycleView,
    116     OrderStatusRevisionView, OrderStatusView, OrderSubmitView, OrderSummaryView, RelayFailureView,
    117 };
    118 
    119 use self::sdk_status::sdk_order_status_view;
    120 
    121 const ORDER_DRAFT_KIND: &str = "order_draft_v1";
    122 const ORDER_SOURCE: &str = "local order drafts · local first";
    123 const ORDER_APP_RECORD_SOURCE: &str = "app-authored shared local order records";
    124 const ORDER_SUBMIT_SOURCE: &str = "SDK order submit · local key";
    125 const ORDER_DECISION_SOURCE: &str = "SDK order decision · local key";
    126 const ORDER_REVISION_PROPOSAL_SOURCE: &str = "SDK order revision proposal · local key";
    127 const ORDER_REVISION_DECISION_SOURCE: &str = "SDK order revision decision · local key";
    128 const ORDER_CANCELLATION_SOURCE: &str = "SDK order cancellation · local key";
    129 const ORDER_EVENT_LIST_SOURCE: &str = "direct Nostr relay fetch · selected seller identity";
    130 const LEGACY_ORDER_PREFLIGHT_STATUS_SOURCE: &str =
    131     "legacy direct Nostr relay preflight status · active order reducer";
    132 const ORDER_STATUS_SDK_SOURCE: &str = "SDK local order projection";
    133 const ORDER_EVENT_LIST_RELAY_ACTION: &str =
    134     "radroots --relay wss://relay.example.com order event list";
    135 const ORDER_BUYER_ACTOR_SOURCE_RESOLVED_ACCOUNT: &str = "resolved_account";
    136 const ORDER_BUYER_ACTOR_SOURCE_REBIND: &str = "order_rebind";
    137 const ORDER_APP_RECORD_LIST_LIMIT: u32 = 500;
    138 const ORDER_ACTOR_CONTEXT_ORDER_DRAFT: &str = "order_draft";
    139 const ORDER_ACTOR_CONTEXT_RESOLVED_ACCOUNT: &str = "resolved_account";
    140 const ORDER_ACTOR_CONTEXT_NETWORK_ONLY: &str = "network_only";
    141 const ORDER_ACTOR_CONTEXT_SDK_LOCAL: &str = "sdk_local_projection";
    142 const ORDERS_DIR: &str = "orders/drafts";
    143 const APP_ORDER_ALREADY_SUBMITTED_ISSUE: &str = "app_order_already_submitted";
    144 const APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE: &str = "app_order_signed_evidence_conflict";
    145 
    146 static ORDER_COUNTER: AtomicU64 = AtomicU64::new(0);
    147 
    148 fn protocol_order_id(value: &str, field: &str) -> Result<RadrootsOrderId, RuntimeError> {
    149     value
    150         .parse()
    151         .map_err(|error| RuntimeError::Config(format!("{field} is not a valid order id: {error}")))
    152 }
    153 
    154 fn protocol_listing_addr(value: &str, field: &str) -> Result<RadrootsListingAddress, RuntimeError> {
    155     value.parse().map_err(|error| {
    156         RuntimeError::Config(format!("{field} is not a valid listing address: {error}"))
    157     })
    158 }
    159 
    160 fn protocol_revision_id(value: &str, field: &str) -> Result<RadrootsOrderRevisionId, RuntimeError> {
    161     value.parse().map_err(|error| {
    162         RuntimeError::Config(format!("{field} is not a valid order revision id: {error}"))
    163     })
    164 }
    165 
    166 fn protocol_quote_id(value: &str, field: &str) -> Result<RadrootsOrderQuoteId, RuntimeError> {
    167     value.parse().map_err(|error| {
    168         RuntimeError::Config(format!("{field} is not a valid order quote id: {error}"))
    169     })
    170 }
    171 
    172 fn protocol_inventory_bin_id(
    173     value: &str,
    174     field: &str,
    175 ) -> Result<RadrootsInventoryBinId, RuntimeError> {
    176     value.parse().map_err(|error| {
    177         RuntimeError::Config(format!("{field} is not a valid inventory bin id: {error}"))
    178     })
    179 }
    180 
    181 fn protocol_economics_digest(
    182     value: &str,
    183     field: &str,
    184 ) -> Result<RadrootsEconomicsDigest, RuntimeError> {
    185     value.parse().map_err(|error| {
    186         RuntimeError::Config(format!("{field} is not a valid economics digest: {error}"))
    187     })
    188 }
    189 
    190 fn protocol_event_id(value: &str, field: &str) -> Result<RadrootsEventId, RuntimeError> {
    191     value
    192         .parse()
    193         .map_err(|error| RuntimeError::Config(format!("{field} is not a valid event id: {error}")))
    194 }
    195 
    196 fn protocol_pubkey(value: &str, field: &str) -> Result<RadrootsPublicKey, RuntimeError> {
    197     value
    198         .parse()
    199         .map_err(|error| RuntimeError::Config(format!("{field} is not a valid pubkey: {error}")))
    200 }
    201 
    202 fn optional_string<T: ToString>(value: Option<T>) -> Option<String> {
    203     value.map(|value| value.to_string())
    204 }
    205 
    206 fn required_order_context_event_id(
    207     event_id: Option<RadrootsEventId>,
    208     tag: &'static str,
    209     message: &'static str,
    210 ) -> Result<RadrootsEventId, RuntimeError> {
    211     event_id.ok_or_else(|| RuntimeError::Config(format!("{message} is missing {tag}")))
    212 }
    213 
    214 #[derive(Debug, Clone, Serialize, Deserialize)]
    215 #[serde(deny_unknown_fields)]
    216 struct OrderDraftDocument {
    217     version: u32,
    218     kind: String,
    219     order: OrderDraft,
    220     buyer_actor: OrderDraftBuyerActor,
    221     #[serde(default, skip_serializing_if = "Option::is_none")]
    222     listing_lookup: Option<String>,
    223 }
    224 
    225 #[derive(Debug, Clone, Serialize, Deserialize)]
    226 #[serde(deny_unknown_fields)]
    227 struct OrderDraft {
    228     order_id: String,
    229     #[serde(default, skip_serializing_if = "String::is_empty")]
    230     listing_addr: String,
    231     #[serde(default, skip_serializing_if = "String::is_empty")]
    232     listing_event_id: String,
    233     #[serde(default, skip_serializing_if = "Vec::is_empty")]
    234     listing_relays: Vec<String>,
    235     #[serde(default, skip_serializing_if = "String::is_empty")]
    236     buyer_pubkey: String,
    237     #[serde(default, skip_serializing_if = "String::is_empty")]
    238     seller_pubkey: String,
    239     #[serde(default, skip_serializing_if = "Vec::is_empty")]
    240     items: Vec<OrderDraftItem>,
    241     #[serde(default, skip_serializing_if = "Option::is_none")]
    242     economics: Option<RadrootsOrderEconomics>,
    243 }
    244 
    245 #[derive(Debug, Clone, Serialize, Deserialize)]
    246 #[serde(deny_unknown_fields)]
    247 struct OrderDraftItem {
    248     bin_id: String,
    249     bin_count: u32,
    250 }
    251 
    252 #[derive(Debug, Clone, Serialize, Deserialize)]
    253 #[serde(deny_unknown_fields)]
    254 struct OrderDraftBuyerActor {
    255     account_id: String,
    256     pubkey: String,
    257     source: String,
    258 }
    259 
    260 #[derive(Debug, Clone)]
    261 struct LoadedOrderDraft {
    262     file: PathBuf,
    263     updated_at_unix: u64,
    264     document: OrderDraftDocument,
    265 }
    266 
    267 #[derive(Debug, Clone)]
    268 struct LoadedAppOrderRecord {
    269     record: LocalEventRecord,
    270     loaded: LoadedOrderDraft,
    271     source_issues: Vec<OrderIssueView>,
    272 }
    273 
    274 #[derive(Debug, Clone)]
    275 struct AppOrderRecordListEntry {
    276     record: LocalEventRecord,
    277     superseded_count: usize,
    278 }
    279 
    280 #[derive(Debug, Clone)]
    281 struct ResolvedOrderListing {
    282     listing_addr: String,
    283     listing_event_id: String,
    284     listing_relays: Vec<String>,
    285     seller_pubkey: String,
    286     economics_product: Option<ResolvedOrderEconomicsProduct>,
    287 }
    288 
    289 #[derive(Debug, Clone)]
    290 struct ResolvedOrderEconomicsProduct {
    291     qty_amt_exact: Option<String>,
    292     qty_unit: String,
    293     price_amt_exact: Option<String>,
    294     price_currency: String,
    295     price_qty_amt_exact: Option<String>,
    296     price_qty_unit: String,
    297     primary_bin_id: Option<String>,
    298     verified_primary_bin_id: Option<String>,
    299     notes: Option<String>,
    300 }
    301 
    302 impl ResolvedOrderEconomicsProduct {
    303     fn from_summary(row: &ReplicaTradeProductSummaryRow) -> Self {
    304         Self {
    305             qty_amt_exact: row.qty_amt_exact.clone(),
    306             qty_unit: row.qty_unit.clone(),
    307             price_amt_exact: row.price_amt_exact.clone(),
    308             price_currency: row.price_currency.clone(),
    309             price_qty_amt_exact: row.price_qty_amt_exact.clone(),
    310             price_qty_unit: row.price_qty_unit.clone(),
    311             primary_bin_id: row.primary_bin_id.clone(),
    312             verified_primary_bin_id: row.verified_primary_bin_id.clone(),
    313             notes: row.notes.clone(),
    314         }
    315     }
    316 
    317     fn from_product(row: TradeProduct) -> Self {
    318         Self {
    319             qty_amt_exact: row.qty_amt_exact,
    320             qty_unit: row.qty_unit,
    321             price_amt_exact: row.price_amt_exact,
    322             price_currency: row.price_currency,
    323             price_qty_amt_exact: row.price_qty_amt_exact,
    324             price_qty_unit: row.price_qty_unit,
    325             primary_bin_id: row.primary_bin_id,
    326             verified_primary_bin_id: row.verified_primary_bin_id,
    327             notes: row.notes,
    328         }
    329     }
    330 }
    331 
    332 #[derive(Debug, Clone, Deserialize)]
    333 struct ResolvedTradeProductNotes {
    334     #[serde(default)]
    335     listing_discounts: Vec<RadrootsCoreDiscount>,
    336 }
    337 
    338 #[derive(Debug, Clone)]
    339 struct ResolvedSellerOrderRequest {
    340     request_event: SdkRadrootsNostrEvent,
    341     request_event_id: RadrootsEventId,
    342     listing_event_id: Option<String>,
    343     order_id: RadrootsOrderId,
    344     listing_addr: RadrootsListingAddress,
    345     buyer_pubkey: RadrootsPublicKey,
    346     seller_pubkey: RadrootsPublicKey,
    347     items: Vec<RadrootsOrderItem>,
    348     economics: RadrootsOrderEconomics,
    349 }
    350 
    351 #[derive(Debug, Clone)]
    352 struct ResolvedOrderSubmitRequest {
    353     request_event_id: String,
    354     listing_event_id: Option<String>,
    355     payload: RadrootsOrderRequest,
    356 }
    357 
    358 #[derive(Debug, Clone)]
    359 struct ResolvedAccountingRequest {
    360     listing_event_id: Option<String>,
    361     record: RadrootsOrderRequestRecord,
    362 }
    363 
    364 #[derive(Debug, Clone)]
    365 struct ResolvedInventoryListing {
    366     event_id: RadrootsEventId,
    367     listing: RadrootsListing,
    368     bins: Vec<RadrootsListingInventoryBinAvailability>,
    369 }
    370 
    371 #[derive(Debug, Clone)]
    372 struct OrderDecisionInventoryPreflight {
    373     invalid_view: Option<OrderDecisionView>,
    374     inventory: Option<OrderInventoryView>,
    375 }
    376 
    377 #[derive(Debug, Clone)]
    378 struct OrderRebindExistingRequestCheck {
    379     state: String,
    380     event_ids: Vec<String>,
    381 }
    382 
    383 #[derive(Debug, Clone)]
    384 struct OrderDraftStatusActorContext {
    385     source: &'static str,
    386     buyer_pubkey: Option<String>,
    387     seller_pubkey: Option<String>,
    388     selected_account_pubkey: Option<String>,
    389 }
    390 
    391 #[derive(Debug, Clone)]
    392 struct OrderEventListActorContext {
    393     source: &'static str,
    394     seller_pubkey: String,
    395 }
    396 
    397 #[derive(Debug, Clone)]
    398 struct OrderBoundBuyerWriteContext {
    399     loaded: LoadedOrderDraft,
    400     account: account::AccountRecordView,
    401 }
    402 
    403 #[derive(Debug, Clone)]
    404 struct OrderBuyerWriteActorContext {
    405     bound: Option<OrderBoundBuyerWriteContext>,
    406     selected_pubkey: String,
    407     status_buyer_pubkey: Option<String>,
    408     status_seller_pubkey: Option<String>,
    409     status_context_source: &'static str,
    410 }
    411 
    412 #[derive(Debug, Clone)]
    413 struct SellerOrderRequestResolution {
    414     target_relays: Vec<String>,
    415     connected_relays: Vec<String>,
    416     failed_relays: Vec<DirectRelayFailure>,
    417     fetched_count: usize,
    418     decoded_count: usize,
    419     skipped_count: usize,
    420     requests: Vec<ResolvedSellerOrderRequest>,
    421     candidate_issues: Vec<OrderIssueView>,
    422 }
    423 
    424 pub fn scaffold(
    425     config: &RuntimeConfig,
    426     args: &OrderDraftCreateArgs,
    427 ) -> Result<OrderNewView, RuntimeError> {
    428     validate_scaffold_args(args)?;
    429 
    430     let listing_lookup = normalize_optional(args.listing.as_deref());
    431     let explicit_listing_addr = normalize_optional(args.listing_addr.as_deref());
    432     let resolved_listing = resolve_order_listing(
    433         config,
    434         listing_lookup.as_deref(),
    435         explicit_listing_addr.as_deref(),
    436     )?;
    437 
    438     let buyer_actor = resolve_initial_buyer_actor(config)?;
    439     let buyer_pubkey = buyer_actor.pubkey.clone();
    440 
    441     let listing_addr = resolved_listing
    442         .as_ref()
    443         .map(|listing| listing.listing_addr.clone())
    444         .unwrap_or_default();
    445     let seller_pubkey = resolved_listing
    446         .as_ref()
    447         .map(|listing| listing.seller_pubkey.clone())
    448         .unwrap_or_default();
    449     let listing_event_id = resolved_listing
    450         .as_ref()
    451         .map(|listing| listing.listing_event_id.clone())
    452         .unwrap_or_default();
    453     let listing_relays = resolved_listing
    454         .as_ref()
    455         .map(|listing| listing.listing_relays.clone())
    456         .unwrap_or_default();
    457 
    458     let items = match normalize_optional(args.bin_id.as_deref()) {
    459         Some(bin_id) => vec![OrderDraftItem {
    460             bin_id,
    461             bin_count: args.bin_count.unwrap_or(1),
    462         }],
    463         None => Vec::new(),
    464     };
    465 
    466     let order_id = next_order_id();
    467     let economics = order_economics_from_resolved_listing(
    468         order_id.as_str(),
    469         resolved_listing.as_ref(),
    470         items.as_slice(),
    471         args.adjustments.as_slice(),
    472     )?;
    473     let drafts_dir = drafts_dir(config);
    474     fs::create_dir_all(&drafts_dir)?;
    475     let file = drafts_dir.join(format!("{order_id}.toml"));
    476 
    477     let document = OrderDraftDocument {
    478         version: 1,
    479         kind: ORDER_DRAFT_KIND.to_owned(),
    480         order: OrderDraft {
    481             order_id: order_id.clone(),
    482             listing_addr,
    483             listing_event_id,
    484             listing_relays,
    485             buyer_pubkey,
    486             seller_pubkey,
    487             items,
    488             economics,
    489         },
    490         buyer_actor,
    491         listing_lookup,
    492     };
    493     save_draft(file.as_path(), &document)?;
    494 
    495     let mut view: OrderNewView = view_from_loaded(
    496         config,
    497         LoadedOrderDraft {
    498             file,
    499             updated_at_unix: now_unix(),
    500             document,
    501         },
    502     )?
    503     .into();
    504     view.actions
    505         .insert(0, format!("radroots order get {}", view.order_id));
    506 
    507     Ok(view)
    508 }
    509 
    510 pub fn scaffold_preflight(
    511     config: &RuntimeConfig,
    512     args: &OrderDraftCreateArgs,
    513 ) -> Result<OrderNewView, RuntimeError> {
    514     validate_scaffold_args(args)?;
    515 
    516     let listing_lookup = normalize_optional(args.listing.as_deref());
    517     let explicit_listing_addr = normalize_optional(args.listing_addr.as_deref());
    518     let resolved_listing = resolve_order_listing(
    519         config,
    520         listing_lookup.as_deref(),
    521         explicit_listing_addr.as_deref(),
    522     )?;
    523 
    524     let buyer_actor = resolve_initial_buyer_actor(config)?;
    525     let buyer_pubkey = buyer_actor.pubkey.clone();
    526 
    527     let listing_addr = resolved_listing
    528         .as_ref()
    529         .map(|listing| listing.listing_addr.clone())
    530         .unwrap_or_default();
    531     let seller_pubkey = resolved_listing
    532         .as_ref()
    533         .map(|listing| listing.seller_pubkey.clone())
    534         .unwrap_or_default();
    535     let listing_event_id = resolved_listing
    536         .as_ref()
    537         .map(|listing| listing.listing_event_id.clone())
    538         .unwrap_or_default();
    539     let listing_relays = resolved_listing
    540         .as_ref()
    541         .map(|listing| listing.listing_relays.clone())
    542         .unwrap_or_default();
    543 
    544     let items = match normalize_optional(args.bin_id.as_deref()) {
    545         Some(bin_id) => vec![OrderDraftItem {
    546             bin_id,
    547             bin_count: args.bin_count.unwrap_or(1),
    548         }],
    549         None => Vec::new(),
    550     };
    551 
    552     let order_id = next_order_id();
    553     let economics = order_economics_from_resolved_listing(
    554         order_id.as_str(),
    555         resolved_listing.as_ref(),
    556         items.as_slice(),
    557         args.adjustments.as_slice(),
    558     )?;
    559     let file = drafts_dir(config).join(format!("{order_id}.toml"));
    560     let document = OrderDraftDocument {
    561         version: 1,
    562         kind: ORDER_DRAFT_KIND.to_owned(),
    563         order: OrderDraft {
    564             order_id: order_id.clone(),
    565             listing_addr,
    566             listing_event_id,
    567             listing_relays,
    568             buyer_pubkey,
    569             seller_pubkey,
    570             items,
    571             economics,
    572         },
    573         buyer_actor,
    574         listing_lookup,
    575     };
    576 
    577     let mut view: OrderNewView = view_from_loaded(
    578         config,
    579         LoadedOrderDraft {
    580             file,
    581             updated_at_unix: now_unix(),
    582             document,
    583         },
    584     )?
    585     .into();
    586     view.state = "dry_run".to_owned();
    587     view.actions
    588         .insert(0, format!("radroots order get {}", view.order_id));
    589 
    590     Ok(view)
    591 }
    592 
    593 pub fn get(config: &RuntimeConfig, args: &RecordLookupArgs) -> Result<OrderGetView, RuntimeError> {
    594     let lookup = args.key.clone();
    595     let file = draft_lookup_path(config, lookup.as_str());
    596     if !file.exists() {
    597         if let Some(app_order) = load_app_order_record_for_lookup(config, lookup.as_str())? {
    598             return view_from_loaded_with_source_issues(
    599                 config,
    600                 app_order.loaded,
    601                 app_order.source_issues.as_slice(),
    602             );
    603         }
    604         return Ok(OrderGetView {
    605             state: "missing".to_owned(),
    606             source: ORDER_SOURCE.to_owned(),
    607             lookup: lookup.clone(),
    608             order_id: None,
    609             file: Some(file.display().to_string()),
    610             listing_lookup: None,
    611             listing_addr: None,
    612             listing_event_id: None,
    613             listing_relays: Vec::new(),
    614             buyer_account_id: None,
    615             buyer_pubkey: None,
    616             buyer_actor_source: None,
    617             buyer_custody: None,
    618             buyer_write_capable: None,
    619             seller_pubkey: None,
    620             ready_for_submit: false,
    621             items: Vec::new(),
    622             economics: None,
    623             updated_at_unix: None,
    624             job: None,
    625             workflow: None,
    626             reason: Some(format!("order draft `{lookup}` was not found")),
    627             issues: Vec::new(),
    628             actions: vec![
    629                 "radroots order list".to_owned(),
    630                 "radroots basket create".to_owned(),
    631             ],
    632         });
    633     }
    634 
    635     match load_draft(file.as_path()) {
    636         Ok(loaded) => view_from_loaded(config, loaded),
    637         Err(reason) => Ok(OrderGetView {
    638             state: "error".to_owned(),
    639             source: ORDER_SOURCE.to_owned(),
    640             lookup,
    641             order_id: None,
    642             file: Some(file.display().to_string()),
    643             listing_lookup: None,
    644             listing_addr: None,
    645             listing_event_id: None,
    646             listing_relays: Vec::new(),
    647             buyer_account_id: None,
    648             buyer_pubkey: None,
    649             buyer_actor_source: None,
    650             buyer_custody: None,
    651             buyer_write_capable: None,
    652             seller_pubkey: None,
    653             ready_for_submit: false,
    654             items: Vec::new(),
    655             economics: None,
    656             updated_at_unix: None,
    657             job: None,
    658             workflow: None,
    659             reason: Some(reason),
    660             issues: Vec::new(),
    661             actions: Vec::new(),
    662         }),
    663     }
    664 }
    665 
    666 pub fn list(config: &RuntimeConfig) -> Result<OrderListView, RuntimeError> {
    667     let dir = drafts_dir(config);
    668     let mut orders = Vec::new();
    669     let mut local_order_ids = HashSet::new();
    670     if dir.exists() {
    671         for entry in fs::read_dir(&dir)? {
    672             let entry = entry?;
    673             let path = entry.path();
    674             if path.extension().and_then(|value| value.to_str()) != Some("toml") {
    675                 continue;
    676             }
    677             match load_draft(path.as_path()) {
    678                 Ok(loaded) => {
    679                     local_order_ids.insert(loaded.document.order.order_id.clone());
    680                     orders.push(summary_from_loaded(config, &loaded)?);
    681                 }
    682                 Err(reason) => orders.push(summary_for_invalid_file(path.as_path(), reason)),
    683             }
    684         }
    685     }
    686     for entry in current_app_order_record_entries(app_order_local_records(config)?) {
    687         let app_order = load_app_order_record_from_record(config, entry.record.clone())?;
    688         if local_order_ids.contains(&app_order.loaded.document.order.order_id) {
    689             continue;
    690         }
    691         orders.push(summary_from_loaded_with_source_issues(
    692             config,
    693             &app_order.loaded,
    694             app_order.source_issues.as_slice(),
    695         )?);
    696     }
    697 
    698     orders.sort_by(|left, right| {
    699         right
    700             .updated_at_unix
    701             .cmp(&left.updated_at_unix)
    702             .then_with(|| left.id.cmp(&right.id))
    703     });
    704 
    705     let state = if orders.is_empty() {
    706         "empty"
    707     } else if orders.iter().any(|order| {
    708         order.state == "error" || (!order.ready_for_submit && order.state != "submitted")
    709     }) {
    710         "degraded"
    711     } else {
    712         "ready"
    713     };
    714     let actions = if orders.is_empty() {
    715         vec!["radroots basket create".to_owned()]
    716     } else {
    717         Vec::new()
    718     };
    719 
    720     Ok(OrderListView {
    721         state: state.to_owned(),
    722         source: ORDER_SOURCE.to_owned(),
    723         count: orders.len(),
    724         orders,
    725         actions,
    726     })
    727 }
    728 
    729 pub fn app_record_list(config: &RuntimeConfig) -> Result<OrderAppRecordListView, RuntimeError> {
    730     let database_path = shared_local_events_db_path(config)?;
    731     let mut entries = current_app_order_record_entries(app_order_local_records(config)?);
    732     let has_more = entries.len() > ORDER_APP_RECORD_LIST_LIMIT as usize;
    733     if has_more {
    734         entries.truncate(ORDER_APP_RECORD_LIST_LIMIT as usize);
    735     }
    736     let next_cursor = if has_more {
    737         entries
    738             .last()
    739             .map(|entry| (entry.record.change_seq, entry.record.seq))
    740     } else {
    741         None
    742     };
    743     let records = entries
    744         .iter()
    745         .map(|entry| app_order_record_summary(config, &entry.record, entry.superseded_count))
    746         .collect::<Result<Vec<_>, _>>()?;
    747     let state = if records.is_empty() { "empty" } else { "ready" };
    748     let actions = if records.is_empty() {
    749         vec!["place a buyer order in radroots_app".to_owned()]
    750     } else {
    751         Vec::new()
    752     };
    753 
    754     Ok(OrderAppRecordListView {
    755         state: state.to_owned(),
    756         source: ORDER_APP_RECORD_SOURCE.to_owned(),
    757         count: records.len(),
    758         limit: ORDER_APP_RECORD_LIST_LIMIT,
    759         has_more,
    760         next_before_change_seq: next_cursor.map(|(change_seq, _)| change_seq),
    761         next_before_seq: next_cursor.map(|(_, seq)| seq),
    762         local_events_db: database_path.display().to_string(),
    763         records,
    764         actions,
    765     })
    766 }
    767 
    768 pub fn app_record_export(
    769     config: &RuntimeConfig,
    770     args: &OrderAppRecordExportArgs,
    771 ) -> Result<OrderAppRecordExportView, RuntimeError> {
    772     let Some(record) = get_shared_record(config, args.record_id.as_str())? else {
    773         return Ok(OrderAppRecordExportView {
    774             state: "missing".to_owned(),
    775             source: ORDER_APP_RECORD_SOURCE.to_owned(),
    776             record_id: args.record_id.clone(),
    777             dry_run: config.output.dry_run,
    778             file: args
    779                 .output
    780                 .as_ref()
    781                 .map(|path| path.display().to_string())
    782                 .unwrap_or_default(),
    783             valid: false,
    784             order_id: None,
    785             listing_addr: None,
    786             listing_event_id: None,
    787             listing_relays: Vec::new(),
    788             buyer_account_id: None,
    789             buyer_pubkey: None,
    790             buyer_actor_source: None,
    791             seller_pubkey: None,
    792             issues: Vec::new(),
    793             reason: Some(format!(
    794                 "app-authored local order record `{}` was not found",
    795                 args.record_id
    796             )),
    797             actions: vec!["radroots order app list".to_owned()],
    798         });
    799     };
    800 
    801     let app_order = load_app_order_record_from_record(config, record)?;
    802     let mut issues = source_and_document_issues(config, &app_order)?;
    803     if !issues.is_empty() {
    804         let state = app_order_export_failure_state(issues.as_slice());
    805         let actions = app_order_export_failure_actions(&app_order.loaded.document, &issues);
    806         return Ok(OrderAppRecordExportView {
    807             state: state.to_owned(),
    808             source: ORDER_APP_RECORD_SOURCE.to_owned(),
    809             record_id: args.record_id.clone(),
    810             dry_run: config.output.dry_run,
    811             file: args
    812                 .output
    813                 .as_ref()
    814                 .map(|path| path.display().to_string())
    815                 .unwrap_or_default(),
    816             valid: false,
    817             order_id: Some(app_order.loaded.document.order.order_id.clone()),
    818             listing_addr: non_empty_string(app_order.loaded.document.order.listing_addr.clone()),
    819             listing_event_id: non_empty_string(
    820                 app_order.loaded.document.order.listing_event_id.clone(),
    821             ),
    822             listing_relays: order_listing_relays(&app_order.loaded.document),
    823             buyer_account_id: buyer_account_id(&app_order.loaded.document),
    824             buyer_pubkey: non_empty_string(app_order.loaded.document.order.buyer_pubkey.clone()),
    825             buyer_actor_source: buyer_actor_source(&app_order.loaded.document),
    826             seller_pubkey: non_empty_string(app_order.loaded.document.order.seller_pubkey.clone()),
    827             issues,
    828             reason: Some(format!(
    829                 "app-authored local order record `{}` is not ready as a CLI order draft",
    830                 args.record_id
    831             )),
    832             actions,
    833         });
    834     }
    835 
    836     let output_path = order_export_output_path(
    837         config,
    838         args.output.as_ref(),
    839         app_order.loaded.document.order.order_id.as_str(),
    840     );
    841     validate_order_export_output_target(output_path.as_path())?;
    842     if !config.output.dry_run {
    843         save_draft(output_path.as_path(), &app_order.loaded.document)?;
    844     }
    845     issues.clear();
    846 
    847     Ok(OrderAppRecordExportView {
    848         state: if config.output.dry_run {
    849             "dry_run"
    850         } else {
    851             "exported"
    852         }
    853         .to_owned(),
    854         source: ORDER_APP_RECORD_SOURCE.to_owned(),
    855         record_id: args.record_id.clone(),
    856         dry_run: config.output.dry_run,
    857         file: output_path.display().to_string(),
    858         valid: true,
    859         order_id: Some(app_order.loaded.document.order.order_id.clone()),
    860         listing_addr: non_empty_string(app_order.loaded.document.order.listing_addr.clone()),
    861         listing_event_id: non_empty_string(
    862             app_order.loaded.document.order.listing_event_id.clone(),
    863         ),
    864         listing_relays: order_listing_relays(&app_order.loaded.document),
    865         buyer_account_id: buyer_account_id(&app_order.loaded.document),
    866         buyer_pubkey: non_empty_string(app_order.loaded.document.order.buyer_pubkey.clone()),
    867         buyer_actor_source: buyer_actor_source(&app_order.loaded.document),
    868         seller_pubkey: non_empty_string(app_order.loaded.document.order.seller_pubkey.clone()),
    869         issues,
    870         reason: Some(if config.output.dry_run {
    871             "dry run requested; order draft was not written".to_owned()
    872         } else {
    873             "app-authored local order record exported as a CLI order draft".to_owned()
    874         }),
    875         actions: vec![
    876             format!(
    877                 "radroots order get {}",
    878                 app_order.loaded.document.order.order_id
    879             ),
    880             format!(
    881                 "radroots --relay wss://relay.example.com order submit {}",
    882                 app_order.loaded.document.order.order_id
    883             ),
    884         ],
    885     })
    886 }
    887 
    888 pub fn submit(
    889     config: &RuntimeConfig,
    890     args: &OrderSubmitArgs,
    891 ) -> Result<OrderSubmitView, CliSdkAdapterError> {
    892     let file = draft_lookup_path(config, args.key.as_str());
    893     let (loaded, source_issues) = if file.exists() {
    894         match load_draft(file.as_path()) {
    895             Ok(loaded) => (loaded, Vec::new()),
    896             Err(reason) => {
    897                 return Ok(OrderSubmitView {
    898                     state: "error".to_owned(),
    899                     source: ORDER_SOURCE.to_owned(),
    900                     order_id: args.key.clone(),
    901                     file: file.display().to_string(),
    902                     listing_lookup: None,
    903                     listing_addr: None,
    904                     listing_event_id: None,
    905                     listing_relays: Vec::new(),
    906                     buyer_account_id: None,
    907                     buyer_pubkey: None,
    908                     buyer_actor_source: None,
    909                     buyer_custody: None,
    910                     buyer_write_capable: None,
    911                     seller_pubkey: None,
    912                     event_id: None,
    913                     event_kind: None,
    914                     dry_run: config.output.dry_run,
    915                     deduplicated: false,
    916                     target_relays: Vec::new(),
    917                     connected_relays: Vec::new(),
    918                     acknowledged_relays: Vec::new(),
    919                     failed_relays: Vec::new(),
    920                     idempotency_key: args.idempotency_key.clone(),
    921                     signer_mode: None,
    922                     reason: Some(reason),
    923                     job: None,
    924                     issues: Vec::new(),
    925                     actions: Vec::new(),
    926                 });
    927             }
    928         }
    929     } else if let Some(app_order) = load_app_order_record_for_lookup(config, args.key.as_str())? {
    930         (app_order.loaded, app_order.source_issues)
    931     } else {
    932         return Ok(OrderSubmitView {
    933             state: "missing".to_owned(),
    934             source: ORDER_SOURCE.to_owned(),
    935             order_id: args.key.clone(),
    936             file: file.display().to_string(),
    937             listing_lookup: None,
    938             listing_addr: None,
    939             listing_event_id: None,
    940             listing_relays: Vec::new(),
    941             buyer_account_id: None,
    942             buyer_pubkey: None,
    943             buyer_actor_source: None,
    944             buyer_custody: None,
    945             buyer_write_capable: None,
    946             seller_pubkey: None,
    947             event_id: None,
    948             event_kind: None,
    949             dry_run: config.output.dry_run,
    950             deduplicated: false,
    951             target_relays: Vec::new(),
    952             connected_relays: Vec::new(),
    953             acknowledged_relays: Vec::new(),
    954             failed_relays: Vec::new(),
    955             idempotency_key: args.idempotency_key.clone(),
    956             signer_mode: None,
    957             reason: Some(format!("order draft `{}` was not found", args.key)),
    958             job: None,
    959             issues: Vec::new(),
    960             actions: vec![
    961                 "radroots order list".to_owned(),
    962                 "radroots basket create".to_owned(),
    963             ],
    964         });
    965     };
    966 
    967     let mut issues = collect_issues(&loaded.document);
    968     issues.extend(source_issues.clone());
    969     if let Some(view) = order_submit_app_signed_evidence_view(config, &loaded, args, &issues) {
    970         return Ok(view);
    971     }
    972     if !issues.is_empty() {
    973         let mut actions = actions_for_document(&loaded.document, loaded.file.as_path(), &issues);
    974         actions.push(format!(
    975             "radroots order get {}",
    976             loaded.document.order.order_id
    977         ));
    978         return Ok(OrderSubmitView {
    979             state: "unconfigured".to_owned(),
    980             source: ORDER_SOURCE.to_owned(),
    981             order_id: loaded.document.order.order_id.clone(),
    982             file: loaded.file.display().to_string(),
    983             listing_lookup: loaded.document.listing_lookup.clone(),
    984             listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
    985             listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
    986             listing_relays: order_listing_relays(&loaded.document),
    987             buyer_account_id: buyer_account_id(&loaded.document),
    988             buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
    989             buyer_actor_source: buyer_actor_source(&loaded.document),
    990             buyer_custody: None,
    991             buyer_write_capable: None,
    992             seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
    993             event_id: None,
    994             event_kind: None,
    995             dry_run: config.output.dry_run,
    996             deduplicated: false,
    997             target_relays: Vec::new(),
    998             connected_relays: Vec::new(),
    999             acknowledged_relays: Vec::new(),
   1000             failed_relays: Vec::new(),
   1001             idempotency_key: args.idempotency_key.clone(),
   1002             signer_mode: None,
   1003             reason: Some("order draft is not ready for submit".to_owned()),
   1004             job: None,
   1005             issues,
   1006             actions,
   1007         });
   1008     }
   1009 
   1010     validate_bound_order_buyer_account(config, &loaded)?;
   1011 
   1012     if let Some(view) = order_submit_listing_freshness_view(config, &loaded, args)? {
   1013         return Ok(view);
   1014     }
   1015     if let Some(view) = order_submit_quantity_preflight_view(config, &loaded, args)? {
   1016         return Ok(view);
   1017     }
   1018 
   1019     if let Some(view) = order_submit_listing_provenance_preflight_view(config, &loaded, args)? {
   1020         return Ok(view);
   1021     }
   1022 
   1023     let signing = match resolve_local_order_signing_identity(config, &loaded) {
   1024         Ok(signing) => signing,
   1025         Err(ActorWriteBindingError::Account(failure)) => {
   1026             return Err(RuntimeError::from(failure).into());
   1027         }
   1028         Err(error) => return Ok(order_binding_error_view(config, &loaded, args, error)),
   1029     };
   1030     let payload = canonical_order_request_payload_from_loaded(
   1031         &loaded,
   1032         signing
   1033             .account
   1034             .record
   1035             .public_identity
   1036             .public_key_hex
   1037             .as_str(),
   1038     )?;
   1039     let input = sdk_order_submit_input(config, &loaded, &signing, payload)?;
   1040 
   1041     if config.output.dry_run {
   1042         return prepare_order_submit_via_sdk(config, &loaded, args, input);
   1043     }
   1044 
   1045     if let Some(view) = order_submit_market_freshness_view(config, &loaded, args)? {
   1046         return Ok(view);
   1047     }
   1048 
   1049     submit_via_sdk(config, &loaded, args, signing, input)
   1050 }
   1051 
   1052 pub fn rebind(
   1053     config: &RuntimeConfig,
   1054     args: &OrderRebindArgs,
   1055 ) -> Result<OrderRebindView, RuntimeError> {
   1056     rebind_inner(config, args, false)
   1057 }
   1058 
   1059 pub fn rebind_preflight(
   1060     config: &RuntimeConfig,
   1061     args: &OrderRebindArgs,
   1062 ) -> Result<OrderRebindView, RuntimeError> {
   1063     rebind_inner(config, args, true)
   1064 }
   1065 
   1066 fn rebind_inner(
   1067     config: &RuntimeConfig,
   1068     args: &OrderRebindArgs,
   1069     dry_run: bool,
   1070 ) -> Result<OrderRebindView, RuntimeError> {
   1071     let file = draft_lookup_path(config, args.key.as_str());
   1072     if !file.exists() {
   1073         return Ok(OrderRebindView {
   1074             state: "missing".to_owned(),
   1075             source: ORDER_SOURCE.to_owned(),
   1076             lookup: args.key.clone(),
   1077             file: file.display().to_string(),
   1078             dry_run,
   1079             from_order_id: args.key.clone(),
   1080             to_order_id: args.key.clone(),
   1081             order_id_changed: false,
   1082             from_buyer_account_id: None,
   1083             from_buyer_pubkey: None,
   1084             from_buyer_actor_source: None,
   1085             to_buyer_account_id: args.selector.clone(),
   1086             to_buyer_pubkey: String::new(),
   1087             to_buyer_actor_source: ORDER_BUYER_ACTOR_SOURCE_REBIND.to_owned(),
   1088             buyer_pubkey_changed: false,
   1089             existing_request_check: "not_checked".to_owned(),
   1090             existing_request_event_ids: Vec::new(),
   1091             reason: Some(format!("order draft `{}` was not found", args.key)),
   1092             actions: vec![
   1093                 "radroots order list".to_owned(),
   1094                 "radroots basket create".to_owned(),
   1095             ],
   1096         });
   1097     }
   1098 
   1099     let loaded = load_draft(file.as_path()).map_err(RuntimeError::Config)?;
   1100     let target_account = account::resolve_account_selector(config, args.selector.as_str())
   1101         .map_err(|error| order_rebind_selector_error(args.selector.as_str(), error))?;
   1102     let existing_request = order_rebind_existing_request_check(config, &loaded)?;
   1103     let from_order_id = loaded.document.order.order_id.clone();
   1104     let from_buyer_account_id = buyer_account_id(&loaded.document);
   1105     let from_buyer_pubkey = non_empty_string(loaded.document.buyer_actor.pubkey.clone());
   1106     let from_buyer_actor_source = buyer_actor_source(&loaded.document);
   1107     let target_account_id = target_account.record.account_id.to_string();
   1108     let target_pubkey = target_account.record.public_identity.public_key_hex.clone();
   1109     let current_buyer_pubkey = from_buyer_pubkey
   1110         .clone()
   1111         .or_else(|| non_empty_string(loaded.document.order.buyer_pubkey.clone()));
   1112     let buyer_pubkey_changed = current_buyer_pubkey
   1113         .as_deref()
   1114         .is_none_or(|pubkey| !pubkey.eq_ignore_ascii_case(target_pubkey.as_str()));
   1115 
   1116     if !existing_request.event_ids.is_empty() {
   1117         return Ok(OrderRebindView {
   1118             state: "invalid".to_owned(),
   1119             source: ORDER_SOURCE.to_owned(),
   1120             lookup: args.key.clone(),
   1121             file: loaded.file.display().to_string(),
   1122             dry_run,
   1123             from_order_id: from_order_id.clone(),
   1124             to_order_id: from_order_id.clone(),
   1125             order_id_changed: false,
   1126             from_buyer_account_id,
   1127             from_buyer_pubkey,
   1128             from_buyer_actor_source,
   1129             to_buyer_account_id: target_account_id,
   1130             to_buyer_pubkey: target_pubkey,
   1131             to_buyer_actor_source: ORDER_BUYER_ACTOR_SOURCE_REBIND.to_owned(),
   1132             buyer_pubkey_changed,
   1133             existing_request_check: existing_request.state,
   1134             existing_request_event_ids: existing_request.event_ids,
   1135             reason: Some(
   1136                 "order rebind refused because a valid order request is already visible for this order id"
   1137                     .to_owned(),
   1138             ),
   1139             actions: vec![
   1140                 format!("radroots order status get {from_order_id}"),
   1141                 "radroots basket quote create <basket-id>".to_owned(),
   1142             ],
   1143         });
   1144     }
   1145 
   1146     let mut document = loaded.document.clone();
   1147     let to_order_id = if buyer_pubkey_changed {
   1148         next_order_id()
   1149     } else {
   1150         from_order_id.clone()
   1151     };
   1152     let order_id_changed = to_order_id != from_order_id;
   1153     document.order.order_id = to_order_id.clone();
   1154     document.order.buyer_pubkey = target_pubkey.clone();
   1155     document.buyer_actor.account_id = target_account_id.clone();
   1156     document.buyer_actor.pubkey = target_pubkey.clone();
   1157     document.buyer_actor.source = ORDER_BUYER_ACTOR_SOURCE_REBIND.to_owned();
   1158     if order_id_changed && let Some(economics) = document.order.economics.as_mut() {
   1159         economics.quote_id =
   1160             protocol_quote_id(format!("quote_{to_order_id}").as_str(), "quote_id")?;
   1161     }
   1162 
   1163     let output_file = if order_id_changed {
   1164         drafts_dir(config).join(format!("{to_order_id}.toml"))
   1165     } else {
   1166         loaded.file.clone()
   1167     };
   1168     if !dry_run {
   1169         if order_id_changed && output_file.exists() {
   1170             return Err(RuntimeError::Config(format!(
   1171                 "order rebind target file {} already exists",
   1172                 output_file.display()
   1173             )));
   1174         }
   1175         save_draft(output_file.as_path(), &document)?;
   1176         if order_id_changed && output_file != loaded.file {
   1177             fs::remove_file(loaded.file.as_path())?;
   1178         }
   1179     }
   1180 
   1181     Ok(OrderRebindView {
   1182         state: if dry_run { "dry_run" } else { "rebound" }.to_owned(),
   1183         source: ORDER_SOURCE.to_owned(),
   1184         lookup: args.key.clone(),
   1185         file: output_file.display().to_string(),
   1186         dry_run,
   1187         from_order_id: from_order_id.clone(),
   1188         to_order_id: to_order_id.clone(),
   1189         order_id_changed,
   1190         from_buyer_account_id,
   1191         from_buyer_pubkey,
   1192         from_buyer_actor_source,
   1193         to_buyer_account_id: target_account_id,
   1194         to_buyer_pubkey: target_pubkey,
   1195         to_buyer_actor_source: ORDER_BUYER_ACTOR_SOURCE_REBIND.to_owned(),
   1196         buyer_pubkey_changed,
   1197         existing_request_check: existing_request.state,
   1198         existing_request_event_ids: Vec::new(),
   1199         reason: Some(if dry_run {
   1200             "dry run requested; order buyer actor binding was not written".to_owned()
   1201         } else {
   1202             "order buyer actor binding updated".to_owned()
   1203         }),
   1204         actions: if dry_run {
   1205             vec![format!(
   1206                 "radroots --approval-token approve order rebind {} {}",
   1207                 args.key, args.selector
   1208             )]
   1209         } else {
   1210             vec![format!("radroots order get {to_order_id}")]
   1211         },
   1212     })
   1213 }
   1214 
   1215 pub fn event_list(
   1216     config: &RuntimeConfig,
   1217     order_id: Option<&str>,
   1218 ) -> Result<OrderEventListView, RuntimeError> {
   1219     if config.relay.urls.is_empty() {
   1220         return Ok(order_event_list_unconfigured(
   1221             None,
   1222             ORDER_ACTOR_CONTEXT_NETWORK_ONLY,
   1223             "order event list requires at least one configured relay".to_owned(),
   1224             Vec::new(),
   1225             vec![ORDER_EVENT_LIST_RELAY_ACTION.to_owned()],
   1226         ));
   1227     }
   1228 
   1229     let actor_context = match order_event_list_actor_context(config, order_id)? {
   1230         Some(context) => context,
   1231         None => {
   1232             return Ok(order_event_list_unconfigured(
   1233                 None,
   1234                 ORDER_ACTOR_CONTEXT_NETWORK_ONLY,
   1235                 "order event list requires a selected seller account".to_owned(),
   1236                 config.relay.urls.clone(),
   1237                 vec!["radroots account create".to_owned()],
   1238             ));
   1239         }
   1240     };
   1241     let seller_pubkey = actor_context.seller_pubkey;
   1242     let filter = order_request_filter(seller_pubkey.as_str(), order_id)?;
   1243     let receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
   1244         Ok(receipt) => receipt,
   1245         Err(DirectRelayFetchError::Connect {
   1246             reason,
   1247             target_relays,
   1248             failed_relays,
   1249         }) => {
   1250             return Ok(order_event_list_unavailable(
   1251                 seller_pubkey,
   1252                 actor_context.source,
   1253                 reason,
   1254                 target_relays,
   1255                 failed_relays,
   1256             ));
   1257         }
   1258         Err(error) => return Err(RuntimeError::Network(error.to_string())),
   1259     };
   1260 
   1261     Ok(order_event_list_from_receipt(
   1262         seller_pubkey,
   1263         order_id,
   1264         actor_context.source,
   1265         receipt,
   1266     ))
   1267 }
   1268 
   1269 pub fn decide(
   1270     config: &RuntimeConfig,
   1271     args: &OrderDecisionArgs,
   1272 ) -> Result<OrderDecisionView, RuntimeError> {
   1273     if config.relay.urls.is_empty() {
   1274         let mut view =
   1275             order_decision_base_view(config, args, "unconfigured", config.output.dry_run);
   1276         view.reason = Some(format!(
   1277             "order {} requires at least one configured relay",
   1278             args.decision.command()
   1279         ));
   1280         return Ok(view);
   1281     }
   1282 
   1283     let seller = match account::resolve_account(config)? {
   1284         Some(account) => account,
   1285         None => {
   1286             let mut view =
   1287                 order_decision_base_view(config, args, "unconfigured", config.output.dry_run);
   1288             view.reason = Some(format!(
   1289                 "order {} requires a selected seller account",
   1290                 args.decision.command()
   1291             ));
   1292             view.actions = vec!["radroots account create".to_owned()];
   1293             return Ok(view);
   1294         }
   1295     };
   1296     let seller_pubkey = seller.record.public_identity.public_key_hex;
   1297     let filter = order_request_filter(seller_pubkey.as_str(), Some(args.key.as_str()))?;
   1298     let receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
   1299         Ok(receipt) => receipt,
   1300         Err(DirectRelayFetchError::Connect {
   1301             reason,
   1302             target_relays,
   1303             failed_relays,
   1304         }) => {
   1305             let mut view =
   1306                 order_decision_base_view(config, args, "unavailable", config.output.dry_run);
   1307             view.seller_pubkey = Some(seller_pubkey);
   1308             view.target_relays = target_relays;
   1309             view.failed_relays = relay_failures(failed_relays);
   1310             view.reason = Some(format!("direct relay connection failed: {reason}"));
   1311             return Ok(view);
   1312         }
   1313         Err(error) => return Err(RuntimeError::Network(error.to_string())),
   1314     };
   1315 
   1316     let resolution = seller_order_request_resolution_from_receipt(
   1317         seller_pubkey.as_str(),
   1318         args.key.as_str(),
   1319         receipt,
   1320     )?;
   1321     if !resolution.candidate_issues.is_empty() {
   1322         return Ok(order_decision_view_from_resolution(
   1323             config,
   1324             args,
   1325             seller_pubkey,
   1326             resolution,
   1327         ));
   1328     }
   1329     if resolution.requests.len() == 1 {
   1330         let request = resolution.requests[0].clone();
   1331         let status_view = legacy_order_preflight_relay_status(
   1332             config,
   1333             &OrderStatusArgs {
   1334                 key: args.key.clone(),
   1335             },
   1336         )?;
   1337         if let Some(view) = order_decision_preflight_view_from_status(
   1338             config,
   1339             args,
   1340             &request,
   1341             &resolution,
   1342             &status_view,
   1343         ) {
   1344             return Ok(view);
   1345         }
   1346         let inventory_preflight = order_accept_inventory_preflight_view(
   1347             config,
   1348             args,
   1349             &request,
   1350             &resolution,
   1351             &status_view,
   1352         )?;
   1353         if let Some(view) = inventory_preflight.invalid_view {
   1354             return Ok(view);
   1355         }
   1356         let signing = match resolve_local_order_decision_signing_identity(
   1357             config,
   1358             request.seller_pubkey.as_str(),
   1359             args.decision,
   1360         ) {
   1361             Ok(signing) => signing,
   1362             Err(ActorWriteBindingError::Account(failure)) => return Err(failure.into()),
   1363             Err(error) => {
   1364                 return Ok(order_decision_binding_error_view(
   1365                     config, args, request, resolution, error,
   1366                 ));
   1367             }
   1368         };
   1369         let payload = {
   1370             let signer_pubkey = signing
   1371                 .account
   1372                 .record
   1373                 .public_identity
   1374                 .public_key_hex
   1375                 .as_str();
   1376             canonical_order_decision_payload(args, &request, signer_pubkey)?
   1377         };
   1378         if config.output.dry_run {
   1379             return Ok(order_decision_dry_run_view(
   1380                 config,
   1381                 args,
   1382                 &request,
   1383                 &status_view,
   1384                 inventory_preflight.inventory,
   1385             ));
   1386         }
   1387         return publish_order_decision(
   1388             config,
   1389             args,
   1390             request,
   1391             resolution,
   1392             signing,
   1393             payload,
   1394             inventory_preflight.inventory,
   1395         );
   1396     }
   1397     Ok(order_decision_view_from_resolution(
   1398         config,
   1399         args,
   1400         seller_pubkey,
   1401         resolution,
   1402     ))
   1403 }
   1404 
   1405 pub fn revision_propose(
   1406     config: &RuntimeConfig,
   1407     args: &OrderRevisionProposeArgs,
   1408 ) -> Result<OrderRevisionProposalView, RuntimeError> {
   1409     if let Some(view) = order_revision_args_preflight_view(config, args) {
   1410         return Ok(view);
   1411     }
   1412     if config.relay.urls.is_empty() {
   1413         let mut view =
   1414             order_revision_base_view(config, args, "unconfigured", config.output.dry_run);
   1415         view.reason =
   1416             Some("order revision propose requires at least one configured relay".to_owned());
   1417         return Ok(view);
   1418     }
   1419 
   1420     let seller = match account::resolve_account(config)? {
   1421         Some(account) => account,
   1422         None => {
   1423             let mut view =
   1424                 order_revision_base_view(config, args, "unconfigured", config.output.dry_run);
   1425             view.reason =
   1426                 Some("order revision propose requires a selected seller account".to_owned());
   1427             view.actions = vec!["radroots account create".to_owned()];
   1428             return Ok(view);
   1429         }
   1430     };
   1431     let selected_pubkey = seller.record.public_identity.public_key_hex;
   1432     let filter = order_status_filter(args.key.as_str())?;
   1433     let receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
   1434         Ok(receipt) => receipt,
   1435         Err(DirectRelayFetchError::Connect {
   1436             reason,
   1437             target_relays,
   1438             failed_relays,
   1439         }) => {
   1440             let mut view =
   1441                 order_revision_base_view(config, args, "unavailable", config.output.dry_run);
   1442             view.seller_pubkey = Some(selected_pubkey);
   1443             view.target_relays = target_relays;
   1444             view.failed_relays = relay_failures(failed_relays);
   1445             view.reason = Some(format!("direct relay connection failed: {reason}"));
   1446             return Ok(view);
   1447         }
   1448         Err(error) => return Err(RuntimeError::Network(error.to_string())),
   1449     };
   1450 
   1451     let evidence_events = order_evidence_from_relay_events(receipt.events.as_slice());
   1452     let revision_candidates =
   1453         order_revision_proposals_from_events(args.key.as_str(), receipt.events.as_slice());
   1454     let reduction = order_status_reduction_from_receipt_with_context(
   1455         OrderStatusContext {
   1456             order_id: args.key.as_str(),
   1457             buyer_pubkey: None,
   1458             seller_pubkey: None,
   1459             selected_account_pubkey: Some(selected_pubkey.as_str()),
   1460             actor_context_source: ORDER_ACTOR_CONTEXT_RESOLVED_ACCOUNT,
   1461         },
   1462         receipt,
   1463     );
   1464     let mut status_view = reduction.view;
   1465     enrich_order_status_inventory(config, &mut status_view)?;
   1466     if let Some(view) = order_revision_preflight_view_from_status(
   1467         config,
   1468         args,
   1469         &status_view,
   1470         selected_pubkey.as_str(),
   1471         &revision_candidates,
   1472     ) {
   1473         return Ok(view);
   1474     }
   1475 
   1476     let seller_pubkey = status_view.seller_pubkey.as_deref().ok_or_else(|| {
   1477         RuntimeError::Config("accepted order is missing seller_pubkey".to_owned())
   1478     })?;
   1479     let signing = match resolve_local_order_revision_signing_identity(config, seller_pubkey) {
   1480         Ok(signing) => signing,
   1481         Err(ActorWriteBindingError::Account(failure)) => return Err(failure.into()),
   1482         Err(error) => {
   1483             return Ok(order_revision_binding_error_view(
   1484                 config,
   1485                 args,
   1486                 &status_view,
   1487                 error,
   1488             ));
   1489         }
   1490     };
   1491     let payload = match order_revision_payload_from_status(args, &status_view) {
   1492         Ok(payload) => payload,
   1493         Err(error) => {
   1494             return Ok(order_revision_invalid_view(
   1495                 config,
   1496                 args,
   1497                 &status_view,
   1498                 format!(
   1499                     "order revision propose inputs for `{}` are invalid",
   1500                     args.key
   1501                 ),
   1502                 vec![issue_with_code(
   1503                     "revision_payload_invalid",
   1504                     "revision",
   1505                     error.to_string(),
   1506                 )],
   1507             ));
   1508         }
   1509     };
   1510     if let Some(view) =
   1511         order_revision_inventory_preflight_view(config, args, &status_view, &payload)
   1512     {
   1513         return Ok(view);
   1514     }
   1515     prepare_order_revision_proposal_dry_run_via_sdk(config, &signing, &payload)?;
   1516     if config.output.dry_run {
   1517         return Ok(order_revision_dry_run_view(
   1518             config,
   1519             args,
   1520             &status_view,
   1521             &payload,
   1522         ));
   1523     }
   1524     publish_order_revision(config, args, status_view, signing, payload, evidence_events)
   1525 }
   1526 
   1527 pub fn revision_decide(
   1528     config: &RuntimeConfig,
   1529     args: &OrderRevisionDecisionArgs,
   1530 ) -> Result<OrderRevisionDecisionView, RuntimeError> {
   1531     if let Some(view) = order_revision_decision_args_preflight_view(config, args) {
   1532         return Ok(view);
   1533     }
   1534     if config.relay.urls.is_empty() {
   1535         let mut view =
   1536             order_revision_decision_base_view(config, args, "unconfigured", config.output.dry_run);
   1537         view.reason =
   1538             Some("order revision decision requires at least one configured relay".to_owned());
   1539         return Ok(view);
   1540     }
   1541 
   1542     let actor_context = match order_buyer_write_actor_context(config, args.key.as_str())? {
   1543         Some(context) => context,
   1544         None => {
   1545             let mut view = order_revision_decision_base_view(
   1546                 config,
   1547                 args,
   1548                 "unconfigured",
   1549                 config.output.dry_run,
   1550             );
   1551             view.reason =
   1552                 Some("order revision decision requires a selected buyer account".to_owned());
   1553             view.actions = vec!["radroots account create".to_owned()];
   1554             return Ok(view);
   1555         }
   1556     };
   1557     let selected_pubkey = actor_context.selected_pubkey.clone();
   1558     let filter = order_status_filter(args.key.as_str())?;
   1559     let receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
   1560         Ok(receipt) => receipt,
   1561         Err(DirectRelayFetchError::Connect {
   1562             reason,
   1563             target_relays,
   1564             failed_relays,
   1565         }) => {
   1566             let mut view = order_revision_decision_base_view(
   1567                 config,
   1568                 args,
   1569                 "unavailable",
   1570                 config.output.dry_run,
   1571             );
   1572             view.buyer_pubkey = Some(selected_pubkey);
   1573             view.target_relays = target_relays;
   1574             view.failed_relays = relay_failures(failed_relays);
   1575             view.reason = Some(format!("direct relay connection failed: {reason}"));
   1576             return Ok(view);
   1577         }
   1578         Err(error) => return Err(RuntimeError::Network(error.to_string())),
   1579     };
   1580 
   1581     let evidence_events = order_evidence_from_relay_events(receipt.events.as_slice());
   1582     let revision_candidates =
   1583         order_revision_proposals_from_events(args.key.as_str(), receipt.events.as_slice());
   1584     let reduction = order_status_reduction_from_receipt_with_context(
   1585         OrderStatusContext {
   1586             order_id: args.key.as_str(),
   1587             buyer_pubkey: actor_context.status_buyer_pubkey.as_deref(),
   1588             seller_pubkey: actor_context.status_seller_pubkey.as_deref(),
   1589             selected_account_pubkey: actor_context
   1590                 .bound
   1591                 .is_none()
   1592                 .then_some(selected_pubkey.as_str()),
   1593             actor_context_source: actor_context.status_context_source,
   1594         },
   1595         receipt,
   1596     );
   1597     let mut status_view = reduction.view;
   1598     enrich_order_status_inventory(config, &mut status_view)?;
   1599     if let Some(view) = order_revision_decision_preflight_view_from_status(
   1600         config,
   1601         args,
   1602         &status_view,
   1603         selected_pubkey.as_str(),
   1604         &revision_candidates,
   1605     ) {
   1606         return Ok(view);
   1607     }
   1608 
   1609     let proposal = pending_revision_proposal_candidate(&status_view, &revision_candidates)
   1610         .ok_or_else(|| {
   1611             RuntimeError::Config("accepted order is missing pending revision proposal".to_owned())
   1612         })?;
   1613     if proposal.payload.revision_id != args.revision_id.trim() {
   1614         let mut view = order_revision_decision_invalid_view(
   1615             config,
   1616             args,
   1617             &status_view,
   1618             format!(
   1619                 "order revision {} refused because revision `{}` is not the latest pending proposal",
   1620                 args.decision.command(),
   1621                 args.revision_id.trim()
   1622             ),
   1623             vec![issue_with_events(
   1624                 "revision_id_not_pending",
   1625                 "revision_id",
   1626                 format!(
   1627                     "latest pending revision is `{}`",
   1628                     proposal.payload.revision_id
   1629                 ),
   1630                 vec![proposal.event_id.clone()],
   1631             )],
   1632         );
   1633         apply_order_revision_decision_proposal(&mut view, proposal);
   1634         return Ok(view);
   1635     }
   1636 
   1637     let buyer_pubkey = status_view
   1638         .buyer_pubkey
   1639         .as_deref()
   1640         .ok_or_else(|| RuntimeError::Config("accepted order is missing buyer_pubkey".to_owned()))?;
   1641     let signing = match actor_context.bound.as_ref() {
   1642         Some(bound) => resolve_local_order_bound_buyer_signing_identity(
   1643             config,
   1644             &bound.loaded,
   1645             format!("order revision {}", args.decision.command()).as_str(),
   1646         ),
   1647         None => resolve_local_order_revision_decision_signing_identity(config, buyer_pubkey, args),
   1648     };
   1649     let signing = match signing {
   1650         Ok(signing) => signing,
   1651         Err(ActorWriteBindingError::Account(failure)) => return Err(failure.into()),
   1652         Err(error) => {
   1653             return Ok(order_revision_decision_binding_error_view(
   1654                 config,
   1655                 args,
   1656                 &status_view,
   1657                 error,
   1658             ));
   1659         }
   1660     };
   1661     if args.decision == OrderRevisionDecisionArg::Accept {
   1662         let issues = order_revision_inventory_issues(&status_view, &proposal.payload);
   1663         if !issues.is_empty() {
   1664             let mut view = order_revision_decision_invalid_view(
   1665                 config,
   1666                 args,
   1667                 &status_view,
   1668                 "order revision accept refused because visible inventory is unavailable for the revised items",
   1669                 issues,
   1670             );
   1671             apply_order_revision_decision_proposal(&mut view, proposal);
   1672             return Ok(view);
   1673         }
   1674     }
   1675     let payload = order_revision_decision_payload_from_proposal(args, proposal)?;
   1676     prepare_order_revision_decision_dry_run_via_sdk(config, &signing, &payload)?;
   1677     if config.output.dry_run {
   1678         return Ok(order_revision_decision_dry_run_view(
   1679             config,
   1680             args,
   1681             &status_view,
   1682             proposal,
   1683             &payload,
   1684         ));
   1685     }
   1686     publish_order_revision_decision(
   1687         config,
   1688         args,
   1689         status_view,
   1690         proposal,
   1691         signing,
   1692         payload,
   1693         evidence_events,
   1694     )
   1695 }
   1696 
   1697 pub fn cancel(
   1698     config: &RuntimeConfig,
   1699     args: &OrderCancelArgs,
   1700 ) -> Result<OrderCancellationView, RuntimeError> {
   1701     if config.relay.urls.is_empty() {
   1702         let mut view =
   1703             order_cancellation_base_view(config, args, "unconfigured", config.output.dry_run);
   1704         view.reason = Some("order cancel requires at least one configured relay".to_owned());
   1705         return Ok(view);
   1706     }
   1707 
   1708     let actor_context = match order_buyer_write_actor_context(config, args.key.as_str())? {
   1709         Some(context) => context,
   1710         None => {
   1711             let mut view =
   1712                 order_cancellation_base_view(config, args, "unconfigured", config.output.dry_run);
   1713             view.reason = Some("order cancel requires a selected buyer account".to_owned());
   1714             view.actions = vec!["radroots account create".to_owned()];
   1715             return Ok(view);
   1716         }
   1717     };
   1718     let selected_pubkey = actor_context.selected_pubkey.clone();
   1719     let filter = order_status_filter(args.key.as_str())?;
   1720     let receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
   1721         Ok(receipt) => receipt,
   1722         Err(DirectRelayFetchError::Connect {
   1723             reason,
   1724             target_relays,
   1725             failed_relays,
   1726         }) => {
   1727             let mut view =
   1728                 order_cancellation_base_view(config, args, "unavailable", config.output.dry_run);
   1729             view.buyer_pubkey = Some(selected_pubkey);
   1730             view.target_relays = target_relays;
   1731             view.failed_relays = relay_failures(failed_relays);
   1732             view.reason = Some(format!("direct relay connection failed: {reason}"));
   1733             return Ok(view);
   1734         }
   1735         Err(error) => return Err(RuntimeError::Network(error.to_string())),
   1736     };
   1737 
   1738     let evidence_events = order_evidence_from_relay_events(receipt.events.as_slice());
   1739     let reduction = order_status_reduction_from_receipt_with_context(
   1740         OrderStatusContext {
   1741             order_id: args.key.as_str(),
   1742             buyer_pubkey: actor_context.status_buyer_pubkey.as_deref(),
   1743             seller_pubkey: actor_context.status_seller_pubkey.as_deref(),
   1744             selected_account_pubkey: actor_context
   1745                 .bound
   1746                 .is_none()
   1747                 .then_some(selected_pubkey.as_str()),
   1748             actor_context_source: actor_context.status_context_source,
   1749         },
   1750         receipt,
   1751     );
   1752     let status_view = reduction.view;
   1753     if let Some(view) = order_cancellation_preflight_view_from_status(
   1754         config,
   1755         args,
   1756         &status_view,
   1757         selected_pubkey.as_str(),
   1758     ) {
   1759         return Ok(view);
   1760     }
   1761 
   1762     let buyer_pubkey = status_view
   1763         .buyer_pubkey
   1764         .as_deref()
   1765         .ok_or_else(|| RuntimeError::Config("order is missing buyer_pubkey".to_owned()))?;
   1766     let signing = match actor_context.bound.as_ref() {
   1767         Some(bound) => {
   1768             resolve_local_order_bound_buyer_signing_identity(config, &bound.loaded, "order cancel")
   1769         }
   1770         None => resolve_local_order_cancellation_signing_identity(config, buyer_pubkey),
   1771     };
   1772     let signing = match signing {
   1773         Ok(signing) => signing,
   1774         Err(ActorWriteBindingError::Account(failure)) => return Err(failure.into()),
   1775         Err(error) => {
   1776             return Ok(order_cancellation_binding_error_view(
   1777                 config,
   1778                 args,
   1779                 &status_view,
   1780                 error,
   1781             ));
   1782         }
   1783     };
   1784     let payload = order_cancellation_payload_from_status(args, &status_view)?;
   1785     prepare_order_cancellation_dry_run_via_sdk(config, &signing, &status_view, &payload)?;
   1786     if config.output.dry_run {
   1787         return Ok(order_cancellation_dry_run_view(config, args, &status_view));
   1788     }
   1789     publish_order_cancellation(config, args, status_view, signing, payload, evidence_events)
   1790 }
   1791 
   1792 pub fn status(
   1793     config: &RuntimeConfig,
   1794     args: &OrderStatusArgs,
   1795 ) -> Result<OrderStatusView, CliSdkAdapterError> {
   1796     let request = OrderStatusRequest::parse(args.key.as_str())?;
   1797     let session = CliSdkSession::connect(config)?;
   1798     let receipt = session.block_on(session.sdk().orders().status(request))?;
   1799     Ok(sdk_order_status_view(receipt))
   1800 }
   1801 
   1802 fn legacy_order_preflight_relay_status(
   1803     config: &RuntimeConfig,
   1804     args: &OrderStatusArgs,
   1805 ) -> Result<OrderStatusView, RuntimeError> {
   1806     if config.relay.urls.is_empty() {
   1807         return Ok(OrderStatusView {
   1808             state: "unconfigured".to_owned(),
   1809             source: LEGACY_ORDER_PREFLIGHT_STATUS_SOURCE.to_owned(),
   1810             order_id: args.key.clone(),
   1811             actor_context_source: ORDER_ACTOR_CONTEXT_NETWORK_ONLY.to_owned(),
   1812             request_event_id: None,
   1813             decision_event_id: None,
   1814             agreement_event_id: None,
   1815             listing_event_id: None,
   1816             listing_addr: None,
   1817             buyer_pubkey: None,
   1818             seller_pubkey: None,
   1819             economics: None,
   1820             last_event_id: None,
   1821             revision: None,
   1822             inventory: None,
   1823             lifecycle: None,
   1824             sdk_receipt: None,
   1825             reducer_issues: Vec::new(),
   1826             target_relays: Vec::new(),
   1827             connected_relays: Vec::new(),
   1828             failed_relays: Vec::new(),
   1829             fetched_count: 0,
   1830             decoded_count: 0,
   1831             skipped_count: 0,
   1832             reason: Some("order status get requires at least one configured relay".to_owned()),
   1833             actions: vec![format!(
   1834                 "radroots --relay wss://relay.example.com order status get {}",
   1835                 args.key
   1836             )],
   1837         });
   1838     }
   1839 
   1840     let filter = order_status_filter(args.key.as_str())?;
   1841     let receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
   1842         Ok(receipt) => receipt,
   1843         Err(DirectRelayFetchError::Connect {
   1844             reason,
   1845             target_relays,
   1846             failed_relays,
   1847         }) => {
   1848             return Ok(OrderStatusView {
   1849                 state: "unavailable".to_owned(),
   1850                 source: LEGACY_ORDER_PREFLIGHT_STATUS_SOURCE.to_owned(),
   1851                 order_id: args.key.clone(),
   1852                 actor_context_source: ORDER_ACTOR_CONTEXT_NETWORK_ONLY.to_owned(),
   1853                 request_event_id: None,
   1854                 decision_event_id: None,
   1855                 agreement_event_id: None,
   1856                 listing_event_id: None,
   1857                 listing_addr: None,
   1858                 buyer_pubkey: None,
   1859                 seller_pubkey: None,
   1860                 economics: None,
   1861                 last_event_id: None,
   1862                 revision: None,
   1863                 inventory: None,
   1864                 lifecycle: None,
   1865                 sdk_receipt: None,
   1866                 reducer_issues: Vec::new(),
   1867                 target_relays,
   1868                 connected_relays: Vec::new(),
   1869                 failed_relays: relay_failures(failed_relays),
   1870                 fetched_count: 0,
   1871                 decoded_count: 0,
   1872                 skipped_count: 0,
   1873                 reason: Some(format!("direct relay connection failed: {reason}")),
   1874                 actions: Vec::new(),
   1875             });
   1876         }
   1877         Err(error) => return Err(RuntimeError::Network(error.to_string())),
   1878     };
   1879 
   1880     let actor_context = order_status_actor_context(config, args.key.as_str())?;
   1881     let mut view = order_status_from_receipt_with_context(
   1882         OrderStatusContext {
   1883             order_id: args.key.as_str(),
   1884             buyer_pubkey: actor_context.buyer_pubkey.as_deref(),
   1885             seller_pubkey: actor_context.seller_pubkey.as_deref(),
   1886             selected_account_pubkey: actor_context.selected_account_pubkey.as_deref(),
   1887             actor_context_source: actor_context.source,
   1888         },
   1889         receipt,
   1890     );
   1891     enrich_order_status_inventory(config, &mut view)?;
   1892     Ok(view)
   1893 }
   1894 
   1895 enum OrderStatusRecord {
   1896     Request {
   1897         listing_event_id: Option<String>,
   1898         record: RadrootsOrderRequestRecord,
   1899     },
   1900     Decision(RadrootsOrderDecisionRecord),
   1901     RevisionProposal(OrderRevisionProposalRecord),
   1902     RevisionDecision(OrderRevisionDecisionRecord),
   1903     Cancellation(RadrootsOrderCancellationRecord),
   1904 }
   1905 
   1906 type OrderRevisionProposalRecord = RadrootsOrderRevisionProposalRecord;
   1907 type OrderRevisionDecisionRecord = RadrootsOrderRevisionDecisionRecord;
   1908 
   1909 #[derive(Debug, Clone)]
   1910 struct OrderRevisionProposalCandidates {
   1911     records: Vec<OrderRevisionProposalRecord>,
   1912     issues: Vec<OrderIssueView>,
   1913 }
   1914 
   1915 #[derive(Debug, Clone)]
   1916 struct OrderStatusReduction {
   1917     view: OrderStatusView,
   1918 }
   1919 
   1920 #[derive(Debug, Clone, Copy)]
   1921 struct OrderRequestCandidateContext<'a> {
   1922     order_id: &'a str,
   1923     seller_pubkey: Option<&'a str>,
   1924 }
   1925 
   1926 #[derive(Debug, Clone, Copy)]
   1927 struct OrderStatusContext<'a> {
   1928     order_id: &'a str,
   1929     buyer_pubkey: Option<&'a str>,
   1930     seller_pubkey: Option<&'a str>,
   1931     selected_account_pubkey: Option<&'a str>,
   1932     actor_context_source: &'static str,
   1933 }
   1934 
   1935 #[cfg(test)]
   1936 fn order_status_from_receipt(order_id: &str, receipt: DirectRelayFetchReceipt) -> OrderStatusView {
   1937     order_status_from_receipt_with_context(
   1938         OrderStatusContext {
   1939             order_id,
   1940             buyer_pubkey: None,
   1941             seller_pubkey: None,
   1942             selected_account_pubkey: None,
   1943             actor_context_source: ORDER_ACTOR_CONTEXT_NETWORK_ONLY,
   1944         },
   1945         receipt,
   1946     )
   1947 }
   1948 
   1949 fn order_status_from_receipt_with_context(
   1950     context: OrderStatusContext<'_>,
   1951     receipt: DirectRelayFetchReceipt,
   1952 ) -> OrderStatusView {
   1953     order_status_reduction_from_receipt_with_context(context, receipt).view
   1954 }
   1955 
   1956 fn order_status_reduction_from_receipt_with_context(
   1957     context: OrderStatusContext<'_>,
   1958     receipt: DirectRelayFetchReceipt,
   1959 ) -> OrderStatusReduction {
   1960     order_status_reduction_from_receipt_inner(context, receipt)
   1961 }
   1962 
   1963 fn order_status_reduction_from_receipt_inner(
   1964     context: OrderStatusContext<'_>,
   1965     receipt: DirectRelayFetchReceipt,
   1966 ) -> OrderStatusReduction {
   1967     let DirectRelayFetchReceipt {
   1968         target_relays,
   1969         connected_relays,
   1970         failed_relays,
   1971         events,
   1972     } = receipt;
   1973     let fetched_count = events.len();
   1974     let mut decoded_count = 0usize;
   1975     let mut skipped_count = 0usize;
   1976     let mut requests = Vec::new();
   1977     let mut decisions = Vec::new();
   1978     let mut revision_proposals = Vec::new();
   1979     let mut revision_decisions = Vec::new();
   1980     let mut cancellations = Vec::new();
   1981     let mut request_listing_events = Vec::new();
   1982     let mut candidate_issues = Vec::new();
   1983 
   1984     for event in events {
   1985         match order_status_record_from_event(&event) {
   1986             Ok(OrderStatusRecord::Request {
   1987                 listing_event_id,
   1988                 record,
   1989             }) => {
   1990                 if !order_status_request_matches_context(&record, context) {
   1991                     skipped_count += 1;
   1992                     continue;
   1993                 }
   1994                 decoded_count += 1;
   1995                 request_listing_events.push((record.event_id.clone(), listing_event_id));
   1996                 requests.push(record);
   1997             }
   1998             Ok(OrderStatusRecord::Decision(record)) => {
   1999                 decoded_count += 1;
   2000                 decisions.push(record);
   2001             }
   2002             Ok(OrderStatusRecord::RevisionProposal(record)) => {
   2003                 decoded_count += 1;
   2004                 revision_proposals.push(record);
   2005             }
   2006             Ok(OrderStatusRecord::RevisionDecision(record)) => {
   2007                 decoded_count += 1;
   2008                 revision_decisions.push(record);
   2009             }
   2010             Ok(OrderStatusRecord::Cancellation(record)) => {
   2011                 decoded_count += 1;
   2012                 cancellations.push(record);
   2013             }
   2014             Err(error) => {
   2015                 skipped_count += 1;
   2016                 if order_status_request_candidate(&event, context) {
   2017                     let event_id = event.id.to_string();
   2018                     candidate_issues.push(issue_with_events(
   2019                         "invalid_request_candidate",
   2020                         "request_event_id",
   2021                         format!(
   2022                             "request event `{event_id}` failed order status validation: {error}"
   2023                         ),
   2024                         vec![event_id],
   2025                     ));
   2026                 }
   2027             }
   2028         }
   2029     }
   2030     candidate_issues.sort_by(|left, right| {
   2031         left.event_ids
   2032             .cmp(&right.event_ids)
   2033             .then_with(|| left.message.cmp(&right.message))
   2034     });
   2035 
   2036     let reducer_order_id = match protocol_order_id(context.order_id, "order_id") {
   2037         Ok(order_id) => order_id,
   2038         Err(error) => {
   2039             let message = error.to_string();
   2040             let view = OrderStatusView {
   2041                 state: "invalid".to_owned(),
   2042                 source: LEGACY_ORDER_PREFLIGHT_STATUS_SOURCE.to_owned(),
   2043                 order_id: context.order_id.to_owned(),
   2044                 actor_context_source: context.actor_context_source.to_owned(),
   2045                 request_event_id: None,
   2046                 decision_event_id: None,
   2047                 agreement_event_id: None,
   2048                 listing_event_id: None,
   2049                 listing_addr: None,
   2050                 buyer_pubkey: None,
   2051                 seller_pubkey: None,
   2052                 economics: None,
   2053                 last_event_id: None,
   2054                 revision: None,
   2055                 inventory: None,
   2056                 lifecycle: None,
   2057                 sdk_receipt: None,
   2058                 reducer_issues: vec![issue("order_id", message.clone())],
   2059                 target_relays,
   2060                 connected_relays,
   2061                 failed_relays: relay_failures(failed_relays),
   2062                 fetched_count,
   2063                 decoded_count,
   2064                 skipped_count,
   2065                 reason: Some(message),
   2066                 actions: Vec::new(),
   2067             };
   2068             return OrderStatusReduction { view };
   2069         }
   2070     };
   2071     let order_id = context.order_id;
   2072     let revision_proposal_records = revision_proposals.clone();
   2073     let revision_decision_records = revision_decisions.clone();
   2074     let cancellation_records = cancellations.clone();
   2075     let projection = reduce_order_events(
   2076         &reducer_order_id,
   2077         RadrootsOrderReductionInputs {
   2078             requests,
   2079             decisions: decisions.clone(),
   2080             revision_proposals,
   2081             revision_decisions,
   2082             cancellations,
   2083         },
   2084     );
   2085     let cancellation_root_event_id =
   2086         projection
   2087             .cancellation_event_id
   2088             .as_ref()
   2089             .and_then(|event_id| {
   2090                 cancellation_records
   2091                     .iter()
   2092                     .find(|record| &record.event_id == event_id)
   2093                     .map(|record| record.root_event_id.clone())
   2094             });
   2095     let cancellation_prev_event_id =
   2096         projection
   2097             .cancellation_event_id
   2098             .as_ref()
   2099             .and_then(|event_id| {
   2100                 cancellation_records
   2101                     .iter()
   2102                     .find(|record| &record.event_id == event_id)
   2103                     .map(|record| record.prev_event_id.clone())
   2104             });
   2105     let cancellation_reason = projection
   2106         .cancellation_event_id
   2107         .as_ref()
   2108         .and_then(|event_id| {
   2109             cancellation_records
   2110                 .iter()
   2111                 .find(|record| &record.event_id == event_id)
   2112                 .map(|record| record.payload.reason.clone())
   2113         });
   2114     let listing_event_id = projection
   2115         .request_event_id
   2116         .as_ref()
   2117         .and_then(|request_event_id| {
   2118             request_listing_events
   2119                 .iter()
   2120                 .find(|(event_id, _)| event_id == request_event_id)
   2121                 .and_then(|(_, listing_event_id)| listing_event_id.clone())
   2122         });
   2123     let mut state = active_order_status_state(&projection.status).to_owned();
   2124     let mut reason = active_order_status_reason(&projection.status, order_id);
   2125     let mut reducer_issues = projection
   2126         .issues
   2127         .into_iter()
   2128         .map(active_order_reducer_issue_view)
   2129         .collect::<Vec<_>>();
   2130     if !candidate_issues.is_empty() {
   2131         state = "invalid".to_owned();
   2132         reason = Some(format!(
   2133             "active order request candidates for `{order_id}` failed status validation"
   2134         ));
   2135         reducer_issues.extend(candidate_issues);
   2136     }
   2137     let inventory = order_status_inventory_view(
   2138         &projection.status,
   2139         listing_event_id.clone(),
   2140         projection.decision_event_id.as_ref(),
   2141         &decisions,
   2142         reducer_issues.as_slice(),
   2143     );
   2144     let lifecycle = order_status_lifecycle_view(
   2145         &projection.status,
   2146         optional_string(projection.request_event_id.clone()),
   2147         optional_string(projection.last_event_id.clone()),
   2148         optional_string(projection.cancellation_event_id.clone()),
   2149         optional_string(cancellation_root_event_id),
   2150         optional_string(cancellation_prev_event_id),
   2151         cancellation_reason,
   2152         reducer_issues.as_slice(),
   2153     );
   2154     let revision = order_status_revision_view(
   2155         projection.last_event_id.as_ref(),
   2156         projection.agreement_event_id.as_ref(),
   2157         &revision_proposal_records,
   2158         &revision_decision_records,
   2159     );
   2160     let view = OrderStatusView {
   2161         state,
   2162         source: LEGACY_ORDER_PREFLIGHT_STATUS_SOURCE.to_owned(),
   2163         order_id: projection.order_id.to_string(),
   2164         actor_context_source: context.actor_context_source.to_owned(),
   2165         request_event_id: optional_string(projection.request_event_id),
   2166         decision_event_id: optional_string(projection.decision_event_id),
   2167         agreement_event_id: optional_string(projection.agreement_event_id),
   2168         listing_event_id,
   2169         listing_addr: optional_string(projection.listing_addr),
   2170         buyer_pubkey: optional_string(projection.buyer_pubkey),
   2171         seller_pubkey: optional_string(projection.seller_pubkey),
   2172         economics: projection.economics,
   2173         last_event_id: optional_string(projection.last_event_id),
   2174         revision,
   2175         inventory,
   2176         lifecycle: Some(lifecycle),
   2177         sdk_receipt: None,
   2178         reducer_issues,
   2179         target_relays,
   2180         connected_relays,
   2181         failed_relays: relay_failures(failed_relays),
   2182         fetched_count,
   2183         decoded_count,
   2184         skipped_count,
   2185         reason,
   2186         actions: Vec::new(),
   2187     };
   2188     OrderStatusReduction { view }
   2189 }
   2190 
   2191 fn order_status_request_matches_context(
   2192     record: &RadrootsOrderRequestRecord,
   2193     context: OrderStatusContext<'_>,
   2194 ) -> bool {
   2195     if record.payload.order_id.to_string() != context.order_id {
   2196         return false;
   2197     }
   2198     order_status_context_is_network_only(context)
   2199         || context.buyer_pubkey.is_some_and(|pubkey| {
   2200             record
   2201                 .payload
   2202                 .buyer_pubkey
   2203                 .to_string()
   2204                 .eq_ignore_ascii_case(pubkey)
   2205         })
   2206         || context.seller_pubkey.is_some_and(|pubkey| {
   2207             record
   2208                 .payload
   2209                 .seller_pubkey
   2210                 .to_string()
   2211                 .eq_ignore_ascii_case(pubkey)
   2212         })
   2213         || context.selected_account_pubkey.is_some_and(|pubkey| {
   2214             record
   2215                 .payload
   2216                 .buyer_pubkey
   2217                 .to_string()
   2218                 .eq_ignore_ascii_case(pubkey)
   2219                 || record
   2220                     .payload
   2221                     .seller_pubkey
   2222                     .to_string()
   2223                     .eq_ignore_ascii_case(pubkey)
   2224         })
   2225 }
   2226 
   2227 fn enrich_order_status_inventory(
   2228     config: &RuntimeConfig,
   2229     view: &mut OrderStatusView,
   2230 ) -> Result<(), RuntimeError> {
   2231     let Some(listing_addr) = view.listing_addr.clone() else {
   2232         return Ok(());
   2233     };
   2234     let Some(listing_event_id) = view.listing_event_id.clone() else {
   2235         return Ok(());
   2236     };
   2237     let Some(seller_pubkey) = view.seller_pubkey.clone() else {
   2238         return Ok(());
   2239     };
   2240     let Some(decision_event_id) = view.decision_event_id.clone() else {
   2241         return Ok(());
   2242     };
   2243 
   2244     let Some(listing) = fetch_current_inventory_listing_for_status(config, listing_addr.as_str())?
   2245     else {
   2246         return Ok(());
   2247     };
   2248     if listing.event_id.to_string() != listing_event_id {
   2249         return Ok(());
   2250     }
   2251 
   2252     let mut requests = fetch_listing_accounting_requests_for_status(
   2253         config,
   2254         seller_pubkey.as_str(),
   2255         listing_addr.as_str(),
   2256         listing.event_id.to_string().as_str(),
   2257     )?;
   2258     let mut request_order_ids = requests
   2259         .iter()
   2260         .map(|record| record.payload.order_id.clone())
   2261         .collect::<Vec<_>>();
   2262     request_order_ids.sort();
   2263     request_order_ids.dedup();
   2264     requests.sort_by(|left, right| left.event_id.cmp(&right.event_id));
   2265 
   2266     let decisions = fetch_listing_accounting_decisions_for_status(config, listing_addr.as_str())?
   2267         .into_iter()
   2268         .filter(|record| request_order_ids.contains(&record.payload.order_id))
   2269         .collect::<Vec<_>>();
   2270     let revision_proposals =
   2271         fetch_listing_accounting_revision_proposals_for_status(config, listing_addr.as_str())?
   2272             .into_iter()
   2273             .filter(|record| request_order_ids.contains(&record.payload.order_id))
   2274             .collect::<Vec<_>>();
   2275     let revision_decisions =
   2276         fetch_listing_accounting_revision_decisions_for_status(config, listing_addr.as_str())?
   2277             .into_iter()
   2278             .filter(|record| request_order_ids.contains(&record.payload.order_id))
   2279             .collect::<Vec<_>>();
   2280     let cancellations =
   2281         fetch_listing_accounting_cancellations_for_status(config, listing_addr.as_str())?
   2282             .into_iter()
   2283             .filter(|record| request_order_ids.contains(&record.payload.order_id))
   2284             .collect::<Vec<_>>();
   2285     let projection = reduce_listing_inventory_accounting(
   2286         &protocol_listing_addr(listing_addr.as_str(), "listing_addr")?,
   2287         &listing.event_id,
   2288         RadrootsListingInventoryAccountingInputs {
   2289             bins: listing.bins,
   2290             requests,
   2291             decisions,
   2292             revision_proposals,
   2293             revision_decisions,
   2294             cancellations,
   2295         },
   2296     );
   2297     let mut relevant_event_ids = Vec::new();
   2298     relevant_event_ids.push(decision_event_id);
   2299     relevant_event_ids.extend(view.agreement_event_id.clone());
   2300     relevant_event_ids.extend(view.last_event_id.clone());
   2301     relevant_event_ids.sort();
   2302     relevant_event_ids.dedup();
   2303     let relevant_issues = projection
   2304         .issues
   2305         .iter()
   2306         .filter(|issue| {
   2307             listing_inventory_issue_involves_order(
   2308                 issue,
   2309                 view.order_id.as_str(),
   2310                 relevant_event_ids.as_slice(),
   2311             )
   2312         })
   2313         .cloned()
   2314         .collect::<Vec<_>>();
   2315     if relevant_issues.is_empty() {
   2316         if matches!(view.state.as_str(), "accepted" | "cancelled") {
   2317             let inventory_state = if view.state == "cancelled" {
   2318                 "released"
   2319             } else {
   2320                 "reserved"
   2321             };
   2322             view.inventory = Some(order_inventory_view_from_listing_projection(
   2323                 &projection,
   2324                 inventory_state,
   2325                 true,
   2326             ));
   2327         }
   2328         return Ok(());
   2329     }
   2330 
   2331     let mut inventory = order_inventory_view_from_listing_projection(&projection, "invalid", false);
   2332     inventory.issues = relevant_issues
   2333         .iter()
   2334         .cloned()
   2335         .map(listing_inventory_accounting_issue_view)
   2336         .collect();
   2337     view.reducer_issues.extend(inventory.issues.clone());
   2338     view.inventory = Some(inventory);
   2339     view.state = "invalid".to_owned();
   2340     view.reason = Some(format!(
   2341         "listing inventory accounting for order `{}` failed reducer validation",
   2342         view.order_id
   2343     ));
   2344     Ok(())
   2345 }
   2346 
   2347 fn fetch_current_inventory_listing_for_status(
   2348     config: &RuntimeConfig,
   2349     listing_addr: &str,
   2350 ) -> Result<Option<ResolvedInventoryListing>, RuntimeError> {
   2351     let parsed = parse_listing_addr(listing_addr).map_err(|error| {
   2352         RuntimeError::Config(format!("order status listing_addr is invalid: {error}"))
   2353     })?;
   2354     let filter = listing_event_filter(&parsed)?;
   2355     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   2356         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   2357     current_inventory_listing_from_parts(parsed, receipt)
   2358 }
   2359 
   2360 fn fetch_listing_accounting_requests_for_status(
   2361     config: &RuntimeConfig,
   2362     seller_pubkey: &str,
   2363     listing_addr: &str,
   2364     listing_event_id: &str,
   2365 ) -> Result<Vec<RadrootsOrderRequestRecord>, RuntimeError> {
   2366     let filter = order_listing_request_filter(seller_pubkey, listing_addr)?;
   2367     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   2368         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   2369     let mut records = Vec::new();
   2370     for event in receipt.events {
   2371         if event_kind_u32(&event) != KIND_ORDER_REQUEST
   2372             || !event_matches_tag_value(&event, "a", listing_addr)
   2373         {
   2374             continue;
   2375         }
   2376         if let Ok(record) = listing_accounting_request_from_event(&event)
   2377             && record.listing_event_id.as_deref() == Some(listing_event_id)
   2378         {
   2379             records.push(record.record);
   2380         }
   2381     }
   2382     Ok(records)
   2383 }
   2384 
   2385 fn fetch_listing_accounting_decisions_for_status(
   2386     config: &RuntimeConfig,
   2387     listing_addr: &str,
   2388 ) -> Result<Vec<RadrootsOrderDecisionRecord>, RuntimeError> {
   2389     let filter = order_listing_decision_filter(listing_addr)?;
   2390     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   2391         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   2392     let mut records = Vec::new();
   2393     for event in receipt.events {
   2394         if event_kind_u32(&event) != KIND_ORDER_DECISION
   2395             || !event_matches_tag_value(&event, "a", listing_addr)
   2396         {
   2397             continue;
   2398         }
   2399         if let Ok(OrderStatusRecord::Decision(record)) = order_status_record_from_event(&event) {
   2400             records.push(record);
   2401         }
   2402     }
   2403     Ok(records)
   2404 }
   2405 
   2406 fn fetch_listing_accounting_revision_proposals_for_status(
   2407     config: &RuntimeConfig,
   2408     listing_addr: &str,
   2409 ) -> Result<Vec<RadrootsOrderRevisionProposalRecord>, RuntimeError> {
   2410     let filter = order_listing_revision_proposal_filter(listing_addr)?;
   2411     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   2412         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   2413     let mut records = Vec::new();
   2414     for event in receipt.events {
   2415         if event_kind_u32(&event) != KIND_ORDER_REVISION_PROPOSAL
   2416             || !event_matches_tag_value(&event, "a", listing_addr)
   2417         {
   2418             continue;
   2419         }
   2420         if let Ok(OrderStatusRecord::RevisionProposal(record)) =
   2421             order_status_record_from_event(&event)
   2422         {
   2423             records.push(record);
   2424         }
   2425     }
   2426     Ok(records)
   2427 }
   2428 
   2429 fn fetch_listing_accounting_revision_decisions_for_status(
   2430     config: &RuntimeConfig,
   2431     listing_addr: &str,
   2432 ) -> Result<Vec<RadrootsOrderRevisionDecisionRecord>, RuntimeError> {
   2433     let filter = order_listing_revision_decision_filter(listing_addr)?;
   2434     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   2435         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   2436     let mut records = Vec::new();
   2437     for event in receipt.events {
   2438         if event_kind_u32(&event) != KIND_ORDER_REVISION_DECISION
   2439             || !event_matches_tag_value(&event, "a", listing_addr)
   2440         {
   2441             continue;
   2442         }
   2443         if let Ok(OrderStatusRecord::RevisionDecision(record)) =
   2444             order_status_record_from_event(&event)
   2445         {
   2446             records.push(record);
   2447         }
   2448     }
   2449     Ok(records)
   2450 }
   2451 
   2452 fn fetch_listing_accounting_cancellations_for_status(
   2453     config: &RuntimeConfig,
   2454     listing_addr: &str,
   2455 ) -> Result<Vec<RadrootsOrderCancellationRecord>, RuntimeError> {
   2456     let filter = order_listing_cancellation_filter(listing_addr)?;
   2457     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   2458         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   2459     let mut records = Vec::new();
   2460     for event in receipt.events {
   2461         if event_kind_u32(&event) != KIND_ORDER_CANCELLATION
   2462             || !event_matches_tag_value(&event, "a", listing_addr)
   2463         {
   2464             continue;
   2465         }
   2466         if let Ok(OrderStatusRecord::Cancellation(record)) = order_status_record_from_event(&event)
   2467         {
   2468             records.push(record);
   2469         }
   2470     }
   2471     Ok(records)
   2472 }
   2473 
   2474 fn listing_inventory_issue_involves_order(
   2475     issue: &RadrootsListingInventoryAccountingIssue,
   2476     order_id: &str,
   2477     event_ids: &[String],
   2478 ) -> bool {
   2479     match issue {
   2480         RadrootsListingInventoryAccountingIssue::InvalidOrder {
   2481             order_id: issue_order_id,
   2482             event_ids: issue_event_ids,
   2483         } => {
   2484             issue_order_id.to_string() == order_id
   2485                 || issue_event_ids
   2486                     .iter()
   2487                     .any(|id| event_ids.contains(&id.to_string()))
   2488         }
   2489         RadrootsListingInventoryAccountingIssue::ArithmeticOverflow {
   2490             event_ids: issue_event_ids,
   2491             ..
   2492         }
   2493         | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin {
   2494             event_ids: issue_event_ids,
   2495             ..
   2496         }
   2497         | RadrootsListingInventoryAccountingIssue::OverReserved {
   2498             event_ids: issue_event_ids,
   2499             ..
   2500         } => issue_event_ids
   2501             .iter()
   2502             .any(|id| event_ids.contains(&id.to_string())),
   2503     }
   2504 }
   2505 
   2506 fn order_status_request_candidate(
   2507     event: &RadrootsNostrEvent,
   2508     context: OrderStatusContext<'_>,
   2509 ) -> bool {
   2510     if event_kind_u32(event) != KIND_ORDER_REQUEST
   2511         || !event_matches_tag_value(event, "d", context.order_id)
   2512     {
   2513         return false;
   2514     }
   2515     order_status_context_is_network_only(context)
   2516         || context
   2517             .buyer_pubkey
   2518             .is_some_and(|pubkey| event.pubkey.to_string().eq_ignore_ascii_case(pubkey))
   2519         || context
   2520             .seller_pubkey
   2521             .is_some_and(|pubkey| event_matches_tag_value(event, "p", pubkey))
   2522         || context.selected_account_pubkey.is_some_and(|pubkey| {
   2523             event.pubkey.to_string().eq_ignore_ascii_case(pubkey)
   2524                 || event_matches_tag_value(event, "p", pubkey)
   2525         })
   2526 }
   2527 
   2528 fn order_status_context_is_network_only(context: OrderStatusContext<'_>) -> bool {
   2529     context.buyer_pubkey.is_none()
   2530         && context.seller_pubkey.is_none()
   2531         && context.selected_account_pubkey.is_none()
   2532 }
   2533 
   2534 fn order_request_candidate_matches(
   2535     event: &RadrootsNostrEvent,
   2536     context: OrderRequestCandidateContext<'_>,
   2537 ) -> bool {
   2538     if event_kind_u32(event) != KIND_ORDER_REQUEST
   2539         || !event_matches_tag_value(event, "d", context.order_id)
   2540     {
   2541         return false;
   2542     }
   2543     context
   2544         .seller_pubkey
   2545         .is_none_or(|seller_pubkey| event_matches_tag_value(event, "p", seller_pubkey))
   2546 }
   2547 
   2548 fn order_status_record_from_event(
   2549     event: &RadrootsNostrEvent,
   2550 ) -> Result<OrderStatusRecord, RuntimeError> {
   2551     match event_kind_u32(event) {
   2552         KIND_ORDER_REQUEST => {
   2553             let event = radroots_event_from_nostr(event);
   2554             let event_id = protocol_event_id(event.id.as_str(), "request_event_id")?;
   2555             let author_pubkey = protocol_pubkey(event.author.as_str(), "request_author_pubkey")?;
   2556             let envelope =
   2557                 order_envelope_from_event::<RadrootsOrderRequest>(&event).map_err(|error| {
   2558                     RuntimeError::Config(format!("decode active order request event: {error}"))
   2559                 })?;
   2560             if envelope.message_type != RadrootsOrderEventType::OrderRequested {
   2561                 return Err(RuntimeError::Config(
   2562                     "active order request event used the wrong message type".to_owned(),
   2563                 ));
   2564             }
   2565             let context =
   2566                 order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags)
   2567                     .map_err(|error| {
   2568                         RuntimeError::Config(format!("decode active order request tags: {error}"))
   2569                     })?;
   2570             if context.counterparty_pubkey != envelope.payload.seller_pubkey {
   2571                 return Err(RuntimeError::Config(
   2572                     "active order request p tag does not match seller_pubkey".to_owned(),
   2573                 ));
   2574             }
   2575             let listing_addr =
   2576                 parse_listing_addr(envelope.payload.listing_addr.as_str()).map_err(|error| {
   2577                     RuntimeError::Config(format!(
   2578                         "active order request listing_addr is invalid: {error}"
   2579                     ))
   2580                 })?;
   2581             if listing_addr.seller_pubkey != envelope.payload.seller_pubkey.to_string() {
   2582                 return Err(RuntimeError::Config(
   2583                     "active order request listing_addr is outside seller authority".to_owned(),
   2584                 ));
   2585             }
   2586             Ok(OrderStatusRecord::Request {
   2587                 listing_event_id: context.listing_event.as_ref().map(|event| event.id.clone()),
   2588                 record: RadrootsOrderRequestRecord {
   2589                     event_id,
   2590                     author_pubkey,
   2591                     payload: envelope.payload,
   2592                 },
   2593             })
   2594         }
   2595         KIND_ORDER_DECISION => {
   2596             let event = radroots_event_from_nostr(event);
   2597             let event_id = protocol_event_id(event.id.as_str(), "decision_event_id")?;
   2598             let author_pubkey = protocol_pubkey(event.author.as_str(), "decision_author_pubkey")?;
   2599             let envelope =
   2600                 order_envelope_from_event::<RadrootsOrderDecision>(&event).map_err(|error| {
   2601                     RuntimeError::Config(format!("decode active order decision event: {error}"))
   2602                 })?;
   2603             if envelope.message_type != RadrootsOrderEventType::OrderDecision {
   2604                 return Err(RuntimeError::Config(
   2605                     "active order decision event used the wrong message type".to_owned(),
   2606                 ));
   2607             }
   2608             let context =
   2609                 order_event_context_from_tags(RadrootsOrderEventType::OrderDecision, &event.tags)
   2610                     .map_err(|error| {
   2611                     RuntimeError::Config(format!("decode active order decision tags: {error}"))
   2612                 })?;
   2613             Ok(OrderStatusRecord::Decision(RadrootsOrderDecisionRecord {
   2614                 event_id,
   2615                 author_pubkey,
   2616                 counterparty_pubkey: context.counterparty_pubkey,
   2617                 root_event_id: required_order_context_event_id(
   2618                     context.root_event_id,
   2619                     "e_root",
   2620                     "active order decision",
   2621                 )?,
   2622                 prev_event_id: required_order_context_event_id(
   2623                     context.prev_event_id,
   2624                     "e_prev",
   2625                     "active order decision",
   2626                 )?,
   2627                 payload: envelope.payload,
   2628             }))
   2629         }
   2630         KIND_ORDER_REVISION_PROPOSAL => {
   2631             let event = radroots_event_from_nostr(event);
   2632             let event_id = protocol_event_id(event.id.as_str(), "revision_event_id")?;
   2633             let author_pubkey = protocol_pubkey(event.author.as_str(), "revision_author_pubkey")?;
   2634             let envelope = order_revision_proposal_from_event(&event).map_err(|error| {
   2635                 RuntimeError::Config(format!(
   2636                     "decode active order revision proposal event: {error}"
   2637                 ))
   2638             })?;
   2639             let context = order_event_context_from_tags(
   2640                 RadrootsOrderEventType::OrderRevisionProposed,
   2641                 &event.tags,
   2642             )
   2643             .map_err(|error| {
   2644                 RuntimeError::Config(format!(
   2645                     "decode active order revision proposal tags: {error}"
   2646                 ))
   2647             })?;
   2648             Ok(OrderStatusRecord::RevisionProposal(
   2649                 RadrootsOrderRevisionProposalRecord {
   2650                     event_id,
   2651                     author_pubkey,
   2652                     counterparty_pubkey: context.counterparty_pubkey,
   2653                     root_event_id: required_order_context_event_id(
   2654                         context.root_event_id,
   2655                         "e_root",
   2656                         "active order revision proposal",
   2657                     )?,
   2658                     prev_event_id: required_order_context_event_id(
   2659                         context.prev_event_id,
   2660                         "e_prev",
   2661                         "active order revision proposal",
   2662                     )?,
   2663                     payload: envelope.payload,
   2664                 },
   2665             ))
   2666         }
   2667         KIND_ORDER_REVISION_DECISION => {
   2668             let event = radroots_event_from_nostr(event);
   2669             let event_id = protocol_event_id(event.id.as_str(), "revision_decision_event_id")?;
   2670             let author_pubkey =
   2671                 protocol_pubkey(event.author.as_str(), "revision_decision_author_pubkey")?;
   2672             let envelope = order_revision_decision_from_event(&event).map_err(|error| {
   2673                 RuntimeError::Config(format!(
   2674                     "decode active order revision decision event: {error}"
   2675                 ))
   2676             })?;
   2677             let context = order_event_context_from_tags(
   2678                 RadrootsOrderEventType::OrderRevisionDecision,
   2679                 &event.tags,
   2680             )
   2681             .map_err(|error| {
   2682                 RuntimeError::Config(format!(
   2683                     "decode active order revision decision tags: {error}"
   2684                 ))
   2685             })?;
   2686             Ok(OrderStatusRecord::RevisionDecision(
   2687                 RadrootsOrderRevisionDecisionRecord {
   2688                     event_id,
   2689                     author_pubkey,
   2690                     counterparty_pubkey: context.counterparty_pubkey,
   2691                     root_event_id: required_order_context_event_id(
   2692                         context.root_event_id,
   2693                         "e_root",
   2694                         "active order revision decision",
   2695                     )?,
   2696                     prev_event_id: required_order_context_event_id(
   2697                         context.prev_event_id,
   2698                         "e_prev",
   2699                         "active order revision decision",
   2700                     )?,
   2701                     payload: envelope.payload,
   2702                 },
   2703             ))
   2704         }
   2705         KIND_ORDER_CANCELLATION => {
   2706             let event = radroots_event_from_nostr(event);
   2707             let event_id = protocol_event_id(event.id.as_str(), "cancellation_event_id")?;
   2708             let author_pubkey =
   2709                 protocol_pubkey(event.author.as_str(), "cancellation_author_pubkey")?;
   2710             let envelope = order_cancellation_from_event(&event).map_err(|error| {
   2711                 RuntimeError::Config(format!("decode active order cancellation event: {error}"))
   2712             })?;
   2713             let context =
   2714                 order_event_context_from_tags(RadrootsOrderEventType::OrderCancelled, &event.tags)
   2715                     .map_err(|error| {
   2716                         RuntimeError::Config(format!(
   2717                             "decode active order cancellation tags: {error}"
   2718                         ))
   2719                     })?;
   2720             Ok(OrderStatusRecord::Cancellation(
   2721                 RadrootsOrderCancellationRecord {
   2722                     event_id,
   2723                     author_pubkey,
   2724                     counterparty_pubkey: context.counterparty_pubkey,
   2725                     root_event_id: required_order_context_event_id(
   2726                         context.root_event_id,
   2727                         "e_root",
   2728                         "active order cancellation",
   2729                     )?,
   2730                     prev_event_id: required_order_context_event_id(
   2731                         context.prev_event_id,
   2732                         "e_prev",
   2733                         "active order cancellation",
   2734                     )?,
   2735                     payload: envelope.payload,
   2736                 },
   2737             ))
   2738         }
   2739         event_kind => Err(RuntimeError::Config(format!(
   2740             "order status received unexpected kind `{event_kind}`"
   2741         ))),
   2742     }
   2743 }
   2744 
   2745 fn order_revision_proposals_from_events(
   2746     order_id: &str,
   2747     events: &[RadrootsNostrEvent],
   2748 ) -> OrderRevisionProposalCandidates {
   2749     let mut records = Vec::new();
   2750     let mut issues = Vec::new();
   2751     for event in events {
   2752         if event_kind_u32(event) != KIND_ORDER_REVISION_PROPOSAL
   2753             || !event_matches_tag_value(event, "d", order_id)
   2754         {
   2755             continue;
   2756         }
   2757         let event_id = event.id.to_string();
   2758         match order_status_record_from_event(event) {
   2759             Ok(OrderStatusRecord::RevisionProposal(record)) => records.push(record),
   2760             Ok(_) => issues.push(issue_with_events(
   2761                 "invalid_revision_candidate",
   2762                 "revision_event_id",
   2763                 format!("revision event `{event_id}` decoded as the wrong active record type"),
   2764                 vec![event_id],
   2765             )),
   2766             Err(error) => issues.push(issue_with_events(
   2767                 "invalid_revision_candidate",
   2768                 "revision_event_id",
   2769                 format!("revision event `{event_id}` failed proposal validation: {error}"),
   2770                 vec![event_id],
   2771             )),
   2772         }
   2773     }
   2774     records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
   2775     issues.sort_by(|left, right| left.event_ids.cmp(&right.event_ids));
   2776     OrderRevisionProposalCandidates { records, issues }
   2777 }
   2778 
   2779 fn active_order_status_state(status: &RadrootsOrderStatus) -> &'static str {
   2780     match status {
   2781         RadrootsOrderStatus::Missing => "missing",
   2782         RadrootsOrderStatus::Requested => "requested",
   2783         RadrootsOrderStatus::Accepted => "accepted",
   2784         RadrootsOrderStatus::Declined => "declined",
   2785         RadrootsOrderStatus::Cancelled => "cancelled",
   2786         RadrootsOrderStatus::Invalid => "invalid",
   2787     }
   2788 }
   2789 
   2790 fn active_order_status_reason(status: &RadrootsOrderStatus, order_id: &str) -> Option<String> {
   2791     match status {
   2792         RadrootsOrderStatus::Missing => {
   2793             Some(format!("no active order events matched `{order_id}`"))
   2794         }
   2795         RadrootsOrderStatus::Invalid => Some(format!(
   2796             "active order events for `{order_id}` failed reducer validation"
   2797         )),
   2798         _ => None,
   2799     }
   2800 }
   2801 
   2802 fn order_status_inventory_view(
   2803     status: &RadrootsOrderStatus,
   2804     listing_event_id: Option<String>,
   2805     decision_event_id: Option<&RadrootsEventId>,
   2806     decisions: &[RadrootsOrderDecisionRecord],
   2807     reducer_issues: &[OrderIssueView],
   2808 ) -> Option<OrderInventoryView> {
   2809     let inventory_issues = reducer_issues
   2810         .iter()
   2811         .filter(|issue| {
   2812             matches!(
   2813                 issue.code.as_str(),
   2814                 "missing_decision_inventory_commitments"
   2815                     | "decision_inventory_commitment_mismatch"
   2816                     | "decision_counterparty_mismatch"
   2817                     | "listing_inventory_arithmetic_overflow"
   2818                     | "unknown_inventory_bin"
   2819                     | "listing_inventory_over_reserved"
   2820                     | "invalid_inventory_order"
   2821             )
   2822         })
   2823         .cloned()
   2824         .collect::<Vec<_>>();
   2825 
   2826     match status {
   2827         RadrootsOrderStatus::Accepted => {
   2828             let bins = decision_event_id
   2829                 .and_then(|event_id| {
   2830                     decisions
   2831                         .iter()
   2832                         .find(|decision| decision.event_id == *event_id)
   2833                 })
   2834                 .map(|decision| inventory_bins_from_decision(&decision.payload.decision))
   2835                 .unwrap_or_default();
   2836             Some(OrderInventoryView {
   2837                 state: if inventory_issues.is_empty() {
   2838                     "reserved".to_owned()
   2839                 } else {
   2840                     "invalid".to_owned()
   2841                 },
   2842                 listing_event_id,
   2843                 commitment_valid: inventory_issues.is_empty(),
   2844                 bins,
   2845                 issues: inventory_issues,
   2846             })
   2847         }
   2848         RadrootsOrderStatus::Cancelled => Some(OrderInventoryView {
   2849             state: if decision_event_id.is_some() {
   2850                 "released".to_owned()
   2851             } else {
   2852                 "not_reserved".to_owned()
   2853             },
   2854             listing_event_id,
   2855             commitment_valid: inventory_issues.is_empty(),
   2856             bins: Vec::new(),
   2857             issues: inventory_issues,
   2858         }),
   2859         RadrootsOrderStatus::Declined => Some(OrderInventoryView {
   2860             state: "not_reserved".to_owned(),
   2861             listing_event_id,
   2862             commitment_valid: true,
   2863             bins: Vec::new(),
   2864             issues: inventory_issues,
   2865         }),
   2866         RadrootsOrderStatus::Invalid if !inventory_issues.is_empty() => Some(OrderInventoryView {
   2867             state: "invalid".to_owned(),
   2868             listing_event_id,
   2869             commitment_valid: false,
   2870             bins: Vec::new(),
   2871             issues: inventory_issues,
   2872         }),
   2873         _ => None,
   2874     }
   2875 }
   2876 
   2877 fn order_status_lifecycle_view(
   2878     status: &RadrootsOrderStatus,
   2879     request_event_id: Option<String>,
   2880     last_event_id: Option<String>,
   2881     cancellation_event_id: Option<String>,
   2882     cancellation_root_event_id: Option<String>,
   2883     cancellation_prev_event_id: Option<String>,
   2884     cancellation_reason: Option<String>,
   2885     reducer_issues: &[OrderIssueView],
   2886 ) -> OrderStatusLifecycleView {
   2887     let phase = order_status_lifecycle_phase(status).to_owned();
   2888     let terminal = matches!(
   2889         status,
   2890         RadrootsOrderStatus::Accepted
   2891             | RadrootsOrderStatus::Cancelled
   2892             | RadrootsOrderStatus::Invalid
   2893     );
   2894     let cancellation =
   2895         cancellation_event_id
   2896             .as_ref()
   2897             .map(|event_id| OrderStatusLifecycleCancellationView {
   2898                 event_id: event_id.clone(),
   2899                 root_event_id: cancellation_root_event_id
   2900                     .clone()
   2901                     .or(request_event_id.clone()),
   2902                 prev_event_id: cancellation_prev_event_id.clone(),
   2903                 reason: cancellation_reason.clone(),
   2904             });
   2905     let event_id = cancellation_event_id;
   2906     let prev_event_id = cancellation_prev_event_id.or(last_event_id);
   2907     OrderStatusLifecycleView {
   2908         phase,
   2909         terminal,
   2910         event_id,
   2911         root_event_id: request_event_id,
   2912         prev_event_id,
   2913         cancellation,
   2914         issues: reducer_issues.to_vec(),
   2915     }
   2916 }
   2917 
   2918 fn order_status_revision_view(
   2919     last_event_id: Option<&RadrootsEventId>,
   2920     agreement_event_id: Option<&RadrootsEventId>,
   2921     proposals: &[RadrootsOrderRevisionProposalRecord],
   2922     decisions: &[RadrootsOrderRevisionDecisionRecord],
   2923 ) -> Option<OrderStatusRevisionView> {
   2924     if let Some(proposal) = last_event_id
   2925         .and_then(|event_id| proposals.iter().find(|record| record.event_id == *event_id))
   2926     {
   2927         return Some(OrderStatusRevisionView {
   2928             state: "pending".to_owned(),
   2929             revision_id: Some(proposal.payload.revision_id.to_string()),
   2930             proposal_event_id: Some(proposal.event_id.to_string()),
   2931             decision_event_id: None,
   2932             root_event_id: Some(proposal.root_event_id.to_string()),
   2933             prev_event_id: Some(proposal.prev_event_id.to_string()),
   2934             agreement_event_id: None,
   2935             reason: Some(proposal.payload.reason.clone()),
   2936         });
   2937     }
   2938 
   2939     if let Some(decision) = last_event_id
   2940         .and_then(|event_id| decisions.iter().find(|record| record.event_id == *event_id))
   2941     {
   2942         return Some(order_status_revision_view_from_decision(
   2943             decision,
   2944             agreement_event_id,
   2945         ));
   2946     }
   2947 
   2948     agreement_event_id
   2949         .and_then(|event_id| decisions.iter().find(|record| record.event_id == *event_id))
   2950         .map(|decision| order_status_revision_view_from_decision(decision, agreement_event_id))
   2951 }
   2952 
   2953 fn order_status_revision_view_from_decision(
   2954     decision: &RadrootsOrderRevisionDecisionRecord,
   2955     agreement_event_id: Option<&RadrootsEventId>,
   2956 ) -> OrderStatusRevisionView {
   2957     let (state, reason) = match &decision.payload.decision {
   2958         RadrootsOrderRevisionOutcome::Accepted => ("accepted", None),
   2959         RadrootsOrderRevisionOutcome::Declined { reason } => ("declined", Some(reason.clone())),
   2960     };
   2961     OrderStatusRevisionView {
   2962         state: state.to_owned(),
   2963         revision_id: Some(decision.payload.revision_id.to_string()),
   2964         proposal_event_id: Some(decision.prev_event_id.to_string()),
   2965         decision_event_id: Some(decision.event_id.to_string()),
   2966         root_event_id: Some(decision.root_event_id.to_string()),
   2967         prev_event_id: Some(decision.prev_event_id.to_string()),
   2968         agreement_event_id: agreement_event_id.map(ToString::to_string),
   2969         reason,
   2970     }
   2971 }
   2972 
   2973 fn order_status_lifecycle_phase(status: &RadrootsOrderStatus) -> &'static str {
   2974     match status {
   2975         RadrootsOrderStatus::Missing => "missing",
   2976         RadrootsOrderStatus::Requested => "requested",
   2977         RadrootsOrderStatus::Accepted => "accepted",
   2978         RadrootsOrderStatus::Declined => "declined",
   2979         RadrootsOrderStatus::Cancelled => "cancelled",
   2980         RadrootsOrderStatus::Invalid => "invalid",
   2981     }
   2982 }
   2983 
   2984 fn inventory_bins_from_decision(
   2985     decision: &RadrootsOrderDecisionOutcome,
   2986 ) -> Vec<OrderInventoryBinView> {
   2987     match decision {
   2988         RadrootsOrderDecisionOutcome::Accepted {
   2989             inventory_commitments,
   2990         } => {
   2991             let mut bins = inventory_commitments
   2992                 .iter()
   2993                 .map(|commitment| OrderInventoryBinView {
   2994                     bin_id: commitment.bin_id.to_string(),
   2995                     committed_count: u64::from(commitment.bin_count),
   2996                     available_count: None,
   2997                     remaining_count: None,
   2998                     over_reserved: false,
   2999                 })
   3000                 .collect::<Vec<_>>();
   3001             bins.sort_by(|left, right| left.bin_id.cmp(&right.bin_id));
   3002             bins
   3003         }
   3004         RadrootsOrderDecisionOutcome::Declined { .. } => Vec::new(),
   3005     }
   3006 }
   3007 
   3008 fn active_order_reducer_issue_view(issue_value: RadrootsOrderIssue) -> OrderIssueView {
   3009     match issue_value {
   3010         RadrootsOrderIssue::MissingRequest => issue_with_code(
   3011             "missing_request",
   3012             "request_event_id",
   3013             "active order reducer reported missing request",
   3014         ),
   3015         RadrootsOrderIssue::MultipleRequests { event_ids } => issue_with_events(
   3016             "multiple_requests",
   3017             "request_event_id",
   3018             "active order reducer reported multiple request events",
   3019             event_ids,
   3020         ),
   3021         RadrootsOrderIssue::RequestPayloadInvalid { event_id } => issue_with_events(
   3022             "invalid_request_payload",
   3023             "request_payload",
   3024             "active order reducer reported invalid request payload",
   3025             vec![event_id],
   3026         ),
   3027         RadrootsOrderIssue::RequestOrderIdMismatch { event_id } => issue_with_events(
   3028             "request_order_id_mismatch",
   3029             "order_id",
   3030             "active order reducer reported request order id mismatch",
   3031             vec![event_id],
   3032         ),
   3033         RadrootsOrderIssue::RequestAuthorMismatch { event_id } => issue_with_events(
   3034             "request_author_mismatch",
   3035             "buyer_pubkey",
   3036             "active order reducer reported request author mismatch",
   3037             vec![event_id],
   3038         ),
   3039         RadrootsOrderIssue::RequestListingAddressInvalid { event_id } => issue_with_events(
   3040             "invalid_request_listing_address",
   3041             "listing_addr",
   3042             "active order reducer reported invalid request listing address",
   3043             vec![event_id],
   3044         ),
   3045         RadrootsOrderIssue::RequestSellerListingMismatch { event_id } => issue_with_events(
   3046             "request_seller_listing_mismatch",
   3047             "seller_pubkey",
   3048             "active order reducer reported request seller/listing mismatch",
   3049             vec![event_id],
   3050         ),
   3051         RadrootsOrderIssue::DecisionPayloadInvalid { event_id } => issue_with_events(
   3052             "invalid_decision_payload",
   3053             "decision_payload",
   3054             "active order reducer reported invalid decision payload",
   3055             vec![event_id],
   3056         ),
   3057         RadrootsOrderIssue::DecisionOrderIdMismatch { event_id } => issue_with_events(
   3058             "decision_order_id_mismatch",
   3059             "order_id",
   3060             "active order reducer reported decision order id mismatch",
   3061             vec![event_id],
   3062         ),
   3063         RadrootsOrderIssue::DecisionAuthorMismatch { event_id } => issue_with_events(
   3064             "decision_author_mismatch",
   3065             "seller_pubkey",
   3066             "active order reducer reported decision author mismatch",
   3067             vec![event_id],
   3068         ),
   3069         RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id } => issue_with_events(
   3070             "decision_counterparty_mismatch",
   3071             "buyer_pubkey",
   3072             "active order reducer reported decision counterparty mismatch",
   3073             vec![event_id],
   3074         ),
   3075         RadrootsOrderIssue::DecisionBuyerMismatch { event_id } => issue_with_events(
   3076             "decision_buyer_mismatch",
   3077             "buyer_pubkey",
   3078             "active order reducer reported decision buyer mismatch",
   3079             vec![event_id],
   3080         ),
   3081         RadrootsOrderIssue::DecisionSellerMismatch { event_id } => issue_with_events(
   3082             "decision_seller_mismatch",
   3083             "seller_pubkey",
   3084             "active order reducer reported decision seller mismatch",
   3085             vec![event_id],
   3086         ),
   3087         RadrootsOrderIssue::DecisionListingAddressInvalid { event_id } => issue_with_events(
   3088             "invalid_decision_listing_address",
   3089             "listing_addr",
   3090             "active order reducer reported invalid decision listing address",
   3091             vec![event_id],
   3092         ),
   3093         RadrootsOrderIssue::DecisionListingMismatch { event_id } => issue_with_events(
   3094             "decision_listing_mismatch",
   3095             "listing_addr",
   3096             "active order reducer reported decision listing mismatch",
   3097             vec![event_id],
   3098         ),
   3099         RadrootsOrderIssue::DecisionRootMismatch { event_id } => issue_with_events(
   3100             "decision_root_mismatch",
   3101             "root_event_id",
   3102             "active order reducer reported decision root mismatch",
   3103             vec![event_id],
   3104         ),
   3105         RadrootsOrderIssue::DecisionPreviousMismatch { event_id } => issue_with_events(
   3106             "decision_previous_mismatch",
   3107             "prev_event_id",
   3108             "active order reducer reported decision previous mismatch",
   3109             vec![event_id],
   3110         ),
   3111         RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id } => issue_with_events(
   3112             "missing_decision_inventory_commitments",
   3113             "inventory_commitments",
   3114             "active order reducer reported missing decision inventory commitments",
   3115             vec![event_id],
   3116         ),
   3117         RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id } => issue_with_events(
   3118             "decision_inventory_commitment_mismatch",
   3119             "inventory_commitments",
   3120             "active order reducer reported decision inventory commitment mismatch",
   3121             vec![event_id],
   3122         ),
   3123         RadrootsOrderIssue::DecisionMissingReason { event_id } => issue_with_events(
   3124             "missing_decision_decline_reason",
   3125             "reason",
   3126             "active order reducer reported missing decision decline reason",
   3127             vec![event_id],
   3128         ),
   3129         RadrootsOrderIssue::ConflictingDecisions { event_ids } => issue_with_events(
   3130             "conflicting_decisions",
   3131             "decision_event_id",
   3132             "active order reducer reported conflicting decisions",
   3133             event_ids,
   3134         ),
   3135         RadrootsOrderIssue::RevisionProposalPayloadInvalid { event_id } => issue_with_events(
   3136             "invalid_revision_proposal_payload",
   3137             "revision_payload",
   3138             "active order reducer reported invalid revision proposal payload",
   3139             vec![event_id],
   3140         ),
   3141         RadrootsOrderIssue::RevisionProposalOrderIdMismatch { event_id } => issue_with_events(
   3142             "revision_proposal_order_id_mismatch",
   3143             "order_id",
   3144             "active order reducer reported revision proposal order id mismatch",
   3145             vec![event_id],
   3146         ),
   3147         RadrootsOrderIssue::RevisionProposalAuthorMismatch { event_id } => issue_with_events(
   3148             "revision_proposal_author_mismatch",
   3149             "seller_pubkey",
   3150             "active order reducer reported revision proposal author mismatch",
   3151             vec![event_id],
   3152         ),
   3153         RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { event_id } => issue_with_events(
   3154             "revision_proposal_counterparty_mismatch",
   3155             "buyer_pubkey",
   3156             "active order reducer reported revision proposal counterparty mismatch",
   3157             vec![event_id],
   3158         ),
   3159         RadrootsOrderIssue::RevisionProposalBuyerMismatch { event_id } => issue_with_events(
   3160             "revision_proposal_buyer_mismatch",
   3161             "buyer_pubkey",
   3162             "active order reducer reported revision proposal buyer mismatch",
   3163             vec![event_id],
   3164         ),
   3165         RadrootsOrderIssue::RevisionProposalSellerMismatch { event_id } => issue_with_events(
   3166             "revision_proposal_seller_mismatch",
   3167             "seller_pubkey",
   3168             "active order reducer reported revision proposal seller mismatch",
   3169             vec![event_id],
   3170         ),
   3171         RadrootsOrderIssue::RevisionProposalListingAddressInvalid { event_id } => {
   3172             issue_with_events(
   3173                 "invalid_revision_proposal_listing_address",
   3174                 "listing_addr",
   3175                 "active order reducer reported invalid revision proposal listing address",
   3176                 vec![event_id],
   3177             )
   3178         }
   3179         RadrootsOrderIssue::RevisionProposalListingMismatch { event_id } => issue_with_events(
   3180             "revision_proposal_listing_mismatch",
   3181             "listing_addr",
   3182             "active order reducer reported revision proposal listing mismatch",
   3183             vec![event_id],
   3184         ),
   3185         RadrootsOrderIssue::RevisionProposalRootMismatch { event_id } => issue_with_events(
   3186             "revision_proposal_root_mismatch",
   3187             "root_event_id",
   3188             "active order reducer reported revision proposal root mismatch",
   3189             vec![event_id],
   3190         ),
   3191         RadrootsOrderIssue::RevisionProposalPreviousMismatch { event_id } => issue_with_events(
   3192             "revision_proposal_previous_mismatch",
   3193             "prev_event_id",
   3194             "active order reducer reported revision proposal previous mismatch",
   3195             vec![event_id],
   3196         ),
   3197         RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id } => issue_with_events(
   3198             "revision_decision_without_proposal",
   3199             "revision_decision_event_id",
   3200             "active order reducer reported revision decision without proposal",
   3201             vec![event_id],
   3202         ),
   3203         RadrootsOrderIssue::RevisionDecisionPayloadInvalid { event_id } => issue_with_events(
   3204             "invalid_revision_decision_payload",
   3205             "revision_decision_payload",
   3206             "active order reducer reported invalid revision decision payload",
   3207             vec![event_id],
   3208         ),
   3209         RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { event_id } => issue_with_events(
   3210             "revision_decision_order_id_mismatch",
   3211             "order_id",
   3212             "active order reducer reported revision decision order id mismatch",
   3213             vec![event_id],
   3214         ),
   3215         RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id } => issue_with_events(
   3216             "revision_decision_author_mismatch",
   3217             "buyer_pubkey",
   3218             "active order reducer reported revision decision author mismatch",
   3219             vec![event_id],
   3220         ),
   3221         RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { event_id } => issue_with_events(
   3222             "revision_decision_counterparty_mismatch",
   3223             "seller_pubkey",
   3224             "active order reducer reported revision decision counterparty mismatch",
   3225             vec![event_id],
   3226         ),
   3227         RadrootsOrderIssue::RevisionDecisionBuyerMismatch { event_id } => issue_with_events(
   3228             "revision_decision_buyer_mismatch",
   3229             "buyer_pubkey",
   3230             "active order reducer reported revision decision buyer mismatch",
   3231             vec![event_id],
   3232         ),
   3233         RadrootsOrderIssue::RevisionDecisionSellerMismatch { event_id } => issue_with_events(
   3234             "revision_decision_seller_mismatch",
   3235             "seller_pubkey",
   3236             "active order reducer reported revision decision seller mismatch",
   3237             vec![event_id],
   3238         ),
   3239         RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { event_id } => {
   3240             issue_with_events(
   3241                 "invalid_revision_decision_listing_address",
   3242                 "listing_addr",
   3243                 "active order reducer reported invalid revision decision listing address",
   3244                 vec![event_id],
   3245             )
   3246         }
   3247         RadrootsOrderIssue::RevisionDecisionListingMismatch { event_id } => issue_with_events(
   3248             "revision_decision_listing_mismatch",
   3249             "listing_addr",
   3250             "active order reducer reported revision decision listing mismatch",
   3251             vec![event_id],
   3252         ),
   3253         RadrootsOrderIssue::RevisionDecisionRootMismatch { event_id } => issue_with_events(
   3254             "revision_decision_root_mismatch",
   3255             "root_event_id",
   3256             "active order reducer reported revision decision root mismatch",
   3257             vec![event_id],
   3258         ),
   3259         RadrootsOrderIssue::RevisionDecisionPreviousMismatch { event_id } => issue_with_events(
   3260             "revision_decision_previous_mismatch",
   3261             "prev_event_id",
   3262             "active order reducer reported revision decision previous mismatch",
   3263             vec![event_id],
   3264         ),
   3265         RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { event_id } => issue_with_events(
   3266             "revision_decision_revision_id_mismatch",
   3267             "revision_id",
   3268             "active order reducer reported revision decision revision id mismatch",
   3269             vec![event_id],
   3270         ),
   3271         RadrootsOrderIssue::CancellationWithoutCancellableOrder { event_id } => issue_with_events(
   3272             "cancellation_without_cancellable_order",
   3273             "cancellation_event_id",
   3274             "active order reducer reported cancellation without cancellable order",
   3275             vec![event_id],
   3276         ),
   3277         RadrootsOrderIssue::CancellationPayloadInvalid { event_id } => issue_with_events(
   3278             "invalid_cancellation_payload",
   3279             "cancellation_payload",
   3280             "active order reducer reported invalid cancellation payload",
   3281             vec![event_id],
   3282         ),
   3283         RadrootsOrderIssue::CancellationOrderIdMismatch { event_id } => issue_with_events(
   3284             "cancellation_order_id_mismatch",
   3285             "order_id",
   3286             "active order reducer reported cancellation order id mismatch",
   3287             vec![event_id],
   3288         ),
   3289         RadrootsOrderIssue::CancellationAuthorMismatch { event_id } => issue_with_events(
   3290             "cancellation_author_mismatch",
   3291             "buyer_pubkey",
   3292             "active order reducer reported cancellation author mismatch",
   3293             vec![event_id],
   3294         ),
   3295         RadrootsOrderIssue::CancellationCounterpartyMismatch { event_id } => issue_with_events(
   3296             "cancellation_counterparty_mismatch",
   3297             "seller_pubkey",
   3298             "active order reducer reported cancellation counterparty mismatch",
   3299             vec![event_id],
   3300         ),
   3301         RadrootsOrderIssue::CancellationBuyerMismatch { event_id } => issue_with_events(
   3302             "cancellation_buyer_mismatch",
   3303             "buyer_pubkey",
   3304             "active order reducer reported cancellation buyer mismatch",
   3305             vec![event_id],
   3306         ),
   3307         RadrootsOrderIssue::CancellationSellerMismatch { event_id } => issue_with_events(
   3308             "cancellation_seller_mismatch",
   3309             "seller_pubkey",
   3310             "active order reducer reported cancellation seller mismatch",
   3311             vec![event_id],
   3312         ),
   3313         RadrootsOrderIssue::CancellationListingAddressInvalid { event_id } => issue_with_events(
   3314             "invalid_cancellation_listing_address",
   3315             "listing_addr",
   3316             "active order reducer reported invalid cancellation listing address",
   3317             vec![event_id],
   3318         ),
   3319         RadrootsOrderIssue::CancellationListingMismatch { event_id } => issue_with_events(
   3320             "cancellation_listing_mismatch",
   3321             "listing_addr",
   3322             "active order reducer reported cancellation listing mismatch",
   3323             vec![event_id],
   3324         ),
   3325         RadrootsOrderIssue::CancellationRootMismatch { event_id } => issue_with_events(
   3326             "cancellation_root_mismatch",
   3327             "root_event_id",
   3328             "active order reducer reported cancellation root mismatch",
   3329             vec![event_id],
   3330         ),
   3331         RadrootsOrderIssue::CancellationPreviousMismatch { event_id } => issue_with_events(
   3332             "cancellation_previous_mismatch",
   3333             "prev_event_id",
   3334             "active order reducer reported cancellation previous mismatch",
   3335             vec![event_id],
   3336         ),
   3337         RadrootsOrderIssue::ForkedLifecycle { event_ids } => issue_with_events(
   3338             "forked_lifecycle",
   3339             "event_id",
   3340             "active order reducer reported forked lifecycle events",
   3341             event_ids,
   3342         ),
   3343     }
   3344 }
   3345 
   3346 fn order_event_list_unconfigured(
   3347     seller_pubkey: Option<String>,
   3348     actor_context_source: &'static str,
   3349     reason: String,
   3350     target_relays: Vec<String>,
   3351     actions: Vec<String>,
   3352 ) -> OrderEventListView {
   3353     OrderEventListView {
   3354         state: "unconfigured".to_owned(),
   3355         source: ORDER_EVENT_LIST_SOURCE.to_owned(),
   3356         actor_context_source: actor_context_source.to_owned(),
   3357         seller_pubkey,
   3358         target_relays,
   3359         connected_relays: Vec::new(),
   3360         failed_relays: Vec::new(),
   3361         fetched_count: 0,
   3362         decoded_count: 0,
   3363         skipped_count: 0,
   3364         count: 0,
   3365         reason: Some(reason),
   3366         orders: Vec::new(),
   3367         actions,
   3368     }
   3369 }
   3370 
   3371 fn order_event_list_unavailable(
   3372     seller_pubkey: String,
   3373     actor_context_source: &'static str,
   3374     reason: String,
   3375     target_relays: Vec<String>,
   3376     failed_relays: Vec<DirectRelayFailure>,
   3377 ) -> OrderEventListView {
   3378     OrderEventListView {
   3379         state: "unavailable".to_owned(),
   3380         source: ORDER_EVENT_LIST_SOURCE.to_owned(),
   3381         actor_context_source: actor_context_source.to_owned(),
   3382         seller_pubkey: Some(seller_pubkey),
   3383         target_relays,
   3384         connected_relays: Vec::new(),
   3385         failed_relays: relay_failures(failed_relays),
   3386         fetched_count: 0,
   3387         decoded_count: 0,
   3388         skipped_count: 0,
   3389         count: 0,
   3390         reason: Some(format!("direct relay connection failed: {reason}")),
   3391         orders: Vec::new(),
   3392         actions: Vec::new(),
   3393     }
   3394 }
   3395 
   3396 fn order_event_list_from_receipt(
   3397     seller_pubkey: String,
   3398     order_id: Option<&str>,
   3399     actor_context_source: &'static str,
   3400     receipt: DirectRelayFetchReceipt,
   3401 ) -> OrderEventListView {
   3402     let DirectRelayFetchReceipt {
   3403         target_relays,
   3404         connected_relays,
   3405         failed_relays,
   3406         events,
   3407     } = receipt;
   3408     let fetched_count = events.len();
   3409     let mut skipped_count = 0usize;
   3410     let mut decoded_count = 0usize;
   3411     let mut orders = Vec::new();
   3412 
   3413     for event in events {
   3414         match order_event_list_entry_from_event(&event, seller_pubkey.as_str()) {
   3415             Ok(entry) => {
   3416                 decoded_count += 1;
   3417                 if order_id.is_none_or(|order_id| entry.id == order_id) {
   3418                     orders.push(entry);
   3419                 }
   3420             }
   3421             Err(_) => skipped_count += 1,
   3422         }
   3423     }
   3424 
   3425     orders.sort_by(|left, right| {
   3426         right
   3427             .updated_at_unix
   3428             .cmp(&left.updated_at_unix)
   3429             .then_with(|| left.id.cmp(&right.id))
   3430     });
   3431 
   3432     let reason = if orders.is_empty() {
   3433         Some(match order_id {
   3434             Some(order_id) => {
   3435                 format!("no relay-backed order request events matched `{order_id}`")
   3436             }
   3437             None => "no relay-backed order request events matched the selected seller".to_owned(),
   3438         })
   3439     } else {
   3440         None
   3441     };
   3442 
   3443     OrderEventListView {
   3444         state: if orders.is_empty() { "empty" } else { "ready" }.to_owned(),
   3445         source: ORDER_EVENT_LIST_SOURCE.to_owned(),
   3446         actor_context_source: actor_context_source.to_owned(),
   3447         seller_pubkey: Some(seller_pubkey),
   3448         target_relays,
   3449         connected_relays,
   3450         failed_relays: relay_failures(failed_relays),
   3451         fetched_count,
   3452         decoded_count,
   3453         skipped_count,
   3454         count: orders.len(),
   3455         reason,
   3456         orders,
   3457         actions: Vec::new(),
   3458     }
   3459 }
   3460 
   3461 fn order_decision_base_view(
   3462     config: &RuntimeConfig,
   3463     args: &OrderDecisionArgs,
   3464     state: &str,
   3465     dry_run: bool,
   3466 ) -> OrderDecisionView {
   3467     OrderDecisionView {
   3468         state: state.to_owned(),
   3469         source: ORDER_DECISION_SOURCE.to_owned(),
   3470         order_id: args.key.clone(),
   3471         listing_addr: None,
   3472         buyer_pubkey: None,
   3473         seller_pubkey: None,
   3474         decision: args.decision.as_str().to_owned(),
   3475         request_event_id: None,
   3476         listing_event_id: None,
   3477         root_event_id: None,
   3478         prev_event_id: None,
   3479         event_id: None,
   3480         event_kind: None,
   3481         inventory: None,
   3482         dry_run,
   3483         target_relays: config.relay.urls.clone(),
   3484         connected_relays: Vec::new(),
   3485         acknowledged_relays: Vec::new(),
   3486         failed_relays: Vec::new(),
   3487         fetched_count: 0,
   3488         decoded_count: 0,
   3489         skipped_count: 0,
   3490         idempotency_key: args.idempotency_key.clone(),
   3491         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   3492         reason: None,
   3493         issues: Vec::new(),
   3494         actions: Vec::new(),
   3495     }
   3496 }
   3497 
   3498 fn order_revision_base_view(
   3499     config: &RuntimeConfig,
   3500     args: &OrderRevisionProposeArgs,
   3501     state: &str,
   3502     dry_run: bool,
   3503 ) -> OrderRevisionProposalView {
   3504     OrderRevisionProposalView {
   3505         state: state.to_owned(),
   3506         source: ORDER_REVISION_PROPOSAL_SOURCE.to_owned(),
   3507         order_id: args.key.clone(),
   3508         revision_id: None,
   3509         listing_addr: None,
   3510         buyer_pubkey: None,
   3511         seller_pubkey: None,
   3512         request_event_id: None,
   3513         decision_event_id: None,
   3514         root_event_id: None,
   3515         prev_event_id: None,
   3516         event_id: None,
   3517         event_kind: None,
   3518         items: Vec::new(),
   3519         economics: None,
   3520         inventory: None,
   3521         dry_run,
   3522         target_relays: config.relay.urls.clone(),
   3523         connected_relays: Vec::new(),
   3524         acknowledged_relays: Vec::new(),
   3525         failed_relays: Vec::new(),
   3526         fetched_count: 0,
   3527         decoded_count: 0,
   3528         skipped_count: 0,
   3529         idempotency_key: args.idempotency_key.clone(),
   3530         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   3531         reason: None,
   3532         issues: Vec::new(),
   3533         actions: Vec::new(),
   3534     }
   3535 }
   3536 
   3537 fn order_revision_decision_base_view(
   3538     config: &RuntimeConfig,
   3539     args: &OrderRevisionDecisionArgs,
   3540     state: &str,
   3541     dry_run: bool,
   3542 ) -> OrderRevisionDecisionView {
   3543     OrderRevisionDecisionView {
   3544         state: state.to_owned(),
   3545         source: ORDER_REVISION_DECISION_SOURCE.to_owned(),
   3546         order_id: args.key.clone(),
   3547         revision_id: Some(args.revision_id.trim().to_owned()).filter(|value| !value.is_empty()),
   3548         decision: Some(args.decision.as_str().to_owned()),
   3549         listing_addr: None,
   3550         buyer_pubkey: None,
   3551         seller_pubkey: None,
   3552         request_event_id: None,
   3553         decision_event_id: None,
   3554         agreement_event_id: None,
   3555         root_event_id: None,
   3556         prev_event_id: None,
   3557         event_id: None,
   3558         event_kind: None,
   3559         economics: None,
   3560         inventory: None,
   3561         dry_run,
   3562         target_relays: config.relay.urls.clone(),
   3563         connected_relays: Vec::new(),
   3564         acknowledged_relays: Vec::new(),
   3565         failed_relays: Vec::new(),
   3566         fetched_count: 0,
   3567         decoded_count: 0,
   3568         skipped_count: 0,
   3569         idempotency_key: args.idempotency_key.clone(),
   3570         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   3571         reason: args.reason.as_ref().map(|reason| reason.trim().to_owned()),
   3572         issues: Vec::new(),
   3573         actions: Vec::new(),
   3574     }
   3575 }
   3576 
   3577 fn order_cancellation_base_view(
   3578     config: &RuntimeConfig,
   3579     args: &OrderCancelArgs,
   3580     state: &str,
   3581     dry_run: bool,
   3582 ) -> OrderCancellationView {
   3583     OrderCancellationView {
   3584         state: state.to_owned(),
   3585         source: ORDER_CANCELLATION_SOURCE.to_owned(),
   3586         order_id: args.key.clone(),
   3587         listing_addr: None,
   3588         buyer_pubkey: None,
   3589         seller_pubkey: None,
   3590         request_event_id: None,
   3591         decision_event_id: None,
   3592         root_event_id: None,
   3593         prev_event_id: None,
   3594         event_id: None,
   3595         event_kind: None,
   3596         cancellation_reason: Some(args.reason.clone()),
   3597         dry_run,
   3598         target_relays: config.relay.urls.clone(),
   3599         connected_relays: Vec::new(),
   3600         acknowledged_relays: Vec::new(),
   3601         failed_relays: Vec::new(),
   3602         fetched_count: 0,
   3603         decoded_count: 0,
   3604         skipped_count: 0,
   3605         idempotency_key: args.idempotency_key.clone(),
   3606         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   3607         reason: None,
   3608         issues: Vec::new(),
   3609         actions: Vec::new(),
   3610     }
   3611 }
   3612 
   3613 fn apply_order_cancellation_status(view: &mut OrderCancellationView, status: &OrderStatusView) {
   3614     view.order_id = status.order_id.clone();
   3615     view.listing_addr = status.listing_addr.clone();
   3616     view.buyer_pubkey = status.buyer_pubkey.clone();
   3617     view.seller_pubkey = status.seller_pubkey.clone();
   3618     view.request_event_id = status.request_event_id.clone();
   3619     view.decision_event_id = status.decision_event_id.clone();
   3620     view.root_event_id = status.request_event_id.clone();
   3621     view.prev_event_id = order_cancellation_prev_event_id(status);
   3622     view.target_relays = status.target_relays.clone();
   3623     view.connected_relays = status.connected_relays.clone();
   3624     view.failed_relays = status.failed_relays.clone();
   3625     view.fetched_count = status.fetched_count;
   3626     view.decoded_count = status.decoded_count;
   3627     view.skipped_count = status.skipped_count;
   3628     view.issues = status.reducer_issues.clone();
   3629 }
   3630 
   3631 fn order_cancellation_prev_event_id(status: &OrderStatusView) -> Option<String> {
   3632     match status.state.as_str() {
   3633         "requested" => status.request_event_id.clone(),
   3634         "accepted" => status
   3635             .last_event_id
   3636             .clone()
   3637             .or(status.decision_event_id.clone()),
   3638         _ => status.last_event_id.clone(),
   3639     }
   3640 }
   3641 
   3642 fn order_cancellation_preflight_view_from_status(
   3643     config: &RuntimeConfig,
   3644     args: &OrderCancelArgs,
   3645     status: &OrderStatusView,
   3646     selected_pubkey: &str,
   3647 ) -> Option<OrderCancellationView> {
   3648     let buyer_matches = status
   3649         .buyer_pubkey
   3650         .as_deref()
   3651         .is_some_and(|buyer| buyer.eq_ignore_ascii_case(selected_pubkey));
   3652     let state = match status.state.as_str() {
   3653         "requested" if buyer_matches => return None,
   3654         "accepted" if buyer_matches => "finalized",
   3655         "cancelled" => "terminal",
   3656         "missing" | "declined" | "invalid" | "unavailable" | "unconfigured" => {
   3657             status.state.as_str()
   3658         }
   3659         _ => "invalid",
   3660     };
   3661     let mut view = order_cancellation_base_view(config, args, state, config.output.dry_run);
   3662     apply_order_cancellation_status(&mut view, status);
   3663     if status.state == "cancelled" {
   3664         view.event_id = status
   3665             .lifecycle
   3666             .as_ref()
   3667             .and_then(|lifecycle| lifecycle.event_id.clone());
   3668         view.event_kind = Some(KIND_ORDER_CANCELLATION);
   3669     }
   3670     view.reason = Some(match state {
   3671         "missing" => format!("no active order events matched `{}`", args.key),
   3672         "declined" => format!(
   3673             "order cancel refused because order `{}` was declined",
   3674             args.key
   3675         ),
   3676         "terminal" => {
   3677             format!(
   3678                 "order cancel refused because order `{}` is already terminal",
   3679                 args.key
   3680             )
   3681         }
   3682         "finalized" => format!(
   3683             "order cancel refused because order `{}` already has an accepted agreement",
   3684             args.key
   3685         ),
   3686         "invalid" if !buyer_matches && status.buyer_pubkey.is_some() => format!(
   3687             "order cancel refused because selected account is not buyer for order `{}`",
   3688             args.key
   3689         ),
   3690         "invalid" => status.reason.clone().unwrap_or_else(|| {
   3691             format!(
   3692                 "order cancel refused because active order events for `{}` are invalid",
   3693                 args.key
   3694             )
   3695         }),
   3696         _ => status.reason.clone().unwrap_or_else(|| {
   3697             format!(
   3698                 "order cancel status preflight failed with state `{}`",
   3699                 status.state
   3700             )
   3701         }),
   3702     });
   3703     view.actions = vec![format!("radroots order status get {}", args.key)];
   3704     Some(view)
   3705 }
   3706 
   3707 fn order_decision_view_from_resolution(
   3708     config: &RuntimeConfig,
   3709     args: &OrderDecisionArgs,
   3710     seller_pubkey: String,
   3711     resolution: SellerOrderRequestResolution,
   3712 ) -> OrderDecisionView {
   3713     let SellerOrderRequestResolution {
   3714         target_relays,
   3715         connected_relays,
   3716         failed_relays,
   3717         fetched_count,
   3718         decoded_count,
   3719         skipped_count,
   3720         requests,
   3721         candidate_issues,
   3722     } = resolution;
   3723     let mut view = order_decision_base_view(config, args, "missing", config.output.dry_run);
   3724     view.seller_pubkey = Some(seller_pubkey);
   3725     view.target_relays = target_relays;
   3726     view.connected_relays = connected_relays;
   3727     view.failed_relays = relay_failures(failed_relays);
   3728     view.fetched_count = fetched_count;
   3729     view.decoded_count = decoded_count;
   3730     view.skipped_count = skipped_count;
   3731     view.issues = candidate_issues;
   3732 
   3733     if !view.issues.is_empty() {
   3734         view.state = "invalid".to_owned();
   3735         view.reason = Some(format!(
   3736             "seller order request preflight found invalid request candidates for `{}`",
   3737             args.key
   3738         ));
   3739         view.actions = vec![format!("radroots order status get {}", args.key)];
   3740         return view;
   3741     }
   3742     match requests.as_slice() {
   3743         [] => {
   3744             view.reason = Some(format!(
   3745                 "no seller-targeted order request event matched `{}`",
   3746                 args.key
   3747             ));
   3748             view
   3749         }
   3750         _ => {
   3751             let event_ids = requests
   3752                 .iter()
   3753                 .map(|request| request.request_event_id.to_string())
   3754                 .collect::<Vec<_>>();
   3755             view.state = "invalid".to_owned();
   3756             view.reason = Some(format!(
   3757                 "multiple seller-targeted order request events matched `{}`; refusing to choose an order root",
   3758                 args.key
   3759             ));
   3760             view.issues = vec![issue_with_events(
   3761                 "multiple_request_candidates",
   3762                 "request_event_id",
   3763                 format!(
   3764                     "matched {} request events for the same order id: {}",
   3765                     requests.len(),
   3766                     event_ids.join(", ")
   3767                 ),
   3768                 event_ids,
   3769             )];
   3770             view.actions = vec![format!("radroots order status get {}", args.key)];
   3771             view
   3772         }
   3773     }
   3774 }
   3775 
   3776 fn apply_order_decision_resolution(
   3777     view: &mut OrderDecisionView,
   3778     resolution: &SellerOrderRequestResolution,
   3779 ) {
   3780     view.target_relays = resolution.target_relays.clone();
   3781     view.connected_relays = resolution.connected_relays.clone();
   3782     view.failed_relays = relay_failures(resolution.failed_relays.clone());
   3783     view.fetched_count = resolution.fetched_count;
   3784     view.decoded_count = resolution.decoded_count;
   3785     view.skipped_count = resolution.skipped_count;
   3786 }
   3787 
   3788 fn apply_order_decision_request(
   3789     view: &mut OrderDecisionView,
   3790     request: &ResolvedSellerOrderRequest,
   3791 ) {
   3792     view.order_id = request.order_id.to_string();
   3793     view.listing_addr = Some(request.listing_addr.to_string());
   3794     view.buyer_pubkey = Some(request.buyer_pubkey.to_string());
   3795     view.seller_pubkey = Some(request.seller_pubkey.to_string());
   3796     view.request_event_id = Some(request.request_event_id.to_string());
   3797     view.listing_event_id = request.listing_event_id.clone();
   3798     view.root_event_id = Some(request.request_event_id.to_string());
   3799     view.prev_event_id = Some(request.request_event_id.to_string());
   3800 }
   3801 
   3802 fn apply_order_decision_status(view: &mut OrderDecisionView, status: &OrderStatusView) {
   3803     view.target_relays = status.target_relays.clone();
   3804     view.connected_relays = status.connected_relays.clone();
   3805     view.failed_relays = status.failed_relays.clone();
   3806     view.fetched_count = status.fetched_count;
   3807     view.decoded_count = status.decoded_count;
   3808     view.skipped_count = status.skipped_count;
   3809     view.issues = status.reducer_issues.clone();
   3810     view.inventory = status.inventory.clone();
   3811 }
   3812 
   3813 fn apply_order_revision_status(view: &mut OrderRevisionProposalView, status: &OrderStatusView) {
   3814     view.order_id = status.order_id.clone();
   3815     view.listing_addr = status.listing_addr.clone();
   3816     view.buyer_pubkey = status.buyer_pubkey.clone();
   3817     view.seller_pubkey = status.seller_pubkey.clone();
   3818     view.request_event_id = status.request_event_id.clone();
   3819     view.decision_event_id = status.decision_event_id.clone();
   3820     view.root_event_id = status.request_event_id.clone();
   3821     view.prev_event_id = status.last_event_id.clone();
   3822     view.economics = status.economics.clone();
   3823     view.inventory = status.inventory.clone();
   3824     view.target_relays = status.target_relays.clone();
   3825     view.connected_relays = status.connected_relays.clone();
   3826     view.failed_relays = status.failed_relays.clone();
   3827     view.fetched_count = status.fetched_count;
   3828     view.decoded_count = status.decoded_count;
   3829     view.skipped_count = status.skipped_count;
   3830     view.issues = status.reducer_issues.clone();
   3831 }
   3832 
   3833 fn apply_order_revision_decision_status(
   3834     view: &mut OrderRevisionDecisionView,
   3835     status: &OrderStatusView,
   3836 ) {
   3837     view.order_id = status.order_id.clone();
   3838     view.listing_addr = status.listing_addr.clone();
   3839     view.buyer_pubkey = status.buyer_pubkey.clone();
   3840     view.seller_pubkey = status.seller_pubkey.clone();
   3841     view.request_event_id = status.request_event_id.clone();
   3842     view.decision_event_id = status.decision_event_id.clone();
   3843     view.agreement_event_id = status.agreement_event_id.clone();
   3844     view.root_event_id = status.request_event_id.clone();
   3845     view.prev_event_id = status.last_event_id.clone();
   3846     view.economics = status.economics.clone();
   3847     view.inventory = status.inventory.clone();
   3848     view.target_relays = status.target_relays.clone();
   3849     view.connected_relays = status.connected_relays.clone();
   3850     view.failed_relays = status.failed_relays.clone();
   3851     view.fetched_count = status.fetched_count;
   3852     view.decoded_count = status.decoded_count;
   3853     view.skipped_count = status.skipped_count;
   3854     view.issues = status.reducer_issues.clone();
   3855 }
   3856 
   3857 fn order_decision_preflight_view_from_status(
   3858     config: &RuntimeConfig,
   3859     args: &OrderDecisionArgs,
   3860     request: &ResolvedSellerOrderRequest,
   3861     resolution: &SellerOrderRequestResolution,
   3862     status: &OrderStatusView,
   3863 ) -> Option<OrderDecisionView> {
   3864     let state = match status.state.as_str() {
   3865         "accepted" | "declined" => "already_decided",
   3866         "cancelled" => "terminal",
   3867         "invalid" => "invalid",
   3868         "unavailable" => "unavailable",
   3869         "unconfigured" => "unconfigured",
   3870         _ => return None,
   3871     };
   3872     let mut view = order_decision_base_view(config, args, state, config.output.dry_run);
   3873     apply_order_decision_resolution(&mut view, resolution);
   3874     apply_order_decision_request(&mut view, request);
   3875     apply_order_decision_status(&mut view, status);
   3876     if let Some(decision_event_id) = &status.decision_event_id {
   3877         view.event_id = Some(decision_event_id.clone());
   3878         view.event_kind = Some(KIND_ORDER_DECISION);
   3879     }
   3880     view.reason = Some(match status.state.as_str() {
   3881         "accepted" | "declined" => format!(
   3882             "order {} refused because order `{}` already has a visible `{}` seller decision",
   3883             args.decision.command(),
   3884             request.order_id,
   3885             status.state
   3886         ),
   3887         "cancelled" => format!(
   3888             "order {} refused because order `{}` is already terminal",
   3889             args.decision.command(),
   3890             request.order_id
   3891         ),
   3892         "invalid" => status.reason.clone().unwrap_or_else(|| {
   3893             format!(
   3894                 "order {} refused because active order events for `{}` are invalid",
   3895                 args.decision.command(),
   3896                 request.order_id
   3897             )
   3898         }),
   3899         _ => status.reason.clone().unwrap_or_else(|| {
   3900             format!(
   3901                 "order {} status preflight failed with state `{}`",
   3902                 args.decision.command(),
   3903                 status.state
   3904             )
   3905         }),
   3906     });
   3907     view.actions = vec![format!("radroots order status get {}", request.order_id)];
   3908     Some(view)
   3909 }
   3910 
   3911 fn order_revision_args_preflight_view(
   3912     config: &RuntimeConfig,
   3913     args: &OrderRevisionProposeArgs,
   3914 ) -> Option<OrderRevisionProposalView> {
   3915     let mut issues = Vec::new();
   3916     let has_bin_id = args.bin_id.as_deref().and_then(non_empty_ref).is_some();
   3917     let has_bin_count = args.bin_count.is_some();
   3918     if has_bin_id != has_bin_count {
   3919         issues.push(issue_with_code(
   3920             "revision_item_change_incomplete",
   3921             "bin_id",
   3922             "`bin_id` and `bin_count` must be supplied together",
   3923         ));
   3924     }
   3925     if args.bin_count == Some(0) {
   3926         issues.push(issue_with_code(
   3927             "revision_bin_count_invalid",
   3928             "bin_count",
   3929             "bin_count must be greater than zero",
   3930         ));
   3931     }
   3932 
   3933     let adjustment_inputs = [
   3934         args.adjustment_id.as_deref(),
   3935         args.adjustment_effect.as_deref(),
   3936         args.adjustment_amount.as_deref(),
   3937         args.adjustment_currency.as_deref(),
   3938         args.adjustment_reason.as_deref(),
   3939     ];
   3940     let adjustment_supplied = adjustment_inputs
   3941         .iter()
   3942         .any(|value| value.and_then(non_empty_ref).is_some());
   3943     let adjustment_complete = adjustment_inputs
   3944         .iter()
   3945         .all(|value| value.and_then(non_empty_ref).is_some());
   3946     if adjustment_supplied && !adjustment_complete {
   3947         issues.push(issue_with_code(
   3948             "revision_adjustment_incomplete",
   3949             "adjustment",
   3950             "all revision adjustment fields must be supplied together",
   3951         ));
   3952     }
   3953 
   3954     if !has_bin_id && !adjustment_supplied {
   3955         issues.push(issue_with_code(
   3956             "revision_no_changes",
   3957             "revision",
   3958             "order revision propose requires a bin-count change or revision adjustment",
   3959         ));
   3960     }
   3961 
   3962     if issues.is_empty() {
   3963         return None;
   3964     }
   3965     let mut view = order_revision_base_view(config, args, "invalid", config.output.dry_run);
   3966     view.reason = Some(format!(
   3967         "order revision propose inputs for `{}` failed validation",
   3968         args.key
   3969     ));
   3970     view.issues = issues;
   3971     Some(view)
   3972 }
   3973 
   3974 fn order_revision_decision_args_preflight_view(
   3975     config: &RuntimeConfig,
   3976     args: &OrderRevisionDecisionArgs,
   3977 ) -> Option<OrderRevisionDecisionView> {
   3978     let mut issues = Vec::new();
   3979     if args.revision_id.trim().is_empty() {
   3980         issues.push(issue_with_code(
   3981             "revision_id_required",
   3982             "revision_id",
   3983             "order revision decision requires --revision-id",
   3984         ));
   3985     }
   3986     if args.decision == OrderRevisionDecisionArg::Decline
   3987         && args
   3988             .reason
   3989             .as_deref()
   3990             .map(str::trim)
   3991             .filter(|reason| !reason.is_empty())
   3992             .is_none()
   3993     {
   3994         issues.push(issue_with_code(
   3995             "revision_decline_reason_required",
   3996             "reason",
   3997             "order revision decline requires a non-empty reason",
   3998         ));
   3999     }
   4000 
   4001     if issues.is_empty() {
   4002         return None;
   4003     }
   4004     let mut view =
   4005         order_revision_decision_base_view(config, args, "invalid", config.output.dry_run);
   4006     view.reason = Some(format!(
   4007         "order revision {} inputs for `{}` failed validation",
   4008         args.decision.command(),
   4009         args.key
   4010     ));
   4011     view.issues = issues;
   4012     Some(view)
   4013 }
   4014 
   4015 fn order_revision_preflight_view_from_status(
   4016     config: &RuntimeConfig,
   4017     args: &OrderRevisionProposeArgs,
   4018     status: &OrderStatusView,
   4019     selected_pubkey: &str,
   4020     candidates: &OrderRevisionProposalCandidates,
   4021 ) -> Option<OrderRevisionProposalView> {
   4022     let pending_revision = pending_revision_proposal_candidate(status, candidates);
   4023     let seller_matches = status
   4024         .seller_pubkey
   4025         .as_deref()
   4026         .is_some_and(|seller| seller.eq_ignore_ascii_case(selected_pubkey));
   4027     let state = match status.state.as_str() {
   4028         "accepted"
   4029             if seller_matches && candidates.issues.is_empty() && pending_revision.is_none() =>
   4030         {
   4031             return None;
   4032         }
   4033         "accepted" if !seller_matches => "invalid",
   4034         "accepted" if !candidates.issues.is_empty() => "invalid",
   4035         "accepted" if pending_revision.is_some() => "forked",
   4036         "cancelled" => "terminal",
   4037         "missing" | "requested" | "declined" | "invalid" | "unavailable" | "unconfigured" => {
   4038             status.state.as_str()
   4039         }
   4040         _ => "invalid",
   4041     };
   4042     let mut view = order_revision_base_view(config, args, state, config.output.dry_run);
   4043     apply_order_revision_status(&mut view, status);
   4044     if let Some(record) = pending_revision {
   4045         view.event_id = Some(record.event_id.to_string());
   4046         view.event_kind = Some(KIND_ORDER_REVISION_PROPOSAL);
   4047         view.revision_id = Some(record.payload.revision_id.to_string());
   4048     }
   4049     view.reason = Some(match state {
   4050         "missing" => format!("no active order events matched `{}`", args.key),
   4051         "requested" => format!(
   4052             "order revision propose refused because order `{}` has no accepted seller decision",
   4053             args.key
   4054         ),
   4055         "declined" => format!(
   4056             "order revision propose refused because order `{}` was declined",
   4057             args.key
   4058         ),
   4059         "terminal" => format!(
   4060             "order revision propose refused because order `{}` is already terminal",
   4061             args.key
   4062         ),
   4063         "forked" => format!(
   4064             "order revision propose refused because order `{}` already has a pending revision proposal",
   4065             args.key
   4066         ),
   4067         "invalid" if !seller_matches && status.seller_pubkey.is_some() => format!(
   4068             "order revision propose refused because selected account is not seller for order `{}`",
   4069             args.key
   4070         ),
   4071         "invalid" if !candidates.issues.is_empty() => format!(
   4072             "order revision propose refused because revision proposal candidates for `{}` are invalid",
   4073             args.key
   4074         ),
   4075         "invalid" => status.reason.clone().unwrap_or_else(|| {
   4076             format!(
   4077                 "order revision propose refused because active order events for `{}` are invalid",
   4078                 args.key
   4079             )
   4080         }),
   4081         _ => status.reason.clone().unwrap_or_else(|| {
   4082             format!(
   4083                 "order revision propose status preflight failed with state `{}`",
   4084                 status.state
   4085             )
   4086         }),
   4087     });
   4088     if state == "forked" {
   4089         view.issues.push(issue_with_events(
   4090             "pending_revision_exists",
   4091             "revision_id",
   4092             "a seller revision proposal is already visible for this accepted order",
   4093             candidates
   4094                 .records
   4095                 .iter()
   4096                 .filter(|record| Some(record.event_id.as_str()) == status.last_event_id.as_deref())
   4097                 .map(|record| record.event_id.clone())
   4098                 .collect(),
   4099         ));
   4100     }
   4101     view.issues.extend(candidates.issues.clone());
   4102     view.actions = vec![format!("radroots order status get {}", args.key)];
   4103     Some(view)
   4104 }
   4105 
   4106 fn order_revision_decision_preflight_view_from_status(
   4107     config: &RuntimeConfig,
   4108     args: &OrderRevisionDecisionArgs,
   4109     status: &OrderStatusView,
   4110     selected_pubkey: &str,
   4111     candidates: &OrderRevisionProposalCandidates,
   4112 ) -> Option<OrderRevisionDecisionView> {
   4113     let pending_revision = pending_revision_proposal_candidate(status, candidates);
   4114     let buyer_matches = status
   4115         .buyer_pubkey
   4116         .as_deref()
   4117         .is_some_and(|buyer| buyer.eq_ignore_ascii_case(selected_pubkey));
   4118     let state = match status.state.as_str() {
   4119         "accepted"
   4120             if buyer_matches && candidates.issues.is_empty() && pending_revision.is_some() =>
   4121         {
   4122             return None;
   4123         }
   4124         "accepted" if !buyer_matches => "invalid",
   4125         "accepted" if !candidates.issues.is_empty() => "invalid",
   4126         "accepted" => "missing",
   4127         "cancelled" => "terminal",
   4128         "declined" => "order_declined",
   4129         "missing" | "requested" | "invalid" | "unavailable" | "unconfigured" => {
   4130             status.state.as_str()
   4131         }
   4132         _ => "invalid",
   4133     };
   4134     let mut view = order_revision_decision_base_view(config, args, state, config.output.dry_run);
   4135     apply_order_revision_decision_status(&mut view, status);
   4136     if let Some(record) = pending_revision {
   4137         apply_order_revision_decision_proposal(&mut view, record);
   4138         view.event_id = Some(record.event_id.to_string());
   4139         view.event_kind = Some(KIND_ORDER_REVISION_PROPOSAL);
   4140     }
   4141     view.reason = Some(match state {
   4142         "missing" if status.state == "accepted" => format!(
   4143             "order revision {} refused because order `{}` has no pending revision proposal",
   4144             args.decision.command(),
   4145             args.key
   4146         ),
   4147         "missing" => format!("no active order events matched `{}`", args.key),
   4148         "requested" => format!(
   4149             "order revision {} refused because order `{}` has no accepted seller decision",
   4150             args.decision.command(),
   4151             args.key
   4152         ),
   4153         "order_declined" => format!(
   4154             "order revision {} refused because order `{}` was declined",
   4155             args.decision.command(),
   4156             args.key
   4157         ),
   4158         "terminal" => format!(
   4159             "order revision {} refused because order `{}` is already terminal",
   4160             args.decision.command(),
   4161             args.key
   4162         ),
   4163         "invalid" if !buyer_matches && status.buyer_pubkey.is_some() => format!(
   4164             "order revision {} refused because selected account is not buyer for order `{}`",
   4165             args.decision.command(),
   4166             args.key
   4167         ),
   4168         "invalid" if !candidates.issues.is_empty() => format!(
   4169             "order revision {} refused because revision proposal candidates for `{}` are invalid",
   4170             args.decision.command(),
   4171             args.key
   4172         ),
   4173         "invalid" => status.reason.clone().unwrap_or_else(|| {
   4174             format!(
   4175                 "order revision {} refused because active order events for `{}` are invalid",
   4176                 args.decision.command(),
   4177                 args.key
   4178             )
   4179         }),
   4180         _ => status.reason.clone().unwrap_or_else(|| {
   4181             format!(
   4182                 "order revision {} status preflight failed with state `{}`",
   4183                 args.decision.command(),
   4184                 status.state
   4185             )
   4186         }),
   4187     });
   4188     view.issues.extend(candidates.issues.clone());
   4189     view.actions = vec![format!("radroots order status get {}", args.key)];
   4190     Some(view)
   4191 }
   4192 
   4193 fn pending_revision_proposal_candidate<'a>(
   4194     status: &OrderStatusView,
   4195     candidates: &'a OrderRevisionProposalCandidates,
   4196 ) -> Option<&'a OrderRevisionProposalRecord> {
   4197     let last_event_id = status.last_event_id.as_deref()?;
   4198     candidates
   4199         .records
   4200         .iter()
   4201         .find(|record| record.event_id == last_event_id)
   4202 }
   4203 
   4204 fn order_accept_inventory_preflight_view(
   4205     config: &RuntimeConfig,
   4206     args: &OrderDecisionArgs,
   4207     request: &ResolvedSellerOrderRequest,
   4208     resolution: &SellerOrderRequestResolution,
   4209     status: &OrderStatusView,
   4210 ) -> Result<OrderDecisionInventoryPreflight, RuntimeError> {
   4211     if args.decision != OrderDecisionArg::Accept {
   4212         return Ok(OrderDecisionInventoryPreflight {
   4213             invalid_view: None,
   4214             inventory: Some(order_declined_inventory_view(request)),
   4215         });
   4216     }
   4217 
   4218     let listing = match fetch_current_inventory_listing(config, args, request, resolution, status)?
   4219     {
   4220         Ok(listing) => listing,
   4221         Err(view) => {
   4222             return Ok(OrderDecisionInventoryPreflight {
   4223                 invalid_view: Some(view),
   4224                 inventory: None,
   4225             });
   4226         }
   4227     };
   4228     if Some(listing.event_id.to_string()) != request.listing_event_id {
   4229         return Ok(OrderDecisionInventoryPreflight {
   4230             invalid_view: Some(order_decision_inventory_invalid_view(
   4231                 config,
   4232                 args,
   4233                 request,
   4234                 resolution,
   4235                 status,
   4236                 "order accept refused because the request listing event is not current",
   4237                 vec![issue_with_events(
   4238                     "stale_request_listing_event",
   4239                     "listing_event_id",
   4240                     format!(
   4241                         "request listing_event_id does not match current listing event `{}`",
   4242                         listing.event_id
   4243                     ),
   4244                     request.listing_event_id.clone().into_iter().collect(),
   4245                 )],
   4246             )),
   4247             inventory: None,
   4248         });
   4249     }
   4250     if !listing_is_active(&listing.listing) {
   4251         return Ok(OrderDecisionInventoryPreflight {
   4252             invalid_view: Some(order_decision_inventory_invalid_view(
   4253                 config,
   4254                 args,
   4255                 request,
   4256                 resolution,
   4257                 status,
   4258                 "order accept refused because the listing is not active",
   4259                 vec![issue_with_code(
   4260                     "listing_not_active",
   4261                     "listing_addr",
   4262                     "current listing event is not active",
   4263                 )],
   4264             )),
   4265             inventory: None,
   4266         });
   4267     }
   4268 
   4269     let accounting_requests = fetch_listing_accounting_requests(config, request, &listing)?;
   4270     let mut requests = accounting_requests
   4271         .into_iter()
   4272         .filter(|record| {
   4273             record.listing_event_id.as_deref() == Some(listing.event_id.to_string().as_str())
   4274         })
   4275         .map(|record| record.record)
   4276         .collect::<Vec<_>>();
   4277     requests.push(active_request_record_from_resolved(request));
   4278     let mut request_order_ids = requests
   4279         .iter()
   4280         .map(|record| record.payload.order_id.clone())
   4281         .collect::<Vec<_>>();
   4282     request_order_ids.sort();
   4283     request_order_ids.dedup();
   4284 
   4285     let mut decisions = fetch_listing_accounting_decisions(config, request)?
   4286         .into_iter()
   4287         .filter(|record| request_order_ids.contains(&record.payload.order_id))
   4288         .collect::<Vec<_>>();
   4289     decisions.push(proposed_accept_decision_record(request)?);
   4290     let revision_proposals = fetch_listing_accounting_revision_proposals_for_status(
   4291         config,
   4292         request.listing_addr.as_str(),
   4293     )?
   4294     .into_iter()
   4295     .filter(|record| request_order_ids.contains(&record.payload.order_id))
   4296     .collect::<Vec<_>>();
   4297     let revision_decisions = fetch_listing_accounting_revision_decisions_for_status(
   4298         config,
   4299         request.listing_addr.as_str(),
   4300     )?
   4301     .into_iter()
   4302     .filter(|record| request_order_ids.contains(&record.payload.order_id))
   4303     .collect::<Vec<_>>();
   4304     let cancellations = fetch_listing_accounting_cancellations(config, request)?
   4305         .into_iter()
   4306         .filter(|record| request_order_ids.contains(&record.payload.order_id))
   4307         .collect::<Vec<_>>();
   4308 
   4309     let projection = reduce_listing_inventory_accounting(
   4310         &request.listing_addr,
   4311         &listing.event_id,
   4312         RadrootsListingInventoryAccountingInputs {
   4313             bins: listing.bins,
   4314             requests,
   4315             decisions,
   4316             revision_proposals,
   4317             revision_decisions,
   4318             cancellations,
   4319         },
   4320     );
   4321     Ok(order_accept_inventory_preflight_view_from_projection(
   4322         config, args, request, resolution, status, projection,
   4323     ))
   4324 }
   4325 
   4326 fn order_accept_inventory_preflight_view_from_projection(
   4327     config: &RuntimeConfig,
   4328     args: &OrderDecisionArgs,
   4329     request: &ResolvedSellerOrderRequest,
   4330     resolution: &SellerOrderRequestResolution,
   4331     status: &OrderStatusView,
   4332     projection: RadrootsListingInventoryAccountingProjection,
   4333 ) -> OrderDecisionInventoryPreflight {
   4334     if projection.issues.is_empty() {
   4335         return OrderDecisionInventoryPreflight {
   4336             invalid_view: None,
   4337             inventory: Some(order_inventory_view_from_listing_projection(
   4338                 &projection,
   4339                 "reserved",
   4340                 true,
   4341             )),
   4342         };
   4343     }
   4344 
   4345     let inventory = order_inventory_view_from_listing_projection(&projection, "invalid", false);
   4346     let issues = projection
   4347         .issues
   4348         .into_iter()
   4349         .map(listing_inventory_accounting_issue_view)
   4350         .collect::<Vec<_>>();
   4351     let mut view = order_decision_inventory_invalid_view(
   4352         config,
   4353         args,
   4354         request,
   4355         resolution,
   4356         status,
   4357         "order accept refused because visible inventory accounting is invalid",
   4358         issues,
   4359     );
   4360     view.inventory = Some(inventory);
   4361     OrderDecisionInventoryPreflight {
   4362         invalid_view: Some(view),
   4363         inventory: None,
   4364     }
   4365 }
   4366 
   4367 fn order_inventory_view_from_listing_projection(
   4368     projection: &RadrootsListingInventoryAccountingProjection,
   4369     state: &str,
   4370     commitment_valid: bool,
   4371 ) -> OrderInventoryView {
   4372     OrderInventoryView {
   4373         state: state.to_owned(),
   4374         listing_event_id: Some(projection.listing_event_id.to_string()),
   4375         commitment_valid,
   4376         bins: projection
   4377             .bins
   4378             .iter()
   4379             .map(|bin| OrderInventoryBinView {
   4380                 bin_id: bin.bin_id.to_string(),
   4381                 committed_count: bin.accepted_reserved_count,
   4382                 available_count: Some(bin.available_count),
   4383                 remaining_count: Some(bin.remaining_count),
   4384                 over_reserved: bin.over_reserved,
   4385             })
   4386             .collect(),
   4387         issues: projection
   4388             .issues
   4389             .iter()
   4390             .cloned()
   4391             .map(listing_inventory_accounting_issue_view)
   4392             .collect(),
   4393     }
   4394 }
   4395 
   4396 fn order_declined_inventory_view(request: &ResolvedSellerOrderRequest) -> OrderInventoryView {
   4397     OrderInventoryView {
   4398         state: "not_reserved".to_owned(),
   4399         listing_event_id: request.listing_event_id.clone(),
   4400         commitment_valid: true,
   4401         bins: Vec::new(),
   4402         issues: Vec::new(),
   4403     }
   4404 }
   4405 
   4406 fn order_decision_inventory_for_view(
   4407     args: &OrderDecisionArgs,
   4408     request: &ResolvedSellerOrderRequest,
   4409     inventory: Option<OrderInventoryView>,
   4410 ) -> Option<OrderInventoryView> {
   4411     match args.decision {
   4412         OrderDecisionArg::Accept => inventory,
   4413         OrderDecisionArg::Decline => Some(order_declined_inventory_view(request)),
   4414     }
   4415 }
   4416 
   4417 fn fetch_current_inventory_listing(
   4418     config: &RuntimeConfig,
   4419     args: &OrderDecisionArgs,
   4420     request: &ResolvedSellerOrderRequest,
   4421     resolution: &SellerOrderRequestResolution,
   4422     status: &OrderStatusView,
   4423 ) -> Result<Result<ResolvedInventoryListing, OrderDecisionView>, RuntimeError> {
   4424     let parsed = parse_listing_addr(request.listing_addr.as_str()).map_err(|error| {
   4425         RuntimeError::Config(format!("order request listing_addr is invalid: {error}"))
   4426     })?;
   4427     let filter = listing_event_filter(&parsed)?;
   4428     let receipt = match fetch_events_from_relays(&config.relay.urls, filter) {
   4429         Ok(receipt) => receipt,
   4430         Err(DirectRelayFetchError::Connect {
   4431             reason,
   4432             target_relays,
   4433             failed_relays,
   4434         }) => {
   4435             let mut view =
   4436                 order_decision_base_view(config, args, "unavailable", config.output.dry_run);
   4437             apply_order_decision_resolution(&mut view, resolution);
   4438             apply_order_decision_request(&mut view, request);
   4439             apply_order_decision_status(&mut view, status);
   4440             view.target_relays = target_relays;
   4441             view.failed_relays = relay_failures(failed_relays);
   4442             view.reason = Some(format!("direct relay connection failed: {reason}"));
   4443             return Ok(Err(view));
   4444         }
   4445         Err(error) => return Err(RuntimeError::Network(error.to_string())),
   4446     };
   4447 
   4448     let listing = current_inventory_listing_from_receipt(request, receipt)?;
   4449     Ok(match listing {
   4450         Some(listing) => Ok(listing),
   4451         None => Err(order_decision_inventory_invalid_view(
   4452             config,
   4453             args,
   4454             request,
   4455             resolution,
   4456             status,
   4457             "order accept refused because the current listing event was not visible",
   4458             vec![issue_with_code(
   4459                 "current_listing_missing",
   4460                 "listing_event_id",
   4461                 "current listing event was not visible on the configured relays",
   4462             )],
   4463         )),
   4464     })
   4465 }
   4466 
   4467 fn current_inventory_listing_from_receipt(
   4468     request: &ResolvedSellerOrderRequest,
   4469     receipt: DirectRelayFetchReceipt,
   4470 ) -> Result<Option<ResolvedInventoryListing>, RuntimeError> {
   4471     let parsed = parse_listing_addr(request.listing_addr.as_str()).map_err(|error| {
   4472         RuntimeError::Config(format!("order request listing_addr is invalid: {error}"))
   4473     })?;
   4474     current_inventory_listing_from_parts(parsed, receipt)
   4475 }
   4476 
   4477 fn current_inventory_listing_from_parts(
   4478     parsed: ParsedListingAddress,
   4479     receipt: DirectRelayFetchReceipt,
   4480 ) -> Result<Option<ResolvedInventoryListing>, RuntimeError> {
   4481     let mut candidates = Vec::new();
   4482     for event in receipt.events {
   4483         if event_kind_u32(&event) != KIND_LISTING {
   4484             continue;
   4485         }
   4486         let event = radroots_event_from_nostr(&event);
   4487         if event.author != parsed.seller_pubkey {
   4488             continue;
   4489         }
   4490         let listing = listing_from_event(event.kind, &event.tags, &event.content)
   4491             .map_err(|error| RuntimeError::Config(format!("decode listing event: {error}")))?;
   4492         if listing.d_tag != parsed.listing_id {
   4493             continue;
   4494         }
   4495         let bins = listing_inventory_bins(&listing)?;
   4496         let event_id = protocol_event_id(event.id.as_str(), "listing_event_id")?;
   4497         candidates.push((event.created_at, event_id, listing, bins));
   4498     }
   4499     candidates.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| right.1.cmp(&left.1)));
   4500     Ok(candidates
   4501         .into_iter()
   4502         .next()
   4503         .map(|(_, event_id, listing, bins)| ResolvedInventoryListing {
   4504             event_id,
   4505             listing,
   4506             bins,
   4507         }))
   4508 }
   4509 
   4510 fn listing_inventory_bins(
   4511     listing: &RadrootsListing,
   4512 ) -> Result<Vec<RadrootsListingInventoryBinAvailability>, RuntimeError> {
   4513     if !listing
   4514         .bins
   4515         .iter()
   4516         .any(|bin| bin.bin_id == listing.primary_bin_id)
   4517     {
   4518         return Err(RuntimeError::Config(
   4519             "current listing primary bin is missing from listing bins".to_owned(),
   4520         ));
   4521     }
   4522     let available_count = listing
   4523         .inventory_available
   4524         .as_ref()
   4525         .ok_or_else(|| {
   4526             RuntimeError::Config("current listing inventory availability is missing".to_owned())
   4527         })?
   4528         .to_u64_exact()
   4529         .ok_or_else(|| {
   4530             RuntimeError::Config(
   4531                 "current listing inventory availability must be a whole number".to_owned(),
   4532             )
   4533         })?;
   4534     Ok(vec![RadrootsListingInventoryBinAvailability {
   4535         bin_id: listing.primary_bin_id.clone(),
   4536         available_count,
   4537     }])
   4538 }
   4539 
   4540 fn listing_is_active(listing: &RadrootsListing) -> bool {
   4541     match listing.availability.as_ref() {
   4542         Some(RadrootsListingAvailability::Status { status }) => {
   4543             matches!(status, RadrootsListingStatus::Active)
   4544         }
   4545         Some(RadrootsListingAvailability::Window { .. }) | None => true,
   4546     }
   4547 }
   4548 
   4549 fn fetch_listing_accounting_requests(
   4550     config: &RuntimeConfig,
   4551     request: &ResolvedSellerOrderRequest,
   4552     listing: &ResolvedInventoryListing,
   4553 ) -> Result<Vec<ResolvedAccountingRequest>, RuntimeError> {
   4554     let filter = order_listing_request_filter(
   4555         request.seller_pubkey.as_str(),
   4556         request.listing_addr.as_str(),
   4557     )?;
   4558     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   4559         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   4560     let mut records = Vec::new();
   4561     for event in receipt.events {
   4562         if event_kind_u32(&event) != KIND_ORDER_REQUEST
   4563             || !event_matches_tag_value(&event, "a", request.listing_addr.as_str())
   4564         {
   4565             continue;
   4566         }
   4567         if let Ok(record) = listing_accounting_request_from_event(&event)
   4568             && record.listing_event_id.as_deref() == Some(listing.event_id.as_str())
   4569         {
   4570             records.push(record);
   4571         }
   4572     }
   4573     Ok(records)
   4574 }
   4575 
   4576 fn fetch_listing_accounting_decisions(
   4577     config: &RuntimeConfig,
   4578     request: &ResolvedSellerOrderRequest,
   4579 ) -> Result<Vec<RadrootsOrderDecisionRecord>, RuntimeError> {
   4580     let filter = order_listing_decision_filter(request.listing_addr.as_str())?;
   4581     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   4582         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   4583     let mut records = Vec::new();
   4584     for event in receipt.events {
   4585         if event_kind_u32(&event) != KIND_ORDER_DECISION
   4586             || !event_matches_tag_value(&event, "a", request.listing_addr.as_str())
   4587         {
   4588             continue;
   4589         }
   4590         if let Ok(OrderStatusRecord::Decision(record)) = order_status_record_from_event(&event) {
   4591             records.push(record);
   4592         }
   4593     }
   4594     Ok(records)
   4595 }
   4596 
   4597 fn fetch_listing_accounting_cancellations(
   4598     config: &RuntimeConfig,
   4599     request: &ResolvedSellerOrderRequest,
   4600 ) -> Result<Vec<RadrootsOrderCancellationRecord>, RuntimeError> {
   4601     let filter = order_listing_cancellation_filter(request.listing_addr.as_str())?;
   4602     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   4603         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   4604     let mut records = Vec::new();
   4605     for event in receipt.events {
   4606         if event_kind_u32(&event) != KIND_ORDER_CANCELLATION
   4607             || !event_matches_tag_value(&event, "a", request.listing_addr.as_str())
   4608         {
   4609             continue;
   4610         }
   4611         if let Ok(OrderStatusRecord::Cancellation(record)) = order_status_record_from_event(&event)
   4612         {
   4613             records.push(record);
   4614         }
   4615     }
   4616     Ok(records)
   4617 }
   4618 
   4619 fn listing_accounting_request_from_event(
   4620     event: &RadrootsNostrEvent,
   4621 ) -> Result<ResolvedAccountingRequest, RuntimeError> {
   4622     let event = radroots_event_from_nostr(event);
   4623     let event_id = protocol_event_id(event.id.as_str(), "event_id")?;
   4624     let author_pubkey = protocol_pubkey(event.author.as_str(), "author_pubkey")?;
   4625     let envelope = order_request_from_event(&event)
   4626         .map_err(|error| RuntimeError::Config(format!("decode order request event: {error}")))?;
   4627     let context =
   4628         order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags)
   4629             .map_err(|error| RuntimeError::Config(format!("decode order request tags: {error}")))?;
   4630     Ok(ResolvedAccountingRequest {
   4631         listing_event_id: context.listing_event.as_ref().map(|event| event.id.clone()),
   4632         record: RadrootsOrderRequestRecord {
   4633             event_id,
   4634             author_pubkey,
   4635             payload: envelope.payload,
   4636         },
   4637     })
   4638 }
   4639 
   4640 fn active_request_record_from_resolved(
   4641     request: &ResolvedSellerOrderRequest,
   4642 ) -> RadrootsOrderRequestRecord {
   4643     RadrootsOrderRequestRecord {
   4644         event_id: request.request_event_id.clone(),
   4645         author_pubkey: request.buyer_pubkey.clone(),
   4646         payload: RadrootsOrderRequest {
   4647             order_id: request.order_id.clone(),
   4648             listing_addr: request.listing_addr.clone(),
   4649             buyer_pubkey: request.buyer_pubkey.clone(),
   4650             seller_pubkey: request.seller_pubkey.clone(),
   4651             items: request.items.clone(),
   4652             economics: request.economics.clone(),
   4653         },
   4654     }
   4655 }
   4656 
   4657 fn proposed_accept_decision_record(
   4658     request: &ResolvedSellerOrderRequest,
   4659 ) -> Result<RadrootsOrderDecisionRecord, RuntimeError> {
   4660     let payload = accepted_order_decision_payload_from_request(request);
   4661     let signer_pubkey = request.seller_pubkey.to_string();
   4662     let payload = canonicalize_order_decision_for_signer(payload, signer_pubkey.as_str())
   4663         .map_err(|error| RuntimeError::Config(format!("canonicalize order decision: {error}")))?;
   4664     Ok(RadrootsOrderDecisionRecord {
   4665         event_id: request.request_event_id.clone(),
   4666         author_pubkey: request.seller_pubkey.clone(),
   4667         counterparty_pubkey: request.buyer_pubkey.clone(),
   4668         root_event_id: request.request_event_id.clone(),
   4669         prev_event_id: request.request_event_id.clone(),
   4670         payload,
   4671     })
   4672 }
   4673 
   4674 fn order_decision_inventory_invalid_view(
   4675     config: &RuntimeConfig,
   4676     args: &OrderDecisionArgs,
   4677     request: &ResolvedSellerOrderRequest,
   4678     resolution: &SellerOrderRequestResolution,
   4679     status: &OrderStatusView,
   4680     reason: impl Into<String>,
   4681     issues: Vec<OrderIssueView>,
   4682 ) -> OrderDecisionView {
   4683     let mut view = order_decision_base_view(config, args, "invalid", config.output.dry_run);
   4684     apply_order_decision_resolution(&mut view, resolution);
   4685     apply_order_decision_request(&mut view, request);
   4686     apply_order_decision_status(&mut view, status);
   4687     view.reason = Some(reason.into());
   4688     view.issues.extend(issues);
   4689     view.actions = vec![format!("radroots order status get {}", request.order_id)];
   4690     view
   4691 }
   4692 
   4693 fn listing_inventory_accounting_issue_view(
   4694     issue_value: RadrootsListingInventoryAccountingIssue,
   4695 ) -> OrderIssueView {
   4696     match issue_value {
   4697         RadrootsListingInventoryAccountingIssue::InvalidOrder {
   4698             order_id,
   4699             event_ids,
   4700         } => issue_with_events(
   4701             "invalid_inventory_order",
   4702             "order_id",
   4703             format!("inventory accounting reported invalid active order `{order_id}`"),
   4704             event_ids,
   4705         ),
   4706         RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { bin_id, event_ids } => {
   4707             issue_with_events(
   4708                 "listing_inventory_arithmetic_overflow",
   4709                 "inventory.count",
   4710                 format!("inventory accounting overflowed for bin `{bin_id}`"),
   4711                 event_ids,
   4712             )
   4713         }
   4714         RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { bin_id, event_ids } => {
   4715             issue_with_events(
   4716                 "unknown_inventory_bin",
   4717                 "inventory.bin_id",
   4718                 format!("inventory accounting reported unknown bin `{bin_id}`"),
   4719                 event_ids,
   4720             )
   4721         }
   4722         RadrootsListingInventoryAccountingIssue::OverReserved {
   4723             bin_id,
   4724             available_count,
   4725             reserved_count,
   4726             event_ids,
   4727         } => issue_with_events(
   4728             "listing_inventory_over_reserved",
   4729             "inventory.available",
   4730             format!(
   4731                 "inventory accounting reported bin `{bin_id}` over-reserved: reserved {reserved_count}, available {available_count}"
   4732             ),
   4733             event_ids,
   4734         ),
   4735     }
   4736 }
   4737 
   4738 fn order_decision_dry_run_view(
   4739     config: &RuntimeConfig,
   4740     args: &OrderDecisionArgs,
   4741     request: &ResolvedSellerOrderRequest,
   4742     status: &OrderStatusView,
   4743     inventory: Option<OrderInventoryView>,
   4744 ) -> OrderDecisionView {
   4745     let decision_reason = args
   4746         .reason
   4747         .as_deref()
   4748         .map(str::trim)
   4749         .filter(|reason| !reason.is_empty());
   4750     let mut view = order_decision_base_view(config, args, "dry_run", true);
   4751     apply_order_decision_request(&mut view, request);
   4752     apply_order_decision_status(&mut view, status);
   4753     view.inventory = order_decision_inventory_for_view(args, request, inventory);
   4754     view.reason = Some(match decision_reason {
   4755         Some(reason) => format!(
   4756             "dry run requested; seller order decision publication skipped with reason `{reason}`"
   4757         ),
   4758         None => "dry run requested; seller order decision publication skipped".to_owned(),
   4759     });
   4760     view.actions = vec![format!("radroots order status get {}", request.order_id)];
   4761     view
   4762 }
   4763 
   4764 fn order_revision_invalid_view(
   4765     config: &RuntimeConfig,
   4766     args: &OrderRevisionProposeArgs,
   4767     status: &OrderStatusView,
   4768     reason: impl Into<String>,
   4769     issues: Vec<OrderIssueView>,
   4770 ) -> OrderRevisionProposalView {
   4771     let mut view = order_revision_base_view(config, args, "invalid", config.output.dry_run);
   4772     apply_order_revision_status(&mut view, status);
   4773     view.reason = Some(reason.into());
   4774     view.issues.extend(issues);
   4775     view.actions = vec![format!("radroots order status get {}", args.key)];
   4776     view
   4777 }
   4778 
   4779 fn order_revision_decision_invalid_view(
   4780     config: &RuntimeConfig,
   4781     args: &OrderRevisionDecisionArgs,
   4782     status: &OrderStatusView,
   4783     reason: impl Into<String>,
   4784     issues: Vec<OrderIssueView>,
   4785 ) -> OrderRevisionDecisionView {
   4786     let mut view =
   4787         order_revision_decision_base_view(config, args, "invalid", config.output.dry_run);
   4788     apply_order_revision_decision_status(&mut view, status);
   4789     view.reason = Some(reason.into());
   4790     view.issues.extend(issues);
   4791     view.actions = vec![format!("radroots order status get {}", args.key)];
   4792     view
   4793 }
   4794 
   4795 fn order_revision_dry_run_view(
   4796     config: &RuntimeConfig,
   4797     args: &OrderRevisionProposeArgs,
   4798     status: &OrderStatusView,
   4799     payload: &RadrootsOrderRevisionProposal,
   4800 ) -> OrderRevisionProposalView {
   4801     let mut view = order_revision_base_view(config, args, "dry_run", true);
   4802     apply_order_revision_status(&mut view, status);
   4803     apply_order_revision_payload(&mut view, payload);
   4804     view.reason =
   4805         Some("dry run requested; seller revision proposal publication skipped".to_owned());
   4806     view.actions = vec![format!("radroots order status get {}", status.order_id)];
   4807     view
   4808 }
   4809 
   4810 fn order_revision_decision_dry_run_view(
   4811     config: &RuntimeConfig,
   4812     args: &OrderRevisionDecisionArgs,
   4813     status: &OrderStatusView,
   4814     proposal: &OrderRevisionProposalRecord,
   4815     payload: &RadrootsOrderRevisionDecision,
   4816 ) -> OrderRevisionDecisionView {
   4817     let mut view = order_revision_decision_base_view(config, args, "dry_run", true);
   4818     apply_order_revision_decision_status(&mut view, status);
   4819     apply_order_revision_decision_payload(&mut view, proposal, payload);
   4820     view.reason = Some(format!(
   4821         "dry run requested; buyer revision {} publication skipped",
   4822         args.decision.command()
   4823     ));
   4824     view.actions = vec![format!("radroots order status get {}", status.order_id)];
   4825     view
   4826 }
   4827 
   4828 fn order_cancellation_dry_run_view(
   4829     config: &RuntimeConfig,
   4830     args: &OrderCancelArgs,
   4831     status: &OrderStatusView,
   4832 ) -> OrderCancellationView {
   4833     let mut view = order_cancellation_base_view(config, args, "dry_run", true);
   4834     apply_order_cancellation_status(&mut view, status);
   4835     view.reason =
   4836         Some("dry run requested; buyer order cancellation publication skipped".to_owned());
   4837     view.actions = vec![format!("radroots order status get {}", status.order_id)];
   4838     view
   4839 }
   4840 
   4841 fn order_cancellation_payload_from_status(
   4842     args: &OrderCancelArgs,
   4843     status: &OrderStatusView,
   4844 ) -> Result<RadrootsOrderCancellation, RuntimeError> {
   4845     Ok(RadrootsOrderCancellation {
   4846         order_id: protocol_order_id(status.order_id.as_str(), "order_id")?,
   4847         listing_addr: protocol_listing_addr(
   4848             status.listing_addr.as_deref().ok_or_else(|| {
   4849                 RuntimeError::Config("cancellable order is missing listing_addr".to_owned())
   4850             })?,
   4851             "listing_addr",
   4852         )?,
   4853         buyer_pubkey: protocol_pubkey(
   4854             status.buyer_pubkey.as_deref().ok_or_else(|| {
   4855                 RuntimeError::Config("cancellable order is missing buyer_pubkey".to_owned())
   4856             })?,
   4857             "buyer_pubkey",
   4858         )?,
   4859         seller_pubkey: protocol_pubkey(
   4860             status.seller_pubkey.as_deref().ok_or_else(|| {
   4861                 RuntimeError::Config("cancellable order is missing seller_pubkey".to_owned())
   4862             })?,
   4863             "seller_pubkey",
   4864         )?,
   4865         reason: args.reason.trim().to_owned(),
   4866     })
   4867 }
   4868 
   4869 fn order_revision_payload_from_status(
   4870     args: &OrderRevisionProposeArgs,
   4871     status: &OrderStatusView,
   4872 ) -> Result<RadrootsOrderRevisionProposal, RuntimeError> {
   4873     let revision_id = protocol_revision_id(next_revision_id().as_str(), "revision_id")?;
   4874     let economics = status.economics.clone().ok_or_else(|| {
   4875         RuntimeError::Config("accepted order is missing current agreement economics".to_owned())
   4876     })?;
   4877     let economics = revised_order_economics(args, revision_id.as_str(), &economics)?;
   4878     let items = economics
   4879         .items
   4880         .iter()
   4881         .map(|item| RadrootsOrderItem {
   4882             bin_id: item.bin_id.clone(),
   4883             bin_count: item.bin_count,
   4884         })
   4885         .collect::<Vec<_>>();
   4886     Ok(RadrootsOrderRevisionProposal {
   4887         revision_id,
   4888         order_id: protocol_order_id(status.order_id.as_str(), "order_id")?,
   4889         listing_addr: protocol_listing_addr(
   4890             status.listing_addr.as_deref().ok_or_else(|| {
   4891                 RuntimeError::Config("accepted order is missing listing_addr".to_owned())
   4892             })?,
   4893             "listing_addr",
   4894         )?,
   4895         buyer_pubkey: protocol_pubkey(
   4896             status.buyer_pubkey.as_deref().ok_or_else(|| {
   4897                 RuntimeError::Config("accepted order is missing buyer_pubkey".to_owned())
   4898             })?,
   4899             "buyer_pubkey",
   4900         )?,
   4901         seller_pubkey: protocol_pubkey(
   4902             status.seller_pubkey.as_deref().ok_or_else(|| {
   4903                 RuntimeError::Config("accepted order is missing seller_pubkey".to_owned())
   4904             })?,
   4905             "seller_pubkey",
   4906         )?,
   4907         root_event_id: protocol_event_id(
   4908             status.request_event_id.as_deref().ok_or_else(|| {
   4909                 RuntimeError::Config("accepted order is missing request_event_id".to_owned())
   4910             })?,
   4911             "request_event_id",
   4912         )?,
   4913         prev_event_id: protocol_event_id(
   4914             status
   4915                 .last_event_id
   4916                 .as_deref()
   4917                 .or(status.decision_event_id.as_deref())
   4918                 .ok_or_else(|| {
   4919                     RuntimeError::Config("accepted order is missing previous event id".to_owned())
   4920                 })?,
   4921             "prev_event_id",
   4922         )?,
   4923         items,
   4924         economics,
   4925         reason: args.reason.trim().to_owned(),
   4926     })
   4927 }
   4928 
   4929 fn revised_order_economics(
   4930     args: &OrderRevisionProposeArgs,
   4931     revision_id: &str,
   4932     current: &RadrootsOrderEconomics,
   4933 ) -> Result<RadrootsOrderEconomics, RuntimeError> {
   4934     let mut current_canonical = current.clone();
   4935     current_canonical.canonicalize();
   4936     let mut economics = current_canonical.clone();
   4937     let mut changed = false;
   4938     economics.quote_id = protocol_quote_id(format!("revision_{revision_id}").as_str(), "quote_id")?;
   4939     economics.quote_version = economics
   4940         .quote_version
   4941         .checked_add(1)
   4942         .ok_or_else(|| RuntimeError::Config("revision quote_version overflowed".to_owned()))?;
   4943 
   4944     if let Some(bin_id) = args.bin_id.as_deref().and_then(non_empty_ref) {
   4945         let bin_id = protocol_inventory_bin_id(bin_id, "revision bin_id")?;
   4946         let bin_count = args.bin_count.ok_or_else(|| {
   4947             RuntimeError::Config("revision bin_count is required with bin_id".to_owned())
   4948         })?;
   4949         let Some(item) = economics
   4950             .items
   4951             .iter_mut()
   4952             .find(|item| item.bin_id == bin_id)
   4953         else {
   4954             return Err(RuntimeError::Config(format!(
   4955                 "revision bin `{bin_id}` is not part of the current agreement"
   4956             )));
   4957         };
   4958         if item.bin_count != bin_count {
   4959             changed = true;
   4960         }
   4961         item.bin_count = bin_count;
   4962         item.line_subtotal = RadrootsCoreMoney::new(
   4963             item.unit_price_amount * item.quantity_amount * RadrootsCoreDecimal::from(bin_count),
   4964             item.unit_price_currency,
   4965         );
   4966     }
   4967 
   4968     if let Some(line) = revision_adjustment_line(args, economics.currency)? {
   4969         changed = true;
   4970         if economics
   4971             .adjustments
   4972             .iter()
   4973             .any(|existing| existing.id == line.id)
   4974         {
   4975             return Err(RuntimeError::Config(format!(
   4976                 "revision adjustment id `{}` already exists in current agreement economics",
   4977                 line.id
   4978             )));
   4979         }
   4980         economics.adjustments.push(line);
   4981     }
   4982 
   4983     economics.canonicalize();
   4984     economics
   4985         .validate()
   4986         .map_err(|error| RuntimeError::Config(format!("build revision economics: {error}")))?;
   4987     if !changed {
   4988         return Err(RuntimeError::Config(
   4989             "order revision propose requires a changed item count or adjustment".to_owned(),
   4990         ));
   4991     }
   4992     Ok(economics)
   4993 }
   4994 
   4995 fn revision_adjustment_line(
   4996     args: &OrderRevisionProposeArgs,
   4997     expected_currency: RadrootsCoreCurrency,
   4998 ) -> Result<Option<RadrootsOrderEconomicLine>, RuntimeError> {
   4999     let Some(id) = args.adjustment_id.as_deref().and_then(non_empty_ref) else {
   5000         return Ok(None);
   5001     };
   5002     let effect = match args
   5003         .adjustment_effect
   5004         .as_deref()
   5005         .and_then(non_empty_ref)
   5006         .ok_or_else(|| RuntimeError::Config("revision adjustment effect is required".to_owned()))?
   5007     {
   5008         "increase" => RadrootsOrderEconomicEffect::Increase,
   5009         "decrease" => RadrootsOrderEconomicEffect::Decrease,
   5010         other => {
   5011             return Err(RuntimeError::Config(format!(
   5012                 "revision adjustment effect `{other}` is invalid"
   5013             )));
   5014         }
   5015     };
   5016     let currency = parse_economics_currency(
   5017         args.adjustment_currency
   5018             .as_deref()
   5019             .and_then(non_empty_ref)
   5020             .ok_or_else(|| {
   5021                 RuntimeError::Config("revision adjustment currency is required".to_owned())
   5022             })?,
   5023         "revision_adjustment_currency",
   5024     )?;
   5025     if currency != expected_currency {
   5026         return Err(RuntimeError::Config(
   5027             "revision adjustment currency must match current agreement currency".to_owned(),
   5028         ));
   5029     }
   5030     let amount = decimal_from_adjustment(
   5031         args.adjustment_amount
   5032             .as_deref()
   5033             .and_then(non_empty_ref)
   5034             .ok_or_else(|| {
   5035                 RuntimeError::Config("revision adjustment amount is required".to_owned())
   5036             })?,
   5037         "revision_adjustment_amount",
   5038     )?;
   5039     if amount.is_zero() {
   5040         return Err(RuntimeError::Config(
   5041             "revision adjustment amount must be greater than zero".to_owned(),
   5042         ));
   5043     }
   5044     let reason = args
   5045         .adjustment_reason
   5046         .as_deref()
   5047         .and_then(non_empty_ref)
   5048         .ok_or_else(|| RuntimeError::Config("revision adjustment reason is required".to_owned()))?;
   5049     Ok(Some(RadrootsOrderEconomicLine {
   5050         id: id.to_owned(),
   5051         kind: RadrootsOrderEconomicLineKind::RevisionAdjustment,
   5052         actor: RadrootsOrderEconomicActor::Seller,
   5053         effect,
   5054         amount: RadrootsCoreMoney::new(amount, currency),
   5055         reason: reason.to_owned(),
   5056     }))
   5057 }
   5058 
   5059 fn order_revision_event_parts(
   5060     status: &OrderStatusView,
   5061     payload: &RadrootsOrderRevisionProposal,
   5062 ) -> Result<WireEventParts, RuntimeError> {
   5063     let root_event_id = status.request_event_id.as_deref().ok_or_else(|| {
   5064         RuntimeError::Config("accepted order is missing request_event_id".to_owned())
   5065     })?;
   5066     let prev_event_id = status
   5067         .last_event_id
   5068         .as_deref()
   5069         .or(status.decision_event_id.as_deref())
   5070         .ok_or_else(|| {
   5071             RuntimeError::Config("accepted order is missing previous event id".to_owned())
   5072         })?;
   5073     let root_event_id = protocol_event_id(root_event_id, "request_event_id")?;
   5074     let prev_event_id = protocol_event_id(prev_event_id, "prev_event_id")?;
   5075     if payload.root_event_id != root_event_id || payload.prev_event_id != prev_event_id {
   5076         return Err(RuntimeError::Config(
   5077             "order revision proposal payload chain does not match order status".to_owned(),
   5078         ));
   5079     }
   5080     order_revision_proposal_event_build(&root_event_id, &prev_event_id, payload).map_err(|error| {
   5081         RuntimeError::Config(format!("encode order revision proposal event: {error}"))
   5082     })
   5083 }
   5084 
   5085 fn order_revision_inventory_preflight_view(
   5086     config: &RuntimeConfig,
   5087     args: &OrderRevisionProposeArgs,
   5088     status: &OrderStatusView,
   5089     payload: &RadrootsOrderRevisionProposal,
   5090 ) -> Option<OrderRevisionProposalView> {
   5091     let issues = order_revision_inventory_issues(status, payload);
   5092     if issues.is_empty() {
   5093         return None;
   5094     }
   5095     let mut view = order_revision_invalid_view(
   5096         config,
   5097         args,
   5098         status,
   5099         "order revision propose refused because visible inventory is unavailable for the revised items",
   5100         issues,
   5101     );
   5102     apply_order_revision_payload(&mut view, payload);
   5103     Some(view)
   5104 }
   5105 
   5106 fn order_revision_inventory_issues(
   5107     status: &OrderStatusView,
   5108     payload: &RadrootsOrderRevisionProposal,
   5109 ) -> Vec<OrderIssueView> {
   5110     let Some(current) = status.economics.as_ref() else {
   5111         return vec![issue_with_code(
   5112             "revision_current_economics_missing",
   5113             "economics",
   5114             "current agreement economics are required before revision proposal",
   5115         )];
   5116     };
   5117 
   5118     let current_counts = current
   5119         .items
   5120         .iter()
   5121         .map(|item| (item.bin_id.as_str(), u64::from(item.bin_count)))
   5122         .collect::<Vec<_>>();
   5123     let mut issues = Vec::new();
   5124     for item in &payload.items {
   5125         let current_count = current_counts
   5126             .iter()
   5127             .find(|(bin_id, _)| *bin_id == item.bin_id)
   5128             .map(|(_, count)| *count)
   5129             .unwrap_or_default();
   5130         let revised_count = u64::from(item.bin_count);
   5131         if revised_count <= current_count {
   5132             continue;
   5133         }
   5134         let Some(bin) = status
   5135             .inventory
   5136             .as_ref()
   5137             .and_then(|inventory| inventory.bins.iter().find(|bin| bin.bin_id == item.bin_id))
   5138         else {
   5139             issues.push(issue_with_code(
   5140                 "revision_inventory_unavailable",
   5141                 "inventory.bin_id",
   5142                 format!(
   5143                     "inventory availability for revised bin `{}` is not visible",
   5144                     item.bin_id
   5145                 ),
   5146             ));
   5147             continue;
   5148         };
   5149         let Some(remaining_count) = bin.remaining_count else {
   5150             issues.push(issue_with_code(
   5151                 "revision_inventory_unavailable",
   5152                 "inventory.remaining_count",
   5153                 format!(
   5154                     "remaining inventory for revised bin `{}` is not visible",
   5155                     item.bin_id
   5156                 ),
   5157             ));
   5158             continue;
   5159         };
   5160         let available_for_revision = remaining_count.saturating_add(current_count);
   5161         if revised_count > available_for_revision {
   5162             issues.push(issue_with_code(
   5163                 "revision_inventory_unavailable",
   5164                 "inventory.remaining_count",
   5165                 format!(
   5166                     "revision requests {revised_count} of bin `{}`, but only {available_for_revision} are available after current reservation",
   5167                     item.bin_id
   5168                 ),
   5169             ));
   5170         }
   5171     }
   5172 
   5173     issues
   5174 }
   5175 
   5176 fn apply_order_revision_payload(
   5177     view: &mut OrderRevisionProposalView,
   5178     payload: &RadrootsOrderRevisionProposal,
   5179 ) {
   5180     view.revision_id = Some(payload.revision_id.to_string());
   5181     view.root_event_id = Some(payload.root_event_id.to_string());
   5182     view.prev_event_id = Some(payload.prev_event_id.to_string());
   5183     view.items = payload
   5184         .items
   5185         .iter()
   5186         .map(|item| OrderDraftItemView {
   5187             bin_id: item.bin_id.to_string(),
   5188             bin_count: item.bin_count,
   5189         })
   5190         .collect();
   5191     view.economics = Some(payload.economics.clone());
   5192 }
   5193 
   5194 fn apply_order_revision_decision_proposal(
   5195     view: &mut OrderRevisionDecisionView,
   5196     proposal: &OrderRevisionProposalRecord,
   5197 ) {
   5198     view.revision_id = Some(proposal.payload.revision_id.to_string());
   5199     view.root_event_id = Some(proposal.payload.root_event_id.to_string());
   5200     view.prev_event_id = Some(proposal.event_id.to_string());
   5201     view.event_id = Some(proposal.event_id.to_string());
   5202     view.event_kind = Some(KIND_ORDER_REVISION_PROPOSAL);
   5203     if view.decision.as_deref() == Some("accepted") {
   5204         view.economics = Some(proposal.payload.economics.clone());
   5205     }
   5206 }
   5207 
   5208 fn apply_order_revision_decision_payload(
   5209     view: &mut OrderRevisionDecisionView,
   5210     proposal: &OrderRevisionProposalRecord,
   5211     payload: &RadrootsOrderRevisionDecision,
   5212 ) {
   5213     view.revision_id = Some(payload.revision_id.to_string());
   5214     view.root_event_id = Some(payload.root_event_id.to_string());
   5215     view.prev_event_id = Some(payload.prev_event_id.to_string());
   5216     view.decision = Some(
   5217         match &payload.decision {
   5218             RadrootsOrderRevisionOutcome::Accepted => "accepted",
   5219             RadrootsOrderRevisionOutcome::Declined { .. } => "declined",
   5220         }
   5221         .to_owned(),
   5222     );
   5223     if matches!(payload.decision, RadrootsOrderRevisionOutcome::Accepted) {
   5224         view.agreement_event_id = view.event_id.clone();
   5225         view.economics = Some(proposal.payload.economics.clone());
   5226     }
   5227 }
   5228 
   5229 fn order_revision_decision_payload_from_proposal(
   5230     args: &OrderRevisionDecisionArgs,
   5231     proposal: &OrderRevisionProposalRecord,
   5232 ) -> Result<RadrootsOrderRevisionDecision, RuntimeError> {
   5233     let decision = match args.decision {
   5234         OrderRevisionDecisionArg::Accept => RadrootsOrderRevisionOutcome::Accepted,
   5235         OrderRevisionDecisionArg::Decline => {
   5236             let reason = args
   5237                 .reason
   5238                 .as_deref()
   5239                 .map(str::trim)
   5240                 .filter(|reason| !reason.is_empty())
   5241                 .ok_or_else(|| {
   5242                     RuntimeError::Config(
   5243                         "order revision decline requires a non-empty reason".to_owned(),
   5244                     )
   5245                 })?;
   5246             RadrootsOrderRevisionOutcome::Declined {
   5247                 reason: reason.to_owned(),
   5248             }
   5249         }
   5250     };
   5251     Ok(RadrootsOrderRevisionDecision {
   5252         revision_id: proposal.payload.revision_id.clone(),
   5253         order_id: proposal.payload.order_id.clone(),
   5254         listing_addr: proposal.payload.listing_addr.clone(),
   5255         buyer_pubkey: proposal.payload.buyer_pubkey.clone(),
   5256         seller_pubkey: proposal.payload.seller_pubkey.clone(),
   5257         root_event_id: proposal.payload.root_event_id.clone(),
   5258         prev_event_id: proposal.event_id.clone(),
   5259         decision,
   5260     })
   5261 }
   5262 
   5263 fn order_revision_decision_event_parts(
   5264     payload: &RadrootsOrderRevisionDecision,
   5265 ) -> Result<WireEventParts, RuntimeError> {
   5266     order_revision_decision_event_build(&payload.root_event_id, &payload.prev_event_id, payload)
   5267         .map_err(|error| {
   5268             RuntimeError::Config(format!("encode order revision decision event: {error}"))
   5269         })
   5270 }
   5271 
   5272 fn publish_order_revision(
   5273     config: &RuntimeConfig,
   5274     args: &OrderRevisionProposeArgs,
   5275     status: OrderStatusView,
   5276     signing: account::AccountSigningIdentity,
   5277     payload: RadrootsOrderRevisionProposal,
   5278     evidence_events: Vec<SdkRadrootsNostrEvent>,
   5279 ) -> Result<OrderRevisionProposalView, RuntimeError> {
   5280     enqueue_order_revision_proposal_via_sdk(config, args, status, signing, payload, evidence_events)
   5281         .map_err(cli_sdk_error_to_runtime)
   5282 }
   5283 
   5284 fn publish_order_revision_decision(
   5285     config: &RuntimeConfig,
   5286     args: &OrderRevisionDecisionArgs,
   5287     status: OrderStatusView,
   5288     proposal: &OrderRevisionProposalRecord,
   5289     signing: account::AccountSigningIdentity,
   5290     payload: RadrootsOrderRevisionDecision,
   5291     evidence_events: Vec<SdkRadrootsNostrEvent>,
   5292 ) -> Result<OrderRevisionDecisionView, RuntimeError> {
   5293     enqueue_order_revision_decision_via_sdk(
   5294         config,
   5295         args,
   5296         status,
   5297         proposal,
   5298         signing,
   5299         payload,
   5300         evidence_events,
   5301     )
   5302     .map_err(cli_sdk_error_to_runtime)
   5303 }
   5304 
   5305 fn publish_order_cancellation(
   5306     config: &RuntimeConfig,
   5307     args: &OrderCancelArgs,
   5308     status: OrderStatusView,
   5309     signing: account::AccountSigningIdentity,
   5310     payload: RadrootsOrderCancellation,
   5311     evidence_events: Vec<SdkRadrootsNostrEvent>,
   5312 ) -> Result<OrderCancellationView, RuntimeError> {
   5313     enqueue_order_cancellation_via_sdk(config, args, status, signing, payload, evidence_events)
   5314         .map_err(cli_sdk_error_to_runtime)
   5315 }
   5316 
   5317 fn prepare_order_revision_proposal_dry_run_via_sdk(
   5318     config: &RuntimeConfig,
   5319     signing: &account::AccountSigningIdentity,
   5320     payload: &RadrootsOrderRevisionProposal,
   5321 ) -> Result<(), RuntimeError> {
   5322     let actor = sdk_order_lifecycle_actor(signing, RadrootsActorRole::Seller, "revision")
   5323         .map_err(cli_sdk_error_to_runtime)?;
   5324     let session = CliSdkSession::connect_memory(config).map_err(cli_sdk_error_to_runtime)?;
   5325     session
   5326         .sdk()
   5327         .orders()
   5328         .prepare_revision_proposal(OrderRevisionProposalPrepareRequest::new(
   5329             actor,
   5330             sdk_order_event_ptr(&payload.root_event_id, config.relay.urls.as_slice()),
   5331             sdk_order_event_ptr(&payload.prev_event_id, config.relay.urls.as_slice()),
   5332             payload.clone(),
   5333         ))
   5334         .map(|_| ())
   5335         .map_err(|error| RuntimeError::Config(error.to_string()))
   5336 }
   5337 
   5338 fn prepare_order_revision_decision_dry_run_via_sdk(
   5339     config: &RuntimeConfig,
   5340     signing: &account::AccountSigningIdentity,
   5341     payload: &RadrootsOrderRevisionDecision,
   5342 ) -> Result<(), RuntimeError> {
   5343     let actor = sdk_order_lifecycle_actor(signing, RadrootsActorRole::Buyer, "revision decision")
   5344         .map_err(cli_sdk_error_to_runtime)?;
   5345     let session = CliSdkSession::connect_memory(config).map_err(cli_sdk_error_to_runtime)?;
   5346     session
   5347         .sdk()
   5348         .orders()
   5349         .prepare_revision_decision(OrderRevisionDecisionPrepareRequest::new(
   5350             actor,
   5351             sdk_order_event_ptr(&payload.root_event_id, config.relay.urls.as_slice()),
   5352             sdk_order_event_ptr(&payload.prev_event_id, config.relay.urls.as_slice()),
   5353             payload.clone(),
   5354         ))
   5355         .map(|_| ())
   5356         .map_err(|error| RuntimeError::Config(error.to_string()))
   5357 }
   5358 
   5359 fn prepare_order_cancellation_dry_run_via_sdk(
   5360     config: &RuntimeConfig,
   5361     signing: &account::AccountSigningIdentity,
   5362     status: &OrderStatusView,
   5363     payload: &RadrootsOrderCancellation,
   5364 ) -> Result<(), RuntimeError> {
   5365     let actor = sdk_order_lifecycle_actor(signing, RadrootsActorRole::Buyer, "cancellation")
   5366         .map_err(cli_sdk_error_to_runtime)?;
   5367     let root_event_id = protocol_event_id(
   5368         status.request_event_id.as_deref().ok_or_else(|| {
   5369             RuntimeError::Config("cancellable order is missing request_event_id".to_owned())
   5370         })?,
   5371         "request_event_id",
   5372     )?;
   5373     let previous_event_id = protocol_event_id(
   5374         order_cancellation_prev_event_id(status)
   5375             .ok_or_else(|| {
   5376                 RuntimeError::Config("cancellable order is missing previous event id".to_owned())
   5377             })?
   5378             .as_str(),
   5379         "prev_event_id",
   5380     )?;
   5381     let session = CliSdkSession::connect_memory(config).map_err(cli_sdk_error_to_runtime)?;
   5382     session
   5383         .sdk()
   5384         .orders()
   5385         .prepare_cancellation(OrderCancellationPrepareRequest::new(
   5386             actor,
   5387             sdk_order_event_ptr(&root_event_id, config.relay.urls.as_slice()),
   5388             sdk_order_event_ptr(&previous_event_id, config.relay.urls.as_slice()),
   5389             payload.clone(),
   5390         ))
   5391         .map(|_| ())
   5392         .map_err(|error| RuntimeError::Config(error.to_string()))
   5393 }
   5394 
   5395 fn enqueue_order_revision_proposal_via_sdk(
   5396     config: &RuntimeConfig,
   5397     args: &OrderRevisionProposeArgs,
   5398     status: OrderStatusView,
   5399     signing: account::AccountSigningIdentity,
   5400     payload: RadrootsOrderRevisionProposal,
   5401     evidence_events: Vec<SdkRadrootsNostrEvent>,
   5402 ) -> Result<OrderRevisionProposalView, CliSdkAdapterError> {
   5403     let target_relays = order_decision_target_relays(config)?;
   5404     let policy = order_decision_relay_url_policy(target_relays.as_slice());
   5405     let actor = sdk_order_lifecycle_actor(&signing, RadrootsActorRole::Seller, "revision")?;
   5406     let signer = sdk_signer_from_account(signing)?;
   5407     let target_policy = SdkRelayTargetPolicy::try_explicit(target_relays.clone(), policy)?;
   5408     let mut request = OrderRevisionProposalEnqueueRequest::new(
   5409         actor,
   5410         sdk_order_event_ptr(&payload.root_event_id, target_relays.as_slice()),
   5411         sdk_order_event_ptr(&payload.prev_event_id, target_relays.as_slice()),
   5412         payload.clone(),
   5413         target_policy,
   5414     );
   5415     if let Some(idempotency_key) = args.idempotency_key.as_deref() {
   5416         request = request.try_with_idempotency_key(idempotency_key)?;
   5417     }
   5418 
   5419     let session = CliSdkSession::connect(config)?;
   5420     ingest_order_evidence_events(&session, evidence_events)?;
   5421     let enqueue = session.block_on(
   5422         session
   5423             .sdk()
   5424             .orders()
   5425             .enqueue_revision_proposal_with_explicit_signer(request, &signer),
   5426     )?;
   5427     let push = push_one_sdk_outbox_event(&session, policy)?;
   5428     Ok(sdk_enqueued_order_revision_view(
   5429         config,
   5430         args,
   5431         &status,
   5432         &payload,
   5433         enqueue,
   5434         push,
   5435         target_relays,
   5436     ))
   5437 }
   5438 
   5439 fn enqueue_order_revision_decision_via_sdk(
   5440     config: &RuntimeConfig,
   5441     args: &OrderRevisionDecisionArgs,
   5442     status: OrderStatusView,
   5443     proposal: &OrderRevisionProposalRecord,
   5444     signing: account::AccountSigningIdentity,
   5445     payload: RadrootsOrderRevisionDecision,
   5446     evidence_events: Vec<SdkRadrootsNostrEvent>,
   5447 ) -> Result<OrderRevisionDecisionView, CliSdkAdapterError> {
   5448     let target_relays = order_decision_target_relays(config)?;
   5449     let policy = order_decision_relay_url_policy(target_relays.as_slice());
   5450     let actor = sdk_order_lifecycle_actor(&signing, RadrootsActorRole::Buyer, "revision decision")?;
   5451     let signer = sdk_signer_from_account(signing)?;
   5452     let target_policy = SdkRelayTargetPolicy::try_explicit(target_relays.clone(), policy)?;
   5453     let mut request = OrderRevisionDecisionEnqueueRequest::new(
   5454         actor,
   5455         sdk_order_event_ptr(&payload.root_event_id, target_relays.as_slice()),
   5456         sdk_order_event_ptr(&payload.prev_event_id, target_relays.as_slice()),
   5457         payload.clone(),
   5458         target_policy,
   5459     );
   5460     if let Some(idempotency_key) = args.idempotency_key.as_deref() {
   5461         request = request.try_with_idempotency_key(idempotency_key)?;
   5462     }
   5463 
   5464     let session = CliSdkSession::connect(config)?;
   5465     ingest_order_evidence_events(&session, evidence_events)?;
   5466     let enqueue = session.block_on(
   5467         session
   5468             .sdk()
   5469             .orders()
   5470             .enqueue_revision_decision_with_explicit_signer(request, &signer),
   5471     )?;
   5472     let push = push_one_sdk_outbox_event(&session, policy)?;
   5473     Ok(sdk_enqueued_order_revision_decision_view(
   5474         config,
   5475         args,
   5476         &status,
   5477         proposal,
   5478         &payload,
   5479         enqueue,
   5480         push,
   5481         target_relays,
   5482     ))
   5483 }
   5484 
   5485 fn enqueue_order_cancellation_via_sdk(
   5486     config: &RuntimeConfig,
   5487     args: &OrderCancelArgs,
   5488     status: OrderStatusView,
   5489     signing: account::AccountSigningIdentity,
   5490     payload: RadrootsOrderCancellation,
   5491     evidence_events: Vec<SdkRadrootsNostrEvent>,
   5492 ) -> Result<OrderCancellationView, CliSdkAdapterError> {
   5493     let root_event_id = protocol_event_id(
   5494         status.request_event_id.as_deref().ok_or_else(|| {
   5495             RuntimeError::Config("cancellable order is missing request_event_id".to_owned())
   5496         })?,
   5497         "request_event_id",
   5498     )?;
   5499     let previous_event_id = protocol_event_id(
   5500         order_cancellation_prev_event_id(&status)
   5501             .ok_or_else(|| {
   5502                 RuntimeError::Config("cancellable order is missing previous event id".to_owned())
   5503             })?
   5504             .as_str(),
   5505         "prev_event_id",
   5506     )?;
   5507     let target_relays = order_decision_target_relays(config)?;
   5508     let policy = order_decision_relay_url_policy(target_relays.as_slice());
   5509     let actor = sdk_order_lifecycle_actor(&signing, RadrootsActorRole::Buyer, "cancellation")?;
   5510     let signer = sdk_signer_from_account(signing)?;
   5511     let target_policy = SdkRelayTargetPolicy::try_explicit(target_relays.clone(), policy)?;
   5512     let mut request = OrderCancellationEnqueueRequest::new(
   5513         actor,
   5514         sdk_order_event_ptr(&root_event_id, target_relays.as_slice()),
   5515         sdk_order_event_ptr(&previous_event_id, target_relays.as_slice()),
   5516         payload,
   5517         target_policy,
   5518     );
   5519     if let Some(idempotency_key) = args.idempotency_key.as_deref() {
   5520         request = request.try_with_idempotency_key(idempotency_key)?;
   5521     }
   5522 
   5523     let session = CliSdkSession::connect(config)?;
   5524     ingest_order_evidence_events(&session, evidence_events)?;
   5525     let enqueue = session.block_on(
   5526         session
   5527             .sdk()
   5528             .orders()
   5529             .enqueue_cancellation_with_explicit_signer(request, &signer),
   5530     )?;
   5531     let push = push_one_sdk_outbox_event(&session, policy)?;
   5532     Ok(sdk_enqueued_order_cancellation_view(
   5533         config,
   5534         args,
   5535         &status,
   5536         enqueue,
   5537         push,
   5538         target_relays,
   5539     ))
   5540 }
   5541 
   5542 fn sdk_order_lifecycle_actor(
   5543     signing: &account::AccountSigningIdentity,
   5544     role: RadrootsActorRole,
   5545     workflow: &str,
   5546 ) -> Result<RadrootsActorContext, CliSdkAdapterError> {
   5547     RadrootsActorContext::local_account(
   5548         signing
   5549             .account
   5550             .record
   5551             .public_identity
   5552             .public_key_hex
   5553             .as_str(),
   5554         signing.account.record.account_id.to_string(),
   5555         [role],
   5556     )
   5557     .map_err(|error| {
   5558         RuntimeError::Config(format!("invalid order {workflow} SDK actor: {error}")).into()
   5559     })
   5560 }
   5561 
   5562 fn sdk_signer_from_account(
   5563     signing: account::AccountSigningIdentity,
   5564 ) -> Result<RadrootsLocalEventSigner, CliSdkAdapterError> {
   5565     let keys: RadrootsNostrKeys = signing.identity.into_keys();
   5566     RadrootsLocalEventSigner::new(keys)
   5567         .map_err(|error| RuntimeError::Config(error.to_string()).into())
   5568 }
   5569 
   5570 fn sdk_order_event_ptr(
   5571     event_id: &RadrootsEventId,
   5572     target_relays: &[String],
   5573 ) -> RadrootsNostrEventPtr {
   5574     RadrootsNostrEventPtr {
   5575         id: event_id.as_str().to_owned(),
   5576         relays: target_relays.first().cloned(),
   5577     }
   5578 }
   5579 
   5580 fn ingest_order_evidence_events(
   5581     session: &CliSdkSession,
   5582     events: Vec<SdkRadrootsNostrEvent>,
   5583 ) -> Result<(), CliSdkAdapterError> {
   5584     for event in events {
   5585         session.block_on(
   5586             session
   5587                 .sdk()
   5588                 .orders()
   5589                 .ingest_evidence(OrderEvidenceIngestRequest::new(event)),
   5590         )?;
   5591     }
   5592     Ok(())
   5593 }
   5594 
   5595 fn push_one_sdk_outbox_event(
   5596     session: &CliSdkSession,
   5597     policy: SdkRelayUrlPolicy,
   5598 ) -> Result<PushOutboxReceipt, CliSdkAdapterError> {
   5599     Ok(session.block_on(
   5600         session.sdk().sync().push_outbox(
   5601             PushOutboxRequest::new()
   5602                 .with_limit(1)
   5603                 .with_relay_url_policy(policy),
   5604         ),
   5605     )?)
   5606 }
   5607 
   5608 fn sdk_enqueued_order_revision_view(
   5609     config: &RuntimeConfig,
   5610     args: &OrderRevisionProposeArgs,
   5611     status: &OrderStatusView,
   5612     payload: &RadrootsOrderRevisionProposal,
   5613     enqueue: OrderRevisionProposalReceipt,
   5614     push: PushOutboxReceipt,
   5615     target_relays: Vec<String>,
   5616 ) -> OrderRevisionProposalView {
   5617     let push_event = sdk_push_event_for_event_id(&enqueue.signed_event_id, &push);
   5618     let mut view = order_revision_base_view(
   5619         config,
   5620         args,
   5621         sdk_order_lifecycle_state("proposed", push_event).as_str(),
   5622         false,
   5623     );
   5624     apply_order_revision_status(&mut view, status);
   5625     apply_order_revision_payload(&mut view, payload);
   5626     view.event_id = Some(enqueue.signed_event_id.as_str().to_owned());
   5627     view.event_kind = Some(KIND_ORDER_REVISION_PROPOSAL);
   5628     view.target_relays = push_event
   5629         .map(sdk_push_target_relays)
   5630         .unwrap_or(target_relays);
   5631     view.connected_relays = push_event
   5632         .map(sdk_push_connected_relays)
   5633         .unwrap_or_default();
   5634     view.acknowledged_relays = push_event
   5635         .map(sdk_push_acknowledged_relays)
   5636         .unwrap_or_default();
   5637     view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default();
   5638     view.reason =
   5639         sdk_order_lifecycle_reason("order revision proposal", &enqueue.workflow, push_event);
   5640     view.actions = sdk_order_lifecycle_actions(push_event);
   5641     view
   5642 }
   5643 
   5644 fn sdk_enqueued_order_revision_decision_view(
   5645     config: &RuntimeConfig,
   5646     args: &OrderRevisionDecisionArgs,
   5647     status: &OrderStatusView,
   5648     proposal: &OrderRevisionProposalRecord,
   5649     payload: &RadrootsOrderRevisionDecision,
   5650     enqueue: OrderRevisionDecisionReceipt,
   5651     push: PushOutboxReceipt,
   5652     target_relays: Vec<String>,
   5653 ) -> OrderRevisionDecisionView {
   5654     let push_event = sdk_push_event_for_event_id(&enqueue.signed_event_id, &push);
   5655     let success_state = match payload.decision {
   5656         RadrootsOrderRevisionOutcome::Accepted => "accepted",
   5657         RadrootsOrderRevisionOutcome::Declined { .. } => "declined",
   5658     };
   5659     let mut view = order_revision_decision_base_view(
   5660         config,
   5661         args,
   5662         sdk_order_lifecycle_state(success_state, push_event).as_str(),
   5663         false,
   5664     );
   5665     apply_order_revision_decision_status(&mut view, status);
   5666     apply_order_revision_decision_payload(&mut view, proposal, payload);
   5667     view.event_id = Some(enqueue.signed_event_id.as_str().to_owned());
   5668     view.event_kind = Some(KIND_ORDER_REVISION_DECISION);
   5669     if matches!(payload.decision, RadrootsOrderRevisionOutcome::Accepted) {
   5670         view.agreement_event_id = Some(enqueue.signed_event_id.as_str().to_owned());
   5671     }
   5672     view.target_relays = push_event
   5673         .map(sdk_push_target_relays)
   5674         .unwrap_or(target_relays);
   5675     view.connected_relays = push_event
   5676         .map(sdk_push_connected_relays)
   5677         .unwrap_or_default();
   5678     view.acknowledged_relays = push_event
   5679         .map(sdk_push_acknowledged_relays)
   5680         .unwrap_or_default();
   5681     view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default();
   5682     view.reason =
   5683         sdk_order_lifecycle_reason("order revision decision", &enqueue.workflow, push_event);
   5684     view.actions = sdk_order_lifecycle_actions(push_event);
   5685     view
   5686 }
   5687 
   5688 fn sdk_enqueued_order_cancellation_view(
   5689     config: &RuntimeConfig,
   5690     args: &OrderCancelArgs,
   5691     status: &OrderStatusView,
   5692     enqueue: OrderCancellationReceipt,
   5693     push: PushOutboxReceipt,
   5694     target_relays: Vec<String>,
   5695 ) -> OrderCancellationView {
   5696     let push_event = sdk_push_event_for_event_id(&enqueue.signed_event_id, &push);
   5697     let mut view = order_cancellation_base_view(
   5698         config,
   5699         args,
   5700         sdk_order_lifecycle_state("cancelled", push_event).as_str(),
   5701         false,
   5702     );
   5703     apply_order_cancellation_status(&mut view, status);
   5704     view.event_id = Some(enqueue.signed_event_id.as_str().to_owned());
   5705     view.event_kind = Some(KIND_ORDER_CANCELLATION);
   5706     view.target_relays = push_event
   5707         .map(sdk_push_target_relays)
   5708         .unwrap_or(target_relays);
   5709     view.connected_relays = push_event
   5710         .map(sdk_push_connected_relays)
   5711         .unwrap_or_default();
   5712     view.acknowledged_relays = push_event
   5713         .map(sdk_push_acknowledged_relays)
   5714         .unwrap_or_default();
   5715     view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default();
   5716     view.reason = sdk_order_lifecycle_reason("order cancellation", &enqueue.workflow, push_event);
   5717     view.actions = sdk_order_lifecycle_actions(push_event);
   5718     view
   5719 }
   5720 
   5721 fn sdk_push_event_for_event_id<'a>(
   5722     event_id: &RadrootsEventId,
   5723     push: &'a PushOutboxReceipt,
   5724 ) -> Option<&'a PushOutboxEventReceipt> {
   5725     push.events.iter().find(|event| event.event_id == *event_id)
   5726 }
   5727 
   5728 fn sdk_order_lifecycle_state(
   5729     published_state: &str,
   5730     push_event: Option<&PushOutboxEventReceipt>,
   5731 ) -> String {
   5732     match push_event.map(|event| event.final_state) {
   5733         Some(PushOutboxEventState::Published) => published_state,
   5734         Some(PushOutboxEventState::PublishRetryable | PushOutboxEventState::FailedTerminal) => {
   5735             "unavailable"
   5736         }
   5737         Some(_) | None => "queued",
   5738     }
   5739     .to_owned()
   5740 }
   5741 
   5742 fn sdk_order_lifecycle_reason(
   5743     workflow: &str,
   5744     enqueue: &OrderWorkflowEnqueueReceipt,
   5745     push_event: Option<&PushOutboxEventReceipt>,
   5746 ) -> Option<String> {
   5747     match push_event.map(|event| event.final_state) {
   5748         Some(PushOutboxEventState::Published) => None,
   5749         Some(PushOutboxEventState::PublishRetryable) => Some(format!(
   5750             "{}; SDK relay publish for {workflow} did not reach accepted quorum; outbox event remains retryable; {}",
   5751             sdk_order_enqueue_summary(enqueue),
   5752             sdk_order_enqueue_retry_summary(enqueue)
   5753         )),
   5754         Some(PushOutboxEventState::FailedTerminal) => Some(format!(
   5755             "{}; SDK relay publish for {workflow} failed terminally; {}",
   5756             sdk_order_enqueue_summary(enqueue),
   5757             sdk_order_enqueue_retry_summary(enqueue)
   5758         )),
   5759         Some(state) => Some(format!(
   5760             "{}; SDK relay push for {workflow} left event in state `{state:?}`; {}",
   5761             sdk_order_enqueue_summary(enqueue),
   5762             sdk_order_enqueue_retry_summary(enqueue)
   5763         )),
   5764         None => Some(format!(
   5765             "{}; {workflow} queued in SDK outbox; no ready SDK outbox event was pushed; {}",
   5766             sdk_order_enqueue_summary(enqueue),
   5767             sdk_order_enqueue_retry_summary(enqueue)
   5768         )),
   5769     }
   5770 }
   5771 
   5772 fn sdk_order_lifecycle_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<String> {
   5773     if !matches!(
   5774         push_event.map(|event| event.final_state),
   5775         Some(PushOutboxEventState::Published)
   5776     ) {
   5777         return sdk_order_push_recovery_actions();
   5778     }
   5779     Vec::new()
   5780 }
   5781 
   5782 fn sdk_order_enqueue_summary(enqueue: &OrderWorkflowEnqueueReceipt) -> String {
   5783     format!(
   5784         "local SDK enqueued `{}` as `{}` with outbox_event_id {}; {}",
   5785         enqueue.operation_kind,
   5786         sdk_mutation_state_label(&enqueue.state),
   5787         enqueue.outbox_event_id,
   5788         sdk_order_idempotency_summary(enqueue)
   5789     )
   5790 }
   5791 
   5792 fn sdk_order_idempotency_summary(enqueue: &OrderWorkflowEnqueueReceipt) -> &'static str {
   5793     if enqueue.idempotency.replayed_existing_operation {
   5794         "idempotency replayed an existing queued operation"
   5795     } else if enqueue.idempotency.safe_to_retry_with_same_idempotency_key {
   5796         "same idempotency key remains retry-safe"
   5797     } else {
   5798         "same idempotency key retry safety is unavailable"
   5799     }
   5800 }
   5801 
   5802 fn sdk_order_enqueue_retry_summary(enqueue: &OrderWorkflowEnqueueReceipt) -> &'static str {
   5803     if enqueue
   5804         .retry
   5805         .safe_to_retry_enqueue_with_same_idempotency_key
   5806     {
   5807         "enqueue is safe to retry with the same idempotency key"
   5808     } else if enqueue.retry.retryable_after_error {
   5809         "inspect local SDK state before retrying enqueue"
   5810     } else {
   5811         "do not retry enqueue before inspecting local SDK state"
   5812     }
   5813 }
   5814 
   5815 fn sdk_mutation_state_label(state: &SdkMutationState) -> &'static str {
   5816     match state {
   5817         SdkMutationState::StoredAndQueued => "stored_and_queued",
   5818         SdkMutationState::AlreadyQueued => "already_queued",
   5819         _ => "unknown",
   5820     }
   5821 }
   5822 
   5823 fn sdk_order_push_recovery_actions() -> Vec<String> {
   5824     vec![
   5825         "radroots sync push".to_owned(),
   5826         "radroots sync status get".to_owned(),
   5827     ]
   5828 }
   5829 
   5830 fn order_actor_write_binding_error_parts(
   5831     error: ActorWriteBindingError,
   5832 ) -> (String, String, Vec<String>) {
   5833     (
   5834         "unconfigured".to_owned(),
   5835         error.reason(),
   5836         vec!["run radroots signer status get".to_owned()],
   5837     )
   5838 }
   5839 
   5840 fn order_revision_binding_error_view(
   5841     config: &RuntimeConfig,
   5842     args: &OrderRevisionProposeArgs,
   5843     status: &OrderStatusView,
   5844     error: ActorWriteBindingError,
   5845 ) -> OrderRevisionProposalView {
   5846     let (state, reason, actions) = order_actor_write_binding_error_parts(error);
   5847     let mut view = order_revision_base_view(config, args, state.as_str(), config.output.dry_run);
   5848     apply_order_revision_status(&mut view, status);
   5849     view.reason = Some(reason);
   5850     view.actions = actions;
   5851     view
   5852 }
   5853 
   5854 fn order_revision_decision_binding_error_view(
   5855     config: &RuntimeConfig,
   5856     args: &OrderRevisionDecisionArgs,
   5857     status: &OrderStatusView,
   5858     error: ActorWriteBindingError,
   5859 ) -> OrderRevisionDecisionView {
   5860     let (state, reason, actions) = order_actor_write_binding_error_parts(error);
   5861     let mut view =
   5862         order_revision_decision_base_view(config, args, state.as_str(), config.output.dry_run);
   5863     apply_order_revision_decision_status(&mut view, status);
   5864     view.reason = Some(reason);
   5865     view.actions = actions;
   5866     view
   5867 }
   5868 
   5869 fn order_cancellation_binding_error_view(
   5870     config: &RuntimeConfig,
   5871     args: &OrderCancelArgs,
   5872     status: &OrderStatusView,
   5873     error: ActorWriteBindingError,
   5874 ) -> OrderCancellationView {
   5875     let (state, reason, actions) = order_actor_write_binding_error_parts(error);
   5876     let mut view =
   5877         order_cancellation_base_view(config, args, state.as_str(), config.output.dry_run);
   5878     apply_order_cancellation_status(&mut view, status);
   5879     view.reason = Some(reason);
   5880     view.actions = actions;
   5881     view
   5882 }
   5883 
   5884 fn seller_order_request_resolution_from_receipt(
   5885     seller_pubkey: &str,
   5886     order_id: &str,
   5887     receipt: DirectRelayFetchReceipt,
   5888 ) -> Result<SellerOrderRequestResolution, RuntimeError> {
   5889     let DirectRelayFetchReceipt {
   5890         target_relays,
   5891         connected_relays,
   5892         failed_relays,
   5893         events,
   5894     } = receipt;
   5895     let fetched_count = events.len();
   5896     let mut skipped_count = 0usize;
   5897     let mut decoded_count = 0usize;
   5898     let mut requests = Vec::new();
   5899     let mut candidate_issues = Vec::new();
   5900     let candidate_context = OrderRequestCandidateContext {
   5901         order_id,
   5902         seller_pubkey: Some(seller_pubkey),
   5903     };
   5904 
   5905     for event in events {
   5906         if !order_request_candidate_matches(&event, candidate_context) {
   5907             skipped_count += 1;
   5908             continue;
   5909         }
   5910         let event_id = event.id.to_string();
   5911         match seller_order_request_from_event(&event, seller_pubkey, order_id) {
   5912             Ok(request) => {
   5913                 decoded_count += 1;
   5914                 requests.push(request);
   5915             }
   5916             Err(error) => {
   5917                 skipped_count += 1;
   5918                 candidate_issues.push(issue_with_events(
   5919                     "invalid_request_candidate",
   5920                     "request_event_id",
   5921                     format!("request event `{event_id}` failed seller decision preflight: {error}"),
   5922                     vec![event_id],
   5923                 ));
   5924             }
   5925         }
   5926     }
   5927 
   5928     requests.sort_by(|left, right| left.request_event_id.cmp(&right.request_event_id));
   5929     candidate_issues.sort_by(|left, right| left.message.cmp(&right.message));
   5930 
   5931     Ok(SellerOrderRequestResolution {
   5932         target_relays,
   5933         connected_relays,
   5934         failed_relays,
   5935         fetched_count,
   5936         decoded_count,
   5937         skipped_count,
   5938         requests,
   5939         candidate_issues,
   5940     })
   5941 }
   5942 
   5943 fn event_matches_tag_value(event: &RadrootsNostrEvent, key: &str, value: &str) -> bool {
   5944     event.tags.iter().any(|tag| {
   5945         let values = tag.as_slice();
   5946         values.first().map(String::as_str) == Some(key)
   5947             && values.get(1).map(String::as_str) == Some(value)
   5948     })
   5949 }
   5950 
   5951 fn seller_order_request_from_event(
   5952     event: &RadrootsNostrEvent,
   5953     seller_pubkey: &str,
   5954     order_id: &str,
   5955 ) -> Result<ResolvedSellerOrderRequest, RuntimeError> {
   5956     let event_kind = event_kind_u32(event);
   5957     if event_kind != KIND_ORDER_REQUEST {
   5958         return Err(RuntimeError::Config(format!(
   5959             "order decision received unexpected kind `{event_kind}`"
   5960         )));
   5961     }
   5962 
   5963     let request_event = radroots_event_from_nostr(event);
   5964     let event_id = protocol_event_id(request_event.id.as_str(), "request_event_id")?;
   5965     let seller_protocol_pubkey = protocol_pubkey(seller_pubkey, "seller_pubkey")?;
   5966     let envelope = order_request_from_event(&request_event)
   5967         .map_err(|error| RuntimeError::Config(format!("decode order request event: {error}")))?;
   5968     let context =
   5969         order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &request_event.tags)
   5970             .map_err(|error| RuntimeError::Config(format!("decode order request tags: {error}")))?;
   5971 
   5972     if envelope.order_id.to_string() != order_id
   5973         || envelope.payload.order_id.to_string() != order_id
   5974     {
   5975         return Err(RuntimeError::Config(
   5976             "order request does not match requested order id".to_owned(),
   5977         ));
   5978     }
   5979     if context.counterparty_pubkey != seller_protocol_pubkey
   5980         || envelope.payload.seller_pubkey != seller_protocol_pubkey
   5981     {
   5982         return Err(RuntimeError::Config(
   5983             "order request is not targeted at the selected seller".to_owned(),
   5984         ));
   5985     }
   5986     let listing_addr =
   5987         parse_listing_addr(envelope.payload.listing_addr.as_str()).map_err(|error| {
   5988             RuntimeError::Config(format!("order request listing_addr is invalid: {error}"))
   5989         })?;
   5990     if listing_addr.seller_pubkey != seller_pubkey {
   5991         return Err(RuntimeError::Config(
   5992             "order request listing address is outside selected seller authority".to_owned(),
   5993         ));
   5994     }
   5995     let listing_event_id = context.listing_event.as_ref().map(|event| event.id.clone());
   5996 
   5997     Ok(ResolvedSellerOrderRequest {
   5998         request_event,
   5999         request_event_id: event_id,
   6000         listing_event_id,
   6001         order_id: envelope.payload.order_id,
   6002         listing_addr: envelope.payload.listing_addr,
   6003         buyer_pubkey: envelope.payload.buyer_pubkey,
   6004         seller_pubkey: envelope.payload.seller_pubkey,
   6005         items: envelope.payload.items,
   6006         economics: envelope.payload.economics,
   6007     })
   6008 }
   6009 
   6010 fn publish_order_decision(
   6011     config: &RuntimeConfig,
   6012     args: &OrderDecisionArgs,
   6013     request: ResolvedSellerOrderRequest,
   6014     resolution: SellerOrderRequestResolution,
   6015     signing: account::AccountSigningIdentity,
   6016     payload: RadrootsOrderDecision,
   6017     inventory: Option<OrderInventoryView>,
   6018 ) -> Result<OrderDecisionView, RuntimeError> {
   6019     let input = sdk_order_decision_input(config, &request, &signing, payload)
   6020         .map_err(cli_sdk_error_to_runtime)?;
   6021     enqueue_order_decision_via_sdk(config, args, request, resolution, signing, input, inventory)
   6022         .map_err(cli_sdk_error_to_runtime)
   6023 }
   6024 
   6025 fn sdk_order_decision_input(
   6026     config: &RuntimeConfig,
   6027     request: &ResolvedSellerOrderRequest,
   6028     signing: &account::AccountSigningIdentity,
   6029     payload: RadrootsOrderDecision,
   6030 ) -> Result<SdkOrderDecisionInput, CliSdkAdapterError> {
   6031     let actor = RadrootsActorContext::local_account(
   6032         signing
   6033             .account
   6034             .record
   6035             .public_identity
   6036             .public_key_hex
   6037             .as_str(),
   6038         signing.account.record.account_id.to_string(),
   6039         [RadrootsActorRole::Seller],
   6040     )
   6041     .map_err(|error| RuntimeError::Config(format!("invalid order decision SDK actor: {error}")))?;
   6042     let target_relays = order_decision_target_relays(config)?;
   6043     Ok(SdkOrderDecisionInput {
   6044         actor,
   6045         request_event: request.request_event.clone(),
   6046         request_event_ptr: order_decision_request_event_ptr(request, target_relays.as_slice()),
   6047         decision: payload,
   6048         target_relays,
   6049     })
   6050 }
   6051 
   6052 #[derive(Debug, Clone)]
   6053 struct SdkOrderDecisionInput {
   6054     actor: RadrootsActorContext,
   6055     request_event: SdkRadrootsNostrEvent,
   6056     request_event_ptr: RadrootsNostrEventPtr,
   6057     decision: RadrootsOrderDecision,
   6058     target_relays: Vec<String>,
   6059 }
   6060 
   6061 fn order_decision_request_event_ptr(
   6062     request: &ResolvedSellerOrderRequest,
   6063     target_relays: &[String],
   6064 ) -> RadrootsNostrEventPtr {
   6065     RadrootsNostrEventPtr {
   6066         id: request.request_event_id.as_str().to_owned(),
   6067         relays: target_relays.first().cloned(),
   6068     }
   6069 }
   6070 
   6071 fn order_decision_target_relays(config: &RuntimeConfig) -> Result<Vec<String>, RuntimeError> {
   6072     let target_relays = normalize_listing_relay_set(config.relay.urls.iter())
   6073         .map_err(|error| RuntimeError::Config(format!("configured relay target: {error}")))?;
   6074     if target_relays.is_empty() {
   6075         return Err(RuntimeError::Config(
   6076             "order decision requires at least one configured relay".to_owned(),
   6077         ));
   6078     }
   6079     Ok(target_relays)
   6080 }
   6081 
   6082 fn order_decision_relay_url_policy(target_relays: &[String]) -> SdkRelayUrlPolicy {
   6083     if target_relays
   6084         .iter()
   6085         .any(|relay_url| relay_url.starts_with("ws://"))
   6086     {
   6087         SdkRelayUrlPolicy::Localhost
   6088     } else {
   6089         SdkRelayUrlPolicy::Public
   6090     }
   6091 }
   6092 
   6093 fn enqueue_order_decision_via_sdk(
   6094     config: &RuntimeConfig,
   6095     args: &OrderDecisionArgs,
   6096     request_context: ResolvedSellerOrderRequest,
   6097     resolution: SellerOrderRequestResolution,
   6098     signing: account::AccountSigningIdentity,
   6099     input: SdkOrderDecisionInput,
   6100     inventory: Option<OrderInventoryView>,
   6101 ) -> Result<OrderDecisionView, CliSdkAdapterError> {
   6102     let target_relays = input.target_relays.clone();
   6103     let policy = order_decision_relay_url_policy(target_relays.as_slice());
   6104     let target_policy = SdkRelayTargetPolicy::try_explicit(target_relays.clone(), policy)?;
   6105     let mut request = OrderDecisionEnqueueRequest::new(
   6106         input.actor,
   6107         input.request_event_ptr,
   6108         input.decision,
   6109         target_policy,
   6110     );
   6111     if let Some(idempotency_key) = args.idempotency_key.as_deref() {
   6112         request = request.try_with_idempotency_key(idempotency_key)?;
   6113     }
   6114 
   6115     let session = CliSdkSession::connect(config)?;
   6116     session.block_on(
   6117         session
   6118             .sdk()
   6119             .orders()
   6120             .ingest_request_evidence(OrderRequestEvidenceIngestRequest::new(input.request_event)),
   6121     )?;
   6122     let keys: RadrootsNostrKeys = signing.identity.into_keys();
   6123     let signer = RadrootsLocalEventSigner::new(keys)
   6124         .map_err(|error| RuntimeError::Config(error.to_string()))?;
   6125     let enqueue = session.block_on(
   6126         session
   6127             .sdk()
   6128             .orders()
   6129             .enqueue_decision_with_explicit_signer(request, &signer),
   6130     )?;
   6131     let push = session.block_on(
   6132         session.sdk().sync().push_outbox(
   6133             PushOutboxRequest::new()
   6134                 .with_limit(1)
   6135                 .with_relay_url_policy(policy),
   6136         ),
   6137     )?;
   6138     Ok(sdk_enqueued_order_decision_view(
   6139         config,
   6140         args,
   6141         request_context,
   6142         resolution,
   6143         enqueue,
   6144         push,
   6145         target_relays,
   6146         inventory,
   6147     ))
   6148 }
   6149 
   6150 fn cli_sdk_error_to_runtime(error: CliSdkAdapterError) -> RuntimeError {
   6151     match error {
   6152         CliSdkAdapterError::Runtime(error) => error,
   6153         CliSdkAdapterError::Sdk(error) => RuntimeError::Config(error.to_string()),
   6154     }
   6155 }
   6156 
   6157 fn canonical_order_decision_payload(
   6158     args: &OrderDecisionArgs,
   6159     request: &ResolvedSellerOrderRequest,
   6160     signer_pubkey: &str,
   6161 ) -> Result<RadrootsOrderDecision, RuntimeError> {
   6162     let payload = order_decision_payload_from_request(args, request)?;
   6163     canonicalize_order_decision_for_signer(payload, signer_pubkey)
   6164         .map_err(|error| RuntimeError::Config(format!("canonicalize order decision: {error}")))
   6165 }
   6166 
   6167 fn order_decision_payload_from_request(
   6168     args: &OrderDecisionArgs,
   6169     request: &ResolvedSellerOrderRequest,
   6170 ) -> Result<RadrootsOrderDecision, RuntimeError> {
   6171     match args.decision {
   6172         OrderDecisionArg::Accept => Ok(accepted_order_decision_payload_from_request(request)),
   6173         OrderDecisionArg::Decline => {
   6174             let reason = args
   6175                 .reason
   6176                 .as_deref()
   6177                 .map(str::trim)
   6178                 .filter(|reason| !reason.is_empty())
   6179                 .ok_or_else(|| {
   6180                     RuntimeError::Config("order decline requires a non-empty reason".to_owned())
   6181                 })?;
   6182             Ok(declined_order_decision_payload_from_request(
   6183                 request, reason,
   6184             ))
   6185         }
   6186     }
   6187 }
   6188 
   6189 fn accepted_order_decision_payload_from_request(
   6190     request: &ResolvedSellerOrderRequest,
   6191 ) -> RadrootsOrderDecision {
   6192     RadrootsOrderDecision {
   6193         order_id: request.order_id.clone(),
   6194         listing_addr: request.listing_addr.clone(),
   6195         buyer_pubkey: request.buyer_pubkey.clone(),
   6196         seller_pubkey: request.seller_pubkey.clone(),
   6197         decision: RadrootsOrderDecisionOutcome::Accepted {
   6198             inventory_commitments: request
   6199                 .items
   6200                 .iter()
   6201                 .map(|item| RadrootsOrderInventoryCommitment {
   6202                     bin_id: item.bin_id.clone(),
   6203                     bin_count: item.bin_count,
   6204                 })
   6205                 .collect(),
   6206         },
   6207     }
   6208 }
   6209 
   6210 fn declined_order_decision_payload_from_request(
   6211     request: &ResolvedSellerOrderRequest,
   6212     reason: &str,
   6213 ) -> RadrootsOrderDecision {
   6214     RadrootsOrderDecision {
   6215         order_id: request.order_id.clone(),
   6216         listing_addr: request.listing_addr.clone(),
   6217         buyer_pubkey: request.buyer_pubkey.clone(),
   6218         seller_pubkey: request.seller_pubkey.clone(),
   6219         decision: RadrootsOrderDecisionOutcome::Declined {
   6220             reason: reason.to_owned(),
   6221         },
   6222     }
   6223 }
   6224 
   6225 fn sdk_enqueued_order_decision_view(
   6226     config: &RuntimeConfig,
   6227     args: &OrderDecisionArgs,
   6228     request: ResolvedSellerOrderRequest,
   6229     resolution: SellerOrderRequestResolution,
   6230     enqueue: OrderDecisionReceipt,
   6231     push: PushOutboxReceipt,
   6232     target_relays: Vec<String>,
   6233     inventory: Option<OrderInventoryView>,
   6234 ) -> OrderDecisionView {
   6235     let push_event = sdk_push_event_for_order_decision(&enqueue, &push);
   6236     let mut view = order_decision_base_view(
   6237         config,
   6238         args,
   6239         sdk_order_decision_state(args.decision, push_event).as_str(),
   6240         false,
   6241     );
   6242     apply_order_decision_request(&mut view, &request);
   6243     view.event_id = Some(enqueue.signed_event_id.as_str().to_owned());
   6244     view.event_kind = Some(KIND_ORDER_DECISION);
   6245     view.target_relays = push_event
   6246         .map(sdk_push_target_relays)
   6247         .unwrap_or(target_relays);
   6248     view.connected_relays = push_event
   6249         .map(sdk_push_connected_relays)
   6250         .unwrap_or_default();
   6251     view.acknowledged_relays = push_event
   6252         .map(sdk_push_acknowledged_relays)
   6253         .unwrap_or_default();
   6254     view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default();
   6255     view.fetched_count = resolution.fetched_count;
   6256     view.decoded_count = resolution.decoded_count;
   6257     view.skipped_count = resolution.skipped_count;
   6258     view.inventory = order_decision_inventory_for_view(args, &request, inventory);
   6259     view.reason = sdk_order_decision_reason(&enqueue.workflow, push_event);
   6260     view.actions = sdk_order_decision_actions(push_event);
   6261     view
   6262 }
   6263 
   6264 fn sdk_push_event_for_order_decision<'a>(
   6265     enqueue: &OrderDecisionReceipt,
   6266     push: &'a PushOutboxReceipt,
   6267 ) -> Option<&'a PushOutboxEventReceipt> {
   6268     push.events
   6269         .iter()
   6270         .find(|event| event.event_id == enqueue.signed_event_id)
   6271 }
   6272 
   6273 fn sdk_order_decision_state(
   6274     decision: OrderDecisionArg,
   6275     push_event: Option<&PushOutboxEventReceipt>,
   6276 ) -> String {
   6277     match push_event.map(|event| event.final_state) {
   6278         Some(PushOutboxEventState::Published) => decision.as_str(),
   6279         Some(PushOutboxEventState::PublishRetryable | PushOutboxEventState::FailedTerminal) => {
   6280             "unavailable"
   6281         }
   6282         Some(_) | None => "queued",
   6283     }
   6284     .to_owned()
   6285 }
   6286 
   6287 fn sdk_order_decision_reason(
   6288     enqueue: &OrderWorkflowEnqueueReceipt,
   6289     push_event: Option<&PushOutboxEventReceipt>,
   6290 ) -> Option<String> {
   6291     match push_event.map(|event| event.final_state) {
   6292         Some(PushOutboxEventState::Published) => None,
   6293         Some(PushOutboxEventState::PublishRetryable) => Some(format!(
   6294             "{}; SDK relay publish did not reach accepted quorum; outbox event remains retryable; {}",
   6295             sdk_order_enqueue_summary(enqueue),
   6296             sdk_order_enqueue_retry_summary(enqueue)
   6297         )),
   6298         Some(PushOutboxEventState::FailedTerminal) => Some(format!(
   6299             "{}; SDK relay publish failed terminally; {}",
   6300             sdk_order_enqueue_summary(enqueue),
   6301             sdk_order_enqueue_retry_summary(enqueue)
   6302         )),
   6303         Some(state) => Some(format!(
   6304             "{}; SDK relay push left event in state `{state:?}`; {}",
   6305             sdk_order_enqueue_summary(enqueue),
   6306             sdk_order_enqueue_retry_summary(enqueue)
   6307         )),
   6308         None => Some(format!(
   6309             "{}; order decision queued in SDK outbox; no ready SDK outbox event was pushed; {}",
   6310             sdk_order_enqueue_summary(enqueue),
   6311             sdk_order_enqueue_retry_summary(enqueue)
   6312         )),
   6313     }
   6314 }
   6315 
   6316 fn sdk_order_decision_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<String> {
   6317     if !matches!(
   6318         push_event.map(|event| event.final_state),
   6319         Some(PushOutboxEventState::Published)
   6320     ) {
   6321         return sdk_order_push_recovery_actions();
   6322     }
   6323     Vec::new()
   6324 }
   6325 
   6326 fn order_decision_binding_error_view(
   6327     config: &RuntimeConfig,
   6328     args: &OrderDecisionArgs,
   6329     request: ResolvedSellerOrderRequest,
   6330     resolution: SellerOrderRequestResolution,
   6331     error: ActorWriteBindingError,
   6332 ) -> OrderDecisionView {
   6333     let (state, reason, actions) = order_actor_write_binding_error_parts(error);
   6334     let mut view = order_decision_base_view(config, args, state.as_str(), config.output.dry_run);
   6335     apply_order_decision_resolution(&mut view, &resolution);
   6336     apply_order_decision_request(&mut view, &request);
   6337     view.reason = Some(reason);
   6338     view.actions = actions;
   6339     view
   6340 }
   6341 
   6342 fn order_event_list_entry_from_event(
   6343     event: &RadrootsNostrEvent,
   6344     seller_pubkey: &str,
   6345 ) -> Result<OrderEventListEntryView, RuntimeError> {
   6346     let event_kind = event_kind_u32(event);
   6347     if event_kind != KIND_ORDER_REQUEST {
   6348         return Err(RuntimeError::Config(format!(
   6349             "order event list received unexpected kind `{event_kind}`"
   6350         )));
   6351     }
   6352 
   6353     let event = radroots_event_from_nostr(event);
   6354     let envelope = order_request_from_event(&event)
   6355         .map_err(|error| RuntimeError::Config(format!("decode order request event: {error}")))?;
   6356     let context =
   6357         order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags)
   6358             .map_err(|error| RuntimeError::Config(format!("decode order request tags: {error}")))?;
   6359 
   6360     if context.counterparty_pubkey != seller_pubkey
   6361         || envelope.payload.seller_pubkey != seller_pubkey
   6362     {
   6363         return Err(RuntimeError::Config(
   6364             "order request is not targeted at the selected seller".to_owned(),
   6365         ));
   6366     }
   6367 
   6368     let listing_event_id = context.listing_event.as_ref().map(|event| event.id.clone());
   6369     let created_at_unix = u64::from(event.created_at);
   6370 
   6371     Ok(OrderEventListEntryView {
   6372         id: envelope.order_id.clone(),
   6373         state: "requested".to_owned(),
   6374         event_id: Some(event.id),
   6375         event_kind: Some(event.kind),
   6376         listing_lookup: None,
   6377         listing_addr: Some(envelope.listing_addr),
   6378         listing_event_id,
   6379         buyer_account_id: None,
   6380         buyer_pubkey: Some(envelope.payload.buyer_pubkey.to_string()),
   6381         seller_pubkey: Some(envelope.payload.seller_pubkey.to_string()),
   6382         item_count: Some(envelope.payload.items.len()),
   6383         created_at_unix: Some(created_at_unix),
   6384         submitted_at_unix: Some(created_at_unix),
   6385         updated_at_unix: created_at_unix,
   6386         job: None,
   6387         workflow: None,
   6388         issues: Vec::new(),
   6389     })
   6390 }
   6391 
   6392 fn order_request_filter(
   6393     seller_pubkey: &str,
   6394     order_id: Option<&str>,
   6395 ) -> Result<RadrootsNostrFilter, RuntimeError> {
   6396     let filter = RadrootsNostrFilter::new()
   6397         .kind(radroots_nostr_kind(KIND_ORDER_REQUEST as u16))
   6398         .limit(1_000);
   6399     let filter = radroots_nostr_filter_tag(filter, "p", vec![seller_pubkey.to_owned()])
   6400         .map_err(|error| RuntimeError::Config(format!("build order event filter: {error}")))?;
   6401     if let Some(order_id) = order_id {
   6402         return radroots_nostr_filter_tag(filter, "d", vec![order_id.to_owned()])
   6403             .map_err(|error| RuntimeError::Config(format!("build order event filter: {error}")));
   6404     }
   6405     Ok(filter)
   6406 }
   6407 
   6408 fn listing_event_filter(
   6409     listing_addr: &ParsedListingAddress,
   6410 ) -> Result<RadrootsNostrFilter, RuntimeError> {
   6411     let filter = RadrootsNostrFilter::new()
   6412         .kind(radroots_nostr_kind(KIND_LISTING as u16))
   6413         .limit(100);
   6414     radroots_nostr_filter_tag(filter, "d", vec![listing_addr.listing_id.clone()])
   6415         .map_err(|error| RuntimeError::Config(format!("build listing event filter: {error}")))
   6416 }
   6417 
   6418 fn order_listing_request_filter(
   6419     seller_pubkey: &str,
   6420     listing_addr: &str,
   6421 ) -> Result<RadrootsNostrFilter, RuntimeError> {
   6422     let filter = RadrootsNostrFilter::new()
   6423         .kind(radroots_nostr_kind(KIND_ORDER_REQUEST as u16))
   6424         .limit(1_000);
   6425     let filter = radroots_nostr_filter_tag(filter, "p", vec![seller_pubkey.to_owned()])
   6426         .map_err(|error| RuntimeError::Config(format!("build order request filter: {error}")))?;
   6427     radroots_nostr_filter_tag(filter, "a", vec![listing_addr.to_owned()])
   6428         .map_err(|error| RuntimeError::Config(format!("build order request filter: {error}")))
   6429 }
   6430 
   6431 fn order_listing_decision_filter(listing_addr: &str) -> Result<RadrootsNostrFilter, RuntimeError> {
   6432     let filter = RadrootsNostrFilter::new()
   6433         .kind(radroots_nostr_kind(KIND_ORDER_DECISION as u16))
   6434         .limit(1_000);
   6435     radroots_nostr_filter_tag(filter, "a", vec![listing_addr.to_owned()])
   6436         .map_err(|error| RuntimeError::Config(format!("build order decision filter: {error}")))
   6437 }
   6438 
   6439 fn order_listing_revision_proposal_filter(
   6440     listing_addr: &str,
   6441 ) -> Result<RadrootsNostrFilter, RuntimeError> {
   6442     let filter = RadrootsNostrFilter::new()
   6443         .kind(radroots_nostr_kind(KIND_ORDER_REVISION_PROPOSAL as u16))
   6444         .limit(1_000);
   6445     radroots_nostr_filter_tag(filter, "a", vec![listing_addr.to_owned()])
   6446         .map_err(|error| RuntimeError::Config(format!("build revision proposal filter: {error}")))
   6447 }
   6448 
   6449 fn order_listing_revision_decision_filter(
   6450     listing_addr: &str,
   6451 ) -> Result<RadrootsNostrFilter, RuntimeError> {
   6452     let filter = RadrootsNostrFilter::new()
   6453         .kind(radroots_nostr_kind(KIND_ORDER_REVISION_DECISION as u16))
   6454         .limit(1_000);
   6455     radroots_nostr_filter_tag(filter, "a", vec![listing_addr.to_owned()])
   6456         .map_err(|error| RuntimeError::Config(format!("build revision decision filter: {error}")))
   6457 }
   6458 
   6459 fn order_listing_cancellation_filter(
   6460     listing_addr: &str,
   6461 ) -> Result<RadrootsNostrFilter, RuntimeError> {
   6462     let filter = RadrootsNostrFilter::new()
   6463         .kind(radroots_nostr_kind(KIND_ORDER_CANCELLATION as u16))
   6464         .limit(1_000);
   6465     radroots_nostr_filter_tag(filter, "a", vec![listing_addr.to_owned()])
   6466         .map_err(|error| RuntimeError::Config(format!("build cancellation filter: {error}")))
   6467 }
   6468 
   6469 fn order_status_filter(order_id: &str) -> Result<RadrootsNostrFilter, RuntimeError> {
   6470     let filter = RadrootsNostrFilter::new()
   6471         .kinds([
   6472             radroots_nostr_kind(KIND_ORDER_REQUEST as u16),
   6473             radroots_nostr_kind(KIND_ORDER_DECISION as u16),
   6474             radroots_nostr_kind(KIND_ORDER_REVISION_PROPOSAL as u16),
   6475             radroots_nostr_kind(KIND_ORDER_REVISION_DECISION as u16),
   6476             radroots_nostr_kind(KIND_ORDER_CANCELLATION as u16),
   6477         ])
   6478         .limit(1_000);
   6479     radroots_nostr_filter_tag(filter, "d", vec![order_id.to_owned()])
   6480         .map_err(|error| RuntimeError::Config(format!("build order status filter: {error}")))
   6481 }
   6482 
   6483 fn event_kind_u32(event: &RadrootsNostrEvent) -> u32 {
   6484     u32::from(event.kind.as_u16())
   6485 }
   6486 
   6487 fn order_evidence_from_relay_events(events: &[RadrootsNostrEvent]) -> Vec<SdkRadrootsNostrEvent> {
   6488     events.iter().map(radroots_event_from_nostr).collect()
   6489 }
   6490 
   6491 fn validate_scaffold_args(args: &OrderDraftCreateArgs) -> Result<(), RuntimeError> {
   6492     match (normalize_optional(args.bin_id.as_deref()), args.bin_count) {
   6493         (None, Some(_)) => Err(RuntimeError::Config(
   6494             "`--qty` requires `--bin` when creating an order draft".to_owned(),
   6495         )),
   6496         (Some(_), Some(0)) => Err(RuntimeError::Config(
   6497             "`--qty` must be greater than zero".to_owned(),
   6498         )),
   6499         (Some(_), None) | (Some(_), Some(_)) | (None, None) => Ok(()),
   6500     }
   6501 }
   6502 
   6503 fn resolve_order_listing(
   6504     config: &RuntimeConfig,
   6505     listing_lookup: Option<&str>,
   6506     explicit_listing_addr: Option<&str>,
   6507 ) -> Result<Option<ResolvedOrderListing>, RuntimeError> {
   6508     if let Some(listing_addr) = explicit_listing_addr {
   6509         let parsed = parse_listing_addr(listing_addr).map_err(|error| {
   6510             RuntimeError::Config(format!("explicit listing_addr is invalid: {error}"))
   6511         })?;
   6512         if parsed.kind != KIND_LISTING {
   6513             return Err(RuntimeError::Config(
   6514                 "explicit listing_addr must reference a public NIP-99 listing".to_owned(),
   6515             ));
   6516         }
   6517         let replica_listing_event_id =
   6518             resolve_active_listing_event_id(config, listing_addr, &parsed)?;
   6519         let shared_provenance = resolve_shared_signed_listing_provenance(
   6520             config,
   6521             listing_addr,
   6522             replica_listing_event_id.as_deref(),
   6523         )?;
   6524         let listing_event_id = replica_listing_event_id
   6525             .or_else(|| {
   6526                 shared_provenance
   6527                     .as_ref()
   6528                     .map(|provenance| provenance.event_id.clone())
   6529             })
   6530             .unwrap_or_default();
   6531         let listing_relays = listing_provenance_relays(
   6532             config,
   6533             listing_event_id.as_str(),
   6534             shared_provenance.as_ref(),
   6535         )?;
   6536         let economics_product = resolve_trade_product_by_listing_addr(config, listing_addr)?;
   6537         return Ok(Some(ResolvedOrderListing {
   6538             listing_addr: listing_addr.to_owned(),
   6539             listing_event_id,
   6540             listing_relays,
   6541             seller_pubkey: parsed.seller_pubkey,
   6542             economics_product,
   6543         }));
   6544     }
   6545 
   6546     let Some(listing_lookup) = listing_lookup else {
   6547         return Ok(None);
   6548     };
   6549 
   6550     if !config.local.replica_db_path.exists() {
   6551         return Err(RuntimeError::Config(format!(
   6552             "order listing lookup `{listing_lookup}` requires local market data; run `radroots store init` and `radroots market refresh` before creating an order from a listing"
   6553         )));
   6554     }
   6555 
   6556     let db = ReplicaSql::new(SqliteExecutor::open(&config.local.replica_db_path)?);
   6557     let rows = db.trade_product_lookup(listing_lookup)?;
   6558     match rows.len() {
   6559         0 => Err(RuntimeError::Config(format!(
   6560             "listing `{listing_lookup}` is not available in the local replica; run `radroots market refresh` or pass `--listing-addr`"
   6561         ))),
   6562         1 => {
   6563             let row = rows.into_iter().next().expect("one row");
   6564             let economics_product = ResolvedOrderEconomicsProduct::from_summary(&row);
   6565             let listing_addr = normalize_optional(row.listing_addr.as_deref()).ok_or_else(|| {
   6566                 RuntimeError::Config(format!(
   6567                     "listing `{listing_lookup}` is missing a canonical listing address; run `radroots market refresh` or pass `--listing-addr`"
   6568                 ))
   6569             })?;
   6570             let parsed = parse_listing_addr(listing_addr.as_str()).map_err(|error| {
   6571                 RuntimeError::Config(format!(
   6572                     "listing `{listing_lookup}` has invalid listing_addr: {error}; run `radroots market refresh` or pass `--listing-addr`"
   6573                 ))
   6574             })?;
   6575             if parsed.kind != KIND_LISTING {
   6576                 return Err(RuntimeError::Config(format!(
   6577                     "listing `{listing_lookup}` listing_addr must reference a public NIP-99 listing; run `radroots market refresh` or pass `--listing-addr`"
   6578                 )));
   6579             }
   6580 
   6581             let listing_event_id = resolve_active_listing_event_id(
   6582                 config,
   6583                 listing_addr.as_str(),
   6584                 &parsed,
   6585             )?
   6586             .ok_or_else(|| {
   6587                 RuntimeError::Config(format!(
   6588                     "listing `{listing_lookup}` is missing the latest listing event pointer; run `radroots market refresh` before creating an order from this listing"
   6589                 ))
   6590             })?;
   6591             let shared_provenance = resolve_shared_signed_listing_provenance(
   6592                 config,
   6593                 listing_addr.as_str(),
   6594                 Some(listing_event_id.as_str()),
   6595             )?;
   6596             let listing_relays = listing_provenance_relays(
   6597                 config,
   6598                 listing_event_id.as_str(),
   6599                 shared_provenance.as_ref(),
   6600             )?;
   6601 
   6602             Ok(Some(ResolvedOrderListing {
   6603                 listing_addr,
   6604                 listing_event_id,
   6605                 listing_relays,
   6606                 seller_pubkey: parsed.seller_pubkey,
   6607                 economics_product: Some(economics_product),
   6608             }))
   6609         }
   6610         count => Err(RuntimeError::Config(format!(
   6611             "listing lookup `{listing_lookup}` matched {count} local listings; use a unique product key or pass `--listing-addr`"
   6612         ))),
   6613     }
   6614 }
   6615 
   6616 fn resolve_trade_product_by_listing_addr(
   6617     config: &RuntimeConfig,
   6618     listing_addr: &str,
   6619 ) -> Result<Option<ResolvedOrderEconomicsProduct>, RuntimeError> {
   6620     if !config.local.replica_db_path.exists() {
   6621         return Ok(None);
   6622     }
   6623 
   6624     let executor = SqliteExecutor::open(&config.local.replica_db_path)?;
   6625     let product_rows = trade_product::find_many(
   6626         &executor,
   6627         &ITradeProductFindMany {
   6628             filter: Some(trade_product_listing_addr_filter(listing_addr)),
   6629         },
   6630     )
   6631     .map_err(|error| RuntimeError::Config(format!("resolve listing product state: {error:?}")))?
   6632     .results;
   6633 
   6634     match product_rows.len() {
   6635         0 => Ok(None),
   6636         1 => Ok(product_rows
   6637             .into_iter()
   6638             .next()
   6639             .map(ResolvedOrderEconomicsProduct::from_product)),
   6640         count => Err(RuntimeError::Config(format!(
   6641             "listing address `{listing_addr}` matched {count} active local listing rows"
   6642         ))),
   6643     }
   6644 }
   6645 
   6646 fn resolve_active_listing_event_id(
   6647     config: &RuntimeConfig,
   6648     listing_addr: &str,
   6649     parsed: &ParsedListingAddress,
   6650 ) -> Result<Option<String>, RuntimeError> {
   6651     if !config.local.replica_db_path.exists() {
   6652         return Ok(None);
   6653     }
   6654 
   6655     let executor = SqliteExecutor::open(&config.local.replica_db_path)?;
   6656     let product_rows = trade_product::find_many(
   6657         &executor,
   6658         &ITradeProductFindMany {
   6659             filter: Some(trade_product_listing_addr_filter(listing_addr)),
   6660         },
   6661     )
   6662     .map_err(|error| RuntimeError::Config(format!("resolve listing product state: {error:?}")))?
   6663     .results;
   6664 
   6665     match product_rows.len() {
   6666         0 => return Ok(None),
   6667         1 => {}
   6668         count => {
   6669             return Err(RuntimeError::Config(format!(
   6670                 "listing address `{listing_addr}` matched {count} active local listing rows"
   6671             )));
   6672         }
   6673     }
   6674 
   6675     let key = format!(
   6676         "{}:{}:{}",
   6677         parsed.kind, parsed.seller_pubkey, parsed.listing_id
   6678     );
   6679     let state = nostr_event_head::find_one(
   6680         &executor,
   6681         &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs {
   6682             on: NostrEventHeadQueryBindValues::Key { key },
   6683         }),
   6684     )
   6685     .map_err(|error| RuntimeError::Config(format!("resolve listing event state: {error:?}")))?
   6686     .result;
   6687 
   6688     let Some(state) = state else {
   6689         return Ok(None);
   6690     };
   6691     if !is_valid_event_id(state.last_event_id.as_str()) {
   6692         return Err(RuntimeError::Config(format!(
   6693             "listing address `{listing_addr}` has invalid latest listing event id in local replica"
   6694         )));
   6695     }
   6696 
   6697     Ok(Some(state.last_event_id))
   6698 }
   6699 
   6700 #[derive(Debug, Clone)]
   6701 struct SharedListingProvenance {
   6702     event_id: String,
   6703     relays: Vec<String>,
   6704 }
   6705 
   6706 fn listing_provenance_relays(
   6707     config: &RuntimeConfig,
   6708     listing_event_id: &str,
   6709     shared_provenance: Option<&SharedListingProvenance>,
   6710 ) -> Result<Vec<String>, RuntimeError> {
   6711     let mut relays = Vec::<String>::new();
   6712     if let Some(provenance) = shared_provenance
   6713         && provenance.event_id == listing_event_id
   6714     {
   6715         relays.extend(provenance.relays.iter().cloned());
   6716     }
   6717     relays.extend(relay_provenance_relays_for_scope(
   6718         config,
   6719         RelayIngestScope::MarketRefresh,
   6720     )?);
   6721     normalize_listing_relay_set(relays)
   6722         .map_err(|error| RuntimeError::Config(format!("listing provenance relays: {error}")))
   6723 }
   6724 
   6725 fn resolve_shared_signed_listing_provenance(
   6726     config: &RuntimeConfig,
   6727     listing_addr: &str,
   6728     listing_event_id: Option<&str>,
   6729 ) -> Result<Option<SharedListingProvenance>, RuntimeError> {
   6730     let mut candidates = list_shared_records_latest(config, ORDER_APP_RECORD_LIST_LIMIT)?
   6731         .into_iter()
   6732         .filter(|record| record.family == LocalRecordFamily::SignedEvent)
   6733         .filter(|record| record.status == LocalRecordStatus::Published)
   6734         .filter(|record| record.event_kind == Some(i64::from(KIND_LISTING)))
   6735         .filter(|record| record.listing_addr.as_deref() == Some(listing_addr))
   6736         .filter(|record| {
   6737             listing_event_id.is_none() || record.event_id.as_deref() == listing_event_id
   6738         })
   6739         .filter_map(|record| {
   6740             let event_id = record.event_id?;
   6741             if !is_valid_event_id(event_id.as_str()) {
   6742                 return None;
   6743             }
   6744             let delivery = record.relay_delivery_json.as_ref()?;
   6745             let evidence = RelayDeliveryEvidence::from_json_value(delivery).ok()?;
   6746             let relays = listing_provenance_relays_from_delivery_evidence(evidence).ok()?;
   6747             if relays.is_empty() {
   6748                 return None;
   6749             }
   6750             Some(SharedListingProvenance { event_id, relays })
   6751         })
   6752         .collect::<Vec<_>>();
   6753     candidates.sort_by(|left, right| left.event_id.cmp(&right.event_id));
   6754     candidates.dedup_by(|left, right| left.event_id == right.event_id);
   6755     if candidates.len() > 1 && listing_event_id.is_none() {
   6756         return Err(RuntimeError::Config(format!(
   6757             "listing address `{listing_addr}` has multiple published shared local listing events; run `radroots market refresh` or pass a current listing event id source"
   6758         )));
   6759     }
   6760     Ok(candidates.pop())
   6761 }
   6762 
   6763 fn listing_provenance_relays_from_delivery_evidence(
   6764     evidence: RelayDeliveryEvidence,
   6765 ) -> Result<Vec<String>, String> {
   6766     let relays = match evidence.state {
   6767         RelayDeliveryState::Acknowledged => evidence.acknowledged_relays,
   6768         RelayDeliveryState::Observed => evidence.observed_relays,
   6769         RelayDeliveryState::Pending | RelayDeliveryState::Failed => Vec::new(),
   6770     };
   6771     normalize_listing_relay_set(relays)
   6772 }
   6773 
   6774 fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsFilter {
   6775     ITradeProductFieldsFilter {
   6776         id: None,
   6777         created_at: None,
   6778         updated_at: None,
   6779         key: None,
   6780         category: None,
   6781         title: None,
   6782         summary: None,
   6783         process: None,
   6784         lot: None,
   6785         profile: None,
   6786         year: None,
   6787         qty_amt: None,
   6788         qty_amt_exact: None,
   6789         qty_unit: None,
   6790         qty_label: None,
   6791         qty_avail: None,
   6792         price_amt: None,
   6793         price_amt_exact: None,
   6794         price_currency: None,
   6795         price_qty_amt: None,
   6796         price_qty_amt_exact: None,
   6797         price_qty_unit: None,
   6798         listing_addr: Some(listing_addr.to_owned()),
   6799         primary_bin_id: None,
   6800         verified_primary_bin_id: None,
   6801         notes: None,
   6802     }
   6803 }
   6804 
   6805 fn order_economics_from_resolved_listing(
   6806     order_id: &str,
   6807     resolved_listing: Option<&ResolvedOrderListing>,
   6808     items: &[OrderDraftItem],
   6809     adjustments: &[crate::cli::global::OrderDraftAdjustmentArgs],
   6810 ) -> Result<Option<RadrootsOrderEconomics>, RuntimeError> {
   6811     let Some(listing) = resolved_listing else {
   6812         return Ok(None);
   6813     };
   6814     let Some(product) = listing.economics_product.as_ref() else {
   6815         return Ok(None);
   6816     };
   6817     let Some(primary_bin_id) = product.primary_bin_id.as_deref().and_then(non_empty_ref) else {
   6818         return Ok(None);
   6819     };
   6820     let Some(verified_primary_bin_id) = product
   6821         .verified_primary_bin_id
   6822         .as_deref()
   6823         .and_then(non_empty_ref)
   6824     else {
   6825         return Err(RuntimeError::Config(format!(
   6826             "listing_primary_bin_invalid: listing `{}` primary bin `{primary_bin_id}` is not verified in the current local replica",
   6827             listing.listing_addr
   6828         )));
   6829     };
   6830     if verified_primary_bin_id != primary_bin_id {
   6831         return Err(RuntimeError::Config(format!(
   6832             "listing_primary_bin_invalid: listing `{}` primary bin `{primary_bin_id}` does not match verified primary bin `{verified_primary_bin_id}` in the current local replica",
   6833             listing.listing_addr
   6834         )));
   6835     }
   6836     if items.is_empty()
   6837         || items
   6838             .iter()
   6839             .any(|item| item.bin_id.as_str() != primary_bin_id)
   6840     {
   6841         return Ok(None);
   6842     }
   6843 
   6844     let currency = parse_economics_currency(product.price_currency.as_str(), "price_currency")?;
   6845     let quantity_amount =
   6846         exact_non_negative_decimal(product.qty_amt_exact.as_deref(), "qty_amt_exact")?;
   6847     let quantity_unit = parse_economics_unit(product.qty_unit.as_str(), "qty_unit")?;
   6848     let price_amount =
   6849         exact_non_negative_decimal(product.price_amt_exact.as_deref(), "price_amt_exact")?;
   6850     let price_quantity_amount = exact_positive_decimal(
   6851         product.price_qty_amt_exact.as_deref(),
   6852         "price_qty_amt_exact",
   6853     )?;
   6854     let price_unit = parse_economics_unit(product.price_qty_unit.as_str(), "price_qty_unit")?;
   6855     let quantity_unit_in_price_units =
   6856         convert_unit_decimal(RadrootsCoreDecimal::ONE, quantity_unit, price_unit).map_err(
   6857             |error| {
   6858                 RuntimeError::Config(format!(
   6859                     "listing quantity unit and price unit are incompatible: {error}"
   6860                 ))
   6861             },
   6862         )?;
   6863     let unit_price_amount = (price_amount / price_quantity_amount) * quantity_unit_in_price_units;
   6864 
   6865     let mut subtotal_amount = RadrootsCoreDecimal::ZERO;
   6866     let mut economic_items = Vec::with_capacity(items.len());
   6867     for item in items {
   6868         let line_amount =
   6869             unit_price_amount * quantity_amount * RadrootsCoreDecimal::from(item.bin_count);
   6870         subtotal_amount = subtotal_amount + line_amount;
   6871         economic_items.push(RadrootsOrderEconomicItem {
   6872             bin_id: protocol_inventory_bin_id(item.bin_id.as_str(), "order item bin_id")?,
   6873             bin_count: item.bin_count,
   6874             quantity_amount,
   6875             quantity_unit,
   6876             unit_price_amount,
   6877             unit_price_currency: currency,
   6878             line_subtotal: RadrootsCoreMoney::new(line_amount, currency),
   6879         });
   6880     }
   6881 
   6882     let subtotal = RadrootsCoreMoney::new(subtotal_amount, currency);
   6883     let discounts = listing_discount_lines_from_product(
   6884         product,
   6885         &subtotal,
   6886         items,
   6887         quantity_amount,
   6888         quantity_unit,
   6889     )?;
   6890     let adjustments = basket_adjustment_lines(adjustments)?;
   6891     let zero = RadrootsCoreMoney::zero(currency);
   6892     let mut economics = RadrootsOrderEconomics {
   6893         quote_id: protocol_quote_id(format!("quote_{order_id}").as_str(), "quote_id")?,
   6894         quote_version: 1,
   6895         pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
   6896         currency,
   6897         items: economic_items,
   6898         discounts,
   6899         adjustments,
   6900         subtotal: subtotal.clone(),
   6901         discount_total: zero.clone(),
   6902         adjustment_total: zero,
   6903         total: subtotal,
   6904     };
   6905     economics.canonicalize();
   6906     economics
   6907         .validate()
   6908         .map_err(|error| RuntimeError::Config(format!("build order economics: {error}")))?;
   6909     Ok(Some(economics))
   6910 }
   6911 
   6912 fn listing_discount_lines_from_product(
   6913     product: &ResolvedOrderEconomicsProduct,
   6914     subtotal: &RadrootsCoreMoney,
   6915     items: &[OrderDraftItem],
   6916     quantity_amount: RadrootsCoreDecimal,
   6917     quantity_unit: RadrootsCoreUnit,
   6918 ) -> Result<Vec<RadrootsOrderEconomicLine>, RuntimeError> {
   6919     let Some(notes) = product.notes.as_deref().and_then(non_empty_ref) else {
   6920         return Ok(Vec::new());
   6921     };
   6922     let parsed = serde_json::from_str::<ResolvedTradeProductNotes>(notes).map_err(|error| {
   6923         RuntimeError::Config(format!("listing discount metadata is invalid: {error}"))
   6924     })?;
   6925     let mut lines = Vec::new();
   6926     for (index, discount) in parsed.listing_discounts.iter().enumerate() {
   6927         if !discount_applies(discount, items, quantity_amount, quantity_unit)? {
   6928             continue;
   6929         }
   6930         let amount = listing_discount_amount(discount, subtotal, items)?;
   6931         if amount.is_zero() {
   6932             return Err(RuntimeError::Config(
   6933                 "listing discount amount must be greater than zero".to_owned(),
   6934             ));
   6935         }
   6936         lines.push(RadrootsOrderEconomicLine {
   6937             id: format!("listing_discount_{}", index + 1),
   6938             kind: RadrootsOrderEconomicLineKind::ListingDiscount,
   6939             actor: RadrootsOrderEconomicActor::Seller,
   6940             effect: RadrootsOrderEconomicEffect::Decrease,
   6941             amount,
   6942             reason: format!("listing discount {}", index + 1),
   6943         });
   6944     }
   6945     Ok(lines)
   6946 }
   6947 
   6948 fn discount_applies(
   6949     discount: &RadrootsCoreDiscount,
   6950     items: &[OrderDraftItem],
   6951     quantity_amount: RadrootsCoreDecimal,
   6952     quantity_unit: RadrootsCoreUnit,
   6953 ) -> Result<bool, RuntimeError> {
   6954     match &discount.threshold {
   6955         RadrootsCoreDiscountThreshold::BinCount { bin_id, min } => Ok(items
   6956             .iter()
   6957             .any(|item| item.bin_id == *bin_id && item.bin_count >= *min)),
   6958         RadrootsCoreDiscountThreshold::OrderQuantity { min } => {
   6959             let requested = items.iter().fold(RadrootsCoreDecimal::ZERO, |total, item| {
   6960                 total + quantity_amount * RadrootsCoreDecimal::from(item.bin_count)
   6961             });
   6962             let converted =
   6963                 convert_unit_decimal(requested, quantity_unit, min.unit).map_err(|error| {
   6964                     RuntimeError::Config(format!(
   6965                         "listing discount quantity threshold is incompatible: {error}"
   6966                     ))
   6967                 })?;
   6968             Ok(converted >= min.amount)
   6969         }
   6970     }
   6971 }
   6972 
   6973 fn listing_discount_amount(
   6974     discount: &RadrootsCoreDiscount,
   6975     subtotal: &RadrootsCoreMoney,
   6976     items: &[OrderDraftItem],
   6977 ) -> Result<RadrootsCoreMoney, RuntimeError> {
   6978     match &discount.value {
   6979         RadrootsCoreDiscountValue::Percent(percent) => Ok(percent.of_money(subtotal)),
   6980         RadrootsCoreDiscountValue::MoneyPerBin(money) => {
   6981             if money.currency != subtotal.currency {
   6982                 return Err(RuntimeError::Config(
   6983                     "listing discount currency must match listing price currency".to_owned(),
   6984                 ));
   6985             }
   6986             let multiplier = match &discount.scope {
   6987                 RadrootsCoreDiscountScope::Bin => {
   6988                     items.iter().map(|item| item.bin_count).sum::<u32>().max(1)
   6989                 }
   6990                 RadrootsCoreDiscountScope::OrderTotal => 1,
   6991             };
   6992             Ok(money.mul_decimal(RadrootsCoreDecimal::from(multiplier)))
   6993         }
   6994     }
   6995 }
   6996 
   6997 fn basket_adjustment_lines(
   6998     adjustments: &[crate::cli::global::OrderDraftAdjustmentArgs],
   6999 ) -> Result<Vec<RadrootsOrderEconomicLine>, RuntimeError> {
   7000     adjustments
   7001         .iter()
   7002         .map(|adjustment| {
   7003             let currency =
   7004                 parse_economics_currency(adjustment.currency.as_str(), "adjustment_currency")?;
   7005             let amount = decimal_from_adjustment(adjustment.amount.as_str(), "adjustment_amount")?;
   7006             if amount.is_zero() {
   7007                 return Err(RuntimeError::Config(
   7008                     "basket adjustment amount must be greater than zero".to_owned(),
   7009                 ));
   7010             }
   7011             let effect = match adjustment.effect.as_str() {
   7012                 "increase" => RadrootsOrderEconomicEffect::Increase,
   7013                 "decrease" => RadrootsOrderEconomicEffect::Decrease,
   7014                 other => {
   7015                     return Err(RuntimeError::Config(format!(
   7016                         "basket adjustment effect `{other}` is invalid"
   7017                     )));
   7018                 }
   7019             };
   7020             if adjustment.id.trim().is_empty() {
   7021                 return Err(RuntimeError::Config(
   7022                     "basket adjustment id must not be empty".to_owned(),
   7023                 ));
   7024             }
   7025             if adjustment.reason.trim().is_empty() {
   7026                 return Err(RuntimeError::Config(
   7027                     "basket adjustment reason must not be empty".to_owned(),
   7028                 ));
   7029             }
   7030             Ok(RadrootsOrderEconomicLine {
   7031                 id: adjustment.id.trim().to_owned(),
   7032                 kind: RadrootsOrderEconomicLineKind::BasketAdjustment,
   7033                 actor: RadrootsOrderEconomicActor::Buyer,
   7034                 effect,
   7035                 amount: RadrootsCoreMoney::new(amount, currency),
   7036                 reason: adjustment.reason.trim().to_owned(),
   7037             })
   7038         })
   7039         .collect()
   7040 }
   7041 
   7042 fn parse_economics_currency(
   7043     value: &str,
   7044     field: &str,
   7045 ) -> Result<RadrootsCoreCurrency, RuntimeError> {
   7046     value
   7047         .parse::<RadrootsCoreCurrency>()
   7048         .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
   7049 }
   7050 
   7051 fn parse_economics_unit(value: &str, field: &str) -> Result<RadrootsCoreUnit, RuntimeError> {
   7052     value
   7053         .parse::<RadrootsCoreUnit>()
   7054         .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
   7055 }
   7056 
   7057 fn exact_non_negative_decimal(
   7058     value: Option<&str>,
   7059     field: &str,
   7060 ) -> Result<RadrootsCoreDecimal, RuntimeError> {
   7061     let parsed = exact_decimal(value, field)?;
   7062     if parsed.is_sign_negative() {
   7063         return Err(RuntimeError::Config(format!(
   7064             "listing {field} must be non-negative"
   7065         )));
   7066     }
   7067     Ok(parsed)
   7068 }
   7069 
   7070 fn exact_positive_decimal(
   7071     value: Option<&str>,
   7072     field: &str,
   7073 ) -> Result<RadrootsCoreDecimal, RuntimeError> {
   7074     let parsed = exact_non_negative_decimal(value, field)?;
   7075     if parsed.is_zero() {
   7076         return Err(RuntimeError::Config(format!(
   7077             "listing {field} must be greater than zero"
   7078         )));
   7079     }
   7080     Ok(parsed)
   7081 }
   7082 
   7083 fn exact_decimal(value: Option<&str>, field: &str) -> Result<RadrootsCoreDecimal, RuntimeError> {
   7084     let Some(value) = value.and_then(non_empty_ref) else {
   7085         return Err(RuntimeError::Config(format!(
   7086             "listing {field} exact source is missing"
   7087         )));
   7088     };
   7089     value
   7090         .parse::<RadrootsCoreDecimal>()
   7091         .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
   7092 }
   7093 
   7094 fn decimal_from_adjustment(value: &str, field: &str) -> Result<RadrootsCoreDecimal, RuntimeError> {
   7095     let parsed = value
   7096         .trim()
   7097         .parse::<RadrootsCoreDecimal>()
   7098         .map_err(|error| RuntimeError::Config(format!("basket {field} is invalid: {error}")))?;
   7099     if parsed.is_sign_negative() {
   7100         return Err(RuntimeError::Config(format!(
   7101             "basket {field} must be non-negative"
   7102         )));
   7103     }
   7104     Ok(parsed)
   7105 }
   7106 
   7107 fn view_from_loaded(
   7108     config: &RuntimeConfig,
   7109     loaded: LoadedOrderDraft,
   7110 ) -> Result<OrderGetView, RuntimeError> {
   7111     view_from_loaded_with_source_issues(config, loaded, &[])
   7112 }
   7113 
   7114 fn view_from_loaded_with_source_issues(
   7115     config: &RuntimeConfig,
   7116     loaded: LoadedOrderDraft,
   7117     source_issues: &[OrderIssueView],
   7118 ) -> Result<OrderGetView, RuntimeError> {
   7119     let OrderInspection {
   7120         state,
   7121         ready_for_submit,
   7122         listing_addr,
   7123         listing_event_id,
   7124         seller_pubkey,
   7125         buyer_custody,
   7126         buyer_write_capable,
   7127         issues,
   7128     } = inspect_document_with_source_issues(config, &loaded.document, source_issues)?;
   7129 
   7130     let actions = actions_for_document(&loaded.document, loaded.file.as_path(), issues.as_slice());
   7131 
   7132     Ok(OrderGetView {
   7133         state,
   7134         source: ORDER_SOURCE.to_owned(),
   7135         lookup: loaded.document.order.order_id.clone(),
   7136         order_id: Some(loaded.document.order.order_id.clone()),
   7137         file: Some(loaded.file.display().to_string()),
   7138         listing_lookup: loaded.document.listing_lookup.clone(),
   7139         listing_addr,
   7140         listing_event_id,
   7141         listing_relays: order_listing_relays(&loaded.document),
   7142         buyer_account_id: buyer_account_id(&loaded.document),
   7143         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   7144         buyer_actor_source: buyer_actor_source(&loaded.document),
   7145         buyer_custody,
   7146         buyer_write_capable,
   7147         seller_pubkey,
   7148         ready_for_submit,
   7149         items: loaded
   7150             .document
   7151             .order
   7152             .items
   7153             .iter()
   7154             .map(|item| OrderDraftItemView {
   7155                 bin_id: item.bin_id.clone(),
   7156                 bin_count: item.bin_count,
   7157             })
   7158             .collect(),
   7159         economics: loaded.document.order.economics.clone(),
   7160         updated_at_unix: Some(loaded.updated_at_unix),
   7161         job: None,
   7162         workflow: None,
   7163         reason: None,
   7164         issues,
   7165         actions,
   7166     })
   7167 }
   7168 
   7169 fn summary_from_loaded(
   7170     config: &RuntimeConfig,
   7171     loaded: &LoadedOrderDraft,
   7172 ) -> Result<OrderSummaryView, RuntimeError> {
   7173     summary_from_loaded_with_source_issues(config, loaded, &[])
   7174 }
   7175 
   7176 fn summary_from_loaded_with_source_issues(
   7177     config: &RuntimeConfig,
   7178     loaded: &LoadedOrderDraft,
   7179     source_issues: &[OrderIssueView],
   7180 ) -> Result<OrderSummaryView, RuntimeError> {
   7181     let OrderInspection {
   7182         state,
   7183         ready_for_submit,
   7184         listing_addr,
   7185         listing_event_id,
   7186         seller_pubkey: _,
   7187         buyer_custody,
   7188         buyer_write_capable,
   7189         issues,
   7190     } = inspect_document_with_source_issues(config, &loaded.document, source_issues)?;
   7191 
   7192     Ok(OrderSummaryView {
   7193         id: loaded.document.order.order_id.clone(),
   7194         state,
   7195         ready_for_submit,
   7196         file: loaded.file.display().to_string(),
   7197         listing_lookup: loaded.document.listing_lookup.clone(),
   7198         listing_addr,
   7199         listing_event_id,
   7200         listing_relays: order_listing_relays(&loaded.document),
   7201         buyer_account_id: buyer_account_id(&loaded.document),
   7202         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   7203         buyer_actor_source: buyer_actor_source(&loaded.document),
   7204         buyer_custody,
   7205         buyer_write_capable,
   7206         item_count: loaded.document.order.items.len(),
   7207         economics: loaded.document.order.economics.clone(),
   7208         updated_at_unix: loaded.updated_at_unix,
   7209         job: None,
   7210         issues,
   7211     })
   7212 }
   7213 
   7214 fn summary_for_invalid_file(path: &Path, reason: String) -> OrderSummaryView {
   7215     let id = path
   7216         .file_stem()
   7217         .and_then(|value| value.to_str())
   7218         .unwrap_or("unknown")
   7219         .to_owned();
   7220     OrderSummaryView {
   7221         id,
   7222         state: "error".to_owned(),
   7223         ready_for_submit: false,
   7224         file: path.display().to_string(),
   7225         listing_lookup: None,
   7226         listing_addr: None,
   7227         listing_event_id: None,
   7228         listing_relays: Vec::new(),
   7229         buyer_account_id: None,
   7230         buyer_pubkey: None,
   7231         buyer_actor_source: None,
   7232         buyer_custody: None,
   7233         buyer_write_capable: None,
   7234         item_count: 0,
   7235         economics: None,
   7236         updated_at_unix: modified_unix(path).unwrap_or_default(),
   7237         job: None,
   7238         issues: vec![issue_with_code("invalid_order_draft", "draft", reason)],
   7239     }
   7240 }
   7241 
   7242 fn app_order_local_records(config: &RuntimeConfig) -> Result<Vec<LocalEventRecord>, RuntimeError> {
   7243     let mut app_records = Vec::new();
   7244     let mut before_cursor = None::<(i64, i64)>;
   7245     loop {
   7246         let shared_records = if let Some((before_change_seq, before_seq)) = before_cursor {
   7247             list_shared_records_before(
   7248                 config,
   7249                 before_change_seq,
   7250                 before_seq,
   7251                 ORDER_APP_RECORD_LIST_LIMIT,
   7252             )?
   7253         } else {
   7254             list_shared_records_latest(config, ORDER_APP_RECORD_LIST_LIMIT)?
   7255         };
   7256         let Some(next_cursor) = shared_records
   7257             .last()
   7258             .map(|record| (record.change_seq, record.seq))
   7259         else {
   7260             break;
   7261         };
   7262         let has_more = shared_records.len() == ORDER_APP_RECORD_LIST_LIMIT as usize;
   7263         app_records.extend(shared_records.into_iter().filter(is_app_order_local_record));
   7264         if !has_more {
   7265             break;
   7266         }
   7267         before_cursor = Some(next_cursor);
   7268     }
   7269     Ok(app_records)
   7270 }
   7271 
   7272 fn is_app_order_local_record(record: &LocalEventRecord) -> bool {
   7273     record.source_runtime == SourceRuntime::App
   7274         && record.family == LocalRecordFamily::LocalWork
   7275         && record.status == LocalRecordStatus::LocalSaved
   7276         && local_record_kind(record).as_deref() == Some(BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND)
   7277 }
   7278 
   7279 fn current_app_order_record_entries(
   7280     mut records: Vec<LocalEventRecord>,
   7281 ) -> Vec<AppOrderRecordListEntry> {
   7282     records.sort_by(|left, right| {
   7283         right
   7284             .change_seq
   7285             .cmp(&left.change_seq)
   7286             .then_with(|| right.seq.cmp(&left.seq))
   7287             .then_with(|| left.record_id.cmp(&right.record_id))
   7288     });
   7289 
   7290     let mut entries = Vec::<AppOrderRecordListEntry>::new();
   7291     let mut seen = HashMap::<String, usize>::new();
   7292     for record in records {
   7293         let key = app_order_record_current_key(&record);
   7294         if let Some(index) = seen.get(&key).copied() {
   7295             entries[index].superseded_count += 1;
   7296         } else {
   7297             seen.insert(key, entries.len());
   7298             entries.push(AppOrderRecordListEntry {
   7299                 record,
   7300                 superseded_count: 0,
   7301             });
   7302         }
   7303     }
   7304     entries
   7305 }
   7306 
   7307 fn current_app_order_record_for(
   7308     config: &RuntimeConfig,
   7309     record: &LocalEventRecord,
   7310 ) -> Result<Option<LocalEventRecord>, RuntimeError> {
   7311     let key = app_order_record_current_key(record);
   7312     Ok(app_order_local_records(config)?
   7313         .into_iter()
   7314         .filter(|candidate| app_order_record_current_key(candidate) == key)
   7315         .max_by(|left, right| {
   7316             left.change_seq
   7317                 .cmp(&right.change_seq)
   7318                 .then_with(|| left.seq.cmp(&right.seq))
   7319         }))
   7320 }
   7321 
   7322 fn app_order_conflicting_record_ids_for(
   7323     config: &RuntimeConfig,
   7324     record: &LocalEventRecord,
   7325 ) -> Result<Vec<String>, RuntimeError> {
   7326     if app_order_record_order_id(record).is_none() {
   7327         return Ok(Vec::new());
   7328     }
   7329     let key = app_order_record_current_key(record);
   7330     let mut record_ids = app_order_local_records(config)?
   7331         .into_iter()
   7332         .filter(|candidate| candidate.record_id != record.record_id)
   7333         .filter(|candidate| app_order_record_current_key(candidate) == key)
   7334         .map(|candidate| candidate.record_id)
   7335         .collect::<Vec<_>>();
   7336     record_ids.sort();
   7337     record_ids.dedup();
   7338     Ok(record_ids)
   7339 }
   7340 
   7341 fn load_app_order_record_for_lookup(
   7342     config: &RuntimeConfig,
   7343     lookup: &str,
   7344 ) -> Result<Option<LoadedAppOrderRecord>, RuntimeError> {
   7345     if let Some(record) = get_shared_record(config, lookup)?
   7346         && is_app_order_local_record(&record)
   7347     {
   7348         return load_app_order_record_from_record(config, record).map(Some);
   7349     }
   7350     for entry in current_app_order_record_entries(app_order_local_records(config)?) {
   7351         if app_order_record_order_id(&entry.record).as_deref() == Some(lookup) {
   7352             return load_app_order_record_from_record(config, entry.record).map(Some);
   7353         }
   7354     }
   7355     Ok(None)
   7356 }
   7357 
   7358 fn load_app_order_record_from_record(
   7359     config: &RuntimeConfig,
   7360     record: LocalEventRecord,
   7361 ) -> Result<LoadedAppOrderRecord, RuntimeError> {
   7362     let mut source_issues = app_order_record_source_issues(config, &record)?;
   7363     let payload = record.local_work_json.clone().unwrap_or(Value::Null);
   7364     let document = match payload.get("document").cloned() {
   7365         Some(value) => match serde_json::from_value::<OrderDraftDocument>(value) {
   7366             Ok(document) => document,
   7367             Err(error) => {
   7368                 source_issues.push(issue_with_code(
   7369                     "invalid_app_order_record",
   7370                     "document",
   7371                     format!("app-authored order document cannot be decoded: {error}"),
   7372                 ));
   7373                 placeholder_app_order_document(&record)
   7374             }
   7375         },
   7376         None => {
   7377             source_issues.push(issue_with_code(
   7378                 "invalid_app_order_record",
   7379                 "document",
   7380                 "app-authored order record is missing document",
   7381             ));
   7382             placeholder_app_order_document(&record)
   7383         }
   7384     };
   7385     let loaded = LoadedOrderDraft {
   7386         file: PathBuf::from(format!("shared-local-events/{}", record.record_id)),
   7387         updated_at_unix: u64::try_from(record.updated_at_ms / 1000).unwrap_or_default(),
   7388         document,
   7389     };
   7390     source_issues.extend(app_order_signed_evidence_issues(config, &loaded)?);
   7391 
   7392     Ok(LoadedAppOrderRecord {
   7393         loaded,
   7394         record,
   7395         source_issues,
   7396     })
   7397 }
   7398 
   7399 fn app_order_record_source_issues(
   7400     config: &RuntimeConfig,
   7401     record: &LocalEventRecord,
   7402 ) -> Result<Vec<OrderIssueView>, RuntimeError> {
   7403     let mut issues = Vec::new();
   7404     if record.source_runtime != SourceRuntime::App {
   7405         issues.push(issue_with_code(
   7406             "app_order_unsupported",
   7407             "source_runtime",
   7408             "order record must come from radroots_app",
   7409         ));
   7410     }
   7411     if record.family != LocalRecordFamily::LocalWork {
   7412         issues.push(issue_with_code(
   7413             "app_order_unsupported",
   7414             "family",
   7415             "order record must be shared local work",
   7416         ));
   7417     }
   7418     if record.status != LocalRecordStatus::LocalSaved {
   7419         issues.push(issue_with_code(
   7420             "app_order_unsupported",
   7421             "status",
   7422             format!(
   7423                 "order record status `{}` is not consumable as local saved work",
   7424                 record.status.as_str()
   7425             ),
   7426         ));
   7427     }
   7428     let Some(payload) = record.local_work_json.as_ref() else {
   7429         issues.push(issue_with_code(
   7430             "invalid_app_order_record",
   7431             "local_work_json",
   7432             "app-authored order record is missing local work payload",
   7433         ));
   7434         return Ok(issues);
   7435     };
   7436     let current = payload["currentness"]["current"].as_bool() == Some(true);
   7437     if !current {
   7438         issues.push(issue_with_code(
   7439             "app_order_stale",
   7440             "currentness.current",
   7441             "app-authored order record is not marked current",
   7442         ));
   7443     }
   7444     if payload["currentness"]["record_id"].as_str() != Some(record.record_id.as_str()) {
   7445         issues.push(issue_with_code(
   7446             "invalid_app_order_record",
   7447             "currentness.record_id",
   7448             "app-authored order record currentness id does not match the shared record id",
   7449         ));
   7450     }
   7451     if current {
   7452         match validate_supported_buyer_order_request_local_work_payload(payload) {
   7453             Ok(_) => {}
   7454             Err(error) => {
   7455                 let support_state = payload["support_status"]["state"].as_str();
   7456                 let support_issues = payload["support_status"]["issues"]
   7457                     .as_array()
   7458                     .cloned()
   7459                     .unwrap_or_default();
   7460                 if support_state == Some("unsupported") {
   7461                     issues.push(issue_with_code(
   7462                         "app_order_unsupported",
   7463                         "support_status.state",
   7464                         "app-authored order record is not marked supported",
   7465                     ));
   7466                     for support_issue in support_issues {
   7467                         if let Some(support_issue) = support_issue.as_str() {
   7468                             issues.push(issue_with_code(
   7469                                 "app_order_unsupported",
   7470                                 "support_status.issues",
   7471                                 format!("app order support issue: {support_issue}"),
   7472                             ));
   7473                         }
   7474                     }
   7475                 } else {
   7476                     issues.push(issue_with_code(
   7477                         "invalid_app_order_record",
   7478                         "local_work_json",
   7479                         error.to_string(),
   7480                     ));
   7481                 }
   7482             }
   7483         }
   7484     }
   7485     if let Some(current_record) = current_app_order_record_for(config, record)?
   7486         && current_record.record_id != record.record_id
   7487     {
   7488         issues.push(issue_with_code(
   7489             "app_order_stale",
   7490             "record_id",
   7491             format!(
   7492                 "app-authored local order record `{}` was superseded by `{}`",
   7493                 record.record_id, current_record.record_id
   7494             ),
   7495         ));
   7496     }
   7497     let conflicting_record_ids = app_order_conflicting_record_ids_for(config, record)?;
   7498     if !conflicting_record_ids.is_empty() {
   7499         issues.push(issue_with_code(
   7500             "app_order_conflict",
   7501             "order_id",
   7502             format!(
   7503                 "app-authored order id conflicts with other shared records: {}",
   7504                 conflicting_record_ids.join(", ")
   7505             ),
   7506         ));
   7507     }
   7508     Ok(issues)
   7509 }
   7510 
   7511 fn app_order_signed_evidence_issues(
   7512     config: &RuntimeConfig,
   7513     loaded: &LoadedOrderDraft,
   7514 ) -> Result<Vec<OrderIssueView>, RuntimeError> {
   7515     let order_id = loaded.document.order.order_id.as_str();
   7516     let candidate_records = visible_signed_order_request_records(config, order_id)?;
   7517     if candidate_records.is_empty() {
   7518         return Ok(Vec::new());
   7519     }
   7520 
   7521     let expected_payload = match canonical_order_request_payload_from_loaded(
   7522         loaded,
   7523         loaded.document.order.buyer_pubkey.as_str(),
   7524     ) {
   7525         Ok(payload) => payload,
   7526         Err(error) => {
   7527             let event_ids = candidate_records
   7528                 .iter()
   7529                 .map(signed_record_event_id)
   7530                 .collect::<Vec<_>>();
   7531             return Ok(vec![issue_with_events(
   7532                 APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE,
   7533                 "signed_event",
   7534                 format!(
   7535                     "signed order request evidence cannot be compared with local work: {error}"
   7536                 ),
   7537                 event_ids,
   7538             )]);
   7539         }
   7540     };
   7541 
   7542     let mut submitted_event_ids = Vec::new();
   7543     let mut conflict_issues = Vec::new();
   7544     for record in candidate_records {
   7545         let event_id = signed_record_event_id(&record);
   7546         match signed_order_request_from_record(&record)
   7547             .and_then(|event| order_submit_request_from_event(&event, loaded))
   7548         {
   7549             Ok(request)
   7550                 if order_submit_request_matches_draft(&request, loaded, &expected_payload) =>
   7551             {
   7552                 submitted_event_ids.push(request.request_event_id);
   7553             }
   7554             Ok(request) => conflict_issues.push(issue_with_events(
   7555                 APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE,
   7556                 "signed_event",
   7557                 format!(
   7558                     "signed order request event `{}` conflicts with the app-authored local order",
   7559                     request.request_event_id
   7560                 ),
   7561                 vec![request.request_event_id],
   7562             )),
   7563             Err(error) => conflict_issues.push(issue_with_events(
   7564                 APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE,
   7565                 "signed_event",
   7566                 format!("signed order request event `{event_id}` cannot be validated: {error}"),
   7567                 vec![event_id],
   7568             )),
   7569         }
   7570     }
   7571 
   7572     conflict_issues.sort_by(|left, right| {
   7573         left.event_ids
   7574             .cmp(&right.event_ids)
   7575             .then_with(|| left.message.cmp(&right.message))
   7576     });
   7577     if !conflict_issues.is_empty() {
   7578         return Ok(conflict_issues);
   7579     }
   7580 
   7581     submitted_event_ids.sort();
   7582     submitted_event_ids.dedup();
   7583     if submitted_event_ids.is_empty() {
   7584         Ok(Vec::new())
   7585     } else {
   7586         Ok(vec![issue_with_events(
   7587             APP_ORDER_ALREADY_SUBMITTED_ISSUE,
   7588             "signed_event",
   7589             "app-authored local order already has matching signed order request evidence",
   7590             submitted_event_ids,
   7591         )])
   7592     }
   7593 }
   7594 
   7595 fn visible_signed_order_request_records(
   7596     config: &RuntimeConfig,
   7597     order_id: &str,
   7598 ) -> Result<Vec<LocalEventRecord>, RuntimeError> {
   7599     let mut records = Vec::new();
   7600     let mut before_cursor = None::<(i64, i64)>;
   7601     loop {
   7602         let shared_records = if let Some((before_change_seq, before_seq)) = before_cursor {
   7603             list_shared_records_before(
   7604                 config,
   7605                 before_change_seq,
   7606                 before_seq,
   7607                 ORDER_APP_RECORD_LIST_LIMIT,
   7608             )?
   7609         } else {
   7610             list_shared_records_latest(config, ORDER_APP_RECORD_LIST_LIMIT)?
   7611         };
   7612         let Some(next_cursor) = shared_records
   7613             .last()
   7614             .map(|record| (record.change_seq, record.seq))
   7615         else {
   7616             break;
   7617         };
   7618         let has_more = shared_records.len() == ORDER_APP_RECORD_LIST_LIMIT as usize;
   7619         records.extend(
   7620             shared_records
   7621                 .into_iter()
   7622                 .filter(|record| is_visible_signed_order_request_record(record, order_id)),
   7623         );
   7624         if !has_more {
   7625             break;
   7626         }
   7627         before_cursor = Some(next_cursor);
   7628     }
   7629     Ok(records)
   7630 }
   7631 
   7632 fn is_visible_signed_order_request_record(record: &LocalEventRecord, order_id: &str) -> bool {
   7633     record.family == LocalRecordFamily::SignedEvent
   7634         && record.status == LocalRecordStatus::Published
   7635         && record.outbox_status == PublishOutboxStatus::Acknowledged
   7636         && record.event_kind == Some(i64::from(KIND_ORDER_REQUEST))
   7637         && signed_record_tag_values(record, "d")
   7638             .iter()
   7639             .any(|value| value == order_id)
   7640 }
   7641 
   7642 fn signed_order_request_from_record(
   7643     record: &LocalEventRecord,
   7644 ) -> Result<RadrootsNostrEvent, RuntimeError> {
   7645     let raw_event_json = record.raw_event_json.as_ref().ok_or_else(|| {
   7646         RuntimeError::Config(format!(
   7647             "signed event record `{}` is missing raw_event_json",
   7648             record.record_id
   7649         ))
   7650     })?;
   7651     serde_json::from_value::<RadrootsNostrEvent>(raw_event_json.clone()).map_err(|error| {
   7652         RuntimeError::Config(format!(
   7653             "signed event record `{}` raw_event_json cannot be decoded: {error}",
   7654             record.record_id
   7655         ))
   7656     })
   7657 }
   7658 
   7659 fn signed_record_tag_values(record: &LocalEventRecord, key: &str) -> Vec<String> {
   7660     record
   7661         .event_tags_json
   7662         .as_ref()
   7663         .or(record
   7664             .raw_event_json
   7665             .as_ref()
   7666             .and_then(|event| event.get("tags")))
   7667         .and_then(Value::as_array)
   7668         .map(|tags| {
   7669             tags.iter()
   7670                 .filter_map(Value::as_array)
   7671                 .filter_map(|tag| {
   7672                     if tag.first().and_then(Value::as_str) == Some(key) {
   7673                         tag.get(1).and_then(Value::as_str).map(str::to_owned)
   7674                     } else {
   7675                         None
   7676                     }
   7677                 })
   7678                 .collect::<Vec<_>>()
   7679         })
   7680         .unwrap_or_default()
   7681 }
   7682 
   7683 fn signed_record_event_id(record: &LocalEventRecord) -> String {
   7684     record
   7685         .event_id
   7686         .clone()
   7687         .unwrap_or_else(|| record.record_id.clone())
   7688 }
   7689 
   7690 fn source_and_document_issues(
   7691     config: &RuntimeConfig,
   7692     app_order: &LoadedAppOrderRecord,
   7693 ) -> Result<Vec<OrderIssueView>, RuntimeError> {
   7694     Ok(inspect_document_with_source_issues(
   7695         config,
   7696         &app_order.loaded.document,
   7697         app_order.source_issues.as_slice(),
   7698     )?
   7699     .issues)
   7700 }
   7701 
   7702 fn app_order_record_summary(
   7703     config: &RuntimeConfig,
   7704     record: &LocalEventRecord,
   7705     superseded_count: usize,
   7706 ) -> Result<OrderAppRecordSummaryView, RuntimeError> {
   7707     let record_kind = local_record_kind(record).unwrap_or_else(|| "unknown".to_owned());
   7708     let app_order = load_app_order_record_from_record(config, record.clone())?;
   7709     let issues = source_and_document_issues(config, &app_order)?;
   7710     let exportable = issues.is_empty();
   7711     let reason = issues.first().map(|issue| issue.message.clone());
   7712     let document = &app_order.loaded.document;
   7713     let status = if app_order_issue_present(&issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
   7714         "submitted".to_owned()
   7715     } else if app_order_issue_present(&issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
   7716         "conflict".to_owned()
   7717     } else {
   7718         record.status.as_str().to_owned()
   7719     };
   7720     let actions = if exportable {
   7721         vec![
   7722             format!("radroots order get {}", document.order.order_id),
   7723             format!("radroots order app export {}", record.record_id),
   7724             format!(
   7725                 "radroots --relay wss://relay.example.com order submit {}",
   7726                 document.order.order_id
   7727             ),
   7728         ]
   7729     } else if app_order_issue_present(&issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
   7730         vec![format!(
   7731             "radroots order status get {}",
   7732             document.order.order_id
   7733         )]
   7734     } else if app_order_issue_present(&issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
   7735         vec![
   7736             format!("radroots order status get {}", document.order.order_id),
   7737             "radroots order app list".to_owned(),
   7738         ]
   7739     } else {
   7740         Vec::new()
   7741     };
   7742     Ok(OrderAppRecordSummaryView {
   7743         record_id: record.record_id.clone(),
   7744         seq: record.seq,
   7745         change_seq: record.change_seq,
   7746         superseded_count,
   7747         record_kind,
   7748         status,
   7749         source_runtime: record.source_runtime.as_str().to_owned(),
   7750         owner_account_id: record.owner_account_id.clone(),
   7751         owner_pubkey: record.owner_pubkey.clone(),
   7752         farm_id: record.farm_id.clone(),
   7753         listing_addr: record
   7754             .listing_addr
   7755             .clone()
   7756             .or_else(|| non_empty_string(app_order.loaded.document.order.listing_addr.clone())),
   7757         listing_relays: order_listing_relays(document),
   7758         order_id: non_empty_string(document.order.order_id.clone()),
   7759         buyer_account_id: buyer_account_id(document),
   7760         buyer_pubkey: non_empty_string(document.order.buyer_pubkey.clone()),
   7761         seller_pubkey: non_empty_string(document.order.seller_pubkey.clone()),
   7762         ready_for_submit: exportable,
   7763         exportable,
   7764         reason,
   7765         actions,
   7766     })
   7767 }
   7768 
   7769 fn app_order_record_current_key(record: &LocalEventRecord) -> String {
   7770     app_order_record_order_id(record)
   7771         .map(|order_id| format!("order:{order_id}"))
   7772         .unwrap_or_else(|| format!("record:{}", record.record_id))
   7773 }
   7774 
   7775 fn app_order_record_order_id(record: &LocalEventRecord) -> Option<String> {
   7776     record
   7777         .local_work_json
   7778         .as_ref()
   7779         .and_then(|payload| payload["document"]["order"]["order_id"].as_str())
   7780         .map(str::trim)
   7781         .filter(|value| !value.is_empty())
   7782         .map(str::to_owned)
   7783 }
   7784 
   7785 fn placeholder_app_order_document(record: &LocalEventRecord) -> OrderDraftDocument {
   7786     OrderDraftDocument {
   7787         version: 0,
   7788         kind: "invalid_app_order_record".to_owned(),
   7789         order: OrderDraft {
   7790             order_id: app_order_record_order_id(record).unwrap_or_else(|| record.record_id.clone()),
   7791             listing_addr: String::new(),
   7792             listing_event_id: String::new(),
   7793             listing_relays: Vec::new(),
   7794             buyer_pubkey: String::new(),
   7795             seller_pubkey: String::new(),
   7796             items: Vec::new(),
   7797             economics: None,
   7798         },
   7799         buyer_actor: OrderDraftBuyerActor {
   7800             account_id: String::new(),
   7801             pubkey: String::new(),
   7802             source: String::new(),
   7803         },
   7804         listing_lookup: None,
   7805     }
   7806 }
   7807 
   7808 fn app_order_export_failure_state(issues: &[OrderIssueView]) -> &'static str {
   7809     if issues
   7810         .iter()
   7811         .any(|issue| issue.code == APP_ORDER_ALREADY_SUBMITTED_ISSUE)
   7812     {
   7813         "already_submitted"
   7814     } else if issues.iter().any(|issue| {
   7815         issue.code == "app_order_conflict" || issue.code == APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE
   7816     }) {
   7817         "conflict"
   7818     } else if issues.iter().any(|issue| issue.code == "app_order_stale") {
   7819         "stale"
   7820     } else if issues
   7821         .iter()
   7822         .any(|issue| issue.code == "invalid_app_order_record")
   7823     {
   7824         "invalid"
   7825     } else if issues
   7826         .iter()
   7827         .any(|issue| issue.code == "app_order_unsupported")
   7828     {
   7829         "unsupported"
   7830     } else {
   7831         "invalid"
   7832     }
   7833 }
   7834 
   7835 fn app_order_export_failure_actions(
   7836     document: &OrderDraftDocument,
   7837     issues: &[OrderIssueView],
   7838 ) -> Vec<String> {
   7839     if app_order_issue_present(issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
   7840         vec![format!(
   7841             "radroots order status get {}",
   7842             document.order.order_id
   7843         )]
   7844     } else if app_order_issue_present(issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
   7845         vec![
   7846             format!("radroots order status get {}", document.order.order_id),
   7847             "radroots order app list".to_owned(),
   7848         ]
   7849     } else {
   7850         vec!["radroots order app list".to_owned()]
   7851     }
   7852 }
   7853 
   7854 fn order_export_output_path(
   7855     config: &RuntimeConfig,
   7856     output: Option<&PathBuf>,
   7857     order_id: &str,
   7858 ) -> PathBuf {
   7859     output
   7860         .cloned()
   7861         .unwrap_or_else(|| drafts_dir(config).join(format!("{order_id}.toml")))
   7862 }
   7863 
   7864 fn validate_order_export_output_target(output_path: &Path) -> Result<(), RuntimeError> {
   7865     if output_path.exists() {
   7866         return Err(RuntimeError::Config(format!(
   7867             "order draft output {} must not already exist",
   7868             output_path.display()
   7869         )));
   7870     }
   7871     if let Some(parent) = output_path.parent() {
   7872         if parent.exists() && !parent.is_dir() {
   7873             return Err(RuntimeError::Config(format!(
   7874                 "order draft parent {} is not a directory",
   7875                 parent.display()
   7876             )));
   7877         }
   7878     }
   7879     Ok(())
   7880 }
   7881 
   7882 fn local_record_kind(record: &LocalEventRecord) -> Option<String> {
   7883     record
   7884         .local_work_json
   7885         .as_ref()
   7886         .and_then(|payload| payload.get("record_kind"))
   7887         .and_then(Value::as_str)
   7888         .map(str::to_owned)
   7889 }
   7890 
   7891 fn inspect_document(
   7892     config: &RuntimeConfig,
   7893     document: &OrderDraftDocument,
   7894 ) -> Result<OrderInspection, RuntimeError> {
   7895     inspect_document_with_source_issues(config, document, &[])
   7896 }
   7897 
   7898 fn inspect_document_with_source_issues(
   7899     config: &RuntimeConfig,
   7900     document: &OrderDraftDocument,
   7901     source_issues: &[OrderIssueView],
   7902 ) -> Result<OrderInspection, RuntimeError> {
   7903     let listing_addr = non_empty_string(document.order.listing_addr.clone());
   7904     let listing_event_id = non_empty_string(document.order.listing_event_id.clone());
   7905     let parsed_listing_addr = listing_addr
   7906         .as_deref()
   7907         .and_then(|value| parse_listing_addr(value).ok());
   7908     let seller_pubkey = non_empty_string(document.order.seller_pubkey.clone()).or_else(|| {
   7909         parsed_listing_addr
   7910             .as_ref()
   7911             .map(|listing| listing.seller_pubkey.clone())
   7912     });
   7913     let mut issues = collect_issues(document);
   7914     let buyer_readiness = inspect_buyer_actor_readiness(config, document)?;
   7915     issues.extend(buyer_readiness.issues);
   7916     issues.extend(source_issues.iter().cloned());
   7917     let ready_for_submit = issues.is_empty();
   7918     let state = if app_order_issue_present(&issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
   7919         "submitted".to_owned()
   7920     } else if app_order_issue_present(&issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
   7921         "conflict".to_owned()
   7922     } else if ready_for_submit {
   7923         "ready".to_owned()
   7924     } else {
   7925         "draft".to_owned()
   7926     };
   7927 
   7928     Ok(OrderInspection {
   7929         state,
   7930         ready_for_submit,
   7931         listing_addr,
   7932         listing_event_id,
   7933         seller_pubkey,
   7934         buyer_custody: buyer_readiness
   7935             .account
   7936             .as_ref()
   7937             .map(|account| account.custody.as_str().to_owned()),
   7938         buyer_write_capable: buyer_readiness
   7939             .account
   7940             .as_ref()
   7941             .map(|account| account.write_capable),
   7942         issues,
   7943     })
   7944 }
   7945 
   7946 #[derive(Debug, Clone)]
   7947 struct OrderBuyerActorReadiness {
   7948     account: Option<account::AccountRecordView>,
   7949     issues: Vec<OrderIssueView>,
   7950 }
   7951 
   7952 fn inspect_buyer_actor_readiness(
   7953     config: &RuntimeConfig,
   7954     document: &OrderDraftDocument,
   7955 ) -> Result<OrderBuyerActorReadiness, RuntimeError> {
   7956     let account_id = document.buyer_actor.account_id.trim();
   7957     let buyer_pubkey = document.buyer_actor.pubkey.trim();
   7958     if account_id.is_empty() || buyer_pubkey.is_empty() {
   7959         return Ok(OrderBuyerActorReadiness {
   7960             account: None,
   7961             issues: Vec::new(),
   7962         });
   7963     }
   7964 
   7965     let snapshot = account::snapshot(config)?;
   7966     let Some(account) = snapshot
   7967         .accounts
   7968         .into_iter()
   7969         .find(|account| account.record.account_id.as_str() == account_id)
   7970     else {
   7971         return Ok(OrderBuyerActorReadiness {
   7972             account: None,
   7973             issues: vec![issue_with_code(
   7974                 "account_unresolved",
   7975                 "buyer_actor.account_id",
   7976                 format!(
   7977                     "order buyer_actor account_id `{account_id}` is not present in the local account store"
   7978                 ),
   7979             )],
   7980         });
   7981     };
   7982 
   7983     let account_pubkey = account.record.public_identity.public_key_hex.as_str();
   7984     let mut issues = Vec::new();
   7985     if !account_pubkey.eq_ignore_ascii_case(buyer_pubkey) {
   7986         issues.push(issue_with_code(
   7987             "account_mismatch",
   7988             "buyer_actor.pubkey",
   7989             format!(
   7990                 "order buyer_actor pubkey `{buyer_pubkey}` does not match local account `{account_id}` pubkey `{account_pubkey}`"
   7991             ),
   7992         ));
   7993     }
   7994     if !account.write_capable {
   7995         issues.push(issue_with_code(
   7996             "account_watch_only",
   7997             "buyer_actor.account_id",
   7998             format!(
   7999                 "order buyer_actor account `{account_id}` is watch_only and cannot sign until a matching secret is attached"
   8000             ),
   8001         ));
   8002     }
   8003 
   8004     Ok(OrderBuyerActorReadiness {
   8005         account: Some(account),
   8006         issues,
   8007     })
   8008 }
   8009 
   8010 fn collect_issues(document: &OrderDraftDocument) -> Vec<OrderIssueView> {
   8011     let mut issues = Vec::new();
   8012     if document.version != 1 {
   8013         issues.push(issue("version", "version must be 1"));
   8014     }
   8015     if document.kind != ORDER_DRAFT_KIND {
   8016         issues.push(issue("kind", format!("kind must be `{ORDER_DRAFT_KIND}`")));
   8017     }
   8018     if !is_valid_order_id(document.order.order_id.as_str()) {
   8019         issues.push(issue(
   8020             "order.order_id",
   8021             "order_id must look like `ord_<base64url>` or a canonical UUID",
   8022         ));
   8023     }
   8024 
   8025     match normalize_optional(Some(document.order.listing_addr.as_str())) {
   8026         Some(listing_addr) => match parse_listing_addr(listing_addr.as_str()) {
   8027             Ok(parsed) => {
   8028                 if parsed.kind != KIND_LISTING {
   8029                     issues.push(issue(
   8030                         "order.listing_addr",
   8031                         "listing_addr must reference a public NIP-99 listing",
   8032                     ));
   8033                 }
   8034                 if let Some(seller_pubkey) = non_empty_string(document.order.seller_pubkey.clone())
   8035                 {
   8036                     if seller_pubkey != parsed.seller_pubkey {
   8037                         issues.push(issue(
   8038                             "order.seller_pubkey",
   8039                             "seller_pubkey must match listing_addr seller when both are set",
   8040                         ));
   8041                     }
   8042                 }
   8043             }
   8044             Err(error) => issues.push(issue(
   8045                 "order.listing_addr",
   8046                 format!("listing_addr is invalid: {error}"),
   8047             )),
   8048         },
   8049         None => issues.push(issue(
   8050             "order.listing_addr",
   8051             "listing_addr is required before order submit",
   8052         )),
   8053     }
   8054 
   8055     match normalize_optional(Some(document.order.listing_event_id.as_str())) {
   8056         Some(listing_event_id) => {
   8057             if !is_valid_event_id(listing_event_id.as_str()) {
   8058                 issues.push(issue(
   8059                     "order.listing_event_id",
   8060                     "listing_event_id must be a 64-character hex Nostr event id",
   8061                 ));
   8062             }
   8063         }
   8064         None => issues.push(issue(
   8065             "order.listing_event_id",
   8066             "latest active listing event id is required before order submit; run `radroots market refresh` and create the order from local market data",
   8067         )),
   8068     }
   8069 
   8070     match normalize_listing_relay_set(document.order.listing_relays.iter()) {
   8071         Ok(listing_relays) if listing_relays.is_empty() => issues.push(issue_with_code(
   8072             "listing_provenance_missing",
   8073             "order.listing_relays",
   8074             "listing relay provenance is required before order submit; run `radroots market refresh` and create the order from current local market data",
   8075         )),
   8076         Ok(_) => {}
   8077         Err(error) => issues.push(issue_with_code(
   8078             "listing_provenance_invalid",
   8079             "order.listing_relays",
   8080             format!("listing relay provenance is invalid: {error}"),
   8081         )),
   8082     }
   8083 
   8084     if document.order.items.is_empty() {
   8085         issues.push(issue(
   8086             "order.items",
   8087             "at least one order item is required before order submit",
   8088         ));
   8089     }
   8090     for (index, item) in document.order.items.iter().enumerate() {
   8091         if item.bin_id.trim().is_empty() {
   8092             issues.push(issue(
   8093                 format!("order.items[{index}].bin_id"),
   8094                 "bin_id must not be empty",
   8095             ));
   8096         }
   8097         if item.bin_count == 0 {
   8098             issues.push(issue(
   8099                 format!("order.items[{index}].bin_count"),
   8100                 "bin_count must be greater than zero",
   8101             ));
   8102         }
   8103     }
   8104 
   8105     match &document.order.economics {
   8106         Some(economics) => {
   8107             if let Err(error) = economics.validate() {
   8108                 issues.push(issue(
   8109                     "order.economics",
   8110                     format!("order economics is invalid: {error}"),
   8111                 ));
   8112             }
   8113             if !order_items_match_economics(document.order.items.as_slice(), economics) {
   8114                 issues.push(issue(
   8115                     "order.economics",
   8116                     "order economics must match the order item bin ids and counts",
   8117                 ));
   8118             }
   8119         }
   8120         None => issues.push(issue(
   8121             "order.economics",
   8122             "quote economics is required before order submit; run `radroots basket quote create` from current local market data",
   8123         )),
   8124     }
   8125 
   8126     if document.buyer_actor.account_id.trim().is_empty() {
   8127         issues.push(issue(
   8128             "buyer_actor.account_id",
   8129             "buyer_actor account_id is required before order submit",
   8130         ));
   8131     }
   8132     if document.buyer_actor.pubkey.trim().is_empty() {
   8133         issues.push(issue(
   8134             "buyer_actor.pubkey",
   8135             "buyer_actor pubkey is required before order submit",
   8136         ));
   8137     }
   8138     if document.buyer_actor.source.trim().is_empty() {
   8139         issues.push(issue(
   8140             "buyer_actor.source",
   8141             "buyer_actor source is required before order submit",
   8142         ));
   8143     } else if !matches!(
   8144         document.buyer_actor.source.as_str(),
   8145         ORDER_BUYER_ACTOR_SOURCE_RESOLVED_ACCOUNT | ORDER_BUYER_ACTOR_SOURCE_REBIND
   8146     ) {
   8147         issues.push(issue(
   8148             "buyer_actor.source",
   8149             format!(
   8150                 "unsupported buyer_actor source `{}`",
   8151                 document.buyer_actor.source
   8152             ),
   8153         ));
   8154     }
   8155     if document.order.buyer_pubkey.trim().is_empty() {
   8156         issues.push(issue(
   8157             "order.buyer_pubkey",
   8158             "order buyer_pubkey is required before order submit",
   8159         ));
   8160     } else if !document
   8161         .order
   8162         .buyer_pubkey
   8163         .eq_ignore_ascii_case(document.buyer_actor.pubkey.as_str())
   8164     {
   8165         issues.push(issue(
   8166             "order.buyer_pubkey",
   8167             "order buyer_pubkey must match buyer_actor pubkey",
   8168         ));
   8169     }
   8170 
   8171     issues
   8172 }
   8173 
   8174 fn order_items_match_economics(
   8175     items: &[OrderDraftItem],
   8176     economics: &RadrootsOrderEconomics,
   8177 ) -> bool {
   8178     let mut order_items = items
   8179         .iter()
   8180         .map(|item| (item.bin_id.as_str(), item.bin_count))
   8181         .collect::<Vec<_>>();
   8182     let mut economic_items = economics
   8183         .items
   8184         .iter()
   8185         .map(|item| (item.bin_id.as_str(), item.bin_count))
   8186         .collect::<Vec<_>>();
   8187     order_items.sort_unstable();
   8188     economic_items.sort_unstable();
   8189     order_items == economic_items
   8190 }
   8191 
   8192 fn actions_for_document(
   8193     document: &OrderDraftDocument,
   8194     file: &Path,
   8195     issues: &[OrderIssueView],
   8196 ) -> Vec<String> {
   8197     if app_order_issue_present(issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
   8198         return vec![format!(
   8199             "radroots order status get {}",
   8200             document.order.order_id
   8201         )];
   8202     }
   8203     if app_order_issue_present(issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
   8204         return vec![
   8205             format!("radroots order status get {}", document.order.order_id),
   8206             "radroots order app list".to_owned(),
   8207         ];
   8208     }
   8209 
   8210     let mut actions = Vec::new();
   8211     actions.push(format!(
   8212         "edit {} and fill the remaining draft fields",
   8213         file.display()
   8214     ));
   8215     if document.buyer_actor.account_id.trim().is_empty()
   8216         || document.buyer_actor.pubkey.trim().is_empty()
   8217         || document.order.buyer_pubkey.trim().is_empty()
   8218         || !document
   8219             .order
   8220             .buyer_pubkey
   8221             .eq_ignore_ascii_case(document.buyer_actor.pubkey.as_str())
   8222     {
   8223         actions.push(format!(
   8224             "radroots order rebind {} <selector>",
   8225             document.order.order_id
   8226         ));
   8227     }
   8228     if issues
   8229         .iter()
   8230         .any(|issue| issue.code == "account_unresolved")
   8231     {
   8232         actions.push("radroots account import <path>".to_owned());
   8233         actions.push(format!(
   8234             "radroots order rebind {} <selector>",
   8235             document.order.order_id
   8236         ));
   8237     }
   8238     if issues
   8239         .iter()
   8240         .any(|issue| issue.code == "account_watch_only")
   8241     {
   8242         actions.push(format!(
   8243             "radroots account attach-secret {} <path>",
   8244             document.buyer_actor.account_id
   8245         ));
   8246         actions.push(format!("radroots order get {}", document.order.order_id));
   8247     }
   8248     if issues.iter().any(|issue| issue.code == "account_mismatch") {
   8249         actions.push(format!(
   8250             "radroots order rebind {} <selector>",
   8251             document.order.order_id
   8252         ));
   8253     }
   8254     if document.order.items.is_empty()
   8255         || issues
   8256             .iter()
   8257             .any(|issue| issue.field.starts_with("order.items["))
   8258     {
   8259         actions.push(format!("radroots order get {}", document.order.order_id));
   8260     }
   8261     let mut deduped = Vec::new();
   8262     for action in actions {
   8263         if !deduped.contains(&action) {
   8264             deduped.push(action);
   8265         }
   8266     }
   8267     deduped
   8268 }
   8269 
   8270 fn app_order_issue_present(issues: &[OrderIssueView], code: &str) -> bool {
   8271     issues.iter().any(|issue| issue.code == code)
   8272 }
   8273 
   8274 fn app_order_issue<'a>(issues: &'a [OrderIssueView], code: &str) -> Option<&'a OrderIssueView> {
   8275     issues.iter().find(|issue| issue.code == code)
   8276 }
   8277 
   8278 fn order_rebind_selector_error(selector: &str, error: RuntimeError) -> RuntimeError {
   8279     match error {
   8280         RuntimeError::Accounts(_) | RuntimeError::Account(_) => {
   8281             account::AccountRuntimeFailure::unresolved_with_detail(
   8282                 format!("order rebind target selector `{selector}` did not resolve"),
   8283                 json!({
   8284                     "selector": selector,
   8285                     "actions": [
   8286                         "radroots account list",
   8287                         "radroots account import <path>",
   8288                         "radroots account create",
   8289                     ],
   8290                 }),
   8291             )
   8292             .into()
   8293         }
   8294         other => other,
   8295     }
   8296 }
   8297 
   8298 fn order_rebind_existing_request_check(
   8299     config: &RuntimeConfig,
   8300     loaded: &LoadedOrderDraft,
   8301 ) -> Result<OrderRebindExistingRequestCheck, RuntimeError> {
   8302     if config.relay.urls.is_empty() {
   8303         return Ok(OrderRebindExistingRequestCheck {
   8304             state: "skipped_no_relays".to_owned(),
   8305             event_ids: Vec::new(),
   8306         });
   8307     }
   8308 
   8309     let filter = order_request_filter(
   8310         loaded.document.order.seller_pubkey.as_str(),
   8311         Some(loaded.document.order.order_id.as_str()),
   8312     )?;
   8313     let receipt = fetch_events_from_relays(&config.relay.urls, filter)
   8314         .map_err(|error| RuntimeError::Network(error.to_string()))?;
   8315     let mut event_ids = receipt
   8316         .events
   8317         .iter()
   8318         .filter_map(|event| {
   8319             order_submit_request_from_event(event, loaded)
   8320                 .ok()
   8321                 .map(|request| request.request_event_id)
   8322         })
   8323         .collect::<Vec<_>>();
   8324     event_ids.sort();
   8325     event_ids.dedup();
   8326 
   8327     Ok(OrderRebindExistingRequestCheck {
   8328         state: if event_ids.is_empty() {
   8329             "clear".to_owned()
   8330         } else {
   8331             "blocked_existing_request".to_owned()
   8332         },
   8333         event_ids,
   8334     })
   8335 }
   8336 
   8337 fn resolve_initial_buyer_actor(
   8338     config: &RuntimeConfig,
   8339 ) -> Result<OrderDraftBuyerActor, RuntimeError> {
   8340     let resolution = account::resolve_account_resolution(config)?;
   8341     let Some(account) = resolution.resolved_account else {
   8342         return Err(account::AccountRuntimeFailure::unresolved_with_detail(
   8343             account::unresolved_account_reason(config)?,
   8344             json!({
   8345                 "buyer_actor_source": ORDER_BUYER_ACTOR_SOURCE_RESOLVED_ACCOUNT,
   8346                 "actions": [
   8347                     "radroots account create",
   8348                     "radroots account import <path>",
   8349                 ],
   8350             }),
   8351         )
   8352         .into());
   8353     };
   8354     Ok(OrderDraftBuyerActor {
   8355         account_id: account.record.account_id.to_string(),
   8356         pubkey: account.record.public_identity.public_key_hex,
   8357         source: ORDER_BUYER_ACTOR_SOURCE_RESOLVED_ACCOUNT.to_owned(),
   8358     })
   8359 }
   8360 
   8361 fn buyer_account_id(document: &OrderDraftDocument) -> Option<String> {
   8362     non_empty_string(document.buyer_actor.account_id.clone())
   8363 }
   8364 
   8365 fn buyer_actor_source(document: &OrderDraftDocument) -> Option<String> {
   8366     non_empty_string(document.buyer_actor.source.clone())
   8367 }
   8368 
   8369 fn load_local_order_draft_if_exists(
   8370     config: &RuntimeConfig,
   8371     lookup: &str,
   8372 ) -> Result<Option<LoadedOrderDraft>, RuntimeError> {
   8373     let file = draft_lookup_path(config, lookup);
   8374     if !file.exists() {
   8375         return Ok(None);
   8376     }
   8377     load_draft(file.as_path())
   8378         .map(Some)
   8379         .map_err(RuntimeError::Config)
   8380 }
   8381 
   8382 fn order_status_actor_context(
   8383     config: &RuntimeConfig,
   8384     order_id: &str,
   8385 ) -> Result<OrderDraftStatusActorContext, RuntimeError> {
   8386     if let Some(loaded) = load_local_order_draft_if_exists(config, order_id)? {
   8387         return Ok(OrderDraftStatusActorContext {
   8388             source: ORDER_ACTOR_CONTEXT_ORDER_DRAFT,
   8389             buyer_pubkey: non_empty_string(loaded.document.buyer_actor.pubkey.clone())
   8390                 .or_else(|| non_empty_string(loaded.document.order.buyer_pubkey.clone())),
   8391             seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey),
   8392             selected_account_pubkey: None,
   8393         });
   8394     }
   8395 
   8396     let selected_account = account::resolve_account(config)?;
   8397     let Some(account) = selected_account else {
   8398         return Ok(OrderDraftStatusActorContext {
   8399             source: ORDER_ACTOR_CONTEXT_NETWORK_ONLY,
   8400             buyer_pubkey: None,
   8401             seller_pubkey: None,
   8402             selected_account_pubkey: None,
   8403         });
   8404     };
   8405 
   8406     Ok(OrderDraftStatusActorContext {
   8407         source: ORDER_ACTOR_CONTEXT_RESOLVED_ACCOUNT,
   8408         buyer_pubkey: None,
   8409         seller_pubkey: None,
   8410         selected_account_pubkey: Some(account.record.public_identity.public_key_hex),
   8411     })
   8412 }
   8413 
   8414 fn order_event_list_actor_context(
   8415     config: &RuntimeConfig,
   8416     order_id: Option<&str>,
   8417 ) -> Result<Option<OrderEventListActorContext>, RuntimeError> {
   8418     if let Some(order_id) = order_id
   8419         && let Some(loaded) = load_local_order_draft_if_exists(config, order_id)?
   8420     {
   8421         let seller_pubkey =
   8422             non_empty_string(loaded.document.order.seller_pubkey).ok_or_else(|| {
   8423                 RuntimeError::Config(format!(
   8424                     "local order draft `{order_id}` is missing seller_pubkey"
   8425                 ))
   8426             })?;
   8427         return Ok(Some(OrderEventListActorContext {
   8428             source: ORDER_ACTOR_CONTEXT_ORDER_DRAFT,
   8429             seller_pubkey,
   8430         }));
   8431     }
   8432 
   8433     Ok(
   8434         account::resolve_account(config)?.map(|account| OrderEventListActorContext {
   8435             source: ORDER_ACTOR_CONTEXT_RESOLVED_ACCOUNT,
   8436             seller_pubkey: account.record.public_identity.public_key_hex,
   8437         }),
   8438     )
   8439 }
   8440 
   8441 fn bound_buyer_write_context_if_exists(
   8442     config: &RuntimeConfig,
   8443     order_id: &str,
   8444 ) -> Result<Option<OrderBoundBuyerWriteContext>, RuntimeError> {
   8445     let Some(loaded) = load_local_order_draft_if_exists(config, order_id)? else {
   8446         return Ok(None);
   8447     };
   8448     let account = validate_bound_order_buyer_account(config, &loaded)?;
   8449     Ok(Some(OrderBoundBuyerWriteContext { loaded, account }))
   8450 }
   8451 
   8452 fn order_buyer_write_actor_context(
   8453     config: &RuntimeConfig,
   8454     order_id: &str,
   8455 ) -> Result<Option<OrderBuyerWriteActorContext>, RuntimeError> {
   8456     if let Some(bound) = bound_buyer_write_context_if_exists(config, order_id)? {
   8457         let selected_pubkey = bound.account.record.public_identity.public_key_hex.clone();
   8458         let status_seller_pubkey =
   8459             non_empty_string(bound.loaded.document.order.seller_pubkey.clone());
   8460         return Ok(Some(OrderBuyerWriteActorContext {
   8461             bound: Some(bound),
   8462             selected_pubkey: selected_pubkey.clone(),
   8463             status_buyer_pubkey: Some(selected_pubkey),
   8464             status_seller_pubkey,
   8465             status_context_source: ORDER_ACTOR_CONTEXT_ORDER_DRAFT,
   8466         }));
   8467     }
   8468 
   8469     Ok(account::resolve_account(config)?.map(|account| {
   8470         let selected_pubkey = account.record.public_identity.public_key_hex;
   8471         OrderBuyerWriteActorContext {
   8472             bound: None,
   8473             selected_pubkey: selected_pubkey.clone(),
   8474             status_buyer_pubkey: None,
   8475             status_seller_pubkey: None,
   8476             status_context_source: ORDER_ACTOR_CONTEXT_RESOLVED_ACCOUNT,
   8477         }
   8478     }))
   8479 }
   8480 
   8481 fn order_submit_listing_freshness_view(
   8482     config: &RuntimeConfig,
   8483     loaded: &LoadedOrderDraft,
   8484     args: &OrderSubmitArgs,
   8485 ) -> Result<Option<OrderSubmitView>, RuntimeError> {
   8486     if !config.local.replica_db_path.exists() {
   8487         return Ok(Some(order_submit_unconfigured_view(
   8488             config,
   8489             loaded,
   8490             args,
   8491             "order submit requires local market data to confirm the listing is still active; run `radroots store init` and `radroots market refresh` before submitting",
   8492             vec![issue(
   8493                 "order.listing_addr",
   8494                 "local replica database is missing; run `radroots store init` and `radroots market refresh` before submitting",
   8495             )],
   8496             vec![
   8497                 "radroots store init".to_owned(),
   8498                 "radroots market refresh".to_owned(),
   8499             ],
   8500         )));
   8501     }
   8502 
   8503     let listing_addr = loaded.document.order.listing_addr.as_str();
   8504     let parsed = parse_listing_addr(listing_addr)
   8505         .map_err(|error| RuntimeError::Config(format!("order listing_addr is invalid: {error}")))?;
   8506     let active_event_id = match resolve_active_listing_event_id(config, listing_addr, &parsed)? {
   8507         Some(event_id) => event_id,
   8508         None => {
   8509             return Ok(Some(order_submit_unconfigured_view(
   8510                 config,
   8511                 loaded,
   8512                 args,
   8513                 "order listing is not active in the local replica; run `radroots market refresh` and create a new order from current market data",
   8514                 vec![issue(
   8515                     "order.listing_addr",
   8516                     "listing is missing, archived, or superseded in the local replica",
   8517                 )],
   8518                 vec!["radroots market refresh".to_owned()],
   8519             )));
   8520         }
   8521     };
   8522 
   8523     if !active_event_id.eq_ignore_ascii_case(loaded.document.order.listing_event_id.as_str()) {
   8524         return Ok(Some(order_submit_unconfigured_view(
   8525             config,
   8526             loaded,
   8527             args,
   8528             "order listing event is no longer current in the local replica; run `radroots market refresh` and create a new order from current market data",
   8529             vec![issue(
   8530                 "order.listing_event_id",
   8531                 format!(
   8532                     "draft listing_event_id does not match latest local listing event `{active_event_id}`"
   8533                 ),
   8534             )],
   8535             vec!["radroots market refresh".to_owned()],
   8536         )));
   8537     }
   8538 
   8539     Ok(None)
   8540 }
   8541 
   8542 fn order_submit_quantity_preflight_view(
   8543     config: &RuntimeConfig,
   8544     loaded: &LoadedOrderDraft,
   8545     args: &OrderSubmitArgs,
   8546 ) -> Result<Option<OrderSubmitView>, RuntimeError> {
   8547     if !config.local.replica_db_path.exists() {
   8548         return Ok(Some(order_submit_unconfigured_view(
   8549             config,
   8550             loaded,
   8551             args,
   8552             "order submit requires local market data to confirm current listing availability; run `radroots store init` and `radroots market refresh` before submitting",
   8553             vec![issue(
   8554                 "order.listing_addr",
   8555                 "local replica database is missing; run `radroots store init` and `radroots market refresh` before submitting",
   8556             )],
   8557             vec![
   8558                 "radroots store init".to_owned(),
   8559                 "radroots market refresh".to_owned(),
   8560             ],
   8561         )));
   8562     }
   8563 
   8564     let requested_count =
   8565         loaded
   8566             .document
   8567             .order
   8568             .items
   8569             .iter()
   8570             .enumerate()
   8571             .try_fold(0u64, |total, (index, item)| {
   8572                 if item.bin_count == 0 {
   8573                     return Err(RuntimeError::Config(format!(
   8574                         "order item {index} quantity must be greater than zero"
   8575                     )));
   8576                 }
   8577                 total.checked_add(u64::from(item.bin_count)).ok_or_else(|| {
   8578                     RuntimeError::Config("order quantity exceeds supported range".to_owned())
   8579                 })
   8580             })?;
   8581 
   8582     let executor = SqliteExecutor::open(&config.local.replica_db_path)?;
   8583     let product_rows = trade_product::find_many(
   8584         &executor,
   8585         &ITradeProductFindMany {
   8586             filter: Some(trade_product_listing_addr_filter(
   8587                 loaded.document.order.listing_addr.as_str(),
   8588             )),
   8589         },
   8590     )
   8591     .map_err(|error| RuntimeError::Config(format!("resolve listing product state: {error:?}")))?
   8592     .results;
   8593 
   8594     let product = match product_rows.as_slice() {
   8595         [product] => product,
   8596         [] => {
   8597             return Ok(Some(order_submit_unconfigured_view(
   8598                 config,
   8599                 loaded,
   8600                 args,
   8601                 "order listing is not active in the local replica; run `radroots market refresh` and create a new order from current market data",
   8602                 vec![issue(
   8603                     "order.listing_addr",
   8604                     "listing is missing, archived, or superseded in the local replica",
   8605                 )],
   8606                 vec!["radroots market refresh".to_owned()],
   8607             )));
   8608         }
   8609         _ => {
   8610             return Err(RuntimeError::Config(format!(
   8611                 "listing address `{}` matched {} active local listing rows",
   8612                 loaded.document.order.listing_addr,
   8613                 product_rows.len()
   8614             )));
   8615         }
   8616     };
   8617 
   8618     let Some(primary_bin_id) = product.primary_bin_id.as_deref().and_then(non_empty_ref) else {
   8619         return Ok(Some(order_submit_invalid_quantity_view(
   8620             config,
   8621             loaded,
   8622             args,
   8623             "order listing bin identity is missing in the local replica",
   8624             vec![issue_with_code(
   8625                 "listing_primary_bin_missing",
   8626                 "inventory.primary_bin_id",
   8627                 "current local replica listing primary bin is required before submit",
   8628             )],
   8629         )));
   8630     };
   8631     let Some(verified_primary_bin_id) = product
   8632         .verified_primary_bin_id
   8633         .as_deref()
   8634         .and_then(non_empty_ref)
   8635     else {
   8636         return Ok(Some(order_submit_invalid_quantity_view(
   8637             config,
   8638             loaded,
   8639             args,
   8640             "order listing bin identity is not verified in the local replica",
   8641             vec![issue_with_code(
   8642                 "listing_primary_bin_invalid",
   8643                 "inventory.primary_bin_id",
   8644                 format!("current local replica primary bin `{primary_bin_id}` is not verified"),
   8645             )],
   8646         )));
   8647     };
   8648     if verified_primary_bin_id != primary_bin_id {
   8649         return Ok(Some(order_submit_invalid_quantity_view(
   8650             config,
   8651             loaded,
   8652             args,
   8653             "order listing bin identity is invalid in the local replica",
   8654             vec![issue_with_code(
   8655                 "listing_primary_bin_invalid",
   8656                 "inventory.primary_bin_id",
   8657                 format!(
   8658                     "current local replica primary bin `{primary_bin_id}` does not match verified primary bin `{verified_primary_bin_id}`"
   8659                 ),
   8660             )],
   8661         )));
   8662     }
   8663 
   8664     let mut bin_issues = Vec::new();
   8665     for (index, item) in loaded.document.order.items.iter().enumerate() {
   8666         if item.bin_id != primary_bin_id {
   8667             bin_issues.push(issue_with_code(
   8668                 "order_bin_unknown",
   8669                 format!("order.items[{index}].bin_id"),
   8670                 format!(
   8671                     "draft bin `{}` is not in the current local listing bin set; expected primary bin `{primary_bin_id}`",
   8672                     item.bin_id
   8673                 ),
   8674             ));
   8675         }
   8676     }
   8677     if !bin_issues.is_empty() {
   8678         return Ok(Some(order_submit_invalid_quantity_view(
   8679             config,
   8680             loaded,
   8681             args,
   8682             "order draft references a bin outside the current local listing",
   8683             bin_issues,
   8684         )));
   8685     }
   8686 
   8687     let available_count = match product.qty_avail {
   8688         Some(value) if value >= 0 => value as u64,
   8689         Some(value) => {
   8690             return Ok(Some(order_submit_invalid_quantity_view(
   8691                 config,
   8692                 loaded,
   8693                 args,
   8694                 "order listing availability is invalid in the local replica",
   8695                 vec![issue_with_code(
   8696                     "listing_inventory_availability_invalid",
   8697                     "inventory.available",
   8698                     format!("current local replica availability is negative: {value}"),
   8699                 )],
   8700             )));
   8701         }
   8702         None => {
   8703             return Ok(Some(order_submit_invalid_quantity_view(
   8704                 config,
   8705                 loaded,
   8706                 args,
   8707                 "order listing availability is missing in the local replica",
   8708                 vec![issue_with_code(
   8709                     "listing_inventory_availability_missing",
   8710                     "inventory.available",
   8711                     "current local replica listing availability is required before submit",
   8712                 )],
   8713             )));
   8714         }
   8715     };
   8716 
   8717     if requested_count > available_count {
   8718         return Ok(Some(order_submit_invalid_quantity_view(
   8719             config,
   8720             loaded,
   8721             args,
   8722             "order requested quantity exceeds current local listing availability",
   8723             vec![issue_with_code(
   8724                 "order_quantity_exceeds_available",
   8725                 "order.items",
   8726                 format!(
   8727                     "requested quantity {requested_count} exceeds current local replica available quantity {available_count}"
   8728                 ),
   8729             )],
   8730         )));
   8731     }
   8732 
   8733     Ok(None)
   8734 }
   8735 
   8736 fn order_submit_unconfigured_view(
   8737     config: &RuntimeConfig,
   8738     loaded: &LoadedOrderDraft,
   8739     args: &OrderSubmitArgs,
   8740     reason: impl Into<String>,
   8741     issues: Vec<OrderIssueView>,
   8742     mut actions: Vec<String>,
   8743 ) -> OrderSubmitView {
   8744     actions.push(format!(
   8745         "radroots order get {}",
   8746         loaded.document.order.order_id
   8747     ));
   8748 
   8749     OrderSubmitView {
   8750         state: "unconfigured".to_owned(),
   8751         source: ORDER_SOURCE.to_owned(),
   8752         order_id: loaded.document.order.order_id.clone(),
   8753         file: loaded.file.display().to_string(),
   8754         listing_lookup: loaded.document.listing_lookup.clone(),
   8755         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   8756         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   8757         listing_relays: order_listing_relays(&loaded.document),
   8758         buyer_account_id: buyer_account_id(&loaded.document),
   8759         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   8760         buyer_actor_source: buyer_actor_source(&loaded.document),
   8761         buyer_custody: None,
   8762         buyer_write_capable: None,
   8763         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   8764         event_id: None,
   8765         event_kind: None,
   8766         dry_run: config.output.dry_run,
   8767         deduplicated: false,
   8768         target_relays: Vec::new(),
   8769         connected_relays: Vec::new(),
   8770         acknowledged_relays: Vec::new(),
   8771         failed_relays: Vec::new(),
   8772         idempotency_key: args.idempotency_key.clone(),
   8773         signer_mode: None,
   8774         reason: Some(reason.into()),
   8775         job: None,
   8776         issues,
   8777         actions,
   8778     }
   8779 }
   8780 
   8781 fn order_submit_app_signed_evidence_view(
   8782     config: &RuntimeConfig,
   8783     loaded: &LoadedOrderDraft,
   8784     args: &OrderSubmitArgs,
   8785     issues: &[OrderIssueView],
   8786 ) -> Option<OrderSubmitView> {
   8787     if let Some(issue) = app_order_issue(issues, APP_ORDER_ALREADY_SUBMITTED_ISSUE) {
   8788         return Some(OrderSubmitView {
   8789             state: "submitted".to_owned(),
   8790             source: ORDER_SUBMIT_SOURCE.to_owned(),
   8791             order_id: loaded.document.order.order_id.clone(),
   8792             file: loaded.file.display().to_string(),
   8793             listing_lookup: loaded.document.listing_lookup.clone(),
   8794             listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   8795             listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   8796             listing_relays: order_listing_relays(&loaded.document),
   8797             buyer_account_id: buyer_account_id(&loaded.document),
   8798             buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   8799             buyer_actor_source: buyer_actor_source(&loaded.document),
   8800             buyer_custody: None,
   8801             buyer_write_capable: None,
   8802             seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   8803             event_id: issue.event_ids.first().cloned(),
   8804             event_kind: Some(KIND_ORDER_REQUEST),
   8805             dry_run: config.output.dry_run,
   8806             deduplicated: true,
   8807             target_relays: Vec::new(),
   8808             connected_relays: Vec::new(),
   8809             acknowledged_relays: Vec::new(),
   8810             failed_relays: Vec::new(),
   8811             idempotency_key: args.idempotency_key.clone(),
   8812             signer_mode: None,
   8813             reason: Some(
   8814                 "matching signed order request evidence already exists; publish skipped".to_owned(),
   8815             ),
   8816             job: None,
   8817             issues: vec![issue.clone()],
   8818             actions: Vec::new(),
   8819         });
   8820     }
   8821 
   8822     if app_order_issue_present(issues, APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE) {
   8823         return Some(OrderSubmitView {
   8824             state: "invalid".to_owned(),
   8825             source: ORDER_SUBMIT_SOURCE.to_owned(),
   8826             order_id: loaded.document.order.order_id.clone(),
   8827             file: loaded.file.display().to_string(),
   8828             listing_lookup: loaded.document.listing_lookup.clone(),
   8829             listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   8830             listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   8831             listing_relays: order_listing_relays(&loaded.document),
   8832             buyer_account_id: buyer_account_id(&loaded.document),
   8833             buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   8834             buyer_actor_source: buyer_actor_source(&loaded.document),
   8835             buyer_custody: None,
   8836             buyer_write_capable: None,
   8837             seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   8838             event_id: None,
   8839             event_kind: Some(KIND_ORDER_REQUEST),
   8840             dry_run: config.output.dry_run,
   8841             deduplicated: false,
   8842             target_relays: Vec::new(),
   8843             connected_relays: Vec::new(),
   8844             acknowledged_relays: Vec::new(),
   8845             failed_relays: Vec::new(),
   8846             idempotency_key: args.idempotency_key.clone(),
   8847             signer_mode: None,
   8848             reason: Some(
   8849                 "signed order request evidence conflicts with the app-authored local order"
   8850                     .to_owned(),
   8851             ),
   8852             job: None,
   8853             issues: issues.to_vec(),
   8854             actions: vec![format!(
   8855                 "radroots order status get {}",
   8856                 loaded.document.order.order_id
   8857             )],
   8858         });
   8859     }
   8860 
   8861     None
   8862 }
   8863 
   8864 fn order_submit_invalid_quantity_view(
   8865     config: &RuntimeConfig,
   8866     loaded: &LoadedOrderDraft,
   8867     args: &OrderSubmitArgs,
   8868     reason: impl Into<String>,
   8869     issues: Vec<OrderIssueView>,
   8870 ) -> OrderSubmitView {
   8871     OrderSubmitView {
   8872         state: "invalid".to_owned(),
   8873         source: ORDER_SOURCE.to_owned(),
   8874         order_id: loaded.document.order.order_id.clone(),
   8875         file: loaded.file.display().to_string(),
   8876         listing_lookup: loaded.document.listing_lookup.clone(),
   8877         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   8878         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   8879         listing_relays: order_listing_relays(&loaded.document),
   8880         buyer_account_id: buyer_account_id(&loaded.document),
   8881         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   8882         buyer_actor_source: buyer_actor_source(&loaded.document),
   8883         buyer_custody: None,
   8884         buyer_write_capable: None,
   8885         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   8886         event_id: None,
   8887         event_kind: None,
   8888         dry_run: config.output.dry_run,
   8889         deduplicated: false,
   8890         target_relays: Vec::new(),
   8891         connected_relays: Vec::new(),
   8892         acknowledged_relays: Vec::new(),
   8893         failed_relays: Vec::new(),
   8894         idempotency_key: args.idempotency_key.clone(),
   8895         signer_mode: None,
   8896         reason: Some(reason.into()),
   8897         job: None,
   8898         issues,
   8899         actions: vec![
   8900             "radroots market refresh".to_owned(),
   8901             format!("radroots order get {}", loaded.document.order.order_id),
   8902         ],
   8903     }
   8904 }
   8905 
   8906 fn order_submit_listing_provenance_preflight_view(
   8907     config: &RuntimeConfig,
   8908     loaded: &LoadedOrderDraft,
   8909     args: &OrderSubmitArgs,
   8910 ) -> Result<Option<OrderSubmitView>, RuntimeError> {
   8911     let listing_relays =
   8912         normalize_listing_relay_set(loaded.document.order.listing_relays.iter())
   8913             .map_err(|error| RuntimeError::Config(format!("listing provenance relays: {error}")))?;
   8914     let target_relays = normalize_listing_relay_set(config.relay.urls.iter())
   8915         .map_err(|error| RuntimeError::Config(format!("configured relay target: {error}")))?;
   8916     if target_relays.is_empty() {
   8917         return Ok(None);
   8918     }
   8919     let reachable_relays = listing_relays
   8920         .iter()
   8921         .filter(|relay| target_relays.contains(relay))
   8922         .cloned()
   8923         .collect::<Vec<_>>();
   8924     if !reachable_relays.is_empty() {
   8925         return Ok(None);
   8926     }
   8927 
   8928     let mut actions = listing_relays
   8929         .iter()
   8930         .map(|relay| {
   8931             format!(
   8932                 "radroots --relay {} order submit {}",
   8933                 relay, loaded.document.order.order_id
   8934             )
   8935         })
   8936         .collect::<Vec<_>>();
   8937     actions.push(format!(
   8938         "radroots order get {}",
   8939         loaded.document.order.order_id
   8940     ));
   8941     Ok(Some(OrderSubmitView {
   8942         state: "unconfigured".to_owned(),
   8943         source: ORDER_SUBMIT_SOURCE.to_owned(),
   8944         order_id: loaded.document.order.order_id.clone(),
   8945         file: loaded.file.display().to_string(),
   8946         listing_lookup: loaded.document.listing_lookup.clone(),
   8947         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   8948         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   8949         listing_relays,
   8950         buyer_account_id: buyer_account_id(&loaded.document),
   8951         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   8952         buyer_actor_source: buyer_actor_source(&loaded.document),
   8953         buyer_custody: None,
   8954         buyer_write_capable: None,
   8955         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   8956         event_id: None,
   8957         event_kind: Some(KIND_ORDER_REQUEST),
   8958         dry_run: config.output.dry_run,
   8959         deduplicated: false,
   8960         target_relays,
   8961         connected_relays: Vec::new(),
   8962         acknowledged_relays: Vec::new(),
   8963         failed_relays: Vec::new(),
   8964         idempotency_key: args.idempotency_key.clone(),
   8965         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   8966         reason: Some(
   8967             "order submit requires at least one configured relay that is known to carry the listing"
   8968                 .to_owned(),
   8969         ),
   8970         job: None,
   8971         issues: vec![issue_with_code(
   8972             "listing_relay_target_mismatch",
   8973             "order.listing_relays",
   8974             format!(
   8975                 "configured relays must include one of the listing provenance relays: {}",
   8976                 loaded.document.order.listing_relays.join(", ")
   8977             ),
   8978         )],
   8979         actions,
   8980     }))
   8981 }
   8982 
   8983 fn order_submit_market_freshness_view(
   8984     config: &RuntimeConfig,
   8985     loaded: &LoadedOrderDraft,
   8986     args: &OrderSubmitArgs,
   8987 ) -> Result<Option<OrderSubmitView>, RuntimeError> {
   8988     if config.output.dry_run || config.relay.urls.is_empty() {
   8989         return Ok(None);
   8990     }
   8991 
   8992     let mut freshness = freshness_for_scope(config, RelayIngestScope::MarketRefresh)?;
   8993     if freshness_requires_refresh(&freshness) {
   8994         let _ = market_refresh(config)?;
   8995         freshness = freshness_for_scope(config, RelayIngestScope::MarketRefresh)?;
   8996     }
   8997     if !freshness_requires_refresh(&freshness) {
   8998         return Ok(None);
   8999     }
   9000 
   9001     Ok(Some(order_submit_unconfigured_view(
   9002         config,
   9003         loaded,
   9004         args,
   9005         "order submit requires a current market refresh before signing; run `radroots market refresh` with the relays you trust, then submit again",
   9006         vec![issue(
   9007             "order.listing_addr",
   9008             format!(
   9009                 "local market freshness is `{}`; current listing state must be refreshed before order submit",
   9010                 freshness.state
   9011             ),
   9012         )],
   9013         vec!["radroots market refresh".to_owned()],
   9014     )))
   9015 }
   9016 
   9017 fn order_submit_existing_request_view_from_receipt(
   9018     config: &RuntimeConfig,
   9019     loaded: &LoadedOrderDraft,
   9020     args: &OrderSubmitArgs,
   9021     payload: &RadrootsOrderRequest,
   9022     receipt: DirectRelayFetchReceipt,
   9023 ) -> Result<Option<OrderSubmitView>, RuntimeError> {
   9024     let DirectRelayFetchReceipt {
   9025         target_relays,
   9026         connected_relays,
   9027         failed_relays,
   9028         events,
   9029     } = receipt;
   9030     let mut requests = Vec::new();
   9031     let mut candidate_issues = Vec::new();
   9032     let candidate_context = OrderRequestCandidateContext {
   9033         order_id: loaded.document.order.order_id.as_str(),
   9034         seller_pubkey: Some(loaded.document.order.seller_pubkey.as_str()),
   9035     };
   9036 
   9037     for event in events {
   9038         if !order_request_candidate_matches(&event, candidate_context) {
   9039             continue;
   9040         }
   9041         let event_id = event.id.to_string();
   9042         match order_submit_request_from_event(&event, loaded) {
   9043             Ok(request) => requests.push(request),
   9044             Err(error) => candidate_issues.push(issue_with_events(
   9045                 "invalid_request_candidate",
   9046                 "request_event_id",
   9047                 format!("request event `{event_id}` failed order submit preflight: {error}"),
   9048                 vec![event_id],
   9049             )),
   9050         }
   9051     }
   9052 
   9053     requests.sort_by(|left, right| left.request_event_id.cmp(&right.request_event_id));
   9054     candidate_issues.sort_by(|left, right| {
   9055         left.event_ids
   9056             .cmp(&right.event_ids)
   9057             .then_with(|| left.message.cmp(&right.message))
   9058     });
   9059     if !candidate_issues.is_empty() {
   9060         return Ok(Some(order_submit_invalid_existing_request_view(
   9061             config,
   9062             loaded,
   9063             args,
   9064             "visible order request candidates failed submit preflight validation",
   9065             candidate_issues,
   9066             target_relays,
   9067             failed_relays,
   9068         )));
   9069     }
   9070 
   9071     let request_event_ids = requests
   9072         .iter()
   9073         .map(|request| request.request_event_id.clone())
   9074         .collect::<Vec<_>>();
   9075 
   9076     match requests.as_slice() {
   9077         [] => Ok(None),
   9078         [request] if order_submit_request_matches_draft(request, loaded, payload) => {
   9079             Ok(Some(order_submit_deduplicated_view(
   9080                 config,
   9081                 loaded,
   9082                 args,
   9083                 request,
   9084                 target_relays,
   9085                 connected_relays,
   9086                 failed_relays,
   9087             )))
   9088         }
   9089         [request] => Ok(Some(order_submit_invalid_existing_request_view(
   9090             config,
   9091             loaded,
   9092             args,
   9093             "visible order request event conflicts with the local order draft; refusing to publish a second request for the same order id",
   9094             vec![issue_with_events(
   9095                 "existing_request_conflict",
   9096                 "request_event_id",
   9097                 format!(
   9098                     "request event `{}` does not match the local order draft",
   9099                     request.request_event_id
   9100                 ),
   9101                 vec![request.request_event_id.clone()],
   9102             )],
   9103             target_relays,
   9104             failed_relays,
   9105         ))),
   9106         _ => Ok(Some(order_submit_invalid_existing_request_view(
   9107             config,
   9108             loaded,
   9109             args,
   9110             "multiple visible order request events matched the local order id; refusing to publish another request",
   9111             vec![issue_with_events(
   9112                 "multiple_request_candidates",
   9113                 "request_event_id",
   9114                 format!(
   9115                     "matched {} request events for the same order id",
   9116                     requests.len()
   9117                 ),
   9118                 request_event_ids,
   9119             )],
   9120             target_relays,
   9121             failed_relays,
   9122         ))),
   9123     }
   9124 }
   9125 
   9126 fn order_submit_request_from_event(
   9127     event: &RadrootsNostrEvent,
   9128     loaded: &LoadedOrderDraft,
   9129 ) -> Result<ResolvedOrderSubmitRequest, RuntimeError> {
   9130     let event = radroots_event_from_nostr(event);
   9131     let envelope = order_request_from_event(&event)
   9132         .map_err(|error| RuntimeError::Config(format!("decode order request event: {error}")))?;
   9133     let context =
   9134         order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags)
   9135             .map_err(|error| RuntimeError::Config(format!("decode order request tags: {error}")))?;
   9136 
   9137     if envelope.order_id != loaded.document.order.order_id
   9138         || envelope.payload.order_id != loaded.document.order.order_id
   9139     {
   9140         return Err(RuntimeError::Config(
   9141             "order request does not match local order id".to_owned(),
   9142         ));
   9143     }
   9144     if context.counterparty_pubkey != envelope.payload.seller_pubkey {
   9145         return Err(RuntimeError::Config(
   9146             "order request p tag does not match seller_pubkey".to_owned(),
   9147         ));
   9148     }
   9149     let listing_addr =
   9150         parse_listing_addr(envelope.payload.listing_addr.as_str()).map_err(|error| {
   9151             RuntimeError::Config(format!("order request listing_addr is invalid: {error}"))
   9152         })?;
   9153     if listing_addr.seller_pubkey != envelope.payload.seller_pubkey {
   9154         return Err(RuntimeError::Config(
   9155             "order request listing address is outside seller authority".to_owned(),
   9156         ));
   9157     }
   9158     let payload = canonicalize_order_request_for_signer(envelope.payload, event.author.as_str())
   9159         .map_err(|error| RuntimeError::Config(format!("canonicalize order request: {error}")))?;
   9160     let listing_event_id = context.listing_event.as_ref().map(|event| event.id.clone());
   9161 
   9162     Ok(ResolvedOrderSubmitRequest {
   9163         request_event_id: event.id,
   9164         listing_event_id,
   9165         payload,
   9166     })
   9167 }
   9168 
   9169 fn order_submit_request_matches_draft(
   9170     request: &ResolvedOrderSubmitRequest,
   9171     loaded: &LoadedOrderDraft,
   9172     payload: &RadrootsOrderRequest,
   9173 ) -> bool {
   9174     request.payload == *payload
   9175         && request.listing_event_id.as_deref()
   9176             == Some(loaded.document.order.listing_event_id.as_str())
   9177 }
   9178 
   9179 fn order_submit_deduplicated_view(
   9180     config: &RuntimeConfig,
   9181     loaded: &LoadedOrderDraft,
   9182     args: &OrderSubmitArgs,
   9183     request: &ResolvedOrderSubmitRequest,
   9184     target_relays: Vec<String>,
   9185     connected_relays: Vec<String>,
   9186     failed_relays: Vec<DirectRelayFailure>,
   9187 ) -> OrderSubmitView {
   9188     OrderSubmitView {
   9189         state: "submitted".to_owned(),
   9190         source: ORDER_SUBMIT_SOURCE.to_owned(),
   9191         order_id: loaded.document.order.order_id.clone(),
   9192         file: loaded.file.display().to_string(),
   9193         listing_lookup: loaded.document.listing_lookup.clone(),
   9194         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   9195         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   9196         listing_relays: order_listing_relays(&loaded.document),
   9197         buyer_account_id: buyer_account_id(&loaded.document),
   9198         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   9199         buyer_actor_source: buyer_actor_source(&loaded.document),
   9200         buyer_custody: None,
   9201         buyer_write_capable: None,
   9202         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   9203         event_id: Some(request.request_event_id.clone()),
   9204         event_kind: Some(KIND_ORDER_REQUEST),
   9205         dry_run: config.output.dry_run,
   9206         deduplicated: true,
   9207         target_relays,
   9208         connected_relays: connected_relays.clone(),
   9209         acknowledged_relays: connected_relays,
   9210         failed_relays: relay_failures(failed_relays),
   9211         idempotency_key: args.idempotency_key.clone(),
   9212         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   9213         reason: Some(
   9214             "an identical order request is already visible on the configured relays; publish skipped"
   9215                 .to_owned(),
   9216         ),
   9217         job: None,
   9218         issues: Vec::new(),
   9219         actions: Vec::new(),
   9220     }
   9221 }
   9222 
   9223 fn order_submit_dry_run_view(
   9224     config: &RuntimeConfig,
   9225     loaded: &LoadedOrderDraft,
   9226     args: &OrderSubmitArgs,
   9227     plan: OrderSubmitPlan,
   9228     target_relays: Vec<String>,
   9229 ) -> OrderSubmitView {
   9230     OrderSubmitView {
   9231         state: "dry_run".to_owned(),
   9232         source: ORDER_SUBMIT_SOURCE.to_owned(),
   9233         order_id: loaded.document.order.order_id.clone(),
   9234         file: loaded.file.display().to_string(),
   9235         listing_lookup: loaded.document.listing_lookup.clone(),
   9236         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   9237         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   9238         listing_relays: order_listing_relays(&loaded.document),
   9239         buyer_account_id: buyer_account_id(&loaded.document),
   9240         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   9241         buyer_actor_source: buyer_actor_source(&loaded.document),
   9242         buyer_custody: None,
   9243         buyer_write_capable: None,
   9244         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   9245         event_id: Some(plan.expected_event_id.as_str().to_owned()),
   9246         event_kind: Some(KIND_ORDER_REQUEST),
   9247         dry_run: true,
   9248         deduplicated: false,
   9249         target_relays,
   9250         connected_relays: Vec::new(),
   9251         acknowledged_relays: Vec::new(),
   9252         failed_relays: Vec::new(),
   9253         idempotency_key: args.idempotency_key.clone(),
   9254         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   9255         reason: Some("dry run requested; SDK enqueue and relay push skipped".to_owned()),
   9256         job: None,
   9257         issues: Vec::new(),
   9258         actions: vec![format!(
   9259             "radroots order submit {}",
   9260             loaded.document.order.order_id
   9261         )],
   9262     }
   9263 }
   9264 
   9265 fn order_submit_invalid_existing_request_view(
   9266     config: &RuntimeConfig,
   9267     loaded: &LoadedOrderDraft,
   9268     args: &OrderSubmitArgs,
   9269     reason: impl Into<String>,
   9270     issues: Vec<OrderIssueView>,
   9271     target_relays: Vec<String>,
   9272     failed_relays: Vec<DirectRelayFailure>,
   9273 ) -> OrderSubmitView {
   9274     OrderSubmitView {
   9275         state: "invalid".to_owned(),
   9276         source: ORDER_SUBMIT_SOURCE.to_owned(),
   9277         order_id: loaded.document.order.order_id.clone(),
   9278         file: loaded.file.display().to_string(),
   9279         listing_lookup: loaded.document.listing_lookup.clone(),
   9280         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   9281         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   9282         listing_relays: order_listing_relays(&loaded.document),
   9283         buyer_account_id: buyer_account_id(&loaded.document),
   9284         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   9285         buyer_actor_source: buyer_actor_source(&loaded.document),
   9286         buyer_custody: None,
   9287         buyer_write_capable: None,
   9288         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   9289         event_id: None,
   9290         event_kind: Some(KIND_ORDER_REQUEST),
   9291         dry_run: config.output.dry_run,
   9292         deduplicated: false,
   9293         target_relays,
   9294         connected_relays: Vec::new(),
   9295         acknowledged_relays: Vec::new(),
   9296         failed_relays: relay_failures(failed_relays),
   9297         idempotency_key: args.idempotency_key.clone(),
   9298         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   9299         reason: Some(reason.into()),
   9300         job: None,
   9301         issues,
   9302         actions: vec![format!(
   9303             "radroots order status get {}",
   9304             loaded.document.order.order_id
   9305         )],
   9306     }
   9307 }
   9308 
   9309 fn canonical_order_request_payload_from_loaded(
   9310     loaded: &LoadedOrderDraft,
   9311     signer_pubkey: &str,
   9312 ) -> Result<RadrootsOrderRequest, RuntimeError> {
   9313     let economics =
   9314         loaded.document.order.economics.clone().ok_or_else(|| {
   9315             RuntimeError::Config("order draft is missing quote economics".to_owned())
   9316         })?;
   9317     let items = loaded
   9318         .document
   9319         .order
   9320         .items
   9321         .iter()
   9322         .map(|item| {
   9323             Ok(RadrootsOrderItem {
   9324                 bin_id: protocol_inventory_bin_id(item.bin_id.as_str(), "order item bin_id")?,
   9325                 bin_count: item.bin_count,
   9326             })
   9327         })
   9328         .collect::<Result<Vec<_>, RuntimeError>>()?;
   9329     let payload = RadrootsOrderRequest {
   9330         order_id: protocol_order_id(loaded.document.order.order_id.as_str(), "order_id")?,
   9331         listing_addr: protocol_listing_addr(
   9332             loaded.document.order.listing_addr.as_str(),
   9333             "listing_addr",
   9334         )?,
   9335         buyer_pubkey: protocol_pubkey(loaded.document.order.buyer_pubkey.as_str(), "buyer_pubkey")?,
   9336         seller_pubkey: protocol_pubkey(
   9337             loaded.document.order.seller_pubkey.as_str(),
   9338             "seller_pubkey",
   9339         )?,
   9340         items,
   9341         economics,
   9342     };
   9343     canonicalize_order_request_for_signer(payload, signer_pubkey)
   9344         .map_err(|error| RuntimeError::Config(format!("canonicalize order request: {error}")))
   9345 }
   9346 
   9347 fn sdk_order_submit_input(
   9348     config: &RuntimeConfig,
   9349     loaded: &LoadedOrderDraft,
   9350     signing: &account::AccountSigningIdentity,
   9351     payload: RadrootsOrderRequest,
   9352 ) -> Result<SdkOrderSubmitInput, CliSdkAdapterError> {
   9353     let actor = RadrootsActorContext::local_account(
   9354         signing
   9355             .account
   9356             .record
   9357             .public_identity
   9358             .public_key_hex
   9359             .as_str(),
   9360         signing.account.record.account_id.to_string(),
   9361         [RadrootsActorRole::Buyer],
   9362     )
   9363     .map_err(|error| RuntimeError::Config(format!("invalid order SDK actor: {error}")))?;
   9364     let listing_event = order_submit_listing_event_ptr(loaded)?;
   9365     let target_relays = order_submit_target_relays(config, loaded)?;
   9366 
   9367     Ok(SdkOrderSubmitInput {
   9368         actor,
   9369         listing_event,
   9370         order: payload,
   9371         target_relays,
   9372     })
   9373 }
   9374 
   9375 #[derive(Debug, Clone)]
   9376 struct SdkOrderSubmitInput {
   9377     actor: RadrootsActorContext,
   9378     listing_event: RadrootsNostrEventPtr,
   9379     order: RadrootsOrderRequest,
   9380     target_relays: Vec<String>,
   9381 }
   9382 
   9383 fn order_submit_listing_event_ptr(
   9384     loaded: &LoadedOrderDraft,
   9385 ) -> Result<RadrootsNostrEventPtr, RuntimeError> {
   9386     let listing_relays =
   9387         normalize_listing_relay_set(loaded.document.order.listing_relays.iter())
   9388             .map_err(|error| RuntimeError::Config(format!("listing provenance relays: {error}")))?;
   9389     Ok(RadrootsNostrEventPtr {
   9390         id: loaded.document.order.listing_event_id.clone(),
   9391         relays: listing_relays.first().cloned(),
   9392     })
   9393 }
   9394 
   9395 fn order_submit_target_relays(
   9396     config: &RuntimeConfig,
   9397     loaded: &LoadedOrderDraft,
   9398 ) -> Result<Vec<String>, RuntimeError> {
   9399     let listing_relays =
   9400         normalize_listing_relay_set(loaded.document.order.listing_relays.iter())
   9401             .map_err(|error| RuntimeError::Config(format!("listing provenance relays: {error}")))?;
   9402     let configured_relays = normalize_listing_relay_set(config.relay.urls.iter())
   9403         .map_err(|error| RuntimeError::Config(format!("configured relay target: {error}")))?;
   9404     if configured_relays.is_empty() {
   9405         return Ok(listing_relays);
   9406     }
   9407     Ok(configured_relays
   9408         .into_iter()
   9409         .filter(|relay| listing_relays.contains(relay))
   9410         .collect())
   9411 }
   9412 
   9413 fn order_submit_relay_url_policy(target_relays: &[String]) -> SdkRelayUrlPolicy {
   9414     if target_relays
   9415         .iter()
   9416         .any(|relay_url| relay_url.starts_with("ws://"))
   9417     {
   9418         SdkRelayUrlPolicy::Localhost
   9419     } else {
   9420         SdkRelayUrlPolicy::Public
   9421     }
   9422 }
   9423 
   9424 fn prepare_order_submit_via_sdk(
   9425     config: &RuntimeConfig,
   9426     loaded: &LoadedOrderDraft,
   9427     args: &OrderSubmitArgs,
   9428     input: SdkOrderSubmitInput,
   9429 ) -> Result<OrderSubmitView, CliSdkAdapterError> {
   9430     let target_relays = input.target_relays.clone();
   9431     let session = CliSdkSession::connect_memory(config)?;
   9432     let plan = session
   9433         .sdk()
   9434         .orders()
   9435         .prepare_submit(OrderSubmitPrepareRequest::new(
   9436             input.actor,
   9437             input.listing_event,
   9438             input.order,
   9439         ))?;
   9440     Ok(order_submit_dry_run_view(
   9441         config,
   9442         loaded,
   9443         args,
   9444         plan,
   9445         target_relays,
   9446     ))
   9447 }
   9448 
   9449 fn submit_via_sdk(
   9450     config: &RuntimeConfig,
   9451     loaded: &LoadedOrderDraft,
   9452     args: &OrderSubmitArgs,
   9453     signing: account::AccountSigningIdentity,
   9454     input: SdkOrderSubmitInput,
   9455 ) -> Result<OrderSubmitView, CliSdkAdapterError> {
   9456     let target_relays = input.target_relays.clone();
   9457     let policy = order_submit_relay_url_policy(target_relays.as_slice());
   9458     let target_policy = SdkRelayTargetPolicy::try_explicit(target_relays, policy)?;
   9459     let mut request = OrderSubmitEnqueueRequest::new(
   9460         input.actor,
   9461         input.listing_event,
   9462         input.order,
   9463         target_policy,
   9464     );
   9465     if let Some(idempotency_key) = args.idempotency_key.as_deref() {
   9466         request = request.try_with_idempotency_key(idempotency_key)?;
   9467     }
   9468 
   9469     let session = CliSdkSession::connect(config)?;
   9470     let keys: RadrootsNostrKeys = signing.identity.into_keys();
   9471     let signer = RadrootsLocalEventSigner::new(keys)
   9472         .map_err(|error| RuntimeError::Config(error.to_string()))?;
   9473     let enqueue = session.block_on(
   9474         session
   9475             .sdk()
   9476             .orders()
   9477             .enqueue_submit_with_explicit_signer(request, &signer),
   9478     )?;
   9479     let push = session.block_on(
   9480         session.sdk().sync().push_outbox(
   9481             PushOutboxRequest::new()
   9482                 .with_limit(1)
   9483                 .with_relay_url_policy(order_submit_relay_url_policy(&enqueue_target_relays(
   9484                     config, loaded,
   9485                 )?)),
   9486         ),
   9487     )?;
   9488     Ok(sdk_enqueued_order_submit_view(
   9489         config, loaded, args, enqueue, push,
   9490     ))
   9491 }
   9492 
   9493 fn enqueue_target_relays(
   9494     config: &RuntimeConfig,
   9495     loaded: &LoadedOrderDraft,
   9496 ) -> Result<Vec<String>, RuntimeError> {
   9497     let target_relays = order_submit_target_relays(config, loaded)?;
   9498     if target_relays.is_empty() {
   9499         return Ok(config.relay.urls.clone());
   9500     }
   9501     Ok(target_relays)
   9502 }
   9503 
   9504 fn sdk_enqueued_order_submit_view(
   9505     config: &RuntimeConfig,
   9506     loaded: &LoadedOrderDraft,
   9507     args: &OrderSubmitArgs,
   9508     enqueue: OrderSubmitReceipt,
   9509     push: PushOutboxReceipt,
   9510 ) -> OrderSubmitView {
   9511     let push_event = sdk_push_event_for_order_submit(&enqueue, &push);
   9512     OrderSubmitView {
   9513         state: sdk_order_submit_state(push_event),
   9514         source: ORDER_SUBMIT_SOURCE.to_owned(),
   9515         order_id: loaded.document.order.order_id.clone(),
   9516         file: loaded.file.display().to_string(),
   9517         listing_lookup: loaded.document.listing_lookup.clone(),
   9518         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   9519         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   9520         listing_relays: order_listing_relays(&loaded.document),
   9521         buyer_account_id: buyer_account_id(&loaded.document),
   9522         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   9523         buyer_actor_source: buyer_actor_source(&loaded.document),
   9524         buyer_custody: None,
   9525         buyer_write_capable: None,
   9526         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   9527         event_id: Some(enqueue.signed_event_id.as_str().to_owned()),
   9528         event_kind: Some(KIND_ORDER_REQUEST),
   9529         dry_run: false,
   9530         deduplicated: matches!(enqueue.state, SdkMutationState::AlreadyQueued),
   9531         target_relays: push_event
   9532             .map(sdk_push_target_relays)
   9533             .unwrap_or_else(|| enqueue_target_relays(config, loaded).unwrap_or_default()),
   9534         connected_relays: push_event
   9535             .map(sdk_push_connected_relays)
   9536             .unwrap_or_default(),
   9537         acknowledged_relays: push_event
   9538             .map(sdk_push_acknowledged_relays)
   9539             .unwrap_or_default(),
   9540         failed_relays: push_event.map(sdk_push_failed_relays).unwrap_or_default(),
   9541         idempotency_key: args.idempotency_key.clone(),
   9542         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   9543         reason: sdk_order_submit_reason(&enqueue.workflow, push_event),
   9544         job: None,
   9545         issues: Vec::new(),
   9546         actions: sdk_order_submit_actions(push_event),
   9547     }
   9548 }
   9549 
   9550 fn sdk_push_event_for_order_submit<'a>(
   9551     enqueue: &OrderSubmitReceipt,
   9552     push: &'a PushOutboxReceipt,
   9553 ) -> Option<&'a PushOutboxEventReceipt> {
   9554     push.events
   9555         .iter()
   9556         .find(|event| event.event_id == enqueue.signed_event_id)
   9557 }
   9558 
   9559 fn sdk_order_submit_state(push_event: Option<&PushOutboxEventReceipt>) -> String {
   9560     match push_event.map(|event| event.final_state) {
   9561         Some(PushOutboxEventState::Published) => "submitted",
   9562         Some(PushOutboxEventState::PublishRetryable | PushOutboxEventState::FailedTerminal) => {
   9563             "unavailable"
   9564         }
   9565         Some(_) | None => "queued",
   9566     }
   9567     .to_owned()
   9568 }
   9569 
   9570 fn sdk_order_submit_reason(
   9571     enqueue: &OrderWorkflowEnqueueReceipt,
   9572     push_event: Option<&PushOutboxEventReceipt>,
   9573 ) -> Option<String> {
   9574     match push_event.map(|event| event.final_state) {
   9575         Some(PushOutboxEventState::Published) => None,
   9576         Some(PushOutboxEventState::PublishRetryable) => Some(format!(
   9577             "{}; SDK relay publish did not reach accepted quorum; outbox event remains retryable; {}",
   9578             sdk_order_enqueue_summary(enqueue),
   9579             sdk_order_enqueue_retry_summary(enqueue)
   9580         )),
   9581         Some(PushOutboxEventState::FailedTerminal) => Some(format!(
   9582             "{}; SDK relay publish failed terminally; {}",
   9583             sdk_order_enqueue_summary(enqueue),
   9584             sdk_order_enqueue_retry_summary(enqueue)
   9585         )),
   9586         Some(state) => Some(format!(
   9587             "{}; SDK relay push left event in state `{state:?}`; {}",
   9588             sdk_order_enqueue_summary(enqueue),
   9589             sdk_order_enqueue_retry_summary(enqueue)
   9590         )),
   9591         None => Some(format!(
   9592             "{}; order submit queued in SDK outbox; no ready SDK outbox event was pushed; {}",
   9593             sdk_order_enqueue_summary(enqueue),
   9594             sdk_order_enqueue_retry_summary(enqueue)
   9595         )),
   9596     }
   9597 }
   9598 
   9599 fn sdk_order_submit_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<String> {
   9600     if !matches!(
   9601         push_event.map(|event| event.final_state),
   9602         Some(PushOutboxEventState::Published)
   9603     ) {
   9604         return sdk_order_push_recovery_actions();
   9605     }
   9606     Vec::new()
   9607 }
   9608 
   9609 fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
   9610     event
   9611         .relays
   9612         .iter()
   9613         .map(|relay| relay.relay_url.clone())
   9614         .collect()
   9615 }
   9616 
   9617 fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
   9618     event
   9619         .relays
   9620         .iter()
   9621         .filter(|relay| relay.attempted)
   9622         .map(|relay| relay.relay_url.clone())
   9623         .collect()
   9624 }
   9625 
   9626 fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
   9627     event
   9628         .relays
   9629         .iter()
   9630         .filter(|relay| {
   9631             matches!(
   9632                 relay.outcome_kind,
   9633                 PushOutboxRelayOutcomeKind::Accepted
   9634                     | PushOutboxRelayOutcomeKind::DuplicateAccepted
   9635             )
   9636         })
   9637         .map(|relay| relay.relay_url.clone())
   9638         .collect()
   9639 }
   9640 
   9641 fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> {
   9642     event
   9643         .relays
   9644         .iter()
   9645         .filter(|relay| {
   9646             !matches!(
   9647                 relay.outcome_kind,
   9648                 PushOutboxRelayOutcomeKind::Accepted
   9649                     | PushOutboxRelayOutcomeKind::DuplicateAccepted
   9650             )
   9651         })
   9652         .map(|relay| RelayFailureView {
   9653             relay: relay.relay_url.clone(),
   9654             reason: relay
   9655                 .message
   9656                 .clone()
   9657                 .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()),
   9658         })
   9659         .collect()
   9660 }
   9661 
   9662 fn sdk_relay_outcome_kind(kind: PushOutboxRelayOutcomeKind) -> &'static str {
   9663     match kind {
   9664         PushOutboxRelayOutcomeKind::Accepted => "accepted",
   9665         PushOutboxRelayOutcomeKind::DuplicateAccepted => "duplicate_accepted",
   9666         PushOutboxRelayOutcomeKind::Blocked => "blocked",
   9667         PushOutboxRelayOutcomeKind::RateLimited => "rate_limited",
   9668         PushOutboxRelayOutcomeKind::Invalid => "invalid",
   9669         PushOutboxRelayOutcomeKind::PowRequired => "pow_required",
   9670         PushOutboxRelayOutcomeKind::Restricted => "restricted",
   9671         PushOutboxRelayOutcomeKind::AuthRequired => "auth_required",
   9672         PushOutboxRelayOutcomeKind::Error => "error",
   9673         PushOutboxRelayOutcomeKind::Timeout => "timeout",
   9674         PushOutboxRelayOutcomeKind::ConnectionFailed => "connection_failed",
   9675         PushOutboxRelayOutcomeKind::Unknown => "unknown",
   9676         _ => "unknown",
   9677     }
   9678 }
   9679 
   9680 fn order_binding_error_view(
   9681     config: &RuntimeConfig,
   9682     loaded: &LoadedOrderDraft,
   9683     args: &OrderSubmitArgs,
   9684     error: ActorWriteBindingError,
   9685 ) -> OrderSubmitView {
   9686     let (state, reason, actions) = order_actor_write_binding_error_parts(error);
   9687 
   9688     let mut actions = actions;
   9689     actions.push(format!(
   9690         "radroots order get {}",
   9691         loaded.document.order.order_id
   9692     ));
   9693 
   9694     OrderSubmitView {
   9695         state: state.clone(),
   9696         source: ORDER_SOURCE.to_owned(),
   9697         order_id: loaded.document.order.order_id.clone(),
   9698         file: loaded.file.display().to_string(),
   9699         listing_lookup: loaded.document.listing_lookup.clone(),
   9700         listing_addr: non_empty_string(loaded.document.order.listing_addr.clone()),
   9701         listing_event_id: non_empty_string(loaded.document.order.listing_event_id.clone()),
   9702         listing_relays: order_listing_relays(&loaded.document),
   9703         buyer_account_id: buyer_account_id(&loaded.document),
   9704         buyer_pubkey: non_empty_string(loaded.document.order.buyer_pubkey.clone()),
   9705         buyer_actor_source: buyer_actor_source(&loaded.document),
   9706         buyer_custody: None,
   9707         buyer_write_capable: None,
   9708         seller_pubkey: non_empty_string(loaded.document.order.seller_pubkey.clone()),
   9709         event_id: None,
   9710         event_kind: None,
   9711         dry_run: config.output.dry_run,
   9712         deduplicated: false,
   9713         target_relays: Vec::new(),
   9714         connected_relays: Vec::new(),
   9715         acknowledged_relays: Vec::new(),
   9716         failed_relays: Vec::new(),
   9717         idempotency_key: args.idempotency_key.clone(),
   9718         signer_mode: Some(config.signer.backend.as_str().to_owned()),
   9719         reason: Some(reason),
   9720         job: None,
   9721         issues: Vec::new(),
   9722         actions,
   9723     }
   9724 }
   9725 
   9726 fn validate_bound_order_buyer_account(
   9727     config: &RuntimeConfig,
   9728     loaded: &LoadedOrderDraft,
   9729 ) -> Result<account::AccountRecordView, RuntimeError> {
   9730     let document = &loaded.document;
   9731     let account_id = document.buyer_actor.account_id.trim();
   9732     let buyer_pubkey = document.buyer_actor.pubkey.trim();
   9733     let snapshot = account::snapshot(config)?;
   9734     let Some(account) = snapshot
   9735         .accounts
   9736         .iter()
   9737         .find(|account| account.record.account_id.as_str() == account_id)
   9738         .cloned()
   9739     else {
   9740         return Err(account::AccountRuntimeFailure::unresolved_with_detail(
   9741             format!(
   9742                 "order-bound buyer account `{account_id}` is not present in the local account store"
   9743             ),
   9744             order_buyer_failure_detail(
   9745                 loaded,
   9746                 json!({
   9747                     "actions": [
   9748                         "radroots account import <path>",
   9749                         format!("radroots order rebind {} <selector>", document.order.order_id),
   9750                         format!("radroots order get {}", document.order.order_id),
   9751                     ],
   9752                 }),
   9753             ),
   9754         )
   9755         .into());
   9756     };
   9757 
   9758     let account_pubkey = account.record.public_identity.public_key_hex.as_str();
   9759     if !account_pubkey.eq_ignore_ascii_case(buyer_pubkey)
   9760         || !document
   9761             .order
   9762             .buyer_pubkey
   9763             .eq_ignore_ascii_case(buyer_pubkey)
   9764     {
   9765         return Err(account::AccountRuntimeFailure::mismatch_with_detail(
   9766             format!(
   9767                 "order-bound buyer account `{account_id}` does not match order buyer pubkey `{buyer_pubkey}`"
   9768             ),
   9769             order_buyer_failure_detail(
   9770                 loaded,
   9771                 json!({
   9772                     "attempted_buyer_account_id": account_id,
   9773                     "attempted_buyer_pubkey": account_pubkey,
   9774                     "actions": [
   9775                         format!("radroots order rebind {} <selector>", document.order.order_id),
   9776                         format!("radroots order get {}", document.order.order_id),
   9777                     ],
   9778                 }),
   9779             ),
   9780         )
   9781         .into());
   9782     }
   9783 
   9784     if !account.write_capable {
   9785         return Err(account::AccountRuntimeFailure::watch_only_with_detail(
   9786             account_id,
   9787             order_buyer_failure_detail(
   9788                 loaded,
   9789                 json!({
   9790                     "actions": [
   9791                         format!("radroots account attach-secret {account_id} <path>"),
   9792                         format!("radroots order get {}", document.order.order_id),
   9793                     ],
   9794                 }),
   9795             ),
   9796         )
   9797         .into());
   9798     }
   9799 
   9800     if let Some(selector) = config.account.selector.as_deref() {
   9801         let attempted = account::resolve_account_selector(config, selector).map_err(|_| {
   9802             account::AccountRuntimeFailure::unresolved_with_detail(
   9803                 format!("account override `{selector}` did not resolve to a local buyer account"),
   9804                 order_buyer_failure_detail(
   9805                     loaded,
   9806                     json!({
   9807                         "attempted_buyer_account_id": selector,
   9808                         "actions": [
   9809                             "radroots account list",
   9810                             format!("radroots order get {}", document.order.order_id),
   9811                         ],
   9812                     }),
   9813                 ),
   9814             )
   9815         })?;
   9816         if attempted.record.account_id.as_str() != account_id {
   9817             let attempted_pubkey = attempted.record.public_identity.public_key_hex.as_str();
   9818             return Err(account::AccountRuntimeFailure::mismatch_with_detail(
   9819                 format!(
   9820                     "account override `{}` cannot retarget order `{}` bound to buyer account `{account_id}`",
   9821                     attempted.record.account_id, document.order.order_id
   9822                 ),
   9823                 order_buyer_failure_detail(
   9824                     loaded,
   9825                     json!({
   9826                         "attempted_buyer_account_id": attempted.record.account_id.to_string(),
   9827                         "attempted_buyer_pubkey": attempted_pubkey,
   9828                         "actions": [
   9829                             format!("radroots --account-id {account_id} order submit {}", document.order.order_id),
   9830                             format!("radroots order rebind {} <selector>", document.order.order_id),
   9831                             format!("radroots order get {}", document.order.order_id),
   9832                         ],
   9833                     }),
   9834                 ),
   9835             )
   9836             .into());
   9837         }
   9838     }
   9839 
   9840     Ok(account)
   9841 }
   9842 
   9843 fn order_buyer_failure_detail(
   9844     loaded: &LoadedOrderDraft,
   9845     mut extra: serde_json::Value,
   9846 ) -> serde_json::Value {
   9847     let mut detail = json!({
   9848         "buyer_actor_source": loaded.document.buyer_actor.source.as_str(),
   9849         "order_buyer_account_id": loaded.document.buyer_actor.account_id.as_str(),
   9850         "order_buyer_pubkey": loaded.document.buyer_actor.pubkey.as_str(),
   9851         "order_file": loaded.file.display().to_string(),
   9852         "order_id": loaded.document.order.order_id.as_str(),
   9853     });
   9854     if let (Some(detail), Some(extra)) = (detail.as_object_mut(), extra.as_object_mut()) {
   9855         for (key, value) in std::mem::take(extra) {
   9856             detail.insert(key, value);
   9857         }
   9858     }
   9859     detail
   9860 }
   9861 
   9862 fn resolve_local_order_signing_identity(
   9863     config: &RuntimeConfig,
   9864     loaded: &LoadedOrderDraft,
   9865 ) -> Result<account::AccountSigningIdentity, ActorWriteBindingError> {
   9866     resolve_local_order_bound_buyer_signing_identity(config, loaded, "order submit")
   9867 }
   9868 
   9869 fn resolve_local_order_bound_buyer_signing_identity(
   9870     config: &RuntimeConfig,
   9871     loaded: &LoadedOrderDraft,
   9872     action: &str,
   9873 ) -> Result<account::AccountSigningIdentity, ActorWriteBindingError> {
   9874     if !matches!(config.signer.backend, SignerBackend::Local) {
   9875         return Err(ActorWriteBindingError::Unconfigured(format!(
   9876             "{action} requires signer mode `local`"
   9877         )));
   9878     }
   9879     let account_id = loaded.document.buyer_actor.account_id.trim();
   9880     let buyer_pubkey = loaded.document.buyer_actor.pubkey.trim();
   9881     let signing = account::resolve_local_signing_identity_for_account(config, account_id)
   9882         .map_err(ActorWriteBindingError::from_runtime)?;
   9883     let selected_pubkey = signing
   9884         .account
   9885         .record
   9886         .public_identity
   9887         .public_key_hex
   9888         .as_str();
   9889     if !selected_pubkey.eq_ignore_ascii_case(buyer_pubkey) {
   9890         return Err(ActorWriteBindingError::Account(
   9891             account::AccountRuntimeFailure::mismatch_with_detail(
   9892                 format!(
   9893                     "account mismatch: order-bound buyer account `{account_id}` pubkey `{selected_pubkey}` cannot sign order buyer_pubkey `{buyer_pubkey}`"
   9894                 ),
   9895                 order_buyer_failure_detail(
   9896                     loaded,
   9897                     json!({
   9898                         "attempted_buyer_account_id": signing.account.record.account_id.to_string(),
   9899                         "attempted_buyer_pubkey": selected_pubkey,
   9900                         "actions": [
   9901                             format!("radroots order rebind {} <selector>", loaded.document.order.order_id),
   9902                             format!("radroots order get {}", loaded.document.order.order_id),
   9903                         ],
   9904                     }),
   9905                 ),
   9906             ),
   9907         ));
   9908     }
   9909     Ok(signing)
   9910 }
   9911 
   9912 fn resolve_local_order_decision_signing_identity(
   9913     config: &RuntimeConfig,
   9914     seller_pubkey: &str,
   9915     decision: OrderDecisionArg,
   9916 ) -> Result<account::AccountSigningIdentity, ActorWriteBindingError> {
   9917     if !matches!(config.signer.backend, SignerBackend::Local) {
   9918         return Err(ActorWriteBindingError::Unconfigured(format!(
   9919             "order {} requires signer mode `local`",
   9920             decision.command()
   9921         )));
   9922     }
   9923     let signing = account::resolve_local_signing_identity(config)
   9924         .map_err(ActorWriteBindingError::from_runtime)?;
   9925     let selected_pubkey = signing
   9926         .account
   9927         .record
   9928         .public_identity
   9929         .public_key_hex
   9930         .as_str();
   9931     if !selected_pubkey.eq_ignore_ascii_case(seller_pubkey) {
   9932         return Err(ActorWriteBindingError::Account(
   9933             account::AccountRuntimeFailure::mismatch(format!(
   9934                 "account mismatch: resolved account pubkey `{selected_pubkey}` cannot sign order seller_pubkey `{seller_pubkey}`"
   9935             )),
   9936         ));
   9937     }
   9938     Ok(signing)
   9939 }
   9940 
   9941 fn resolve_local_order_revision_signing_identity(
   9942     config: &RuntimeConfig,
   9943     seller_pubkey: &str,
   9944 ) -> Result<account::AccountSigningIdentity, ActorWriteBindingError> {
   9945     if !matches!(config.signer.backend, SignerBackend::Local) {
   9946         return Err(ActorWriteBindingError::Unconfigured(
   9947             "order revision propose requires signer mode `local`".to_owned(),
   9948         ));
   9949     }
   9950     let signing = account::resolve_local_signing_identity(config)
   9951         .map_err(ActorWriteBindingError::from_runtime)?;
   9952     let selected_pubkey = signing
   9953         .account
   9954         .record
   9955         .public_identity
   9956         .public_key_hex
   9957         .as_str();
   9958     if !selected_pubkey.eq_ignore_ascii_case(seller_pubkey) {
   9959         return Err(ActorWriteBindingError::Account(
   9960             account::AccountRuntimeFailure::mismatch(format!(
   9961                 "account mismatch: resolved account pubkey `{selected_pubkey}` cannot sign order seller_pubkey `{seller_pubkey}`"
   9962             )),
   9963         ));
   9964     }
   9965     Ok(signing)
   9966 }
   9967 
   9968 fn resolve_local_order_cancellation_signing_identity(
   9969     config: &RuntimeConfig,
   9970     buyer_pubkey: &str,
   9971 ) -> Result<account::AccountSigningIdentity, ActorWriteBindingError> {
   9972     if !matches!(config.signer.backend, SignerBackend::Local) {
   9973         return Err(ActorWriteBindingError::Unconfigured(
   9974             "order cancel requires signer mode `local`".to_owned(),
   9975         ));
   9976     }
   9977     let signing = account::resolve_local_signing_identity(config)
   9978         .map_err(ActorWriteBindingError::from_runtime)?;
   9979     let selected_pubkey = signing
   9980         .account
   9981         .record
   9982         .public_identity
   9983         .public_key_hex
   9984         .as_str();
   9985     if !selected_pubkey.eq_ignore_ascii_case(buyer_pubkey) {
   9986         return Err(ActorWriteBindingError::Account(
   9987             account::AccountRuntimeFailure::mismatch(format!(
   9988                 "account mismatch: resolved account pubkey `{selected_pubkey}` cannot sign order buyer_pubkey `{buyer_pubkey}`"
   9989             )),
   9990         ));
   9991     }
   9992     Ok(signing)
   9993 }
   9994 
   9995 fn resolve_local_order_revision_decision_signing_identity(
   9996     config: &RuntimeConfig,
   9997     buyer_pubkey: &str,
   9998     args: &OrderRevisionDecisionArgs,
   9999 ) -> Result<account::AccountSigningIdentity, ActorWriteBindingError> {
  10000     if !matches!(config.signer.backend, SignerBackend::Local) {
  10001         return Err(ActorWriteBindingError::Unconfigured(format!(
  10002             "order revision {} requires signer mode `local`",
  10003             args.decision.command()
  10004         )));
  10005     }
  10006     let signing = account::resolve_local_signing_identity(config)
  10007         .map_err(ActorWriteBindingError::from_runtime)?;
  10008     let selected_pubkey = signing
  10009         .account
  10010         .record
  10011         .public_identity
  10012         .public_key_hex
  10013         .as_str();
  10014     if !selected_pubkey.eq_ignore_ascii_case(buyer_pubkey) {
  10015         return Err(ActorWriteBindingError::Account(
  10016             account::AccountRuntimeFailure::mismatch(format!(
  10017                 "account mismatch: resolved account pubkey `{selected_pubkey}` cannot sign order buyer_pubkey `{buyer_pubkey}`"
  10018             )),
  10019         ));
  10020     }
  10021     Ok(signing)
  10022 }
  10023 
  10024 fn relay_failures(failures: Vec<DirectRelayFailure>) -> Vec<RelayFailureView> {
  10025     failures
  10026         .into_iter()
  10027         .map(|failure| RelayFailureView {
  10028             relay: failure.relay,
  10029             reason: failure.reason,
  10030         })
  10031         .collect()
  10032 }
  10033 
  10034 fn load_draft(path: &Path) -> Result<LoadedOrderDraft, String> {
  10035     let contents = fs::read_to_string(path)
  10036         .map_err(|error| format!("read order draft {}: {error}", path.display()))?;
  10037     let document = toml::from_str::<OrderDraftDocument>(contents.as_str())
  10038         .map_err(|error| format!("parse order draft {}: {error}", path.display()))?;
  10039     Ok(LoadedOrderDraft {
  10040         file: path.to_path_buf(),
  10041         updated_at_unix: modified_unix(path).unwrap_or_default(),
  10042         document,
  10043     })
  10044 }
  10045 
  10046 fn save_draft(path: &Path, draft: &OrderDraftDocument) -> Result<(), RuntimeError> {
  10047     if let Some(parent) = path.parent() {
  10048         fs::create_dir_all(parent)?;
  10049     }
  10050     fs::write(path, scaffold_contents(draft)?)?;
  10051     Ok(())
  10052 }
  10053 
  10054 fn scaffold_contents(draft: &OrderDraftDocument) -> Result<String, RuntimeError> {
  10055     let toml = toml::to_string_pretty(draft)
  10056         .map_err(|error| RuntimeError::Config(format!("render order draft: {error}")))?;
  10057     Ok(format!(
  10058         "# radroots order draft v1\n# fill listing_addr and any missing order items before submit\n\n{toml}"
  10059     ))
  10060 }
  10061 
  10062 fn drafts_dir(config: &RuntimeConfig) -> PathBuf {
  10063     config.paths.app_data_root.join(ORDERS_DIR)
  10064 }
  10065 
  10066 fn draft_lookup_path(config: &RuntimeConfig, lookup: &str) -> PathBuf {
  10067     let candidate = PathBuf::from(lookup);
  10068     if candidate.is_absolute() || lookup.contains(std::path::MAIN_SEPARATOR) {
  10069         return candidate;
  10070     }
  10071     let file_name = if lookup.ends_with(".toml") {
  10072         lookup.to_owned()
  10073     } else {
  10074         format!("{lookup}.toml")
  10075     };
  10076     drafts_dir(config).join(file_name)
  10077 }
  10078 
  10079 #[derive(Debug, Clone)]
  10080 struct ParsedListingAddress {
  10081     kind: u32,
  10082     seller_pubkey: String,
  10083     listing_id: String,
  10084 }
  10085 
  10086 fn parse_listing_addr(raw: &str) -> Result<ParsedListingAddress, String> {
  10087     let parsed = RadrootsListingAddress::parse(raw).map_err(|error| error.to_string())?;
  10088     let (kind, rest) = parsed
  10089         .as_str()
  10090         .split_once(':')
  10091         .ok_or_else(|| "listing address has invalid format".to_owned())?;
  10092     let (seller_pubkey, listing_id) = rest
  10093         .split_once(':')
  10094         .ok_or_else(|| "listing address has invalid format".to_owned())?;
  10095     let kind = kind
  10096         .parse::<u32>()
  10097         .map_err(|_| "listing address kind is invalid".to_owned())?;
  10098     Ok(ParsedListingAddress {
  10099         kind,
  10100         seller_pubkey: seller_pubkey.to_owned(),
  10101         listing_id: listing_id.to_owned(),
  10102     })
  10103 }
  10104 
  10105 fn issue(field: impl Into<String>, message: impl Into<String>) -> OrderIssueView {
  10106     let field = field.into();
  10107     issue_with_code(validation_issue_code(&field), field, message)
  10108 }
  10109 
  10110 fn issue_with_code(
  10111     code: impl Into<String>,
  10112     field: impl Into<String>,
  10113     message: impl Into<String>,
  10114 ) -> OrderIssueView {
  10115     OrderIssueView {
  10116         code: code.into(),
  10117         field: field.into(),
  10118         message: message.into(),
  10119         event_ids: Vec::new(),
  10120     }
  10121 }
  10122 
  10123 fn issue_with_events(
  10124     code: impl Into<String>,
  10125     field: impl Into<String>,
  10126     message: impl Into<String>,
  10127     event_ids: Vec<impl ToString>,
  10128 ) -> OrderIssueView {
  10129     let mut event_ids = event_ids
  10130         .into_iter()
  10131         .map(|event_id| event_id.to_string())
  10132         .collect::<Vec<_>>();
  10133     event_ids.sort();
  10134     event_ids.dedup();
  10135     OrderIssueView {
  10136         code: code.into(),
  10137         field: field.into(),
  10138         message: message.into(),
  10139         event_ids,
  10140     }
  10141 }
  10142 
  10143 fn validation_issue_code(field: &str) -> String {
  10144     let mut code = String::new();
  10145     let mut previous_separator = false;
  10146     for character in field.chars() {
  10147         if character.is_ascii_alphanumeric() {
  10148             code.push(character.to_ascii_lowercase());
  10149             previous_separator = false;
  10150         } else if !previous_separator {
  10151             code.push('_');
  10152             previous_separator = true;
  10153         }
  10154     }
  10155     let code = code.trim_matches('_');
  10156     if code.is_empty() {
  10157         "validation_failed".to_owned()
  10158     } else {
  10159         format!("{code}_invalid")
  10160     }
  10161 }
  10162 
  10163 fn normalize_optional(value: Option<&str>) -> Option<String> {
  10164     let value = value?;
  10165     let trimmed = value.trim();
  10166     if trimmed.is_empty() {
  10167         None
  10168     } else {
  10169         Some(trimmed.to_owned())
  10170     }
  10171 }
  10172 
  10173 fn normalize_listing_relay_set<I, S>(values: I) -> Result<Vec<String>, String>
  10174 where
  10175     I: IntoIterator<Item = S>,
  10176     S: AsRef<str>,
  10177 {
  10178     normalize_relay_urls(values).map_err(|error| error.to_string())
  10179 }
  10180 
  10181 fn order_listing_relays(document: &OrderDraftDocument) -> Vec<String> {
  10182     normalize_listing_relay_set(document.order.listing_relays.iter())
  10183         .unwrap_or_else(|_| document.order.listing_relays.clone())
  10184 }
  10185 
  10186 fn non_empty_string(value: String) -> Option<String> {
  10187     if value.trim().is_empty() {
  10188         None
  10189     } else {
  10190         Some(value)
  10191     }
  10192 }
  10193 
  10194 fn non_empty_ref(value: &str) -> Option<&str> {
  10195     if value.trim().is_empty() {
  10196         None
  10197     } else {
  10198         Some(value)
  10199     }
  10200 }
  10201 
  10202 fn modified_unix(path: &Path) -> Option<u64> {
  10203     let modified = fs::metadata(path).ok()?.modified().ok()?;
  10204     modified
  10205         .duration_since(UNIX_EPOCH)
  10206         .ok()
  10207         .map(|value| value.as_secs())
  10208 }
  10209 
  10210 fn now_unix() -> u64 {
  10211     SystemTime::now()
  10212         .duration_since(UNIX_EPOCH)
  10213         .map(|value| value.as_secs())
  10214         .unwrap_or_default()
  10215 }
  10216 
  10217 fn next_order_id() -> String {
  10218     let nanos = SystemTime::now()
  10219         .duration_since(UNIX_EPOCH)
  10220         .map(|duration| duration.as_nanos())
  10221         .unwrap_or_default();
  10222     let counter = ORDER_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
  10223     format!(
  10224         "ord_{}",
  10225         encode_base64url_no_pad((nanos ^ counter).to_be_bytes())
  10226     )
  10227 }
  10228 
  10229 fn next_revision_id() -> String {
  10230     let nanos = SystemTime::now()
  10231         .duration_since(UNIX_EPOCH)
  10232         .map(|duration| duration.as_nanos())
  10233         .unwrap_or_default();
  10234     let counter = ORDER_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
  10235     format!(
  10236         "rev_{}",
  10237         encode_base64url_no_pad((nanos ^ counter).to_be_bytes())
  10238     )
  10239 }
  10240 
  10241 fn is_valid_order_id(value: &str) -> bool {
  10242     if let Some(encoded) = value.strip_prefix("ord_") {
  10243         return encoded.len() == 22 && is_d_tag_base64url(encoded);
  10244     }
  10245     is_canonical_uuid(value)
  10246 }
  10247 
  10248 fn is_canonical_uuid(value: &str) -> bool {
  10249     if value.len() != 36 {
  10250         return false;
  10251     }
  10252     for (index, character) in value.chars().enumerate() {
  10253         if matches!(index, 8 | 13 | 18 | 23) {
  10254             if character != '-' {
  10255                 return false;
  10256             }
  10257         } else if !character.is_ascii_hexdigit() {
  10258             return false;
  10259         }
  10260     }
  10261     true
  10262 }
  10263 
  10264 fn is_valid_event_id(value: &str) -> bool {
  10265     value.len() == 64 && value.chars().all(|ch| ch.is_ascii_hexdigit())
  10266 }
  10267 
  10268 fn encode_base64url_no_pad(bytes: [u8; 16]) -> String {
  10269     const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
  10270     let mut output = String::with_capacity(22);
  10271     let mut index = 0usize;
  10272     while index + 3 <= bytes.len() {
  10273         let block = ((bytes[index] as u32) << 16)
  10274             | ((bytes[index + 1] as u32) << 8)
  10275             | (bytes[index + 2] as u32);
  10276         output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
  10277         output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
  10278         output.push(ALPHABET[((block >> 6) & 0x3f) as usize] as char);
  10279         output.push(ALPHABET[(block & 0x3f) as usize] as char);
  10280         index += 3;
  10281     }
  10282     let remaining = bytes.len() - index;
  10283     if remaining == 1 {
  10284         let block = (bytes[index] as u32) << 16;
  10285         output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
  10286         output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
  10287     } else if remaining == 2 {
  10288         let block = ((bytes[index] as u32) << 16) | ((bytes[index + 1] as u32) << 8);
  10289         output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
  10290         output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
  10291         output.push(ALPHABET[((block >> 6) & 0x3f) as usize] as char);
  10292     }
  10293     output
  10294 }
  10295 
  10296 #[derive(Debug, Clone)]
  10297 struct OrderInspection {
  10298     state: String,
  10299     ready_for_submit: bool,
  10300     listing_addr: Option<String>,
  10301     listing_event_id: Option<String>,
  10302     seller_pubkey: Option<String>,
  10303     buyer_custody: Option<String>,
  10304     buyer_write_capable: Option<bool>,
  10305     issues: Vec<OrderIssueView>,
  10306 }
  10307 
  10308 impl From<OrderGetView> for OrderNewView {
  10309     fn from(view: OrderGetView) -> Self {
  10310         Self {
  10311             state: "draft_created".to_owned(),
  10312             source: view.source,
  10313             order_id: view.order_id.unwrap_or_default(),
  10314             file: view.file.unwrap_or_default(),
  10315             listing_lookup: view.listing_lookup,
  10316             listing_addr: view.listing_addr,
  10317             listing_event_id: view.listing_event_id,
  10318             listing_relays: view.listing_relays,
  10319             buyer_account_id: view.buyer_account_id,
  10320             buyer_pubkey: view.buyer_pubkey,
  10321             buyer_actor_source: view.buyer_actor_source,
  10322             buyer_custody: view.buyer_custody,
  10323             buyer_write_capable: view.buyer_write_capable,
  10324             seller_pubkey: view.seller_pubkey,
  10325             ready_for_submit: view.ready_for_submit,
  10326             items: view.items,
  10327             economics: view.economics,
  10328             issues: view.issues,
  10329             actions: view.actions,
  10330         }
  10331     }
  10332 }