order.rs (73901B)
1 use std::path::PathBuf; 2 3 use serde::Serialize; 4 use serde_json::{Value, json}; 5 6 use crate::cli::global::{ 7 OrderAppRecordExportArgs, OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs, 8 OrderRebindArgs, OrderRevisionDecisionArg, OrderRevisionDecisionArgs, OrderRevisionProposeArgs, 9 OrderStatusArgs, OrderSubmitArgs, RecordLookupArgs, 10 }; 11 use crate::ops::{ 12 OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload, 13 OperationResult, OperationResultData, OperationService, OrderAcceptRequest, OrderAcceptResult, 14 OrderAppExportRequest, OrderAppExportResult, OrderAppListRequest, OrderAppListResult, 15 OrderCancelRequest, OrderCancelResult, OrderDeclineRequest, OrderDeclineResult, 16 OrderEventListRequest, OrderEventListResult, OrderEventWatchRequest, OrderEventWatchResult, 17 OrderGetRequest, OrderGetResult, OrderListRequest, OrderListResult, OrderRebindRequest, 18 OrderRebindResult, OrderRevisionAcceptRequest, OrderRevisionAcceptResult, 19 OrderRevisionDeclineRequest, OrderRevisionDeclineResult, OrderRevisionProposeRequest, 20 OrderRevisionProposeResult, OrderStatusGetRequest, OrderStatusGetResult, OrderSubmitRequest, 21 OrderSubmitResult, 22 }; 23 use crate::runtime::RuntimeError; 24 use crate::runtime::config::RuntimeConfig; 25 use crate::view::runtime::{ 26 CommandDisposition, OrderAppRecordExportView, OrderCancellationView, OrderDecisionView, 27 OrderRebindView, OrderRevisionDecisionView, OrderRevisionProposalView, OrderStatusView, 28 OrderSubmitView, 29 }; 30 31 const ORDER_EVENT_WATCH_DEFERRED_REASON: &str = "relay-backed order event watch is not implemented"; 32 33 pub struct OrderOperationService<'a> { 34 config: &'a RuntimeConfig, 35 } 36 37 impl<'a> OrderOperationService<'a> { 38 pub fn new(config: &'a RuntimeConfig) -> Self { 39 Self { config } 40 } 41 } 42 43 impl OperationService<OrderSubmitRequest> for OrderOperationService<'_> { 44 type Result = OrderSubmitResult; 45 46 fn execute( 47 &self, 48 request: OperationRequest<OrderSubmitRequest>, 49 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 50 if request.context.requires_approval_token() { 51 return Err(OperationAdapterError::approval_required( 52 request.operation_id(), 53 )); 54 } 55 56 let key = required_order_key(&request)?; 57 let args = OrderSubmitArgs { 58 key, 59 idempotency_key: request 60 .context 61 .idempotency_key 62 .clone() 63 .or_else(|| string_input(&request, "idempotency_key")), 64 }; 65 let mut config = self.config.clone(); 66 if request.context.dry_run { 67 config.output.dry_run = true; 68 } 69 let view = crate::runtime::order::submit(&config, &args).map_err(|error| { 70 OperationAdapterError::sdk_adapter_failure(request.operation_id(), error) 71 })?; 72 submit_result::<OrderSubmitResult>(request.operation_id(), &view) 73 } 74 } 75 76 impl OperationService<OrderGetRequest> for OrderOperationService<'_> { 77 type Result = OrderGetResult; 78 79 fn execute( 80 &self, 81 request: OperationRequest<OrderGetRequest>, 82 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 83 let args = RecordLookupArgs { 84 key: required_order_key(&request)?, 85 }; 86 let view = map_runtime(crate::runtime::order::get(self.config, &args))?; 87 serialized_target_result::<OrderGetResult, _>(&view) 88 } 89 } 90 91 impl OperationService<OrderListRequest> for OrderOperationService<'_> { 92 type Result = OrderListResult; 93 94 fn execute( 95 &self, 96 _request: OperationRequest<OrderListRequest>, 97 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 98 let view = map_runtime(crate::runtime::order::list(self.config))?; 99 serialized_target_result::<OrderListResult, _>(&view) 100 } 101 } 102 103 impl OperationService<OrderAppListRequest> for OrderOperationService<'_> { 104 type Result = OrderAppListResult; 105 106 fn execute( 107 &self, 108 _request: OperationRequest<OrderAppListRequest>, 109 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 110 let view = map_runtime(crate::runtime::order::app_record_list(self.config))?; 111 serialized_target_result::<OrderAppListResult, _>(&view) 112 } 113 } 114 115 impl OperationService<OrderAppExportRequest> for OrderOperationService<'_> { 116 type Result = OrderAppExportResult; 117 118 fn execute( 119 &self, 120 request: OperationRequest<OrderAppExportRequest>, 121 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 122 let args = OrderAppRecordExportArgs { 123 record_id: required_string_input(&request, "record_id")?, 124 output: optional_path_input(&request, "output"), 125 }; 126 let mut config = self.config.clone(); 127 if request.context.dry_run { 128 config.output.dry_run = true; 129 } 130 let view = crate::runtime::order::app_record_export(&config, &args).map_err(|error| { 131 OperationAdapterError::runtime_failure(request.operation_id(), error) 132 })?; 133 order_app_record_export_result::<OrderAppExportResult>(request.operation_id(), &view) 134 } 135 } 136 137 impl OperationService<OrderRebindRequest> for OrderOperationService<'_> { 138 type Result = OrderRebindResult; 139 140 fn execute( 141 &self, 142 request: OperationRequest<OrderRebindRequest>, 143 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 144 let args = OrderRebindArgs { 145 key: required_order_key(&request)?, 146 selector: required_string_input(&request, "selector")?, 147 }; 148 if request.context.dry_run { 149 let view = 150 crate::runtime::order::rebind_preflight(self.config, &args).map_err(|error| { 151 OperationAdapterError::runtime_failure(request.operation_id(), error) 152 })?; 153 return order_rebind_result::<OrderRebindResult>(request.operation_id(), &view); 154 } 155 if request.context.requires_approval_token() { 156 return Err(OperationAdapterError::approval_required( 157 request.operation_id(), 158 )); 159 } 160 161 let view = crate::runtime::order::rebind(self.config, &args).map_err(|error| { 162 OperationAdapterError::runtime_failure(request.operation_id(), error) 163 })?; 164 order_rebind_result::<OrderRebindResult>(request.operation_id(), &view) 165 } 166 } 167 168 impl OperationService<OrderAcceptRequest> for OrderOperationService<'_> { 169 type Result = OrderAcceptResult; 170 171 fn execute( 172 &self, 173 request: OperationRequest<OrderAcceptRequest>, 174 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 175 if request.context.requires_approval_token() { 176 return Err(OperationAdapterError::approval_required( 177 request.operation_id(), 178 )); 179 } 180 181 let args = OrderDecisionArgs { 182 key: required_order_key(&request)?, 183 decision: OrderDecisionArg::Accept, 184 reason: None, 185 idempotency_key: request 186 .context 187 .idempotency_key 188 .clone() 189 .or_else(|| string_input(&request, "idempotency_key")), 190 }; 191 let mut config = self.config.clone(); 192 if request.context.dry_run { 193 config.output.dry_run = true; 194 } 195 let view = crate::runtime::order::decide(&config, &args).map_err(|error| { 196 OperationAdapterError::runtime_failure(request.operation_id(), error) 197 })?; 198 decision_result::<OrderAcceptResult>(request.operation_id(), &view) 199 } 200 } 201 202 impl OperationService<OrderDeclineRequest> for OrderOperationService<'_> { 203 type Result = OrderDeclineResult; 204 205 fn execute( 206 &self, 207 request: OperationRequest<OrderDeclineRequest>, 208 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 209 let reason = string_input(&request, "reason") 210 .map(|reason| reason.trim().to_owned()) 211 .filter(|reason| !reason.is_empty()) 212 .ok_or_else(|| { 213 invalid_input( 214 request.operation_id(), 215 "missing required `reason` input".to_owned(), 216 ) 217 })?; 218 if request.context.requires_approval_token() { 219 return Err(OperationAdapterError::approval_required( 220 request.operation_id(), 221 )); 222 } 223 224 let args = OrderDecisionArgs { 225 key: required_order_key(&request)?, 226 decision: OrderDecisionArg::Decline, 227 reason: Some(reason), 228 idempotency_key: request 229 .context 230 .idempotency_key 231 .clone() 232 .or_else(|| string_input(&request, "idempotency_key")), 233 }; 234 let mut config = self.config.clone(); 235 if request.context.dry_run { 236 config.output.dry_run = true; 237 } 238 let view = crate::runtime::order::decide(&config, &args).map_err(|error| { 239 OperationAdapterError::runtime_failure(request.operation_id(), error) 240 })?; 241 decision_result::<OrderDeclineResult>(request.operation_id(), &view) 242 } 243 } 244 245 impl OperationService<OrderCancelRequest> for OrderOperationService<'_> { 246 type Result = OrderCancelResult; 247 248 fn execute( 249 &self, 250 request: OperationRequest<OrderCancelRequest>, 251 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 252 let reason = string_input(&request, "reason") 253 .map(|reason| reason.trim().to_owned()) 254 .filter(|reason| !reason.is_empty()) 255 .ok_or_else(|| { 256 invalid_input( 257 request.operation_id(), 258 "missing required `reason` input".to_owned(), 259 ) 260 })?; 261 if request.context.requires_approval_token() { 262 return Err(OperationAdapterError::approval_required( 263 request.operation_id(), 264 )); 265 } 266 267 let args = OrderCancelArgs { 268 key: required_order_key(&request)?, 269 reason, 270 idempotency_key: request 271 .context 272 .idempotency_key 273 .clone() 274 .or_else(|| string_input(&request, "idempotency_key")), 275 }; 276 let mut config = self.config.clone(); 277 if request.context.dry_run { 278 config.output.dry_run = true; 279 } 280 let view = crate::runtime::order::cancel(&config, &args).map_err(|error| { 281 OperationAdapterError::runtime_failure(request.operation_id(), error) 282 })?; 283 cancellation_result::<OrderCancelResult>(request.operation_id(), &view) 284 } 285 } 286 287 impl OperationService<OrderRevisionProposeRequest> for OrderOperationService<'_> { 288 type Result = OrderRevisionProposeResult; 289 290 fn execute( 291 &self, 292 request: OperationRequest<OrderRevisionProposeRequest>, 293 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 294 let reason = string_input(&request, "reason") 295 .map(|reason| reason.trim().to_owned()) 296 .filter(|reason| !reason.is_empty()) 297 .ok_or_else(|| { 298 invalid_input( 299 request.operation_id(), 300 "missing required `reason` input".to_owned(), 301 ) 302 })?; 303 if request.context.requires_approval_token() { 304 return Err(OperationAdapterError::approval_required( 305 request.operation_id(), 306 )); 307 } 308 309 let args = OrderRevisionProposeArgs { 310 key: required_order_key(&request)?, 311 reason, 312 bin_id: string_input(&request, "bin_id"), 313 bin_count: u32_input(&request, "bin_count"), 314 adjustment_id: string_input(&request, "adjustment_id"), 315 adjustment_effect: string_input(&request, "adjustment_effect"), 316 adjustment_amount: string_input(&request, "adjustment_amount"), 317 adjustment_currency: string_input(&request, "adjustment_currency"), 318 adjustment_reason: string_input(&request, "adjustment_reason"), 319 idempotency_key: request 320 .context 321 .idempotency_key 322 .clone() 323 .or_else(|| string_input(&request, "idempotency_key")), 324 }; 325 let mut config = self.config.clone(); 326 if request.context.dry_run { 327 config.output.dry_run = true; 328 } 329 let view = crate::runtime::order::revision_propose(&config, &args).map_err(|error| { 330 OperationAdapterError::runtime_failure(request.operation_id(), error) 331 })?; 332 revision_proposal_result::<OrderRevisionProposeResult>(request.operation_id(), &view) 333 } 334 } 335 336 impl OperationService<OrderRevisionAcceptRequest> for OrderOperationService<'_> { 337 type Result = OrderRevisionAcceptResult; 338 339 fn execute( 340 &self, 341 request: OperationRequest<OrderRevisionAcceptRequest>, 342 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 343 let args = OrderRevisionDecisionArgs { 344 key: required_order_key(&request)?, 345 revision_id: required_string_input(&request, "revision_id")?, 346 decision: OrderRevisionDecisionArg::Accept, 347 reason: None, 348 idempotency_key: request 349 .context 350 .idempotency_key 351 .clone() 352 .or_else(|| string_input(&request, "idempotency_key")), 353 }; 354 if request.context.requires_approval_token() { 355 return Err(OperationAdapterError::approval_required( 356 request.operation_id(), 357 )); 358 } 359 360 let mut config = self.config.clone(); 361 if request.context.dry_run { 362 config.output.dry_run = true; 363 } 364 let view = crate::runtime::order::revision_decide(&config, &args).map_err(|error| { 365 OperationAdapterError::runtime_failure(request.operation_id(), error) 366 })?; 367 revision_decision_result::<OrderRevisionAcceptResult>(request.operation_id(), &view) 368 } 369 } 370 371 impl OperationService<OrderRevisionDeclineRequest> for OrderOperationService<'_> { 372 type Result = OrderRevisionDeclineResult; 373 374 fn execute( 375 &self, 376 request: OperationRequest<OrderRevisionDeclineRequest>, 377 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 378 let reason = string_input(&request, "reason") 379 .map(|reason| reason.trim().to_owned()) 380 .filter(|reason| !reason.is_empty()) 381 .ok_or_else(|| { 382 invalid_input( 383 request.operation_id(), 384 "missing required `reason` input".to_owned(), 385 ) 386 })?; 387 let args = OrderRevisionDecisionArgs { 388 key: required_order_key(&request)?, 389 revision_id: required_string_input(&request, "revision_id")?, 390 decision: OrderRevisionDecisionArg::Decline, 391 reason: Some(reason), 392 idempotency_key: request 393 .context 394 .idempotency_key 395 .clone() 396 .or_else(|| string_input(&request, "idempotency_key")), 397 }; 398 if request.context.requires_approval_token() { 399 return Err(OperationAdapterError::approval_required( 400 request.operation_id(), 401 )); 402 } 403 404 let mut config = self.config.clone(); 405 if request.context.dry_run { 406 config.output.dry_run = true; 407 } 408 let view = crate::runtime::order::revision_decide(&config, &args).map_err(|error| { 409 OperationAdapterError::runtime_failure(request.operation_id(), error) 410 })?; 411 revision_decision_result::<OrderRevisionDeclineResult>(request.operation_id(), &view) 412 } 413 } 414 415 impl OperationService<OrderStatusGetRequest> for OrderOperationService<'_> { 416 type Result = OrderStatusGetResult; 417 418 fn execute( 419 &self, 420 request: OperationRequest<OrderStatusGetRequest>, 421 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 422 let args = OrderStatusArgs { 423 key: required_order_key(&request)?, 424 }; 425 let view = crate::runtime::order::status(self.config, &args).map_err(|error| { 426 OperationAdapterError::sdk_adapter_failure(request.operation_id(), error) 427 })?; 428 status_result::<OrderStatusGetResult>(request.operation_id(), &view) 429 } 430 } 431 432 impl OperationService<OrderEventListRequest> for OrderOperationService<'_> { 433 type Result = OrderEventListResult; 434 435 fn execute( 436 &self, 437 request: OperationRequest<OrderEventListRequest>, 438 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 439 let order_id = string_input(&request, "order_id"); 440 let view = crate::runtime::order::event_list(self.config, order_id.as_deref()).map_err( 441 |error| OperationAdapterError::runtime_failure(request.operation_id(), error), 442 )?; 443 event_list_result::<OrderEventListResult>(request.operation_id(), &view) 444 } 445 } 446 447 impl OperationService<OrderEventWatchRequest> for OrderOperationService<'_> { 448 type Result = OrderEventWatchResult; 449 450 fn execute( 451 &self, 452 request: OperationRequest<OrderEventWatchRequest>, 453 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 454 let order_id = required_order_key(&request)?; 455 let action = format!("radroots order status get {order_id}"); 456 Err(OperationAdapterError::not_implemented_with_detail( 457 request.operation_id(), 458 ORDER_EVENT_WATCH_DEFERRED_REASON.to_owned(), 459 json!({ 460 "state": "not_implemented", 461 "order_id": order_id, 462 "reason": ORDER_EVENT_WATCH_DEFERRED_REASON, 463 "actions": [action], 464 }), 465 )) 466 } 467 } 468 469 fn decision_result<R>( 470 operation_id: &str, 471 view: &OrderDecisionView, 472 ) -> Result<OperationResult<R>, OperationAdapterError> 473 where 474 R: OperationResultData, 475 { 476 match view.disposition() { 477 CommandDisposition::Success => serialized_target_result::<R, _>(view), 478 CommandDisposition::ValidationFailed => { 479 let message = view.reason.clone().unwrap_or_else(|| { 480 format!( 481 "order decision failed validation with state `{}`", 482 view.state 483 ) 484 }); 485 Err(OperationAdapterError::validation_failed_with_detail( 486 operation_id, 487 message, 488 order_decision_error_detail(view), 489 )) 490 } 491 disposition => { 492 let message = view 493 .reason 494 .clone() 495 .unwrap_or_else(|| format!("order decision finished with state `{}`", view.state)); 496 if disposition == CommandDisposition::ExternalUnavailable { 497 let detail = order_decision_error_detail(view); 498 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { 499 Err(OperationAdapterError::network_unavailable_with_detail( 500 operation_id, 501 message, 502 detail, 503 )) 504 } else { 505 Err(OperationAdapterError::operation_unavailable_with_detail( 506 operation_id, 507 message, 508 detail, 509 )) 510 } 511 } else if disposition == CommandDisposition::Unconfigured { 512 Err(OperationAdapterError::operation_unavailable_with_detail( 513 operation_id, 514 message, 515 order_decision_error_detail(view), 516 )) 517 } else if disposition == CommandDisposition::NotFound { 518 Err(OperationAdapterError::not_found_with_detail( 519 operation_id, 520 message, 521 order_decision_error_detail(view), 522 )) 523 } else { 524 Err(OperationAdapterError::from_command_disposition( 525 operation_id, 526 disposition, 527 message, 528 )) 529 } 530 } 531 } 532 } 533 534 fn order_decision_error_detail(view: &OrderDecisionView) -> Value { 535 json!({ 536 "state": &view.state, 537 "order_id": &view.order_id, 538 "listing_addr": &view.listing_addr, 539 "listing_event_id": &view.listing_event_id, 540 "request_event_id": &view.request_event_id, 541 "root_event_id": &view.root_event_id, 542 "prev_event_id": &view.prev_event_id, 543 "event_id": &view.event_id, 544 "event_kind": view.event_kind, 545 "inventory": &view.inventory, 546 "buyer_pubkey": &view.buyer_pubkey, 547 "seller_pubkey": &view.seller_pubkey, 548 "decision": &view.decision, 549 "dry_run": view.dry_run, 550 "target_relays": &view.target_relays, 551 "connected_relays": &view.connected_relays, 552 "acknowledged_relays": &view.acknowledged_relays, 553 "failed_relays": &view.failed_relays, 554 "fetched_count": view.fetched_count, 555 "decoded_count": view.decoded_count, 556 "skipped_count": view.skipped_count, 557 "idempotency_key": &view.idempotency_key, 558 "signer_mode": &view.signer_mode, 559 "issues": &view.issues, 560 "actions": &view.actions, 561 }) 562 } 563 564 fn cancellation_result<R>( 565 operation_id: &str, 566 view: &OrderCancellationView, 567 ) -> Result<OperationResult<R>, OperationAdapterError> 568 where 569 R: OperationResultData, 570 { 571 match view.disposition() { 572 CommandDisposition::Success => serialized_target_result::<R, _>(view), 573 CommandDisposition::ValidationFailed => { 574 let message = view.reason.clone().unwrap_or_else(|| { 575 format!("order cancel failed validation with state `{}`", view.state) 576 }); 577 Err(OperationAdapterError::validation_failed_with_detail( 578 operation_id, 579 message, 580 order_cancellation_error_detail(view), 581 )) 582 } 583 disposition => { 584 let message = view 585 .reason 586 .clone() 587 .unwrap_or_else(|| format!("order cancel finished with state `{}`", view.state)); 588 if disposition == CommandDisposition::ExternalUnavailable { 589 let detail = order_cancellation_error_detail(view); 590 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { 591 Err(OperationAdapterError::network_unavailable_with_detail( 592 operation_id, 593 message, 594 detail, 595 )) 596 } else { 597 Err(OperationAdapterError::operation_unavailable_with_detail( 598 operation_id, 599 message, 600 detail, 601 )) 602 } 603 } else if disposition == CommandDisposition::Unconfigured { 604 Err(OperationAdapterError::operation_unavailable_with_detail( 605 operation_id, 606 message, 607 order_cancellation_error_detail(view), 608 )) 609 } else { 610 Err(OperationAdapterError::from_command_disposition( 611 operation_id, 612 disposition, 613 message, 614 )) 615 } 616 } 617 } 618 } 619 620 fn order_cancellation_error_detail(view: &OrderCancellationView) -> Value { 621 json!({ 622 "state": &view.state, 623 "order_id": &view.order_id, 624 "listing_addr": &view.listing_addr, 625 "request_event_id": &view.request_event_id, 626 "decision_event_id": &view.decision_event_id, 627 "root_event_id": &view.root_event_id, 628 "prev_event_id": &view.prev_event_id, 629 "event_id": &view.event_id, 630 "event_kind": view.event_kind, 631 "buyer_pubkey": &view.buyer_pubkey, 632 "seller_pubkey": &view.seller_pubkey, 633 "cancellation_reason": &view.cancellation_reason, 634 "dry_run": view.dry_run, 635 "target_relays": &view.target_relays, 636 "connected_relays": &view.connected_relays, 637 "acknowledged_relays": &view.acknowledged_relays, 638 "failed_relays": &view.failed_relays, 639 "fetched_count": view.fetched_count, 640 "decoded_count": view.decoded_count, 641 "skipped_count": view.skipped_count, 642 "idempotency_key": &view.idempotency_key, 643 "signer_mode": &view.signer_mode, 644 "issues": &view.issues, 645 "actions": &view.actions, 646 }) 647 } 648 649 fn revision_proposal_result<R>( 650 operation_id: &str, 651 view: &OrderRevisionProposalView, 652 ) -> Result<OperationResult<R>, OperationAdapterError> 653 where 654 R: OperationResultData, 655 { 656 match view.disposition() { 657 CommandDisposition::Success => serialized_target_result::<R, _>(view), 658 CommandDisposition::ValidationFailed => { 659 let message = view.reason.clone().unwrap_or_else(|| { 660 format!( 661 "order revision propose failed validation with state `{}`", 662 view.state 663 ) 664 }); 665 Err(OperationAdapterError::validation_failed_with_detail( 666 operation_id, 667 message, 668 order_revision_proposal_error_detail(view), 669 )) 670 } 671 disposition => { 672 let message = view.reason.clone().unwrap_or_else(|| { 673 format!( 674 "order revision propose finished with state `{}`", 675 view.state 676 ) 677 }); 678 if disposition == CommandDisposition::ExternalUnavailable { 679 let detail = order_revision_proposal_error_detail(view); 680 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { 681 Err(OperationAdapterError::network_unavailable_with_detail( 682 operation_id, 683 message, 684 detail, 685 )) 686 } else { 687 Err(OperationAdapterError::operation_unavailable_with_detail( 688 operation_id, 689 message, 690 detail, 691 )) 692 } 693 } else if disposition == CommandDisposition::Unconfigured { 694 Err(OperationAdapterError::operation_unavailable_with_detail( 695 operation_id, 696 message, 697 order_revision_proposal_error_detail(view), 698 )) 699 } else { 700 Err(OperationAdapterError::from_command_disposition( 701 operation_id, 702 disposition, 703 message, 704 )) 705 } 706 } 707 } 708 } 709 710 fn order_revision_proposal_error_detail(view: &OrderRevisionProposalView) -> Value { 711 json!({ 712 "state": &view.state, 713 "order_id": &view.order_id, 714 "revision_id": &view.revision_id, 715 "listing_addr": &view.listing_addr, 716 "request_event_id": &view.request_event_id, 717 "decision_event_id": &view.decision_event_id, 718 "root_event_id": &view.root_event_id, 719 "prev_event_id": &view.prev_event_id, 720 "event_id": &view.event_id, 721 "event_kind": view.event_kind, 722 "items": &view.items, 723 "economics": &view.economics, 724 "inventory": &view.inventory, 725 "buyer_pubkey": &view.buyer_pubkey, 726 "seller_pubkey": &view.seller_pubkey, 727 "dry_run": view.dry_run, 728 "target_relays": &view.target_relays, 729 "connected_relays": &view.connected_relays, 730 "acknowledged_relays": &view.acknowledged_relays, 731 "failed_relays": &view.failed_relays, 732 "fetched_count": view.fetched_count, 733 "decoded_count": view.decoded_count, 734 "skipped_count": view.skipped_count, 735 "idempotency_key": &view.idempotency_key, 736 "signer_mode": &view.signer_mode, 737 "issues": &view.issues, 738 "actions": &view.actions, 739 }) 740 } 741 742 fn revision_decision_result<R>( 743 operation_id: &str, 744 view: &OrderRevisionDecisionView, 745 ) -> Result<OperationResult<R>, OperationAdapterError> 746 where 747 R: OperationResultData, 748 { 749 match view.disposition() { 750 CommandDisposition::Success => serialized_target_result::<R, _>(view), 751 CommandDisposition::ValidationFailed => { 752 let message = view.reason.clone().unwrap_or_else(|| { 753 format!( 754 "order revision {} failed validation with state `{}`", 755 view.decision.as_deref().unwrap_or("decision"), 756 view.state 757 ) 758 }); 759 Err(OperationAdapterError::validation_failed_with_detail( 760 operation_id, 761 message, 762 order_revision_decision_error_detail(view), 763 )) 764 } 765 disposition => { 766 let message = view.reason.clone().unwrap_or_else(|| { 767 format!( 768 "order revision {} finished with state `{}`", 769 view.decision.as_deref().unwrap_or("decision"), 770 view.state 771 ) 772 }); 773 if disposition == CommandDisposition::ExternalUnavailable { 774 let detail = order_revision_decision_error_detail(view); 775 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { 776 Err(OperationAdapterError::network_unavailable_with_detail( 777 operation_id, 778 message, 779 detail, 780 )) 781 } else { 782 Err(OperationAdapterError::operation_unavailable_with_detail( 783 operation_id, 784 message, 785 detail, 786 )) 787 } 788 } else if disposition == CommandDisposition::Unconfigured { 789 Err(OperationAdapterError::operation_unavailable_with_detail( 790 operation_id, 791 message, 792 order_revision_decision_error_detail(view), 793 )) 794 } else { 795 Err(OperationAdapterError::from_command_disposition( 796 operation_id, 797 disposition, 798 message, 799 )) 800 } 801 } 802 } 803 } 804 805 fn order_revision_decision_error_detail(view: &OrderRevisionDecisionView) -> Value { 806 json!({ 807 "state": &view.state, 808 "order_id": &view.order_id, 809 "revision_id": &view.revision_id, 810 "decision": &view.decision, 811 "listing_addr": &view.listing_addr, 812 "request_event_id": &view.request_event_id, 813 "decision_event_id": &view.decision_event_id, 814 "agreement_event_id": &view.agreement_event_id, 815 "root_event_id": &view.root_event_id, 816 "prev_event_id": &view.prev_event_id, 817 "event_id": &view.event_id, 818 "event_kind": view.event_kind, 819 "economics": &view.economics, 820 "inventory": &view.inventory, 821 "buyer_pubkey": &view.buyer_pubkey, 822 "seller_pubkey": &view.seller_pubkey, 823 "dry_run": view.dry_run, 824 "target_relays": &view.target_relays, 825 "connected_relays": &view.connected_relays, 826 "acknowledged_relays": &view.acknowledged_relays, 827 "failed_relays": &view.failed_relays, 828 "fetched_count": view.fetched_count, 829 "decoded_count": view.decoded_count, 830 "skipped_count": view.skipped_count, 831 "idempotency_key": &view.idempotency_key, 832 "signer_mode": &view.signer_mode, 833 "issues": &view.issues, 834 "actions": &view.actions, 835 }) 836 } 837 838 fn status_result<R>( 839 operation_id: &str, 840 view: &OrderStatusView, 841 ) -> Result<OperationResult<R>, OperationAdapterError> 842 where 843 R: OperationResultData, 844 { 845 match view.disposition() { 846 CommandDisposition::Success => serialized_target_result::<R, _>(view), 847 disposition => { 848 let message = view 849 .reason 850 .clone() 851 .unwrap_or_else(|| format!("order status finished with state `{}`", view.state)); 852 if disposition == CommandDisposition::ExternalUnavailable { 853 let detail = order_status_error_detail(view); 854 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { 855 Err(OperationAdapterError::network_unavailable_with_detail( 856 operation_id, 857 message, 858 detail, 859 )) 860 } else { 861 Err(OperationAdapterError::operation_unavailable_with_detail( 862 operation_id, 863 message, 864 detail, 865 )) 866 } 867 } else if disposition == CommandDisposition::Unconfigured { 868 Err(OperationAdapterError::operation_unavailable_with_detail( 869 operation_id, 870 message, 871 order_status_error_detail(view), 872 )) 873 } else { 874 Err(OperationAdapterError::from_command_disposition( 875 operation_id, 876 disposition, 877 message, 878 )) 879 } 880 } 881 } 882 } 883 884 fn order_status_error_detail(view: &OrderStatusView) -> Value { 885 json!({ 886 "state": &view.state, 887 "order_id": &view.order_id, 888 "request_event_id": &view.request_event_id, 889 "decision_event_id": &view.decision_event_id, 890 "agreement_event_id": &view.agreement_event_id, 891 "listing_event_id": &view.listing_event_id, 892 "listing_addr": &view.listing_addr, 893 "buyer_pubkey": &view.buyer_pubkey, 894 "seller_pubkey": &view.seller_pubkey, 895 "last_event_id": &view.last_event_id, 896 "revision": &view.revision, 897 "inventory": &view.inventory, 898 "lifecycle": &view.lifecycle, 899 "reducer_issues": &view.reducer_issues, 900 "target_relays": &view.target_relays, 901 "connected_relays": &view.connected_relays, 902 "failed_relays": &view.failed_relays, 903 "fetched_count": view.fetched_count, 904 "decoded_count": view.decoded_count, 905 "skipped_count": view.skipped_count, 906 "actions": &view.actions, 907 }) 908 } 909 910 fn serialized_target_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError> 911 where 912 R: OperationResultData, 913 T: Serialize, 914 { 915 OperationResult::new(R::from_serializable(value)?) 916 } 917 918 fn submit_result<R>( 919 operation_id: &str, 920 view: &OrderSubmitView, 921 ) -> Result<OperationResult<R>, OperationAdapterError> 922 where 923 R: OperationResultData, 924 { 925 match view.disposition() { 926 CommandDisposition::Success => serialized_target_result::<R, _>(view), 927 disposition => { 928 let message = view 929 .reason 930 .clone() 931 .unwrap_or_else(|| format!("order submit finished with state `{}`", view.state)); 932 let detail = order_submit_error_detail(view); 933 match disposition { 934 CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail( 935 operation_id, 936 message, 937 detail, 938 )), 939 CommandDisposition::ValidationFailed => { 940 Err(OperationAdapterError::validation_failed_with_detail( 941 operation_id, 942 message, 943 detail, 944 )) 945 } 946 CommandDisposition::Unconfigured => { 947 Err(OperationAdapterError::operation_unavailable_with_detail( 948 operation_id, 949 message, 950 detail, 951 )) 952 } 953 CommandDisposition::ExternalUnavailable => { 954 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { 955 Err(OperationAdapterError::network_unavailable_with_detail( 956 operation_id, 957 message, 958 detail, 959 )) 960 } else { 961 Err(OperationAdapterError::operation_unavailable_with_detail( 962 operation_id, 963 message, 964 detail, 965 )) 966 } 967 } 968 _ => Err(OperationAdapterError::from_command_disposition( 969 operation_id, 970 disposition, 971 message, 972 )), 973 } 974 } 975 } 976 } 977 978 fn order_app_record_export_result<R>( 979 operation_id: &str, 980 view: &OrderAppRecordExportView, 981 ) -> Result<OperationResult<R>, OperationAdapterError> 982 where 983 R: OperationResultData, 984 { 985 match view.disposition() { 986 CommandDisposition::Success => serialized_target_result::<R, _>(view), 987 CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail( 988 operation_id, 989 view.reason.clone().unwrap_or_else(|| { 990 format!( 991 "app-authored local order record `{}` was not found", 992 view.record_id 993 ) 994 }), 995 serde_json::to_value(view).unwrap_or(Value::Null), 996 )), 997 CommandDisposition::ValidationFailed => { 998 Err(OperationAdapterError::validation_failed_with_detail( 999 operation_id, 1000 view.reason.clone().unwrap_or_else(|| { 1001 format!( 1002 "app-authored local order record `{}` cannot be exported", 1003 view.record_id 1004 ) 1005 }), 1006 serde_json::to_value(view).unwrap_or(Value::Null), 1007 )) 1008 } 1009 disposition => Err(OperationAdapterError::from_command_disposition( 1010 operation_id, 1011 disposition, 1012 view.reason.clone().unwrap_or_else(|| { 1013 format!( 1014 "app-authored local order record export finished with state `{}`", 1015 view.state 1016 ) 1017 }), 1018 )), 1019 } 1020 } 1021 1022 fn order_rebind_result<R>( 1023 operation_id: &str, 1024 view: &OrderRebindView, 1025 ) -> Result<OperationResult<R>, OperationAdapterError> 1026 where 1027 R: OperationResultData, 1028 { 1029 match view.disposition() { 1030 CommandDisposition::Success => serialized_target_result::<R, _>(view), 1031 CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail( 1032 operation_id, 1033 view.reason 1034 .clone() 1035 .unwrap_or_else(|| format!("order draft `{}` was not found", view.lookup)), 1036 order_rebind_error_detail(view), 1037 )), 1038 CommandDisposition::ValidationFailed => { 1039 Err(OperationAdapterError::validation_failed_with_detail( 1040 operation_id, 1041 view.reason.clone().unwrap_or_else(|| { 1042 format!("order rebind finished with state `{}`", view.state) 1043 }), 1044 order_rebind_error_detail(view), 1045 )) 1046 } 1047 disposition => Err(OperationAdapterError::from_command_disposition( 1048 operation_id, 1049 disposition, 1050 view.reason 1051 .clone() 1052 .unwrap_or_else(|| format!("order rebind finished with state `{}`", view.state)), 1053 )), 1054 } 1055 } 1056 1057 fn order_rebind_error_detail(view: &OrderRebindView) -> Value { 1058 json!({ 1059 "state": &view.state, 1060 "source": &view.source, 1061 "lookup": &view.lookup, 1062 "file": &view.file, 1063 "dry_run": view.dry_run, 1064 "from_order_id": &view.from_order_id, 1065 "to_order_id": &view.to_order_id, 1066 "order_id_changed": view.order_id_changed, 1067 "from_buyer_account_id": &view.from_buyer_account_id, 1068 "from_buyer_pubkey": &view.from_buyer_pubkey, 1069 "from_buyer_actor_source": &view.from_buyer_actor_source, 1070 "to_buyer_account_id": &view.to_buyer_account_id, 1071 "to_buyer_pubkey": &view.to_buyer_pubkey, 1072 "to_buyer_actor_source": &view.to_buyer_actor_source, 1073 "buyer_pubkey_changed": view.buyer_pubkey_changed, 1074 "existing_request_check": &view.existing_request_check, 1075 "existing_request_event_ids": &view.existing_request_event_ids, 1076 "actions": &view.actions, 1077 }) 1078 } 1079 1080 fn order_submit_error_detail(view: &OrderSubmitView) -> Value { 1081 json!({ 1082 "state": &view.state, 1083 "source": &view.source, 1084 "order_id": &view.order_id, 1085 "file": &view.file, 1086 "listing_lookup": &view.listing_lookup, 1087 "listing_addr": &view.listing_addr, 1088 "listing_event_id": &view.listing_event_id, 1089 "listing_relays": &view.listing_relays, 1090 "buyer_account_id": &view.buyer_account_id, 1091 "buyer_pubkey": &view.buyer_pubkey, 1092 "seller_pubkey": &view.seller_pubkey, 1093 "event_id": &view.event_id, 1094 "event_kind": view.event_kind, 1095 "dry_run": view.dry_run, 1096 "deduplicated": view.deduplicated, 1097 "target_relays": &view.target_relays, 1098 "connected_relays": &view.connected_relays, 1099 "acknowledged_relays": &view.acknowledged_relays, 1100 "failed_relays": &view.failed_relays, 1101 "idempotency_key": &view.idempotency_key, 1102 "signer_mode": &view.signer_mode, 1103 "issues": &view.issues, 1104 "actions": &view.actions, 1105 }) 1106 } 1107 1108 fn event_list_result<R>( 1109 operation_id: &str, 1110 view: &crate::view::runtime::OrderEventListView, 1111 ) -> Result<OperationResult<R>, OperationAdapterError> 1112 where 1113 R: OperationResultData, 1114 { 1115 match view.disposition() { 1116 CommandDisposition::Success => serialized_target_result::<R, _>(view), 1117 disposition => { 1118 let message = view.reason.clone().unwrap_or_else(|| { 1119 format!("order event list finished with state `{}`", view.state) 1120 }); 1121 if disposition == CommandDisposition::ExternalUnavailable { 1122 let detail = order_event_list_error_detail(view); 1123 Err(OperationAdapterError::network_unavailable_with_detail( 1124 operation_id, 1125 message, 1126 detail, 1127 )) 1128 } else if disposition == CommandDisposition::Unconfigured { 1129 Err(OperationAdapterError::operation_unavailable_with_detail( 1130 operation_id, 1131 message, 1132 order_event_list_error_detail(view), 1133 )) 1134 } else { 1135 Err(OperationAdapterError::from_command_disposition( 1136 operation_id, 1137 disposition, 1138 message, 1139 )) 1140 } 1141 } 1142 } 1143 } 1144 1145 fn order_event_list_error_detail(view: &crate::view::runtime::OrderEventListView) -> Value { 1146 json!({ 1147 "state": &view.state, 1148 "seller_pubkey": &view.seller_pubkey, 1149 "target_relays": &view.target_relays, 1150 "connected_relays": &view.connected_relays, 1151 "failed_relays": &view.failed_relays, 1152 "fetched_count": view.fetched_count, 1153 "decoded_count": view.decoded_count, 1154 "skipped_count": view.skipped_count, 1155 "count": view.count, 1156 "actions": &view.actions, 1157 }) 1158 } 1159 1160 fn required_order_key<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError> 1161 where 1162 P: OperationRequestPayload + OperationRequestData, 1163 { 1164 string_input(request, "order_id") 1165 .or_else(|| string_input(request, "key")) 1166 .ok_or_else(|| { 1167 invalid_input( 1168 request.operation_id(), 1169 "missing required `order_id` input".to_owned(), 1170 ) 1171 }) 1172 } 1173 1174 fn required_string_input<P>( 1175 request: &OperationRequest<P>, 1176 key: &str, 1177 ) -> Result<String, OperationAdapterError> 1178 where 1179 P: OperationRequestPayload + OperationRequestData, 1180 { 1181 string_input(request, key) 1182 .map(|value| value.trim().to_owned()) 1183 .filter(|value| !value.is_empty()) 1184 .ok_or_else(|| { 1185 invalid_input( 1186 request.operation_id(), 1187 format!("missing required `{key}` input"), 1188 ) 1189 }) 1190 } 1191 1192 fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String> 1193 where 1194 P: OperationRequestPayload + OperationRequestData, 1195 { 1196 request 1197 .payload 1198 .input() 1199 .get(key) 1200 .and_then(Value::as_str) 1201 .map(str::to_owned) 1202 } 1203 1204 fn optional_path_input<P>(request: &OperationRequest<P>, key: &str) -> Option<PathBuf> 1205 where 1206 P: OperationRequestPayload + OperationRequestData, 1207 { 1208 string_input(request, key).map(PathBuf::from) 1209 } 1210 1211 fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool> 1212 where 1213 P: OperationRequestPayload + OperationRequestData, 1214 { 1215 request.payload.input().get(key).and_then(Value::as_bool) 1216 } 1217 1218 fn u32_input<P>(request: &OperationRequest<P>, key: &str) -> Option<u32> 1219 where 1220 P: OperationRequestPayload + OperationRequestData, 1221 { 1222 request 1223 .payload 1224 .input() 1225 .get(key) 1226 .and_then(Value::as_u64) 1227 .and_then(|value| u32::try_from(value).ok()) 1228 } 1229 1230 fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> { 1231 result.map_err(|error| OperationAdapterError::Runtime(error.to_string())) 1232 } 1233 1234 fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError { 1235 OperationAdapterError::InvalidInput { 1236 operation_id: operation_id.to_owned(), 1237 message, 1238 } 1239 } 1240 1241 #[cfg(test)] 1242 mod tests { 1243 use std::path::{Path, PathBuf}; 1244 1245 use radroots_runtime_paths::RadrootsMigrationReport; 1246 use radroots_secret_vault::RadrootsSecretBackend; 1247 use serde_json::{Map, Value}; 1248 use tempfile::tempdir; 1249 1250 use super::{OrderOperationService, decision_result}; 1251 use crate::ops::{ 1252 OperationAdapter, OperationContext, OperationData, OperationRequest, OrderAcceptRequest, 1253 OrderAcceptResult, OrderCancelRequest, OrderDeclineRequest, OrderDeclineResult, 1254 OrderEventListRequest, OrderEventWatchRequest, OrderGetRequest, OrderListRequest, 1255 OrderRevisionAcceptRequest, OrderRevisionDeclineRequest, OrderRevisionProposeRequest, 1256 OrderStatusGetRequest, OrderSubmitRequest, 1257 }; 1258 use crate::runtime::config::{ 1259 AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, 1260 LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat, 1261 PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig, 1262 RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, 1263 SignerConfig, Verbosity, 1264 }; 1265 use crate::view::runtime::OrderDecisionView; 1266 1267 #[test] 1268 fn order_service_get_and_list_preserve_order_truth() { 1269 let dir = tempdir().expect("tempdir"); 1270 let config = sample_config(dir.path()); 1271 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1272 let get = OperationRequest::new( 1273 OperationContext::default(), 1274 OrderGetRequest::from_data(data(&[("order_id", "ord_missing")])), 1275 ) 1276 .expect("order get request"); 1277 let get_envelope = service 1278 .execute(get) 1279 .expect("order get result") 1280 .to_envelope(OperationContext::default().envelope_context("req_order_get")) 1281 .expect("order get envelope"); 1282 1283 assert_eq!(get_envelope.operation_id, "order.get"); 1284 assert_eq!(get_envelope.result["state"], "missing"); 1285 assert_eq!(get_envelope.result["actions"][0], "radroots order list"); 1286 assert_eq!(get_envelope.result["actions"][1], "radroots basket create"); 1287 1288 let list = OperationRequest::new(OperationContext::default(), OrderListRequest::default()) 1289 .expect("order list request"); 1290 let list_envelope = service 1291 .execute(list) 1292 .expect("order list result") 1293 .to_envelope(OperationContext::default().envelope_context("req_order_list")) 1294 .expect("order list envelope"); 1295 assert_eq!(list_envelope.operation_id, "order.list"); 1296 assert_eq!(list_envelope.result["state"], "empty"); 1297 assert_eq!(list_envelope.result["actions"][0], "radroots basket create"); 1298 } 1299 1300 #[test] 1301 fn order_submit_requires_approval_token() { 1302 let dir = tempdir().expect("tempdir"); 1303 let config = sample_config(dir.path()); 1304 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1305 let submit = OperationRequest::new( 1306 OperationContext::default(), 1307 OrderSubmitRequest::from_data(data(&[("order_id", "ord_missing")])), 1308 ) 1309 .expect("order submit request"); 1310 let error = service.execute(submit).expect_err("approval required"); 1311 1312 assert!(format!("{error}").contains("approval_token")); 1313 assert_eq!(error.to_output_error().code, "approval_required"); 1314 assert_eq!(error.to_output_error().exit_code, 6); 1315 } 1316 1317 #[test] 1318 fn order_submit_with_approval_returns_not_found_for_missing_order() { 1319 let dir = tempdir().expect("tempdir"); 1320 let config = sample_config(dir.path()); 1321 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1322 let mut context = OperationContext::default(); 1323 context.approval_token = Some("approve_test".to_owned()); 1324 let submit = OperationRequest::new( 1325 context.clone(), 1326 OrderSubmitRequest::from_data(data(&[("order_id", "ord_missing")])), 1327 ) 1328 .expect("order submit request"); 1329 let error = service.execute(submit).expect_err("missing order error"); 1330 let output_error = error.to_output_error(); 1331 1332 assert_eq!(output_error.code, "not_found"); 1333 assert_eq!(output_error.exit_code, 4); 1334 assert!(output_error.message.contains("ord_missing")); 1335 let envelope = crate::out::envelope::OutputEnvelope::failure( 1336 "order.submit", 1337 output_error, 1338 context.envelope_context("req_order_submit"), 1339 ); 1340 let detail = envelope.errors[0].detail.as_ref().expect("submit detail"); 1341 assert_eq!(detail["state"], "missing"); 1342 assert_eq!(detail["order_id"], "ord_missing"); 1343 assert_eq!(detail["actions"][0], "radroots order list"); 1344 assert_eq!(detail["actions"][1], "radroots basket create"); 1345 assert_eq!( 1346 envelope.next_actions[0].command.as_deref(), 1347 Some("radroots order list") 1348 ); 1349 assert_eq!( 1350 envelope.next_actions[1].command.as_deref(), 1351 Some("radroots basket create") 1352 ); 1353 } 1354 1355 #[test] 1356 fn order_accept_requires_approval_token() { 1357 let dir = tempdir().expect("tempdir"); 1358 let config = sample_config(dir.path()); 1359 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1360 let accept = OperationRequest::new( 1361 OperationContext::default(), 1362 OrderAcceptRequest::from_data(data(&[("order_id", "ord_pending")])), 1363 ) 1364 .expect("order accept request"); 1365 let error = service.execute(accept).expect_err("approval required"); 1366 1367 assert_eq!(error.to_output_error().code, "approval_required"); 1368 } 1369 1370 #[test] 1371 fn order_accept_unconfigured_preserves_decision_detail() { 1372 let dir = tempdir().expect("tempdir"); 1373 let config = sample_config(dir.path()); 1374 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1375 let mut context = OperationContext::default(); 1376 context.dry_run = true; 1377 let accept = OperationRequest::new( 1378 context, 1379 OrderAcceptRequest::from_data(data(&[("order_id", "ord_pending")])), 1380 ) 1381 .expect("order accept request"); 1382 let error = service 1383 .execute(accept) 1384 .expect_err("order accept unconfigured"); 1385 let output_error = error.to_output_error(); 1386 let detail = output_error.detail.as_ref().expect("decision detail"); 1387 1388 assert_eq!(output_error.code, "operation_unavailable"); 1389 assert_eq!(detail["state"], "unconfigured"); 1390 assert_eq!(detail["order_id"], "ord_pending"); 1391 assert_eq!(detail["decision"], "accepted"); 1392 assert!(detail["target_relays"].as_array().unwrap().is_empty()); 1393 } 1394 1395 #[test] 1396 fn order_decision_already_decided_maps_to_validation_failure() { 1397 let view = already_decided_view(); 1398 let error = match decision_result::<OrderAcceptResult>("order.accept", &view) { 1399 Ok(_) => panic!("already decided view should fail validation"), 1400 Err(error) => error, 1401 }; 1402 let output_error = error.to_output_error(); 1403 1404 assert_eq!(output_error.code, "validation_failed"); 1405 assert_eq!(output_error.exit_code, 10); 1406 let detail = output_error.detail.expect("validation detail"); 1407 assert_eq!(detail["state"], "already_decided"); 1408 assert_eq!(detail["operation_id"], "order.accept"); 1409 assert_eq!(detail["listing_event_id"], "l".repeat(64)); 1410 assert_eq!(detail["event_id"], "d".repeat(64)); 1411 assert_eq!(detail["event_kind"], 3423); 1412 assert_eq!(detail["idempotency_key"], "idem_test"); 1413 assert_eq!(detail["signer_mode"], "local"); 1414 assert_eq!(detail["actions"][0], "radroots order status get ord_test"); 1415 } 1416 1417 #[test] 1418 fn order_decision_invalid_maps_to_validation_failure() { 1419 let view = invalid_decision_view(); 1420 let error = match decision_result::<OrderDeclineResult>("order.decline", &view) { 1421 Ok(_) => panic!("invalid view should fail validation"), 1422 Err(error) => error, 1423 }; 1424 let output_error = error.to_output_error(); 1425 1426 assert_eq!(output_error.code, "validation_failed"); 1427 assert_eq!(output_error.exit_code, 10); 1428 assert_eq!( 1429 output_error.message, 1430 "active order events for `ord_test` failed reducer validation" 1431 ); 1432 let detail = output_error.detail.expect("validation detail"); 1433 assert_eq!(detail["state"], "invalid"); 1434 assert_eq!(detail["operation_id"], "order.decline"); 1435 assert_eq!(detail["listing_event_id"], "l".repeat(64)); 1436 assert_eq!(detail["event_id"], Value::Null); 1437 assert_eq!(detail["event_kind"], Value::Null); 1438 assert_eq!(detail["idempotency_key"], "idem_test"); 1439 assert_eq!(detail["signer_mode"], "local"); 1440 } 1441 1442 #[test] 1443 fn order_decline_requires_reason_before_approval() { 1444 let dir = tempdir().expect("tempdir"); 1445 let config = sample_config(dir.path()); 1446 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1447 let decline = OperationRequest::new( 1448 OperationContext::default(), 1449 OrderDeclineRequest::from_data(data(&[("order_id", "ord_pending")])), 1450 ) 1451 .expect("order decline request"); 1452 let error = service.execute(decline).expect_err("reason required"); 1453 let output_error = error.to_output_error(); 1454 1455 assert_eq!(output_error.code, "invalid_input"); 1456 assert!(output_error.message.contains("reason")); 1457 } 1458 1459 #[test] 1460 fn order_decline_rejects_blank_reason_before_approval() { 1461 let dir = tempdir().expect("tempdir"); 1462 let config = sample_config(dir.path()); 1463 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1464 let decline = OperationRequest::new( 1465 OperationContext::default(), 1466 OrderDeclineRequest::from_data(data(&[("order_id", "ord_pending"), ("reason", " ")])), 1467 ) 1468 .expect("order decline request"); 1469 let error = service.execute(decline).expect_err("reason required"); 1470 let output_error = error.to_output_error(); 1471 1472 assert_eq!(output_error.code, "invalid_input"); 1473 assert!(output_error.message.contains("reason")); 1474 } 1475 1476 #[test] 1477 fn order_cancel_requires_reason_before_approval() { 1478 let dir = tempdir().expect("tempdir"); 1479 let config = sample_config(dir.path()); 1480 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1481 let cancel = OperationRequest::new( 1482 OperationContext::default(), 1483 OrderCancelRequest::from_data(data(&[("order_id", "ord_pending")])), 1484 ) 1485 .expect("order cancel request"); 1486 let error = service.execute(cancel).expect_err("reason required"); 1487 let output_error = error.to_output_error(); 1488 1489 assert_eq!(output_error.code, "invalid_input"); 1490 assert!(output_error.message.contains("reason")); 1491 } 1492 1493 #[test] 1494 fn order_cancel_requires_approval_token() { 1495 let dir = tempdir().expect("tempdir"); 1496 let config = sample_config(dir.path()); 1497 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1498 let cancel = OperationRequest::new( 1499 OperationContext::default(), 1500 OrderCancelRequest::from_data(data(&[ 1501 ("order_id", "ord_pending"), 1502 ("reason", "changed plans"), 1503 ])), 1504 ) 1505 .expect("order cancel request"); 1506 let error = service.execute(cancel).expect_err("approval required"); 1507 1508 assert_eq!(error.to_output_error().code, "approval_required"); 1509 } 1510 1511 #[test] 1512 fn order_revision_propose_requires_reason_before_approval() { 1513 let dir = tempdir().expect("tempdir"); 1514 let config = sample_config(dir.path()); 1515 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1516 let revision = OperationRequest::new( 1517 OperationContext::default(), 1518 OrderRevisionProposeRequest::from_data(data(&[("order_id", "ord_pending")])), 1519 ) 1520 .expect("order revision request"); 1521 let error = service.execute(revision).expect_err("reason required"); 1522 let output_error = error.to_output_error(); 1523 1524 assert_eq!(output_error.code, "invalid_input"); 1525 assert!(output_error.message.contains("reason")); 1526 } 1527 1528 #[test] 1529 fn order_revision_propose_requires_approval_token() { 1530 let dir = tempdir().expect("tempdir"); 1531 let config = sample_config(dir.path()); 1532 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1533 let mut input = data(&[ 1534 ("order_id", "ord_pending"), 1535 ("reason", "update count"), 1536 ("bin_id", "bin-1"), 1537 ]); 1538 input.insert("bin_count".to_owned(), Value::from(3)); 1539 let revision = OperationRequest::new( 1540 OperationContext::default(), 1541 OrderRevisionProposeRequest::from_data(input), 1542 ) 1543 .expect("order revision request"); 1544 let error = service.execute(revision).expect_err("approval required"); 1545 1546 assert_eq!(error.to_output_error().code, "approval_required"); 1547 } 1548 1549 #[test] 1550 fn order_revision_accept_requires_approval_token() { 1551 let dir = tempdir().expect("tempdir"); 1552 let config = sample_config(dir.path()); 1553 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1554 let revision = OperationRequest::new( 1555 OperationContext::default(), 1556 OrderRevisionAcceptRequest::from_data(data(&[ 1557 ("order_id", "ord_pending"), 1558 ("revision_id", "rev_pending"), 1559 ])), 1560 ) 1561 .expect("order revision accept request"); 1562 let error = service.execute(revision).expect_err("approval required"); 1563 1564 assert_eq!(error.to_output_error().code, "approval_required"); 1565 } 1566 1567 #[test] 1568 fn order_revision_decline_requires_reason_before_approval() { 1569 let dir = tempdir().expect("tempdir"); 1570 let config = sample_config(dir.path()); 1571 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1572 let revision = OperationRequest::new( 1573 OperationContext::default(), 1574 OrderRevisionDeclineRequest::from_data(data(&[ 1575 ("order_id", "ord_pending"), 1576 ("revision_id", "rev_pending"), 1577 ])), 1578 ) 1579 .expect("order revision decline request"); 1580 let error = service.execute(revision).expect_err("reason required"); 1581 let output_error = error.to_output_error(); 1582 1583 assert_eq!(output_error.code, "invalid_input"); 1584 assert!(output_error.message.contains("reason")); 1585 } 1586 1587 #[test] 1588 fn order_revision_decline_requires_approval_token() { 1589 let dir = tempdir().expect("tempdir"); 1590 let config = sample_config(dir.path()); 1591 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1592 let revision = OperationRequest::new( 1593 OperationContext::default(), 1594 OrderRevisionDeclineRequest::from_data(data(&[ 1595 ("order_id", "ord_pending"), 1596 ("revision_id", "rev_pending"), 1597 ("reason", "keep original order"), 1598 ])), 1599 ) 1600 .expect("order revision decline request"); 1601 let error = service.execute(revision).expect_err("approval required"); 1602 1603 assert_eq!(error.to_output_error().code, "approval_required"); 1604 } 1605 1606 #[test] 1607 fn order_status_get_uses_local_sdk_projection_without_relay() { 1608 let dir = tempdir().expect("tempdir"); 1609 let config = sample_config(dir.path()); 1610 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1611 let status = OperationRequest::new( 1612 OperationContext::default(), 1613 OrderStatusGetRequest::from_data(data(&[("order_id", "ord_pending")])), 1614 ) 1615 .expect("order status request"); 1616 let envelope = service 1617 .execute(status) 1618 .expect("status result") 1619 .to_envelope(OperationContext::default().envelope_context("req_order_status")) 1620 .expect("status envelope"); 1621 1622 assert_eq!(envelope.operation_id, "order.status.get"); 1623 assert_eq!(envelope.result["state"], "missing"); 1624 assert_eq!(envelope.result["source"], "SDK local order projection"); 1625 assert_eq!( 1626 envelope.result["actor_context_source"], 1627 "sdk_local_projection" 1628 ); 1629 assert_eq!(envelope.result["order_id"], "ord_pending"); 1630 assert_eq!(envelope.result["fetched_count"], 0); 1631 assert_eq!(envelope.result["decoded_count"], 0); 1632 assert_eq!(envelope.result["skipped_count"], 0); 1633 assert!(envelope.next_actions.is_empty()); 1634 } 1635 1636 #[test] 1637 fn order_event_list_requires_relay_configuration() { 1638 let dir = tempdir().expect("tempdir"); 1639 let config = sample_config(dir.path()); 1640 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1641 let context = OperationContext::default(); 1642 let request = OperationRequest::new(context.clone(), OrderEventListRequest::default()) 1643 .expect("order event list request"); 1644 let output_error = service 1645 .execute(request) 1646 .expect_err("order event list unconfigured") 1647 .to_output_error(); 1648 let envelope = crate::out::envelope::OutputEnvelope::failure( 1649 "order.event.list", 1650 output_error, 1651 context.envelope_context("req_order_event_list"), 1652 ); 1653 1654 assert_eq!(envelope.errors[0].code, "operation_unavailable"); 1655 assert_eq!(envelope.errors[0].exit_code, 3); 1656 assert!(envelope.errors[0].message.contains("configured relay")); 1657 assert_eq!( 1658 envelope.errors[0].detail.as_ref().unwrap()["actions"][0], 1659 "radroots --relay wss://relay.example.com order event list" 1660 ); 1661 assert_eq!( 1662 envelope.next_actions[0].command.as_deref(), 1663 Some("radroots --relay wss://relay.example.com order event list") 1664 ); 1665 } 1666 1667 #[test] 1668 fn order_event_list_requires_seller_account_with_account_action() { 1669 let dir = tempdir().expect("tempdir"); 1670 let mut config = sample_config(dir.path()); 1671 config.relay.urls = vec!["ws://127.0.0.1:9".to_owned()]; 1672 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1673 let context = OperationContext::default(); 1674 let request = OperationRequest::new(context.clone(), OrderEventListRequest::default()) 1675 .expect("order event list request"); 1676 let output_error = service 1677 .execute(request) 1678 .expect_err("order event list missing account") 1679 .to_output_error(); 1680 let envelope = crate::out::envelope::OutputEnvelope::failure( 1681 "order.event.list", 1682 output_error, 1683 context.envelope_context("req_order_event_list"), 1684 ); 1685 1686 assert_eq!(envelope.errors[0].code, "operation_unavailable"); 1687 assert!( 1688 envelope.errors[0] 1689 .message 1690 .contains("selected seller account") 1691 ); 1692 assert_eq!( 1693 envelope.errors[0].detail.as_ref().unwrap()["actions"][0], 1694 "radroots account create" 1695 ); 1696 assert_eq!( 1697 envelope.next_actions[0].command.as_deref(), 1698 Some("radroots account create") 1699 ); 1700 } 1701 1702 #[test] 1703 fn order_event_watch_returns_deferred_error_with_target_action() { 1704 let dir = tempdir().expect("tempdir"); 1705 let config = sample_config(dir.path()); 1706 let service = OperationAdapter::new(OrderOperationService::new(&config)); 1707 let request = OperationRequest::new( 1708 OperationContext::default(), 1709 OrderEventWatchRequest::from_data(data(&[("order_id", "ord_missing")])), 1710 ) 1711 .expect("order event watch request"); 1712 let error = service 1713 .execute(request) 1714 .expect_err("order event watch deferred"); 1715 let envelope = crate::out::envelope::OutputEnvelope::failure( 1716 "order.event.watch", 1717 error.to_output_error(), 1718 OperationContext::default().envelope_context("req_order_watch"), 1719 ); 1720 1721 assert_eq!(envelope.operation_id, "order.event.watch"); 1722 assert!(envelope.result.is_null()); 1723 assert_eq!(envelope.errors[0].code, "not_implemented"); 1724 assert_eq!( 1725 envelope.errors[0].detail.as_ref().unwrap()["state"], 1726 "not_implemented" 1727 ); 1728 assert_eq!( 1729 envelope.errors[0].detail.as_ref().unwrap()["order_id"], 1730 "ord_missing" 1731 ); 1732 assert_eq!( 1733 envelope.next_actions[0].command.as_deref(), 1734 Some("radroots order status get ord_missing") 1735 ); 1736 } 1737 1738 fn sample_config(root: &Path) -> RuntimeConfig { 1739 let data = root.join("data"); 1740 let logs = root.join("logs"); 1741 let secrets = root.join("secrets"); 1742 RuntimeConfig { 1743 output: OutputConfig { 1744 format: OutputFormat::Human, 1745 verbosity: Verbosity::Normal, 1746 color: true, 1747 dry_run: false, 1748 }, 1749 interaction: InteractionConfig { 1750 input_enabled: true, 1751 assume_yes: false, 1752 stdin_tty: false, 1753 stdout_tty: false, 1754 prompts_allowed: false, 1755 confirmations_allowed: false, 1756 }, 1757 paths: PathsConfig { 1758 profile: "interactive_user".into(), 1759 profile_source: "test".into(), 1760 allowed_profiles: vec!["interactive_user".into(), "repo_local".into()], 1761 root_source: "test".into(), 1762 repo_local_root: None, 1763 repo_local_root_source: None, 1764 subordinate_path_override_source: "runtime_config".into(), 1765 app_namespace: "apps/cli".into(), 1766 shared_accounts_namespace: "shared/accounts".into(), 1767 shared_identities_namespace: "shared/identities".into(), 1768 app_config_path: root.join("config/apps/cli/config.toml"), 1769 workspace_config_path: None, 1770 app_data_root: data.join("apps/cli"), 1771 app_logs_root: logs.join("apps/cli"), 1772 shared_accounts_data_root: data.join("shared/accounts"), 1773 shared_accounts_secrets_root: secrets.join("shared/accounts"), 1774 default_identity_path: secrets.join("shared/identities/default.json"), 1775 }, 1776 migration: MigrationConfig { 1777 report: RadrootsMigrationReport::empty(), 1778 }, 1779 logging: LoggingConfig { 1780 filter: "info".into(), 1781 directory: None, 1782 stdout: false, 1783 }, 1784 account: AccountConfig { 1785 selector: None, 1786 store_path: data.join("shared/accounts/store.json"), 1787 secrets_dir: secrets.join("shared/accounts"), 1788 secret_backend: RadrootsSecretBackend::EncryptedFile, 1789 secret_fallback: None, 1790 }, 1791 account_secret_contract: AccountSecretContractConfig { 1792 default_backend: "host_vault".into(), 1793 default_fallback: Some("encrypted_file".into()), 1794 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()], 1795 host_vault_policy: Some("desktop".into()), 1796 uses_protected_store: true, 1797 }, 1798 identity: IdentityConfig { 1799 path: secrets.join("shared/identities/default.json"), 1800 }, 1801 signer: SignerConfig { 1802 backend: SignerBackend::Local, 1803 }, 1804 publish: PublishConfig { 1805 transport: PublishTransport::DirectNostrRelay, 1806 source: PublishTransportSource::Defaults, 1807 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(), 1808 }, 1809 relay: RelayConfig { 1810 urls: Vec::new(), 1811 publish_policy: RelayPublishPolicy::Any, 1812 source: RelayConfigSource::Defaults, 1813 }, 1814 local: LocalConfig { 1815 root: data.join("apps/cli/replica"), 1816 replica_db_path: data.join("apps/cli/replica/replica.sqlite"), 1817 backups_dir: data.join("apps/cli/replica/backups"), 1818 exports_dir: data.join("apps/cli/replica/exports"), 1819 }, 1820 myc: MycConfig { 1821 executable: PathBuf::from("myc"), 1822 status_timeout_ms: 2_000, 1823 }, 1824 hyf: HyfConfig { 1825 enabled: false, 1826 executable: PathBuf::from("hyfd"), 1827 }, 1828 rpc: RpcConfig { 1829 url: "http://127.0.0.1:7070".into(), 1830 }, 1831 rhi: crate::runtime::config::RhiConfig { 1832 trusted_worker_pubkeys: Vec::new(), 1833 }, 1834 capability_bindings: Vec::new(), 1835 } 1836 } 1837 1838 fn data(entries: &[(&str, &str)]) -> OperationData { 1839 entries 1840 .iter() 1841 .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned()))) 1842 .collect::<Map<String, Value>>() 1843 } 1844 1845 fn already_decided_view() -> OrderDecisionView { 1846 OrderDecisionView { 1847 state: "already_decided".to_owned(), 1848 source: "test".to_owned(), 1849 order_id: "ord_test".to_owned(), 1850 listing_addr: Some("30402:seller:listing".to_owned()), 1851 buyer_pubkey: Some("b".repeat(64)), 1852 seller_pubkey: Some("s".repeat(64)), 1853 decision: "accepted".to_owned(), 1854 request_event_id: Some("r".repeat(64)), 1855 listing_event_id: Some("l".repeat(64)), 1856 root_event_id: Some("r".repeat(64)), 1857 prev_event_id: Some("r".repeat(64)), 1858 event_id: Some("d".repeat(64)), 1859 event_kind: Some(3423), 1860 inventory: None, 1861 dry_run: false, 1862 target_relays: vec!["ws://relay.test".to_owned()], 1863 connected_relays: vec!["ws://relay.test".to_owned()], 1864 acknowledged_relays: Vec::new(), 1865 failed_relays: Vec::new(), 1866 fetched_count: 2, 1867 decoded_count: 2, 1868 skipped_count: 0, 1869 idempotency_key: Some("idem_test".to_owned()), 1870 signer_mode: Some("local".to_owned()), 1871 reason: Some( 1872 "order accept refused because order `ord_test` already has a visible `accepted` seller decision" 1873 .to_owned(), 1874 ), 1875 issues: Vec::new(), 1876 actions: vec!["radroots order status get ord_test".to_owned()], 1877 } 1878 } 1879 1880 fn invalid_decision_view() -> OrderDecisionView { 1881 let mut view = already_decided_view(); 1882 view.state = "invalid".to_owned(); 1883 view.decision = "declined".to_owned(); 1884 view.event_id = None; 1885 view.event_kind = None; 1886 view.reason = 1887 Some("active order events for `ord_test` failed reducer validation".to_owned()); 1888 view 1889 } 1890 }