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 }