rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

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 }