dvm.rs (44583B)
1 #![forbid(unsafe_code)] 2 #![cfg_attr(coverage_nightly, coverage(off))] 3 4 use std::{sync::Arc, time::Duration}; 5 6 use radroots_events::farm::RadrootsFarmRef; 7 use radroots_events::kinds::{ 8 KIND_FARM, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_FULFILLMENT_UPDATE, 9 KIND_ORDER_PAYMENT_RECORD, KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST, 10 KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_ORDER_SETTLEMENT_DECISION, 11 KIND_TRADE_LISTING_VALIDATION_REQUEST, KIND_TRADE_LISTING_VALIDATION_RESULT, 12 KIND_TRADE_TRANSITION_PROOF_REQUEST, KIND_TRADE_TRANSITION_PROOF_RESULT, is_listing_kind, 13 is_order_event_kind, is_trade_validation_service_event_kind, 14 }; 15 use radroots_events::order::{ 16 RadrootsOrderDecisionOutcome, RadrootsOrderFulfillmentState, RadrootsOrderReceipt, 17 RadrootsOrderRevisionOutcome, 18 }; 19 use radroots_events::trade_validation::{ 20 RadrootsTradeValidationListingError as TradeListingValidationError, 21 RadrootsTradeValidationListingRequest as TradeListingValidateRequest, 22 RadrootsTradeValidationListingResult as TradeListingValidateResult, 23 }; 24 use radroots_events_codec::order::{ 25 RadrootsOrderEnvelopeParseError, order_cancellation_from_event, order_decision_from_event, 26 order_fulfillment_update_from_event, order_payment_record_from_event, order_receipt_from_event, 27 order_request_from_event, order_revision_decision_from_event, 28 order_revision_proposal_from_event, order_settlement_decision_from_event, 29 parse_order_listing_event_tag, parse_order_prev_tag, parse_order_root_tag, 30 }; 31 use radroots_nostr::prelude::{ 32 RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, 33 RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrTag, radroots_event_from_nostr, 34 radroots_nostr_build_event, radroots_nostr_build_event_job_feedback, 35 radroots_nostr_fetch_event_by_id, radroots_nostr_parse_pubkey, radroots_nostr_send_event, 36 }; 37 use radroots_trade::listing::{ 38 parse_listing_address, parse_public_listing_address, validation::validate_listing_event, 39 }; 40 use thiserror::Error; 41 42 use crate::features::trade_listing::state::{ 43 TradeListingState, TradeListingStateError, TradeOrderState, TradeOrderStatus, 44 }; 45 use crate::features::trade_validation_receipt::{ 46 TradeValidationReceiptJobError, TradeValidationReceiptProverPolicy, 47 handle_trade_validation_receipt_job_request, 48 }; 49 50 #[derive(Debug, Error)] 51 pub enum TradeListingDvmError { 52 #[error("event kind not supported")] 53 UnsupportedKind, 54 #[error("missing recipient tag")] 55 MissingRecipient, 56 #[error("missing required tag: {0}")] 57 MissingTag(&'static str), 58 #[error("tag mismatch: {0}")] 59 TagMismatch(&'static str), 60 #[error("invalid envelope: {0}")] 61 InvalidEnvelope(String), 62 #[error("invalid envelope payload: {0}")] 63 InvalidPayload(String), 64 #[error("invalid listing address")] 65 InvalidListingAddr, 66 #[error("invalid order request payload")] 67 InvalidOrder, 68 #[error("state error: {0}")] 69 State(#[from] TradeListingStateError), 70 #[error("nostr error: {0}")] 71 Nostr(#[from] radroots_nostr::error::RadrootsNostrError), 72 #[error("serde error: {0}")] 73 Serde(#[from] serde_json::Error), 74 #[error("unauthorized sender")] 75 Unauthorized, 76 } 77 78 #[cfg(test)] 79 #[derive(Default)] 80 struct DvmTestHooks { 81 fetch_event_by_id_results: 82 std::collections::VecDeque<Result<RadrootsNostrEvent, TradeListingDvmError>>, 83 fetch_events_results: 84 std::collections::VecDeque<Result<Vec<RadrootsNostrEvent>, TradeListingDvmError>>, 85 send_event_results: std::collections::VecDeque<Result<(), TradeListingDvmError>>, 86 validate_listing_results: 87 std::collections::VecDeque<Result<(String, RadrootsFarmRef), TradeListingValidationError>>, 88 farm_validation_results: 89 std::collections::VecDeque<Result<Vec<TradeListingValidationError>, TradeListingDvmError>>, 90 } 91 92 #[cfg(test)] 93 static DVM_TEST_HOOKS: std::sync::OnceLock<std::sync::Mutex<DvmTestHooks>> = 94 std::sync::OnceLock::new(); 95 96 #[cfg(test)] 97 fn dvm_test_hooks() -> &'static std::sync::Mutex<DvmTestHooks> { 98 DVM_TEST_HOOKS.get_or_init(|| std::sync::Mutex::new(DvmTestHooks::default())) 99 } 100 101 #[cfg(test)] 102 fn pop_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeListingDvmError>> { 103 dvm_test_hooks() 104 .lock() 105 .expect("dvm test hooks lock") 106 .fetch_event_by_id_results 107 .pop_front() 108 } 109 110 #[cfg(test)] 111 fn pop_fetch_events_hook() -> Option<Result<Vec<RadrootsNostrEvent>, TradeListingDvmError>> { 112 dvm_test_hooks() 113 .lock() 114 .expect("dvm test hooks lock") 115 .fetch_events_results 116 .pop_front() 117 } 118 119 #[cfg(test)] 120 fn pop_send_event_hook() -> Option<Result<(), TradeListingDvmError>> { 121 dvm_test_hooks() 122 .lock() 123 .expect("dvm test hooks lock") 124 .send_event_results 125 .pop_front() 126 } 127 128 #[cfg(test)] 129 fn pop_validate_listing_hook() 130 -> Option<Result<(String, RadrootsFarmRef), TradeListingValidationError>> { 131 dvm_test_hooks() 132 .lock() 133 .expect("dvm test hooks lock") 134 .validate_listing_results 135 .pop_front() 136 } 137 138 #[cfg(test)] 139 fn pop_farm_validation_hook() 140 -> Option<Result<Vec<TradeListingValidationError>, TradeListingDvmError>> { 141 dvm_test_hooks() 142 .lock() 143 .expect("dvm test hooks lock") 144 .farm_validation_results 145 .pop_front() 146 } 147 148 #[cfg(test)] 149 fn take_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeListingDvmError>> { 150 pop_fetch_event_by_id_hook() 151 } 152 153 #[cfg(not(test))] 154 #[cfg_attr(coverage_nightly, coverage(off))] 155 fn take_fetch_event_by_id_hook() -> Option<Result<RadrootsNostrEvent, TradeListingDvmError>> { 156 None 157 } 158 159 #[cfg(test)] 160 fn take_fetch_events_hook() -> Option<Result<Vec<RadrootsNostrEvent>, TradeListingDvmError>> { 161 pop_fetch_events_hook() 162 } 163 164 #[cfg(not(test))] 165 #[cfg_attr(coverage_nightly, coverage(off))] 166 fn take_fetch_events_hook() -> Option<Result<Vec<RadrootsNostrEvent>, TradeListingDvmError>> { 167 None 168 } 169 170 #[cfg(test)] 171 fn take_send_event_hook() -> Option<Result<(), TradeListingDvmError>> { 172 pop_send_event_hook() 173 } 174 175 #[cfg(not(test))] 176 #[cfg_attr(coverage_nightly, coverage(off))] 177 fn take_send_event_hook() -> Option<Result<(), TradeListingDvmError>> { 178 None 179 } 180 181 #[cfg(test)] 182 fn take_validate_listing_hook() 183 -> Option<Result<(String, RadrootsFarmRef), TradeListingValidationError>> { 184 pop_validate_listing_hook() 185 } 186 187 #[cfg(not(test))] 188 #[cfg_attr(coverage_nightly, coverage(off))] 189 fn take_validate_listing_hook() 190 -> Option<Result<(String, RadrootsFarmRef), TradeListingValidationError>> { 191 None 192 } 193 194 async fn fetch_event_by_id_io( 195 client: &RadrootsNostrClient, 196 id: &str, 197 ) -> Result<RadrootsNostrEvent, TradeListingDvmError> { 198 match take_fetch_event_by_id_hook() { 199 Some(result) => result, 200 None => radroots_nostr_fetch_event_by_id(client, id) 201 .await 202 .map_err(TradeListingDvmError::from), 203 } 204 } 205 206 async fn fetch_events_io( 207 client: &RadrootsNostrClient, 208 filter: RadrootsNostrFilter, 209 timeout: Duration, 210 ) -> Result<Vec<RadrootsNostrEvent>, TradeListingDvmError> { 211 match take_fetch_events_hook() { 212 Some(result) => result, 213 None => client 214 .fetch_events(filter, timeout) 215 .await 216 .map_err(TradeListingDvmError::from), 217 } 218 } 219 220 #[cfg_attr(all(not(test), coverage_nightly), coverage(off))] 221 async fn send_event_io( 222 client: &RadrootsNostrClient, 223 builder: RadrootsNostrEventBuilder, 224 ) -> Result<(), TradeListingDvmError> { 225 match take_send_event_hook() { 226 Some(result) => result, 227 None => radroots_nostr_send_event(client, builder) 228 .await 229 .map(|_| ()) 230 .map_err(TradeListingDvmError::from), 231 } 232 } 233 234 #[cfg_attr(all(not(test), coverage_nightly), coverage(off))] 235 fn validate_listing_event_io( 236 event: &RadrootsNostrEvent, 237 ) -> Result<(String, RadrootsFarmRef), TradeListingValidationError> { 238 match take_validate_listing_hook() { 239 Some(result) => result, 240 None => validate_listing_event(&radroots_event_from_nostr(event)) 241 .map(|listing| (listing.listing_addr, listing.listing.farm)), 242 } 243 } 244 245 pub async fn handle_event_with_policy( 246 event: RadrootsNostrEvent, 247 _tags: Vec<RadrootsNostrTag>, 248 keys: RadrootsNostrKeys, 249 client: RadrootsNostrClient, 250 state: Arc<tokio::sync::Mutex<TradeListingState>>, 251 proof_policy: &TradeValidationReceiptProverPolicy, 252 ) -> Result<(), TradeListingDvmError> { 253 let kind = event_kind_u32(&event)?; 254 if is_listing_kind(kind) { 255 return handle_listing_event(&event, &state).await; 256 } 257 if event.pubkey == keys.public_key() { 258 return Ok(()); 259 } 260 if kind == KIND_TRADE_TRANSITION_PROOF_REQUEST { 261 return handle_trade_validation_receipt_job_request(&event, &keys, &client, proof_policy) 262 .await 263 .map_err(map_trade_validation_receipt_job_error); 264 } 265 if kind == KIND_TRADE_LISTING_VALIDATION_REQUEST { 266 ensure_service_recipient(&event, &keys)?; 267 return handle_listing_validate_request(&event, &client, &state).await; 268 } 269 if kind == KIND_TRADE_LISTING_VALIDATION_RESULT || kind == KIND_TRADE_TRANSITION_PROOF_RESULT { 270 state 271 .lock() 272 .await 273 .mark_non_order_event_seen(&event.id.to_string()); 274 return Ok(()); 275 } 276 if is_order_event_kind(kind) { 277 return handle_order_event(&event, kind, &client, &state).await; 278 } 279 if is_trade_validation_service_event_kind(kind) { 280 return Err(TradeListingDvmError::UnsupportedKind); 281 } 282 Err(TradeListingDvmError::UnsupportedKind) 283 } 284 285 #[cfg(test)] 286 pub async fn handle_event( 287 event: RadrootsNostrEvent, 288 tags: Vec<RadrootsNostrTag>, 289 keys: RadrootsNostrKeys, 290 client: RadrootsNostrClient, 291 state: Arc<tokio::sync::Mutex<TradeListingState>>, 292 ) -> Result<(), TradeListingDvmError> { 293 handle_event_with_policy( 294 event, 295 tags, 296 keys, 297 client, 298 state, 299 &TradeValidationReceiptProverPolicy::default(), 300 ) 301 .await 302 } 303 304 fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeListingDvmError> { 305 match event.kind { 306 RadrootsNostrKind::Custom(value) => Ok(u32::from(value)), 307 _ => Err(TradeListingDvmError::UnsupportedKind), 308 } 309 } 310 311 fn map_trade_validation_receipt_job_error( 312 error: TradeValidationReceiptJobError, 313 ) -> TradeListingDvmError { 314 match error { 315 TradeValidationReceiptJobError::UnsupportedKind => TradeListingDvmError::UnsupportedKind, 316 TradeValidationReceiptJobError::MissingRecipient => TradeListingDvmError::MissingRecipient, 317 TradeValidationReceiptJobError::Nostr(error) => TradeListingDvmError::Nostr(error), 318 other => TradeListingDvmError::InvalidPayload(other.to_string()), 319 } 320 } 321 322 fn map_order_parse_error(error: RadrootsOrderEnvelopeParseError) -> TradeListingDvmError { 323 match error { 324 RadrootsOrderEnvelopeParseError::InvalidKind(_) => TradeListingDvmError::UnsupportedKind, 325 RadrootsOrderEnvelopeParseError::MissingTag(tag) => TradeListingDvmError::MissingTag(tag), 326 RadrootsOrderEnvelopeParseError::ListingAddrTagMismatch => { 327 TradeListingDvmError::TagMismatch("a") 328 } 329 RadrootsOrderEnvelopeParseError::OrderIdTagMismatch => { 330 TradeListingDvmError::TagMismatch("d") 331 } 332 RadrootsOrderEnvelopeParseError::InvalidListingAddr(_) => { 333 TradeListingDvmError::InvalidListingAddr 334 } 335 RadrootsOrderEnvelopeParseError::InvalidEnvelope(error) => { 336 TradeListingDvmError::InvalidEnvelope(error.to_string()) 337 } 338 other => TradeListingDvmError::InvalidPayload(other.to_string()), 339 } 340 } 341 342 fn ensure_service_recipient( 343 event: &RadrootsNostrEvent, 344 keys: &RadrootsNostrKeys, 345 ) -> Result<(), TradeListingDvmError> { 346 let tags = radroots_event_from_nostr(event).tags; 347 if tag_has_value(&tags, "p", &keys.public_key().to_string()) { 348 Ok(()) 349 } else { 350 Err(TradeListingDvmError::MissingRecipient) 351 } 352 } 353 354 async fn handle_listing_event( 355 event: &RadrootsNostrEvent, 356 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 357 ) -> Result<(), TradeListingDvmError> { 358 let event_id = event.id.to_string(); 359 { 360 let state = state.lock().await; 361 if state.is_non_order_event_seen(&event_id) { 362 return Ok(()); 363 } 364 } 365 let validated = validate_listing_event(&radroots_event_from_nostr(event)) 366 .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?; 367 let kind = event_kind_u32(event)?; 368 let mut state = state.lock().await; 369 state.upsert_listing_event(&validated.listing_addr, &event_id, kind); 370 state.mark_non_order_event_seen(&event_id); 371 Ok(()) 372 } 373 374 #[cfg_attr(all(not(test), coverage_nightly), coverage(off))] 375 async fn handle_listing_validate_request( 376 event: &RadrootsNostrEvent, 377 client: &RadrootsNostrClient, 378 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 379 ) -> Result<(), TradeListingDvmError> { 380 let event_id = event.id.to_string(); 381 { 382 let state = state.lock().await; 383 if state.is_non_order_event_seen(&event_id) { 384 return Ok(()); 385 } 386 } 387 let rr_event = radroots_event_from_nostr(event); 388 let listing_addr = required_tag_value(&rr_event.tags, "a")?; 389 parse_listing_address(&listing_addr).map_err(|_| TradeListingDvmError::InvalidListingAddr)?; 390 let payload: TradeListingValidateRequest = serde_json::from_str(&event.content)?; 391 let listing_event = resolve_listing_event(client, &listing_addr, payload.listing_event).await; 392 let (validated_event_id, errors) = match listing_event { 393 Ok(Some(listing_event)) => match validate_listing_event_io(&listing_event) { 394 Ok((validated_listing_addr, farm)) if validated_listing_addr == listing_addr => { 395 let errors = validate_farm_dependencies(client, &farm).await?; 396 if errors.is_empty() { 397 (Some(listing_event.id.to_string()), errors) 398 } else { 399 (None, errors) 400 } 401 } 402 Ok(_) => ( 403 None, 404 vec![TradeListingValidationError::ListingEventNotFound { 405 listing_addr: listing_addr.clone(), 406 }], 407 ), 408 Err(error) => (None, vec![error]), 409 }, 410 Ok(None) => ( 411 None, 412 vec![TradeListingValidationError::ListingEventNotFound { 413 listing_addr: listing_addr.clone(), 414 }], 415 ), 416 Err(_) => ( 417 None, 418 vec![TradeListingValidationError::ListingEventFetchFailed { 419 listing_addr: listing_addr.clone(), 420 }], 421 ), 422 }; 423 { 424 let mut state = state.lock().await; 425 match validated_event_id { 426 Some(validated_event_id) => { 427 state.mark_listing_validated(&listing_addr, &validated_event_id); 428 } 429 None => state.clear_listing_validation(&listing_addr), 430 } 431 state.mark_non_order_event_seen(&event_id); 432 } 433 send_validate_result(event, client, &listing_addr, errors).await 434 } 435 436 async fn resolve_listing_event( 437 client: &RadrootsNostrClient, 438 listing_addr: &str, 439 listing_event: Option<radroots_events::RadrootsNostrEventPtr>, 440 ) -> Result<Option<RadrootsNostrEvent>, TradeListingDvmError> { 441 match listing_event { 442 Some(ptr) => fetch_event_by_id_io(client, &ptr.id).await.map(Some), 443 None => fetch_listing_by_addr(client, listing_addr).await, 444 } 445 } 446 447 async fn send_validate_result( 448 event: &RadrootsNostrEvent, 449 client: &RadrootsNostrClient, 450 listing_addr: &str, 451 errors: Vec<TradeListingValidationError>, 452 ) -> Result<(), TradeListingDvmError> { 453 let payload = TradeListingValidateResult { 454 valid: errors.is_empty(), 455 errors, 456 }; 457 let content = serde_json::to_string(&payload)?; 458 let tags = vec![ 459 vec!["p".to_string(), event.pubkey.to_string()], 460 vec!["a".to_string(), listing_addr.to_string()], 461 vec!["e".to_string(), event.id.to_string()], 462 ]; 463 let builder = radroots_nostr_build_event(KIND_TRADE_LISTING_VALIDATION_RESULT, content, tags)?; 464 send_event_io(client, builder).await 465 } 466 467 async fn handle_order_event( 468 event: &RadrootsNostrEvent, 469 kind: u32, 470 client: &RadrootsNostrClient, 471 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 472 ) -> Result<(), TradeListingDvmError> { 473 match kind { 474 KIND_ORDER_REQUEST => handle_order_request(event, client, state).await, 475 KIND_ORDER_DECISION => handle_order_decision(event, state).await, 476 KIND_ORDER_REVISION_PROPOSAL => handle_order_revision_proposal(event, state).await, 477 KIND_ORDER_REVISION_DECISION => handle_order_revision_decision(event, state).await, 478 KIND_ORDER_CANCELLATION => handle_order_cancellation(event, state).await, 479 KIND_ORDER_FULFILLMENT_UPDATE => handle_order_fulfillment_update(event, state).await, 480 KIND_ORDER_RECEIPT => handle_order_receipt(event, state).await, 481 KIND_ORDER_PAYMENT_RECORD => handle_order_payment_record(event, state).await, 482 KIND_ORDER_SETTLEMENT_DECISION => handle_order_settlement_decision(event, state).await, 483 _ => Err(TradeListingDvmError::UnsupportedKind), 484 } 485 } 486 487 async fn handle_order_request( 488 event: &RadrootsNostrEvent, 489 client: &RadrootsNostrClient, 490 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 491 ) -> Result<(), TradeListingDvmError> { 492 let rr_event = radroots_event_from_nostr(event); 493 let envelope = order_request_from_event(&rr_event).map_err(map_order_parse_error)?; 494 let listing_addr = parse_public_listing_address(&envelope.listing_addr) 495 .map_err(|_| TradeListingDvmError::InvalidListingAddr)?; 496 if envelope.payload.seller_pubkey != listing_addr.seller_pubkey { 497 return Err(TradeListingDvmError::InvalidListingAddr); 498 } 499 let listing_event = parse_order_listing_event_tag(&rr_event.tags) 500 .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))? 501 .ok_or(TradeListingDvmError::MissingTag("listing_event"))?; 502 let listing_snapshot_event_id = 503 ensure_listing_snapshot(&envelope.listing_addr, &listing_event, client, state).await?; 504 let event_id = event.id.to_string(); 505 let mut state = state.lock().await; 506 if state.order_exists(&envelope.order_id) { 507 return Ok(()); 508 } 509 let mut seen = std::collections::HashSet::new(); 510 seen.insert(event_id.clone()); 511 state.insert_order(TradeOrderState { 512 order_id: envelope.order_id, 513 listing_addr: envelope.payload.listing_addr.to_string(), 514 buyer_pubkey: envelope.payload.buyer_pubkey.to_string(), 515 seller_pubkey: envelope.payload.seller_pubkey.to_string(), 516 status: TradeOrderStatus::Requested, 517 listing_snapshot_event_id: Some(listing_snapshot_event_id), 518 root_event_id: Some(event_id.clone()), 519 last_event_id: Some(event_id), 520 seen_event_ids: seen, 521 }); 522 Ok(()) 523 } 524 525 async fn ensure_listing_snapshot( 526 listing_addr: &str, 527 listing_event: &radroots_events::RadrootsNostrEventPtr, 528 client: &RadrootsNostrClient, 529 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 530 ) -> Result<String, TradeListingDvmError> { 531 { 532 let state = state.lock().await; 533 if state.listing_event_id(listing_addr) == Some(listing_event.id.as_str()) { 534 return Ok(listing_event.id.clone()); 535 } 536 } 537 let event = fetch_event_by_id_io(client, &listing_event.id).await?; 538 let (validated_listing_addr, _) = validate_listing_event_io(&event) 539 .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?; 540 if validated_listing_addr != listing_addr { 541 return Err(TradeListingDvmError::InvalidOrder); 542 } 543 let kind = event_kind_u32(&event)?; 544 let mut state = state.lock().await; 545 state.upsert_listing_event(listing_addr, &listing_event.id, kind); 546 Ok(listing_event.id.clone()) 547 } 548 549 async fn handle_order_decision( 550 event: &RadrootsNostrEvent, 551 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 552 ) -> Result<(), TradeListingDvmError> { 553 let rr_event = radroots_event_from_nostr(event); 554 let envelope = order_decision_from_event(&rr_event).map_err(map_order_parse_error)?; 555 let next_status = match envelope.payload.decision { 556 RadrootsOrderDecisionOutcome::Accepted { .. } => TradeOrderStatus::Accepted, 557 RadrootsOrderDecisionOutcome::Declined { .. } => TradeOrderStatus::Declined, 558 }; 559 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 560 ensure_order_binding( 561 order, 562 &envelope.payload.listing_addr, 563 &envelope.payload.buyer_pubkey, 564 &envelope.payload.seller_pubkey, 565 )?; 566 ensure_transition(&order.status, &next_status)?; 567 order.status = next_status; 568 Ok(()) 569 }) 570 .await 571 } 572 573 async fn handle_order_revision_proposal( 574 event: &RadrootsNostrEvent, 575 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 576 ) -> Result<(), TradeListingDvmError> { 577 let rr_event = radroots_event_from_nostr(event); 578 let envelope = order_revision_proposal_from_event(&rr_event).map_err(map_order_parse_error)?; 579 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 580 ensure_order_binding( 581 order, 582 &envelope.payload.listing_addr, 583 &envelope.payload.buyer_pubkey, 584 &envelope.payload.seller_pubkey, 585 )?; 586 ensure_transition(&order.status, &TradeOrderStatus::Accepted)?; 587 Ok(()) 588 }) 589 .await 590 } 591 592 async fn handle_order_revision_decision( 593 event: &RadrootsNostrEvent, 594 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 595 ) -> Result<(), TradeListingDvmError> { 596 let rr_event = radroots_event_from_nostr(event); 597 let envelope = order_revision_decision_from_event(&rr_event).map_err(map_order_parse_error)?; 598 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 599 ensure_order_binding( 600 order, 601 &envelope.payload.listing_addr, 602 &envelope.payload.buyer_pubkey, 603 &envelope.payload.seller_pubkey, 604 )?; 605 match envelope.payload.decision { 606 RadrootsOrderRevisionOutcome::Accepted 607 | RadrootsOrderRevisionOutcome::Declined { .. } => { 608 ensure_transition(&order.status, &TradeOrderStatus::Accepted)?; 609 Ok(()) 610 } 611 } 612 }) 613 .await 614 } 615 616 async fn handle_order_cancellation( 617 event: &RadrootsNostrEvent, 618 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 619 ) -> Result<(), TradeListingDvmError> { 620 let rr_event = radroots_event_from_nostr(event); 621 let envelope = order_cancellation_from_event(&rr_event).map_err(map_order_parse_error)?; 622 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 623 ensure_order_binding( 624 order, 625 &envelope.payload.listing_addr, 626 &envelope.payload.buyer_pubkey, 627 &envelope.payload.seller_pubkey, 628 )?; 629 ensure_transition(&order.status, &TradeOrderStatus::Cancelled)?; 630 order.status = TradeOrderStatus::Cancelled; 631 Ok(()) 632 }) 633 .await 634 } 635 636 async fn handle_order_fulfillment_update( 637 event: &RadrootsNostrEvent, 638 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 639 ) -> Result<(), TradeListingDvmError> { 640 let rr_event = radroots_event_from_nostr(event); 641 let envelope = order_fulfillment_update_from_event(&rr_event).map_err(map_order_parse_error)?; 642 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 643 ensure_order_binding( 644 order, 645 &envelope.payload.listing_addr, 646 &envelope.payload.buyer_pubkey, 647 &envelope.payload.seller_pubkey, 648 )?; 649 ensure_transition(&order.status, &TradeOrderStatus::Accepted)?; 650 if envelope.payload.status == RadrootsOrderFulfillmentState::SellerCancelled { 651 order.status = TradeOrderStatus::Cancelled; 652 } 653 Ok(()) 654 }) 655 .await 656 } 657 658 async fn handle_order_receipt( 659 event: &RadrootsNostrEvent, 660 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 661 ) -> Result<(), TradeListingDvmError> { 662 let rr_event = radroots_event_from_nostr(event); 663 let envelope = order_receipt_from_event(&rr_event).map_err(map_order_parse_error)?; 664 let next_status = receipt_status(&envelope.payload); 665 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 666 ensure_order_binding( 667 order, 668 &envelope.payload.listing_addr, 669 &envelope.payload.buyer_pubkey, 670 &envelope.payload.seller_pubkey, 671 )?; 672 ensure_transition(&order.status, &next_status)?; 673 order.status = next_status; 674 Ok(()) 675 }) 676 .await 677 } 678 679 async fn handle_order_payment_record( 680 event: &RadrootsNostrEvent, 681 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 682 ) -> Result<(), TradeListingDvmError> { 683 let rr_event = radroots_event_from_nostr(event); 684 let envelope = order_payment_record_from_event(&rr_event).map_err(map_order_parse_error)?; 685 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 686 ensure_order_binding( 687 order, 688 &envelope.payload.listing_addr, 689 &envelope.payload.buyer_pubkey, 690 &envelope.payload.seller_pubkey, 691 ) 692 }) 693 .await 694 } 695 696 async fn handle_order_settlement_decision( 697 event: &RadrootsNostrEvent, 698 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 699 ) -> Result<(), TradeListingDvmError> { 700 let rr_event = radroots_event_from_nostr(event); 701 let envelope = 702 order_settlement_decision_from_event(&rr_event).map_err(map_order_parse_error)?; 703 update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| { 704 ensure_order_binding( 705 order, 706 &envelope.payload.listing_addr, 707 &envelope.payload.buyer_pubkey, 708 &envelope.payload.seller_pubkey, 709 ) 710 }) 711 .await 712 } 713 714 async fn update_existing_order<F>( 715 event: &RadrootsNostrEvent, 716 tags: &[Vec<String>], 717 state: &Arc<tokio::sync::Mutex<TradeListingState>>, 718 order_id: &str, 719 update: F, 720 ) -> Result<(), TradeListingDvmError> 721 where 722 F: FnOnce(&mut TradeOrderState) -> Result<(), TradeListingDvmError>, 723 { 724 let event_id = event.id.to_string(); 725 let mut state = state.lock().await; 726 if state.is_event_seen(order_id, &event_id) { 727 return Ok(()); 728 } 729 let order = state 730 .get_order_mut(order_id) 731 .ok_or(TradeListingStateError::MissingOrder)?; 732 ensure_order_chain(order, tags)?; 733 update(order)?; 734 order.last_event_id = Some(event_id.clone()); 735 order.seen_event_ids.insert(event_id); 736 Ok(()) 737 } 738 739 fn ensure_order_binding( 740 order: &TradeOrderState, 741 listing_addr: &str, 742 buyer_pubkey: &str, 743 seller_pubkey: &str, 744 ) -> Result<(), TradeListingDvmError> { 745 if order.listing_addr == listing_addr 746 && order.buyer_pubkey == buyer_pubkey 747 && order.seller_pubkey == seller_pubkey 748 { 749 Ok(()) 750 } else { 751 Err(TradeListingDvmError::InvalidOrder) 752 } 753 } 754 755 fn ensure_order_chain( 756 order: &TradeOrderState, 757 tags: &[Vec<String>], 758 ) -> Result<(), TradeListingDvmError> { 759 let root_event_id = parse_order_root_tag(tags) 760 .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))? 761 .ok_or(TradeListingDvmError::MissingTag("e:root"))?; 762 let prev_event_id = parse_order_prev_tag(tags) 763 .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))? 764 .ok_or(TradeListingDvmError::MissingTag("e:prev"))?; 765 if order.root_event_id.as_deref() == Some(root_event_id.as_str()) 766 && order.last_event_id.as_deref() == Some(prev_event_id.as_str()) 767 { 768 Ok(()) 769 } else { 770 Err(TradeListingDvmError::InvalidOrder) 771 } 772 } 773 774 fn receipt_status(payload: &RadrootsOrderReceipt) -> TradeOrderStatus { 775 if payload.received { 776 TradeOrderStatus::Completed 777 } else { 778 TradeOrderStatus::Disputed 779 } 780 } 781 782 fn ensure_transition( 783 from: &TradeOrderStatus, 784 to: &TradeOrderStatus, 785 ) -> Result<(), TradeListingStateError> { 786 if from == to { 787 return Ok(()); 788 } 789 let allowed = match from { 790 TradeOrderStatus::Requested => matches!( 791 to, 792 TradeOrderStatus::Accepted | TradeOrderStatus::Declined | TradeOrderStatus::Cancelled 793 ), 794 TradeOrderStatus::Accepted => matches!( 795 to, 796 TradeOrderStatus::Cancelled | TradeOrderStatus::Completed | TradeOrderStatus::Disputed 797 ), 798 TradeOrderStatus::Declined 799 | TradeOrderStatus::Cancelled 800 | TradeOrderStatus::Completed 801 | TradeOrderStatus::Disputed 802 | TradeOrderStatus::Invalid => false, 803 }; 804 if allowed { 805 Ok(()) 806 } else { 807 Err(TradeListingStateError::InvalidTransition { 808 from: from.clone(), 809 to: to.clone(), 810 }) 811 } 812 } 813 814 #[cfg_attr(all(not(test), coverage_nightly), coverage(off))] 815 async fn fetch_listing_by_addr( 816 client: &RadrootsNostrClient, 817 listing_addr: &str, 818 ) -> Result<Option<RadrootsNostrEvent>, TradeListingDvmError> { 819 let addr = parse_listing_address(listing_addr) 820 .map_err(|_| TradeListingDvmError::InvalidListingAddr)?; 821 let author = radroots_nostr_parse_pubkey(addr.seller_pubkey.as_str()) 822 .map_err(|_| TradeListingDvmError::InvalidListingAddr)?; 823 let kind = u16::try_from(addr.kind).map_err(|_| TradeListingDvmError::InvalidListingAddr)?; 824 let filter = RadrootsNostrFilter::new() 825 .kind(RadrootsNostrKind::Custom(kind)) 826 .author(author) 827 .identifier(addr.listing_id.into_string()); 828 let events = fetch_events_io(client, filter, Duration::from_secs(10)).await?; 829 Ok(events 830 .into_iter() 831 .filter(|event| event.kind == RadrootsNostrKind::Custom(kind)) 832 .max_by_key(|event| event.created_at)) 833 } 834 835 #[cfg_attr(all(not(test), coverage_nightly), coverage(off))] 836 async fn fetch_latest_event_by_kind( 837 client: &RadrootsNostrClient, 838 filter: RadrootsNostrFilter, 839 kind: RadrootsNostrKind, 840 ) -> Result<Option<RadrootsNostrEvent>, TradeListingDvmError> { 841 let events = fetch_events_io(client, filter, Duration::from_secs(10)).await?; 842 Ok(events 843 .into_iter() 844 .filter(|event| event.kind == kind) 845 .max_by_key(|event| event.created_at)) 846 } 847 848 async fn validate_farm_dependencies( 849 client: &RadrootsNostrClient, 850 farm: &RadrootsFarmRef, 851 ) -> Result<Vec<TradeListingValidationError>, TradeListingDvmError> { 852 #[cfg(test)] 853 if let Some(result) = pop_farm_validation_hook() { 854 return result; 855 } 856 let mut errors = Vec::new(); 857 let farm_pubkey = farm.pubkey.trim(); 858 let farm_d_tag = farm.d_tag.trim(); 859 let author = match radroots_nostr_parse_pubkey(farm_pubkey) { 860 Ok(author) => author, 861 Err(_) => { 862 errors.push(TradeListingValidationError::MissingFarmProfile); 863 errors.push(TradeListingValidationError::MissingFarmRecord); 864 return Ok(errors); 865 } 866 }; 867 let profile_filter = RadrootsNostrFilter::new() 868 .kind(RadrootsNostrKind::Metadata) 869 .author(author); 870 let profile_event = 871 fetch_latest_event_by_kind(client, profile_filter, RadrootsNostrKind::Metadata).await?; 872 let has_profile = profile_event 873 .map(|event| { 874 let rr_event = radroots_event_from_nostr(&event); 875 tag_has_value(&rr_event.tags, "t", "radroots:type:farm") 876 }) 877 .unwrap_or(false); 878 if !has_profile { 879 errors.push(TradeListingValidationError::MissingFarmProfile); 880 } 881 if farm_d_tag.is_empty() { 882 errors.push(TradeListingValidationError::MissingFarmRecord); 883 return Ok(errors); 884 } 885 let author = radroots_nostr_parse_pubkey(farm_pubkey) 886 .map_err(|_| TradeListingDvmError::InvalidPayload("invalid farm pubkey".to_string()))?; 887 let record_filter = RadrootsNostrFilter::new() 888 .kind(RadrootsNostrKind::Custom(KIND_FARM as u16)) 889 .author(author) 890 .identifier(farm_d_tag.to_string()); 891 let record_event = fetch_latest_event_by_kind( 892 client, 893 record_filter, 894 RadrootsNostrKind::Custom(KIND_FARM as u16), 895 ) 896 .await?; 897 if record_event.is_none() { 898 errors.push(TradeListingValidationError::MissingFarmRecord); 899 } 900 Ok(errors) 901 } 902 903 fn required_tag_value( 904 tags: &[Vec<String>], 905 key: &'static str, 906 ) -> Result<String, TradeListingDvmError> { 907 tags.iter() 908 .find_map(|tag| { 909 if tag.first().map(String::as_str) == Some(key) { 910 tag.get(1).cloned() 911 } else { 912 None 913 } 914 }) 915 .filter(|value| !value.trim().is_empty()) 916 .ok_or(TradeListingDvmError::MissingTag(key)) 917 } 918 919 fn tag_has_value(tags: &[Vec<String>], key: &str, value: &str) -> bool { 920 tags.iter().any(|tag| { 921 tag.first().map(String::as_str) == Some(key) 922 && tag.get(1).map(String::as_str) == Some(value) 923 }) 924 } 925 926 pub async fn handle_error( 927 error: TradeListingDvmError, 928 event: &RadrootsNostrEvent, 929 client: &RadrootsNostrClient, 930 ) -> Result<(), TradeListingDvmError> { 931 let builder = 932 radroots_nostr_build_event_job_feedback(event, "error", Some(error.to_string()), None)?; 933 send_event_io(client, builder).await 934 } 935 936 #[cfg(test)] 937 #[cfg_attr(coverage_nightly, coverage(off))] 938 mod tests { 939 use super::{ 940 DvmTestHooks, TradeListingDvmError, dvm_test_hooks, ensure_transition, handle_error, 941 handle_event, tag_has_value, 942 }; 943 use crate::features::trade_listing::state::{TradeListingState, TradeOrderStatus}; 944 use radroots_core::{ 945 RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, 946 }; 947 use radroots_events::RadrootsNostrEventPtr; 948 use radroots_events::farm::RadrootsFarmRef; 949 use radroots_events::ids::{ 950 RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsOrderQuoteId, 951 RadrootsPublicKey, 952 }; 953 use radroots_events::kinds::{ 954 KIND_LISTING, KIND_ORDER_REQUEST, KIND_TRADE_LISTING_VALIDATION_REQUEST, 955 }; 956 use radroots_events::order::{ 957 RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, 958 RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, 959 }; 960 use radroots_events::trade_validation::RadrootsTradeValidationListingRequest; 961 use radroots_events_codec::order::order_request_event_build; 962 use radroots_nostr::prelude::{ 963 RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys, 964 RadrootsNostrKind, radroots_nostr_build_event, 965 }; 966 use std::sync::Arc; 967 use tokio::sync::{Mutex, MutexGuard}; 968 969 static TEST_LOCK: Mutex<()> = Mutex::const_new(()); 970 971 async fn test_guard() -> MutexGuard<'static, ()> { 972 let guard = TEST_LOCK.lock().await; 973 *dvm_test_hooks().lock().expect("hooks") = DvmTestHooks::default(); 974 guard 975 } 976 977 fn listing_id() -> &'static str { 978 "AAAAAAAAAAAAAAAAAAAAAg" 979 } 980 981 fn listing_addr(seller: &RadrootsNostrKeys) -> String { 982 format!("{}:{}:{}", KIND_LISTING, seller.public_key(), listing_id()) 983 } 984 985 fn listing_event_id() -> &'static str { 986 "0000000000000000000000000000000000000000000000000000000000000001" 987 } 988 989 fn typed_listing_addr(seller: &RadrootsNostrKeys) -> RadrootsListingAddress { 990 RadrootsListingAddress::parse(listing_addr(seller)).expect("listing address") 991 } 992 993 fn typed_order_id(order_id: &str) -> RadrootsOrderId { 994 RadrootsOrderId::parse(order_id).expect("order id") 995 } 996 997 fn typed_quote_id(order_id: &str) -> RadrootsOrderQuoteId { 998 RadrootsOrderQuoteId::parse(format!("{order_id}-quote")).expect("quote id") 999 } 1000 1001 fn typed_bin_id() -> RadrootsInventoryBinId { 1002 RadrootsInventoryBinId::parse("bin-1").expect("bin id") 1003 } 1004 1005 fn typed_pubkey(keys: &RadrootsNostrKeys) -> RadrootsPublicKey { 1006 RadrootsPublicKey::parse(keys.public_key().to_string()).expect("public key") 1007 } 1008 1009 fn listing_event_ptr() -> RadrootsNostrEventPtr { 1010 RadrootsNostrEventPtr { 1011 id: listing_event_id().to_string(), 1012 relays: None, 1013 } 1014 } 1015 1016 fn order_economics(order_id: &str) -> RadrootsOrderEconomics { 1017 RadrootsOrderEconomics { 1018 quote_id: typed_quote_id(order_id), 1019 quote_version: 1, 1020 pricing_basis: RadrootsOrderPricingBasis::ListingEvent, 1021 currency: RadrootsCoreCurrency::USD, 1022 items: vec![RadrootsOrderEconomicItem { 1023 bin_id: typed_bin_id(), 1024 bin_count: 2, 1025 quantity_amount: RadrootsCoreDecimal::from(1u32), 1026 quantity_unit: RadrootsCoreUnit::Each, 1027 unit_price_amount: RadrootsCoreDecimal::from(5u32), 1028 unit_price_currency: RadrootsCoreCurrency::USD, 1029 line_subtotal: RadrootsCoreMoney::new( 1030 RadrootsCoreDecimal::from(10u32), 1031 RadrootsCoreCurrency::USD, 1032 ), 1033 }], 1034 discounts: Vec::<RadrootsOrderEconomicLine>::new(), 1035 adjustments: Vec::<RadrootsOrderEconomicLine>::new(), 1036 subtotal: RadrootsCoreMoney::new( 1037 RadrootsCoreDecimal::from(10u32), 1038 RadrootsCoreCurrency::USD, 1039 ), 1040 discount_total: RadrootsCoreMoney::new( 1041 RadrootsCoreDecimal::from(0u32), 1042 RadrootsCoreCurrency::USD, 1043 ), 1044 adjustment_total: RadrootsCoreMoney::new( 1045 RadrootsCoreDecimal::from(0u32), 1046 RadrootsCoreCurrency::USD, 1047 ), 1048 total: RadrootsCoreMoney::new( 1049 RadrootsCoreDecimal::from(10u32), 1050 RadrootsCoreCurrency::USD, 1051 ), 1052 } 1053 } 1054 1055 fn order_request( 1056 order_id: &str, 1057 buyer: &RadrootsNostrKeys, 1058 seller: &RadrootsNostrKeys, 1059 ) -> RadrootsOrderRequest { 1060 RadrootsOrderRequest { 1061 order_id: typed_order_id(order_id), 1062 listing_addr: typed_listing_addr(seller), 1063 buyer_pubkey: typed_pubkey(buyer), 1064 seller_pubkey: typed_pubkey(seller), 1065 items: vec![RadrootsOrderItem { 1066 bin_id: typed_bin_id(), 1067 bin_count: 2, 1068 }], 1069 economics: order_economics(order_id), 1070 } 1071 } 1072 1073 fn signed_order_request_event( 1074 buyer: &RadrootsNostrKeys, 1075 seller: &RadrootsNostrKeys, 1076 ) -> RadrootsNostrEvent { 1077 let payload = order_request("order-1", buyer, seller); 1078 let wire = order_request_event_build(&listing_event_ptr(), &payload).expect("wire"); 1079 radroots_nostr_build_event(wire.kind, wire.content, wire.tags) 1080 .expect("builder") 1081 .sign_with_keys(buyer) 1082 .expect("event") 1083 } 1084 1085 fn listing_event(seller: &RadrootsNostrKeys) -> RadrootsNostrEvent { 1086 RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(KIND_LISTING as u16), "{}") 1087 .tags(vec![radroots_nostr::prelude::RadrootsNostrTag::identifier( 1088 listing_id(), 1089 )]) 1090 .sign_with_keys(seller) 1091 .expect("listing event") 1092 } 1093 1094 #[tokio::test] 1095 async fn order_request_inserts_canonical_order_state() { 1096 let _guard = test_guard().await; 1097 let worker = RadrootsNostrKeys::generate(); 1098 let buyer = RadrootsNostrKeys::generate(); 1099 let seller = RadrootsNostrKeys::generate(); 1100 let client = RadrootsNostrClient::new(worker.clone()); 1101 let state = Arc::new(Mutex::new(TradeListingState::default())); 1102 state.lock().await.upsert_listing_event( 1103 &listing_addr(&seller), 1104 listing_event_id(), 1105 KIND_LISTING, 1106 ); 1107 1108 handle_event( 1109 signed_order_request_event(&buyer, &seller), 1110 Vec::new(), 1111 worker, 1112 client, 1113 state.clone(), 1114 ) 1115 .await 1116 .expect("order request"); 1117 1118 let mut state = state.lock().await; 1119 let order = state.get_order_mut("order-1").expect("order"); 1120 assert_eq!(order.status, TradeOrderStatus::Requested); 1121 assert_eq!(order.buyer_pubkey, buyer.public_key().to_string()); 1122 assert_eq!(order.seller_pubkey, seller.public_key().to_string()); 1123 } 1124 1125 #[tokio::test] 1126 async fn listing_validation_request_sends_result_and_marks_listing_validated() { 1127 let _guard = test_guard().await; 1128 let worker = RadrootsNostrKeys::generate(); 1129 let seller = RadrootsNostrKeys::generate(); 1130 let requester = RadrootsNostrKeys::generate(); 1131 let client = RadrootsNostrClient::new(worker.clone()); 1132 let state = Arc::new(Mutex::new(TradeListingState::default())); 1133 let listing_addr = listing_addr(&seller); 1134 { 1135 let mut hooks = dvm_test_hooks().lock().expect("hooks"); 1136 hooks 1137 .fetch_event_by_id_results 1138 .push_back(Ok(listing_event(&seller))); 1139 hooks.validate_listing_results.push_back(Ok(( 1140 listing_addr.clone(), 1141 RadrootsFarmRef { 1142 pubkey: seller.public_key().to_string(), 1143 d_tag: "farm-1".to_string(), 1144 }, 1145 ))); 1146 hooks.farm_validation_results.push_back(Ok(Vec::new())); 1147 hooks.send_event_results.push_back(Ok(())); 1148 } 1149 let payload = RadrootsTradeValidationListingRequest { 1150 listing_event: Some(listing_event_ptr()), 1151 }; 1152 let event = radroots_nostr_build_event( 1153 KIND_TRADE_LISTING_VALIDATION_REQUEST, 1154 serde_json::to_string(&payload).expect("payload"), 1155 vec![ 1156 vec!["p".to_string(), worker.public_key().to_string()], 1157 vec!["a".to_string(), listing_addr.clone()], 1158 ], 1159 ) 1160 .expect("builder") 1161 .sign_with_keys(&requester) 1162 .expect("event"); 1163 1164 handle_event(event, Vec::new(), worker, client, state.clone()) 1165 .await 1166 .expect("validation request"); 1167 1168 assert!(state.lock().await.is_listing_validated(&listing_addr)); 1169 } 1170 1171 #[tokio::test] 1172 async fn unsupported_kind_is_rejected() { 1173 let _guard = test_guard().await; 1174 let worker = RadrootsNostrKeys::generate(); 1175 let client = RadrootsNostrClient::new(worker.clone()); 1176 let state = Arc::new(Mutex::new(TradeListingState::default())); 1177 let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(4999), "test") 1178 .sign_with_keys(&RadrootsNostrKeys::generate()) 1179 .expect("event"); 1180 assert!(matches!( 1181 handle_event(event, Vec::new(), worker, client, state).await, 1182 Err(TradeListingDvmError::UnsupportedKind) 1183 )); 1184 } 1185 1186 #[test] 1187 fn transition_and_tag_helpers_cover_core_paths() { 1188 assert!( 1189 ensure_transition(&TradeOrderStatus::Requested, &TradeOrderStatus::Accepted).is_ok() 1190 ); 1191 assert!( 1192 ensure_transition(&TradeOrderStatus::Declined, &TradeOrderStatus::Accepted).is_err() 1193 ); 1194 assert!(tag_has_value( 1195 &[vec!["p".to_string(), "pubkey".to_string()]], 1196 "p", 1197 "pubkey" 1198 )); 1199 } 1200 1201 #[tokio::test] 1202 async fn handle_error_uses_send_hook() { 1203 let _guard = test_guard().await; 1204 dvm_test_hooks() 1205 .lock() 1206 .expect("hooks") 1207 .send_event_results 1208 .push_back(Ok(())); 1209 let keys = RadrootsNostrKeys::generate(); 1210 let client = RadrootsNostrClient::new(keys.clone()); 1211 let event = RadrootsNostrEventBuilder::new( 1212 RadrootsNostrKind::Custom(KIND_ORDER_REQUEST as u16), 1213 "bad", 1214 ) 1215 .sign_with_keys(&keys) 1216 .expect("event"); 1217 assert!( 1218 handle_error(TradeListingDvmError::InvalidOrder, &event, &client) 1219 .await 1220 .is_ok() 1221 ); 1222 } 1223 }