commit 628e9b57a169ad83e2080706b2ce66c27291ef45
parent 2a8e39f82b9f67561d0d917304440ab79a2ea194
Author: triesap <tyson@radroots.org>
Date: Fri, 12 Jun 2026 20:11:47 -0700
rhi: align order event worker consumers
Diffstat:
7 files changed, 741 insertions(+), 5540 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3275,7 +3275,6 @@ dependencies = [
"rust_decimal",
"rust_decimal_macros",
"serde",
- "typeshare",
]
[[package]]
@@ -3284,8 +3283,6 @@ version = "0.1.0-alpha.2"
dependencies = [
"radroots_core",
"serde",
- "ts-rs",
- "typeshare",
]
[[package]]
@@ -3428,7 +3425,6 @@ dependencies = [
"serde_json",
"sha2",
"thiserror 1.0.69",
- "ts-rs",
]
[[package]]
@@ -5362,15 +5358,6 @@ dependencies = [
]
[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5876,28 +5863,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
-name = "ts-rs"
-version = "11.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424"
-dependencies = [
- "thiserror 2.0.18",
- "ts-rs-macros",
-]
-
-[[package]]
-name = "ts-rs-macros"
-version = "11.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
- "termcolor",
-]
-
-[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5944,28 +5909,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
-name = "typeshare"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da1bf9fe204f358ffea7f8f779b53923a20278b3ab8e8d97962c5e1b3a54edb7"
-dependencies = [
- "chrono",
- "serde",
- "serde_json",
- "typeshare-annotation",
-]
-
-[[package]]
-name = "typeshare-annotation"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "621963e302416b389a1ec177397e9e62de849a78bd8205d428608553def75350"
-dependencies = [
- "quote",
- "syn 2.0.117",
-]
-
-[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6280,15 +6223,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
-name = "winapi-util"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
-dependencies = [
- "windows-sys 0.61.2",
-]
-
-[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -33,7 +33,7 @@ sp1_cuda_proving = ["sp1_proving", "radroots_sp1_host_trade/sp1_cuda"]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
[dependencies]
-radroots_core = { workspace = true, features = ["std", "serde", "typeshare"] }
+radroots_core = { workspace = true, features = ["std", "serde"] }
radroots_events = { workspace = true, features = ["serde"] }
radroots_events_codec = { workspace = true, features = ["nostr"] }
radroots_identity = { workspace = true }
diff --git a/src/features/trade_listing/handlers/dvm.rs b/src/features/trade_listing/handlers/dvm.rs
@@ -5,35 +5,29 @@ use std::{sync::Arc, time::Duration};
use radroots_events::farm::RadrootsFarmRef;
use radroots_events::kinds::{
- KIND_FARM, KIND_TRADE_ORDER_REQUEST, KIND_WORKER_TRADE_TRANSITION_PROOF_REQ, is_listing_kind,
- is_trade_kind,
+ KIND_FARM, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_FULFILLMENT_UPDATE,
+ KIND_ORDER_PAYMENT_RECORD, KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST,
+ KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_ORDER_SETTLEMENT_DECISION,
+ KIND_TRADE_LISTING_VALIDATION_REQUEST, KIND_TRADE_LISTING_VALIDATION_RESULT,
+ KIND_TRADE_TRANSITION_PROOF_REQUEST, KIND_TRADE_TRANSITION_PROOF_RESULT, is_listing_kind,
+ is_order_event_kind, is_trade_validation_service_event_kind,
};
-use radroots_events::trade::{
- RadrootsTradeAnswer as TradeAnswer, RadrootsTradeDiscountDecision as TradeDiscountDecision,
- RadrootsTradeDiscountOffer as TradeDiscountOffer,
- RadrootsTradeDiscountRequest as TradeDiscountRequest,
- RadrootsTradeEnvelope as TradeListingEnvelope,
- RadrootsTradeEnvelopeError as TradeListingEnvelopeError,
- RadrootsTradeFulfillmentStatus as TradeFulfillmentStatus,
- RadrootsTradeFulfillmentUpdate as TradeFulfillmentUpdate,
- RadrootsTradeListingCancel as TradeListingCancel,
- RadrootsTradeListingValidateRequest as TradeListingValidateRequest,
- RadrootsTradeListingValidateResult as TradeListingValidateResult,
- RadrootsTradeListingValidationError as TradeListingValidationError,
- RadrootsTradeMessagePayload as TradeListingMessagePayload,
- RadrootsTradeMessageType as TradeListingMessageType,
- RadrootsTradeOrderRequested as TradeOrderRequested,
- RadrootsTradeOrderResponse as TradeOrderResponse,
- RadrootsTradeOrderRevision as TradeOrderRevision,
- RadrootsTradeOrderRevisionResponse as TradeOrderRevisionResponse,
- RadrootsTradeOrderStatus as TradeOrderStatus, RadrootsTradeQuestion as TradeQuestion,
- RadrootsTradeReceipt as TradeReceipt,
+use radroots_events::order::{
+ RadrootsOrderDecisionOutcome, RadrootsOrderFulfillmentState, RadrootsOrderReceipt,
+ RadrootsOrderRevisionOutcome,
};
-use radroots_events_codec::trade::{
- RadrootsActiveTradeEnvelopeParseError as ActiveTradeEnvelopeParseError,
- RadrootsTradeEnvelopeParseError as TradeListingEnvelopeParseError,
- RadrootsTradeListingAddress as TradeListingAddress, active_trade_order_request_from_event,
- trade_envelope_event_build as trade_listing_envelope_event_build, trade_envelope_from_event,
+use radroots_events::trade_validation::{
+ RadrootsTradeValidationListingError as TradeListingValidationError,
+ RadrootsTradeValidationListingRequest as TradeListingValidateRequest,
+ RadrootsTradeValidationListingResult as TradeListingValidateResult,
+};
+use radroots_events_codec::order::{
+ RadrootsOrderEnvelopeParseError, RadrootsOrderListingAddress as OrderListingAddress,
+ order_cancellation_from_event, order_decision_from_event, order_fulfillment_update_from_event,
+ order_payment_record_from_event, order_receipt_from_event, order_request_from_event,
+ order_revision_decision_from_event, order_revision_proposal_from_event,
+ order_settlement_decision_from_event, parse_order_listing_event_tag, parse_order_prev_tag,
+ parse_order_root_tag,
};
use radroots_nostr::prelude::{
RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter,
@@ -41,15 +35,11 @@ use radroots_nostr::prelude::{
radroots_nostr_build_event, radroots_nostr_build_event_job_feedback,
radroots_nostr_fetch_event_by_id, radroots_nostr_parse_pubkey, radroots_nostr_send_event,
};
-use radroots_trade::listing::projection::RadrootsTradeOrderWorkflowMessage;
use radroots_trade::listing::validation::validate_listing_event;
-#[cfg(test)]
-use serde::de::DeserializeOwned;
-use std::convert::TryFrom;
use thiserror::Error;
use crate::features::trade_listing::state::{
- TradeListingState, TradeListingStateError, TradeOrderState,
+ TradeListingState, TradeListingStateError, TradeOrderState, TradeOrderStatus,
};
use crate::features::trade_validation_receipt::{
TradeValidationReceiptJobError, TradeValidationReceiptProverPolicy,
@@ -67,7 +57,7 @@ pub enum TradeListingDvmError {
#[error("tag mismatch: {0}")]
TagMismatch(&'static str),
#[error("invalid envelope: {0}")]
- InvalidEnvelope(#[from] TradeListingEnvelopeError),
+ InvalidEnvelope(String),
#[error("invalid envelope payload: {0}")]
InvalidPayload(String),
#[error("invalid listing address")]
@@ -82,8 +72,6 @@ pub enum TradeListingDvmError {
Serde(#[from] serde_json::Error),
#[error("unauthorized sender")]
Unauthorized,
- #[error("listing not validated")]
- ListingNotValidated,
}
#[cfg(test)]
@@ -206,12 +194,12 @@ async fn fetch_event_by_id_io(
client: &RadrootsNostrClient,
id: &str,
) -> Result<RadrootsNostrEvent, TradeListingDvmError> {
- let hook_result = take_fetch_event_by_id_hook();
- let event = match hook_result {
- Some(result) => result?,
- None => radroots_nostr_fetch_event_by_id(client, id).await?,
- };
- Ok(event)
+ match take_fetch_event_by_id_hook() {
+ Some(result) => result,
+ None => radroots_nostr_fetch_event_by_id(client, id)
+ .await
+ .map_err(TradeListingDvmError::from),
+ }
}
async fn fetch_events_io(
@@ -219,12 +207,13 @@ async fn fetch_events_io(
filter: RadrootsNostrFilter,
timeout: Duration,
) -> Result<Vec<RadrootsNostrEvent>, TradeListingDvmError> {
- let hook_result = take_fetch_events_hook();
- let events = match hook_result {
- Some(result) => result?,
- None => client.fetch_events(filter, timeout).await?,
- };
- Ok(events)
+ match take_fetch_events_hook() {
+ Some(result) => result,
+ None => client
+ .fetch_events(filter, timeout)
+ .await
+ .map_err(TradeListingDvmError::from),
+ }
}
#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
@@ -232,29 +221,24 @@ async fn send_event_io(
client: &RadrootsNostrClient,
builder: RadrootsNostrEventBuilder,
) -> Result<(), TradeListingDvmError> {
- let hook_result = take_send_event_hook();
- let send_result: Result<(), TradeListingDvmError> = match hook_result {
+ match take_send_event_hook() {
Some(result) => result,
None => radroots_nostr_send_event(client, builder)
.await
.map(|_| ())
.map_err(TradeListingDvmError::from),
- };
- send_result?;
- Ok(())
+ }
}
#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
fn validate_listing_event_io(
event: &RadrootsNostrEvent,
) -> Result<(String, RadrootsFarmRef), TradeListingValidationError> {
- let hook_result = take_validate_listing_hook();
- let validated = match hook_result {
- Some(result) => result?,
+ match take_validate_listing_hook() {
+ Some(result) => result,
None => validate_listing_event(&radroots_event_from_nostr(event))
- .map(|listing| (listing.listing_addr, listing.listing.farm))?,
- };
- Ok(validated)
+ .map(|listing| (listing.listing_addr, listing.listing.farm)),
+ }
}
pub async fn handle_event_with_policy(
@@ -265,220 +249,36 @@ pub async fn handle_event_with_policy(
state: Arc<tokio::sync::Mutex<TradeListingState>>,
proof_policy: &TradeValidationReceiptProverPolicy,
) -> Result<(), TradeListingDvmError> {
- let kind = match event.kind {
- RadrootsNostrKind::Custom(v) => u32::from(v),
- _ => return Err(TradeListingDvmError::UnsupportedKind),
- };
+ let kind = event_kind_u32(&event)?;
if is_listing_kind(kind) {
- handle_listing_event(&event, &state).await?;
- return Ok(());
+ return handle_listing_event(&event, &state).await;
}
- if !is_trade_kind(kind) {
- return Err(TradeListingDvmError::UnsupportedKind);
- }
-
if event.pubkey == keys.public_key() {
return Ok(());
}
-
- if kind == KIND_WORKER_TRADE_TRANSITION_PROOF_REQ {
+ if kind == KIND_TRADE_TRANSITION_PROOF_REQUEST {
return handle_trade_validation_receipt_job_request(&event, &keys, &client, proof_policy)
.await
.map_err(map_trade_validation_receipt_job_error);
}
-
- if kind == KIND_TRADE_ORDER_REQUEST {
- let envelope = active_trade_order_request_from_event(&radroots_event_from_nostr(&event))
- .map_err(map_active_trade_envelope_parse_error)?;
- let listing_addr_parsed = TradeListingAddress::parse(&envelope.listing_addr)
- .map_err(|_| TradeListingDvmError::InvalidListingAddr)?;
- if !is_listing_kind(listing_addr_parsed.kind) {
- return Err(TradeListingDvmError::InvalidListingAddr);
- }
- handle_order_request(
- &event,
- envelope.payload,
- &listing_addr_parsed,
- Some(envelope.order_id.as_str()),
- &client,
- &state,
- )
- .await?;
- return Ok(());
- }
-
- let envelope_hint: TradeListingEnvelope<serde_json::Value> =
- serde_json::from_str(&event.content)
- .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?;
- if envelope_hint.message_type.kind() != kind {
- return Err(TradeListingDvmError::TagMismatch("kind"));
- }
-
- let tag_slices: Vec<Vec<String>> = event.tags.iter().map(|t| t.as_slice().to_vec()).collect();
- if envelope_hint.message_type.is_service() {
- let rhi_pubkey = keys.public_key().to_string();
- if !tag_has_value(&tag_slices, "p", &rhi_pubkey) {
- return Err(TradeListingDvmError::MissingRecipient);
- }
+ if kind == KIND_TRADE_LISTING_VALIDATION_REQUEST {
+ ensure_service_recipient(&event, &keys)?;
+ return handle_listing_validate_request(&event, &client, &state).await;
}
-
- let envelope: TradeListingEnvelope<TradeListingMessagePayload> =
- trade_envelope_from_event(&radroots_event_from_nostr(&event))
- .map_err(map_trade_envelope_parse_error)?;
- if envelope.payload.message_type() != envelope.message_type {
- return Err(TradeListingDvmError::InvalidPayload(
- "trade envelope payload does not match message type".to_string(),
- ));
+ if kind == KIND_TRADE_LISTING_VALIDATION_RESULT || kind == KIND_TRADE_TRANSITION_PROOF_RESULT {
+ state
+ .lock()
+ .await
+ .mark_non_order_event_seen(&event.id.to_string());
+ return Ok(());
}
-
- let order_id = envelope.order_id.as_deref();
- let listing_addr = envelope.listing_addr.clone();
- let listing_addr_parsed = TradeListingAddress::parse(&listing_addr)
- .map_err(|_| TradeListingDvmError::InvalidListingAddr)?;
- if !is_listing_kind(listing_addr_parsed.kind) {
- return Err(TradeListingDvmError::InvalidListingAddr);
+ if is_order_event_kind(kind) {
+ return handle_order_event(&event, kind, &client, &state).await;
}
-
- match envelope.payload {
- TradeListingMessagePayload::ListingValidateRequest(payload) => {
- handle_listing_validate_request(&event, payload, &listing_addr, &client, &state)
- .await?;
- }
- TradeListingMessagePayload::TradeOrderRequested(_) => {
- return Err(TradeListingDvmError::InvalidPayload(
- "active order requests must be decoded through the active order-request path"
- .to_string(),
- ));
- }
- TradeListingMessagePayload::OrderResponse(payload) => {
- handle_order_response(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::OrderRevision(payload) => {
- handle_order_revision(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::OrderRevisionAccept(payload)
- | TradeListingMessagePayload::OrderRevisionDecline(payload) => {
- handle_order_revision_response(
- &event,
- envelope.message_type,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::Question(payload) => {
- handle_question(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::Answer(payload) => {
- handle_answer(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::DiscountRequest(payload) => {
- handle_discount_request(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::DiscountOffer(payload) => {
- handle_discount_offer(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::DiscountAccept(payload)
- | TradeListingMessagePayload::DiscountDecline(payload) => {
- handle_discount_decision(
- &event,
- envelope.message_type,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::Cancel(payload) => {
- handle_cancel(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::FulfillmentUpdate(payload) => {
- handle_fulfillment_update(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::Receipt(payload) => {
- handle_receipt(
- &event,
- payload,
- &listing_addr_parsed,
- order_id,
- &client,
- &state,
- )
- .await?;
- }
- TradeListingMessagePayload::ListingValidateResult(_) => {}
+ if is_trade_validation_service_event_kind(kind) {
+ return Err(TradeListingDvmError::UnsupportedKind);
}
-
- Ok(())
+ Err(TradeListingDvmError::UnsupportedKind)
}
#[cfg(test)]
@@ -500,6 +300,13 @@ pub async fn handle_event(
.await
}
+fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeListingDvmError> {
+ match event.kind {
+ RadrootsNostrKind::Custom(value) => Ok(u32::from(value)),
+ _ => Err(TradeListingDvmError::UnsupportedKind),
+ }
+}
+
fn map_trade_validation_receipt_job_error(
error: TradeValidationReceiptJobError,
) -> TradeListingDvmError {
@@ -511,59 +318,35 @@ fn map_trade_validation_receipt_job_error(
}
}
-fn map_trade_envelope_parse_error(error: TradeListingEnvelopeParseError) -> TradeListingDvmError {
+fn map_order_parse_error(error: RadrootsOrderEnvelopeParseError) -> TradeListingDvmError {
match error {
- TradeListingEnvelopeParseError::InvalidKind(_) => TradeListingDvmError::UnsupportedKind,
- TradeListingEnvelopeParseError::InvalidJson
- | TradeListingEnvelopeParseError::InvalidTag(_) => {
- TradeListingDvmError::InvalidPayload(error.to_string())
- }
- TradeListingEnvelopeParseError::InvalidEnvelope(inner) => {
- TradeListingDvmError::InvalidEnvelope(inner)
- }
- TradeListingEnvelopeParseError::MessageTypeKindMismatch { .. } => {
- TradeListingDvmError::TagMismatch("kind")
- }
- TradeListingEnvelopeParseError::MissingTag(tag) => TradeListingDvmError::MissingTag(tag),
- TradeListingEnvelopeParseError::ListingAddrTagMismatch => {
+ RadrootsOrderEnvelopeParseError::InvalidKind(_) => TradeListingDvmError::UnsupportedKind,
+ RadrootsOrderEnvelopeParseError::MissingTag(tag) => TradeListingDvmError::MissingTag(tag),
+ RadrootsOrderEnvelopeParseError::ListingAddrTagMismatch => {
TradeListingDvmError::TagMismatch("a")
}
- TradeListingEnvelopeParseError::OrderIdTagMismatch => {
+ RadrootsOrderEnvelopeParseError::OrderIdTagMismatch => {
TradeListingDvmError::TagMismatch("d")
}
- TradeListingEnvelopeParseError::InvalidListingAddr(_) => {
+ RadrootsOrderEnvelopeParseError::InvalidListingAddr(_) => {
TradeListingDvmError::InvalidListingAddr
}
+ RadrootsOrderEnvelopeParseError::InvalidEnvelope(error) => {
+ TradeListingDvmError::InvalidEnvelope(error.to_string())
+ }
+ other => TradeListingDvmError::InvalidPayload(other.to_string()),
}
}
-fn map_active_trade_envelope_parse_error(
- error: ActiveTradeEnvelopeParseError,
-) -> TradeListingDvmError {
- match error {
- ActiveTradeEnvelopeParseError::InvalidKind(_) => TradeListingDvmError::UnsupportedKind,
- ActiveTradeEnvelopeParseError::InvalidJson
- | ActiveTradeEnvelopeParseError::InvalidTag(_)
- | ActiveTradeEnvelopeParseError::InvalidPayload(_)
- | ActiveTradeEnvelopeParseError::PayloadBindingMismatch(_)
- | ActiveTradeEnvelopeParseError::AuthorMismatch
- | ActiveTradeEnvelopeParseError::CounterpartyTagMismatch => {
- TradeListingDvmError::InvalidPayload(error.to_string())
- }
- ActiveTradeEnvelopeParseError::InvalidEnvelope(inner) => {
- TradeListingDvmError::InvalidPayload(inner.to_string())
- }
- ActiveTradeEnvelopeParseError::MessageTypeKindMismatch { .. } => {
- TradeListingDvmError::TagMismatch("kind")
- }
- ActiveTradeEnvelopeParseError::MissingTag(tag) => TradeListingDvmError::MissingTag(tag),
- ActiveTradeEnvelopeParseError::ListingAddrTagMismatch => {
- TradeListingDvmError::TagMismatch("a")
- }
- ActiveTradeEnvelopeParseError::OrderIdTagMismatch => TradeListingDvmError::TagMismatch("d"),
- ActiveTradeEnvelopeParseError::InvalidListingAddr(_) => {
- TradeListingDvmError::InvalidListingAddr
- }
+fn ensure_service_recipient(
+ event: &RadrootsNostrEvent,
+ keys: &RadrootsNostrKeys,
+) -> Result<(), TradeListingDvmError> {
+ let tags = radroots_event_from_nostr(event).tags;
+ if tag_has_value(&tags, "p", &keys.public_key().to_string()) {
+ Ok(())
+ } else {
+ Err(TradeListingDvmError::MissingRecipient)
}
}
@@ -578,14 +361,9 @@ async fn handle_listing_event(
return Ok(());
}
}
-
let validated = validate_listing_event(&radroots_event_from_nostr(event))
.map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?;
- let kind = match event.kind {
- RadrootsNostrKind::Custom(value) => u32::from(value),
- _ => return Err(TradeListingDvmError::UnsupportedKind),
- };
-
+ let kind = event_kind_u32(event)?;
let mut state = state.lock().await;
state.upsert_listing_event(&validated.listing_addr, &event_id, kind);
state.mark_non_order_event_seen(&event_id);
@@ -595,99 +373,78 @@ async fn handle_listing_event(
#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
async fn handle_listing_validate_request(
event: &RadrootsNostrEvent,
- payload: TradeListingValidateRequest,
- listing_addr: &str,
client: &RadrootsNostrClient,
state: &Arc<tokio::sync::Mutex<TradeListingState>>,
) -> Result<(), TradeListingDvmError> {
+ let event_id = event.id.to_string();
{
let state = state.lock().await;
- if state.is_non_order_event_seen(&event.id.to_string()) {
+ if state.is_non_order_event_seen(&event_id) {
return Ok(());
}
}
-
- let listing_event = if let Some(ptr) = payload.listing_event {
- match fetch_event_by_id_io(client, &ptr.id).await {
- Ok(evt) => Some(evt),
- Err(err) => {
- let error = match err {
- TradeListingDvmError::Nostr(
- radroots_nostr::error::RadrootsNostrError::EventNotFound(_),
- ) => TradeListingValidationError::ListingEventNotFound {
- listing_addr: listing_addr.to_string(),
- },
- _ => TradeListingValidationError::ListingEventFetchFailed {
- listing_addr: listing_addr.to_string(),
- },
- };
- state.lock().await.clear_listing_validation(listing_addr);
- send_validate_result(event, client, listing_addr, vec![error]).await?;
- return Ok(());
- }
- }
- } else {
- match fetch_listing_by_addr(client, listing_addr).await {
- Ok(event) => event,
- Err(_) => {
- let error = TradeListingValidationError::ListingEventFetchFailed {
- listing_addr: listing_addr.to_string(),
- };
- state.lock().await.clear_listing_validation(listing_addr);
- send_validate_result(event, client, listing_addr, vec![error]).await?;
- return Ok(());
- }
- }
- };
-
- let (validated_event_id, errors): (Option<String>, Vec<TradeListingValidationError>) =
- if let Some(listing_event) = listing_event {
- match validate_listing_event_io(&listing_event) {
- Ok((validated_listing_addr, farm)) => {
- if validated_listing_addr != listing_addr {
- (
- None,
- vec![TradeListingValidationError::ListingEventNotFound {
- listing_addr: listing_addr.to_string(),
- }],
- )
- } else {
- let errors: Vec<TradeListingValidationError> =
- validate_farm_dependencies(client, &farm).await?;
- if errors.is_empty() {
- (Some(listing_event.id.to_string()), errors)
- } else {
- (None, errors)
- }
- }
+ let rr_event = radroots_event_from_nostr(event);
+ let listing_addr = required_tag_value(&rr_event.tags, "a")?;
+ let parsed_listing_addr = OrderListingAddress::parse(&listing_addr)
+ .map_err(|_| TradeListingDvmError::InvalidListingAddr)?;
+ if !is_listing_kind(parsed_listing_addr.kind) {
+ return Err(TradeListingDvmError::InvalidListingAddr);
+ }
+ let payload: TradeListingValidateRequest = serde_json::from_str(&event.content)?;
+ let listing_event = resolve_listing_event(client, &listing_addr, payload.listing_event).await;
+ let (validated_event_id, errors) = match listing_event {
+ Ok(Some(listing_event)) => match validate_listing_event_io(&listing_event) {
+ Ok((validated_listing_addr, farm)) if validated_listing_addr == listing_addr => {
+ let errors = validate_farm_dependencies(client, &farm).await?;
+ if errors.is_empty() {
+ (Some(listing_event.id.to_string()), errors)
+ } else {
+ (None, errors)
}
- Err(err) => (None, vec![err]),
}
- } else {
- (
+ Ok(_) => (
None,
vec![TradeListingValidationError::ListingEventNotFound {
- listing_addr: listing_addr.to_string(),
+ listing_addr: listing_addr.clone(),
}],
- )
- };
-
+ ),
+ Err(error) => (None, vec![error]),
+ },
+ Ok(None) => (
+ None,
+ vec![TradeListingValidationError::ListingEventNotFound {
+ listing_addr: listing_addr.clone(),
+ }],
+ ),
+ Err(_) => (
+ None,
+ vec![TradeListingValidationError::ListingEventFetchFailed {
+ listing_addr: listing_addr.clone(),
+ }],
+ ),
+ };
{
let mut state = state.lock().await;
match validated_event_id {
Some(validated_event_id) => {
- state.mark_listing_validated(listing_addr, &validated_event_id);
+ state.mark_listing_validated(&listing_addr, &validated_event_id);
}
- None => state.clear_listing_validation(listing_addr),
+ None => state.clear_listing_validation(&listing_addr),
}
+ state.mark_non_order_event_seen(&event_id);
}
+ send_validate_result(event, client, &listing_addr, errors).await
+}
- send_validate_result(event, client, listing_addr, errors).await?;
- state
- .lock()
- .await
- .mark_non_order_event_seen(&event.id.to_string());
- Ok(())
+async fn resolve_listing_event(
+ client: &RadrootsNostrClient,
+ listing_addr: &str,
+ listing_event: Option<radroots_events::RadrootsNostrEventPtr>,
+) -> Result<Option<RadrootsNostrEvent>, TradeListingDvmError> {
+ match listing_event {
+ Some(ptr) => fetch_event_by_id_io(client, &ptr.id).await.map(Some),
+ None => fetch_listing_by_addr(client, listing_addr).await,
+ }
}
async fn send_validate_result(
@@ -696,584 +453,367 @@ async fn send_validate_result(
listing_addr: &str,
errors: Vec<TradeListingValidationError>,
) -> Result<(), TradeListingDvmError> {
- let payload = TradeListingMessagePayload::ListingValidateResult(TradeListingValidateResult {
+ let payload = TradeListingValidateResult {
valid: errors.is_empty(),
errors,
- });
- send_envelope(
- client,
- event.pubkey.to_string(),
- TradeListingMessageType::ListingValidateResult,
- listing_addr,
- None,
- &payload,
- )
- .await
+ };
+ let content = serde_json::to_string(&payload)?;
+ let tags = vec![
+ vec!["p".to_string(), event.pubkey.to_string()],
+ vec!["a".to_string(), listing_addr.to_string()],
+ vec!["e".to_string(), event.id.to_string()],
+ ];
+ let builder = radroots_nostr_build_event(KIND_TRADE_LISTING_VALIDATION_RESULT, content, tags)?;
+ send_event_io(client, builder).await
}
-fn workflow_message_from_event(
+async fn handle_order_event(
event: &RadrootsNostrEvent,
-) -> Result<RadrootsTradeOrderWorkflowMessage, TradeListingDvmError> {
- RadrootsTradeOrderWorkflowMessage::from_event(&radroots_event_from_nostr(event))
- .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))
-}
-
-fn ensure_order_counterparty(actual: &str, expected: &str) -> Result<(), TradeListingDvmError> {
- if actual == expected {
- Ok(())
- } else {
- Err(TradeListingDvmError::Unauthorized)
+ kind: u32,
+ client: &RadrootsNostrClient,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+) -> Result<(), TradeListingDvmError> {
+ match kind {
+ KIND_ORDER_REQUEST => handle_order_request(event, client, state).await,
+ KIND_ORDER_DECISION => handle_order_decision(event, state).await,
+ KIND_ORDER_REVISION_PROPOSAL => handle_order_revision_proposal(event, state).await,
+ KIND_ORDER_REVISION_DECISION => handle_order_revision_decision(event, state).await,
+ KIND_ORDER_CANCELLATION => handle_order_cancellation(event, state).await,
+ KIND_ORDER_FULFILLMENT_UPDATE => handle_order_fulfillment_update(event, state).await,
+ KIND_ORDER_RECEIPT => handle_order_receipt(event, state).await,
+ KIND_ORDER_PAYMENT_RECORD => handle_order_payment_record(event, state).await,
+ KIND_ORDER_SETTLEMENT_DECISION => handle_order_settlement_decision(event, state).await,
+ _ => Err(TradeListingDvmError::UnsupportedKind),
}
}
-fn ensure_trade_chain(
- order: &TradeOrderState,
- message: &RadrootsTradeOrderWorkflowMessage,
+async fn handle_order_request(
+ event: &RadrootsNostrEvent,
+ client: &RadrootsNostrClient,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
) -> Result<(), TradeListingDvmError> {
- let root_event_id = message
- .root_event_id
- .as_deref()
- .ok_or(TradeListingDvmError::MissingTag("e:root"))?;
- if order.root_event_id.as_deref() != Some(root_event_id) {
- return Err(TradeListingDvmError::InvalidOrder);
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_request_from_event(&rr_event).map_err(map_order_parse_error)?;
+ let listing_addr = OrderListingAddress::parse(&envelope.listing_addr)
+ .map_err(|_| TradeListingDvmError::InvalidListingAddr)?;
+ if !is_listing_kind(listing_addr.kind)
+ || envelope.payload.seller_pubkey != listing_addr.seller_pubkey
+ {
+ return Err(TradeListingDvmError::InvalidListingAddr);
}
-
- let prev_event_id = message
- .prev_event_id
- .as_deref()
- .ok_or(TradeListingDvmError::MissingTag("e:prev"))?;
- if order.last_event_id.as_deref() != Some(prev_event_id) {
- return Err(TradeListingDvmError::InvalidOrder);
+ let listing_event = parse_order_listing_event_tag(&rr_event.tags)
+ .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?
+ .ok_or(TradeListingDvmError::MissingTag("listing_event"))?;
+ let listing_snapshot_event_id =
+ ensure_listing_snapshot(&envelope.listing_addr, &listing_event, client, state).await?;
+ let event_id = event.id.to_string();
+ let mut state = state.lock().await;
+ if state.order_exists(&envelope.order_id) {
+ return Ok(());
}
-
+ let mut seen = std::collections::HashSet::new();
+ seen.insert(event_id.clone());
+ state.insert_order(TradeOrderState {
+ order_id: envelope.order_id,
+ listing_addr: envelope.payload.listing_addr,
+ buyer_pubkey: envelope.payload.buyer_pubkey,
+ seller_pubkey: envelope.payload.seller_pubkey,
+ status: TradeOrderStatus::Requested,
+ listing_snapshot_event_id: Some(listing_snapshot_event_id),
+ root_event_id: Some(event_id.clone()),
+ last_event_id: Some(event_id),
+ seen_event_ids: seen,
+ });
Ok(())
}
async fn ensure_listing_snapshot(
- message: &RadrootsTradeOrderWorkflowMessage,
+ listing_addr: &str,
+ listing_event: &radroots_events::RadrootsNostrEventPtr,
client: &RadrootsNostrClient,
state: &Arc<tokio::sync::Mutex<TradeListingState>>,
) -> Result<String, TradeListingDvmError> {
- let listing_event = message
- .listing_event
- .as_ref()
- .ok_or(TradeListingDvmError::MissingTag("listing_event"))?;
- let snapshot_id = listing_event.id.clone();
-
{
let state = state.lock().await;
- if state.listing_event_id(&message.listing_addr) == Some(snapshot_id.as_str()) {
- return Ok(snapshot_id);
+ if state.listing_event_id(listing_addr) == Some(listing_event.id.as_str()) {
+ return Ok(listing_event.id.clone());
}
}
-
- let snapshot_event = fetch_event_by_id_io(client, &snapshot_id).await?;
- let validated = validate_listing_event_io(&snapshot_event)
+ let event = fetch_event_by_id_io(client, &listing_event.id).await?;
+ let (validated_listing_addr, _) = validate_listing_event_io(&event)
.map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?;
- if validated.0 != message.listing_addr {
+ if validated_listing_addr != listing_addr {
return Err(TradeListingDvmError::InvalidOrder);
}
- let snapshot_kind = match snapshot_event.kind {
- RadrootsNostrKind::Custom(value) => u32::from(value),
- _ => return Err(TradeListingDvmError::InvalidListingAddr),
- };
-
+ let kind = event_kind_u32(&event)?;
let mut state = state.lock().await;
- state.upsert_listing_event(&message.listing_addr, &snapshot_id, snapshot_kind);
- Ok(snapshot_id)
+ state.upsert_listing_event(listing_addr, &listing_event.id, kind);
+ Ok(listing_event.id.clone())
}
-#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
-async fn handle_order_request(
+async fn handle_order_decision(
event: &RadrootsNostrEvent,
- payload: TradeOrderRequested,
- listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- client: &RadrootsNostrClient,
state: &Arc<tokio::sync::Mutex<TradeListingState>>,
) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- if payload.order_id != order_id || payload.listing_addr != listing_addr.as_str() {
- return Err(TradeListingDvmError::InvalidOrder);
- }
- if payload.buyer_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &payload.seller_pubkey)?;
- let listing_snapshot_event_id = ensure_listing_snapshot(&message, client, state).await?;
- let event_id = event.id.to_string();
-
- {
- let state = state.lock().await;
- if state.order_exists(order_id) {
- return Ok(());
- }
- }
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_decision_from_event(&rr_event).map_err(map_order_parse_error)?;
+ let next_status = match envelope.payload.decision {
+ RadrootsOrderDecisionOutcome::Accepted { .. } => TradeOrderStatus::Accepted,
+ RadrootsOrderDecisionOutcome::Declined { .. } => TradeOrderStatus::Declined,
+ };
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )?;
+ ensure_transition(&order.status, &next_status)?;
+ order.status = next_status;
+ Ok(())
+ })
+ .await
+}
- let mut state = state.lock().await;
- if state.order_exists(order_id) {
- return Ok(());
- }
+async fn handle_order_revision_proposal(
+ event: &RadrootsNostrEvent,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+) -> Result<(), TradeListingDvmError> {
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_revision_proposal_from_event(&rr_event).map_err(map_order_parse_error)?;
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )?;
+ ensure_transition(&order.status, &TradeOrderStatus::Accepted)?;
+ Ok(())
+ })
+ .await
+}
- if payload.buyer_pubkey != event.pubkey.to_string()
- || payload.seller_pubkey != listing_addr.seller_pubkey
- {
- return Err(TradeListingDvmError::Unauthorized);
- }
+async fn handle_order_revision_decision(
+ event: &RadrootsNostrEvent,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+) -> Result<(), TradeListingDvmError> {
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_revision_decision_from_event(&rr_event).map_err(map_order_parse_error)?;
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )?;
+ match envelope.payload.decision {
+ RadrootsOrderRevisionOutcome::Accepted
+ | RadrootsOrderRevisionOutcome::Declined { .. } => {
+ ensure_transition(&order.status, &TradeOrderStatus::Accepted)?;
+ Ok(())
+ }
+ }
+ })
+ .await
+}
- let mut seen = std::collections::HashSet::new();
- seen.insert(event_id.clone());
+async fn handle_order_cancellation(
+ event: &RadrootsNostrEvent,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+) -> Result<(), TradeListingDvmError> {
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_cancellation_from_event(&rr_event).map_err(map_order_parse_error)?;
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )?;
+ ensure_transition(&order.status, &TradeOrderStatus::Cancelled)?;
+ order.status = TradeOrderStatus::Cancelled;
+ Ok(())
+ })
+ .await
+}
- state.insert_order(TradeOrderState {
- order_id: order_id.to_string(),
- listing_addr: payload.listing_addr.clone(),
- buyer_pubkey: payload.buyer_pubkey.clone(),
- seller_pubkey: payload.seller_pubkey.clone(),
- status: TradeOrderStatus::Requested,
- listing_snapshot_event_id: Some(listing_snapshot_event_id),
- root_event_id: Some(event_id.clone()),
- last_event_id: Some(event_id.clone()),
- seen_event_ids: seen,
- });
+async fn handle_order_fulfillment_update(
+ event: &RadrootsNostrEvent,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+) -> Result<(), TradeListingDvmError> {
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_fulfillment_update_from_event(&rr_event).map_err(map_order_parse_error)?;
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )?;
+ ensure_transition(&order.status, &TradeOrderStatus::Accepted)?;
+ if envelope.payload.status == RadrootsOrderFulfillmentState::SellerCancelled {
+ order.status = TradeOrderStatus::Cancelled;
+ }
+ Ok(())
+ })
+ .await
+}
- drop(state);
+async fn handle_order_receipt(
+ event: &RadrootsNostrEvent,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+) -> Result<(), TradeListingDvmError> {
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_receipt_from_event(&rr_event).map_err(map_order_parse_error)?;
+ let next_status = receipt_status(&envelope.payload);
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )?;
+ ensure_transition(&order.status, &next_status)?;
+ order.status = next_status;
+ Ok(())
+ })
+ .await
+}
- Ok(())
+async fn handle_order_payment_record(
+ event: &RadrootsNostrEvent,
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+) -> Result<(), TradeListingDvmError> {
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope = order_payment_record_from_event(&rr_event).map_err(map_order_parse_error)?;
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )
+ })
+ .await
}
-async fn handle_order_response(
+async fn handle_order_settlement_decision(
event: &RadrootsNostrEvent,
- payload: TradeOrderResponse,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
state: &Arc<tokio::sync::Mutex<TradeListingState>>,
) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
+ let rr_event = radroots_event_from_nostr(event);
+ let envelope =
+ order_settlement_decision_from_event(&rr_event).map_err(map_order_parse_error)?;
+ update_existing_order(event, &rr_event.tags, state, &envelope.order_id, |order| {
+ ensure_order_binding(
+ order,
+ &envelope.payload.listing_addr,
+ &envelope.payload.buyer_pubkey,
+ &envelope.payload.seller_pubkey,
+ )
+ })
+ .await
+}
+
+async fn update_existing_order<F>(
+ event: &RadrootsNostrEvent,
+ tags: &[Vec<String>],
+ state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+ order_id: &str,
+ update: F,
+) -> Result<(), TradeListingDvmError>
+where
+ F: FnOnce(&mut TradeOrderState) -> Result<(), TradeListingDvmError>,
+{
let event_id = event.id.to_string();
+ let mut state = state.lock().await;
if state.is_event_seen(order_id, &event_id) {
return Ok(());
}
let order = state
.get_order_mut(order_id)
.ok_or(TradeListingStateError::MissingOrder)?;
- if order.seller_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &order.buyer_pubkey)?;
- ensure_trade_chain(order, &message)?;
-
- let next_status = if payload.accepted {
- TradeOrderStatus::Accepted
- } else {
- TradeOrderStatus::Declined
- };
- ensure_transition(order.status.clone(), next_status.clone())?;
- order.status = next_status;
+ ensure_order_chain(order, tags)?;
+ update(order)?;
order.last_event_id = Some(event_id.clone());
order.seen_event_ids.insert(event_id);
-
- drop(state);
-
Ok(())
}
-#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
-async fn handle_order_revision(
- event: &RadrootsNostrEvent,
- _payload: TradeOrderRevision,
- listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let listing_snapshot_event_id = ensure_listing_snapshot(&message, client, state).await?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.seller_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &order.buyer_pubkey)?;
- ensure_trade_chain(order, &message)?;
- if listing_addr.seller_pubkey != order.seller_pubkey {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_transition(order.status.clone(), TradeOrderStatus::Revised)?;
- order.status = TradeOrderStatus::Revised;
- order.listing_snapshot_event_id = Some(listing_snapshot_event_id);
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
-}
-
-async fn handle_order_revision_response(
- event: &RadrootsNostrEvent,
- message_type: TradeListingMessageType,
- payload: TradeOrderRevisionResponse,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+fn ensure_order_binding(
+ order: &TradeOrderState,
+ listing_addr: &str,
+ buyer_pubkey: &str,
+ seller_pubkey: &str,
) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.buyer_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &order.seller_pubkey)?;
- ensure_trade_chain(order, &message)?;
- if message_type == TradeListingMessageType::OrderRevisionAccept && !payload.accepted {
- return Err(TradeListingDvmError::InvalidOrder);
- }
- if message_type == TradeListingMessageType::OrderRevisionDecline && payload.accepted {
- return Err(TradeListingDvmError::InvalidOrder);
- }
-
- let next_status = if matches!(message_type, TradeListingMessageType::OrderRevisionAccept) {
- TradeOrderStatus::Accepted
+ if order.listing_addr == listing_addr
+ && order.buyer_pubkey == buyer_pubkey
+ && order.seller_pubkey == seller_pubkey
+ {
+ Ok(())
} else {
- TradeOrderStatus::Declined
- };
- ensure_transition(order.status.clone(), next_status.clone())?;
- order.status = next_status;
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
-}
-
-async fn handle_question(
- event: &RadrootsNostrEvent,
- _payload: TradeQuestion,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.buyer_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
+ Err(TradeListingDvmError::InvalidOrder)
}
- ensure_order_counterparty(&message.counterparty_pubkey, &order.seller_pubkey)?;
- ensure_trade_chain(order, &message)?;
- ensure_transition(order.status.clone(), TradeOrderStatus::Questioned)?;
- order.status = TradeOrderStatus::Questioned;
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
}
-#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
-async fn handle_answer(
- event: &RadrootsNostrEvent,
- _payload: TradeAnswer,
- listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
+fn ensure_order_chain(
+ order: &TradeOrderState,
+ tags: &[Vec<String>],
) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.seller_pubkey != event.pubkey.to_string()
- || listing_addr.seller_pubkey != order.seller_pubkey
+ let root_event_id = parse_order_root_tag(tags)
+ .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?
+ .ok_or(TradeListingDvmError::MissingTag("e:root"))?;
+ let prev_event_id = parse_order_prev_tag(tags)
+ .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?
+ .ok_or(TradeListingDvmError::MissingTag("e:prev"))?;
+ if order.root_event_id.as_deref() == Some(root_event_id.as_str())
+ && order.last_event_id.as_deref() == Some(prev_event_id.as_str())
{
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &order.buyer_pubkey)?;
- ensure_trade_chain(order, &message)?;
- ensure_transition(order.status.clone(), TradeOrderStatus::Requested)?;
- order.status = TradeOrderStatus::Requested;
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
-}
-
-async fn handle_discount_request(
- event: &RadrootsNostrEvent,
- _payload: TradeDiscountRequest,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let listing_snapshot_event_id = ensure_listing_snapshot(&message, client, state).await?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.buyer_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
+ Ok(())
+ } else {
+ Err(TradeListingDvmError::InvalidOrder)
}
- ensure_order_counterparty(&message.counterparty_pubkey, &order.seller_pubkey)?;
- ensure_trade_chain(order, &message)?;
- order.listing_snapshot_event_id = Some(listing_snapshot_event_id);
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
}
-async fn handle_discount_offer(
- event: &RadrootsNostrEvent,
- _payload: TradeDiscountOffer,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let listing_snapshot_event_id = ensure_listing_snapshot(&message, client, state).await?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.seller_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
+fn receipt_status(payload: &RadrootsOrderReceipt) -> TradeOrderStatus {
+ if payload.received {
+ TradeOrderStatus::Completed
+ } else {
+ TradeOrderStatus::Disputed
}
- ensure_order_counterparty(&message.counterparty_pubkey, &order.buyer_pubkey)?;
- ensure_trade_chain(order, &message)?;
- ensure_transition(order.status.clone(), TradeOrderStatus::Revised)?;
- order.status = TradeOrderStatus::Revised;
- order.listing_snapshot_event_id = Some(listing_snapshot_event_id);
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
}
-async fn handle_discount_decision(
- event: &RadrootsNostrEvent,
- message_type: TradeListingMessageType,
- payload: TradeDiscountDecision,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
+fn ensure_transition(
+ from: &TradeOrderStatus,
+ to: &TradeOrderStatus,
+) -> Result<(), TradeListingStateError> {
+ if from == to {
return Ok(());
}
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.buyer_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &order.seller_pubkey)?;
- ensure_trade_chain(order, &message)?;
- let payload_is_accept = matches!(payload, TradeDiscountDecision::Accept { .. });
- let payload_is_decline = matches!(payload, TradeDiscountDecision::Decline { .. });
- if message_type == TradeListingMessageType::DiscountAccept && !payload_is_accept {
- return Err(TradeListingDvmError::InvalidOrder);
- }
- if message_type == TradeListingMessageType::DiscountDecline && !payload_is_decline {
- return Err(TradeListingDvmError::InvalidOrder);
- }
- let next_status = match message_type {
- TradeListingMessageType::DiscountAccept => TradeOrderStatus::Accepted,
- TradeListingMessageType::DiscountDecline => TradeOrderStatus::Requested,
- _ => order.status.clone(),
+ let allowed = match from {
+ TradeOrderStatus::Requested => matches!(
+ to,
+ TradeOrderStatus::Accepted | TradeOrderStatus::Declined | TradeOrderStatus::Cancelled
+ ),
+ TradeOrderStatus::Accepted => matches!(
+ to,
+ TradeOrderStatus::Cancelled | TradeOrderStatus::Completed | TradeOrderStatus::Disputed
+ ),
+ TradeOrderStatus::Declined
+ | TradeOrderStatus::Cancelled
+ | TradeOrderStatus::Completed
+ | TradeOrderStatus::Disputed
+ | TradeOrderStatus::Invalid => false,
};
- ensure_transition(order.status.clone(), next_status.clone())?;
- order.status = next_status;
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
-}
-
-async fn handle_cancel(
- event: &RadrootsNostrEvent,
- _payload: TradeListingCancel,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- let sender = event.pubkey.to_string();
- if sender != order.buyer_pubkey && sender != order.seller_pubkey {
- return Err(TradeListingDvmError::Unauthorized);
- }
- let expected_counterparty = if sender == order.buyer_pubkey {
- order.seller_pubkey.as_str()
+ if allowed {
+ Ok(())
} else {
- order.buyer_pubkey.as_str()
- };
- ensure_order_counterparty(&message.counterparty_pubkey, expected_counterparty)?;
- ensure_trade_chain(order, &message)?;
- ensure_transition(order.status.clone(), TradeOrderStatus::Cancelled)?;
- order.status = TradeOrderStatus::Cancelled;
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
-}
-
-async fn handle_fulfillment_update(
- event: &RadrootsNostrEvent,
- payload: TradeFulfillmentUpdate,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.seller_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &order.buyer_pubkey)?;
- ensure_trade_chain(order, &message)?;
- if let Some(next_status) = next_status_for_fulfillment_update(&order.status, &payload.status)? {
- order.status = next_status;
- }
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
-}
-
-async fn handle_receipt(
- event: &RadrootsNostrEvent,
- payload: TradeReceipt,
- _listing_addr: &TradeListingAddress,
- order_id: Option<&str>,
- _client: &RadrootsNostrClient,
- state: &Arc<tokio::sync::Mutex<TradeListingState>>,
-) -> Result<(), TradeListingDvmError> {
- let message = workflow_message_from_event(event)?;
- let order_id = order_id.ok_or(TradeListingDvmError::MissingTag("d"))?;
- let mut state = state.lock().await;
- let event_id = event.id.to_string();
- if state.is_event_seen(order_id, &event_id) {
- return Ok(());
- }
- let order = state
- .get_order_mut(order_id)
- .ok_or(TradeListingStateError::MissingOrder)?;
- if order.buyer_pubkey != event.pubkey.to_string() {
- return Err(TradeListingDvmError::Unauthorized);
- }
- ensure_order_counterparty(&message.counterparty_pubkey, &order.seller_pubkey)?;
- ensure_trade_chain(order, &message)?;
- if let Some(next_status) = next_status_for_receipt(&order.status, payload.acknowledged)? {
- order.status = next_status;
+ Err(TradeListingStateError::InvalidTransition {
+ from: from.clone(),
+ to: to.clone(),
+ })
}
- order.last_event_id = Some(event_id.clone());
- order.seen_event_ids.insert(event_id);
- drop(state);
-
- Ok(())
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
-async fn send_envelope(
- client: &RadrootsNostrClient,
- recipient_pubkey: String,
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: Option<&str>,
- payload: &TradeListingMessagePayload,
-) -> Result<(), TradeListingDvmError> {
- let envelope_event = trade_listing_envelope_event_build(
- recipient_pubkey,
- message_type,
- listing_addr,
- order_id.map(|value| value.to_string()),
- None,
- None,
- None,
- payload,
- )
- .map_err(|error| TradeListingDvmError::InvalidPayload(error.to_string()))?;
- let builder = radroots_nostr_build_event(
- envelope_event.kind as u32,
- envelope_event.content,
- envelope_event.tags,
- )?;
- send_event_io(client, builder).await?;
- Ok(())
}
#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
@@ -1281,7 +821,7 @@ async fn fetch_listing_by_addr(
client: &RadrootsNostrClient,
listing_addr: &str,
) -> Result<Option<RadrootsNostrEvent>, TradeListingDvmError> {
- let addr = TradeListingAddress::parse(listing_addr)
+ let addr = OrderListingAddress::parse(listing_addr)
.map_err(|_| TradeListingDvmError::InvalidListingAddr)?;
let author = radroots_nostr_parse_pubkey(&addr.seller_pubkey)
.map_err(|_| TradeListingDvmError::InvalidListingAddr)?;
@@ -1291,11 +831,10 @@ async fn fetch_listing_by_addr(
.author(author)
.identifier(addr.listing_id);
let events = fetch_events_io(client, filter, Duration::from_secs(10)).await?;
- let latest = events
+ Ok(events
.into_iter()
- .filter(|ev| ev.kind == RadrootsNostrKind::Custom(kind))
- .max_by_key(|ev| ev.created_at);
- Ok(latest)
+ .filter(|event| event.kind == RadrootsNostrKind::Custom(kind))
+ .max_by_key(|event| event.created_at))
}
#[cfg_attr(all(not(test), coverage_nightly), coverage(off))]
@@ -1305,11 +844,10 @@ async fn fetch_latest_event_by_kind(
kind: RadrootsNostrKind,
) -> Result<Option<RadrootsNostrEvent>, TradeListingDvmError> {
let events = fetch_events_io(client, filter, Duration::from_secs(10)).await?;
- let latest = events
+ Ok(events
.into_iter()
- .filter(|ev| ev.kind == kind)
- .max_by_key(|ev| ev.created_at);
- Ok(latest)
+ .filter(|event| event.kind == kind)
+ .max_by_key(|event| event.created_at))
}
async fn validate_farm_dependencies(
@@ -1320,7 +858,6 @@ async fn validate_farm_dependencies(
if let Some(result) = pop_farm_validation_hook() {
return result;
}
-
let mut errors = Vec::new();
let farm_pubkey = farm.pubkey.trim();
let farm_d_tag = farm.d_tag.trim();
@@ -1332,16 +869,11 @@ async fn validate_farm_dependencies(
return Ok(errors);
}
};
-
let profile_filter = RadrootsNostrFilter::new()
.kind(RadrootsNostrKind::Metadata)
- .author(author.clone());
+ .author(author);
let profile_event =
- match fetch_latest_event_by_kind(client, profile_filter, RadrootsNostrKind::Metadata).await
- {
- Ok(event) => event,
- Err(_) => None,
- };
+ fetch_latest_event_by_kind(client, profile_filter, RadrootsNostrKind::Metadata).await?;
let has_profile = profile_event
.map(|event| {
let rr_event = radroots_event_from_nostr(&event);
@@ -1351,4593 +883,318 @@ async fn validate_farm_dependencies(
if !has_profile {
errors.push(TradeListingValidationError::MissingFarmProfile);
}
-
- if !farm_d_tag.is_empty() {
- let record_filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(KIND_FARM as u16))
- .author(author)
- .identifier(farm_d_tag.to_string());
- let record_event = match fetch_latest_event_by_kind(
- client,
- record_filter,
- RadrootsNostrKind::Custom(KIND_FARM as u16),
- )
- .await
- {
- Ok(event) => event,
- Err(_) => None,
- };
- if record_event.is_none() {
- errors.push(TradeListingValidationError::MissingFarmRecord);
- }
- } else {
+ if farm_d_tag.is_empty() {
+ errors.push(TradeListingValidationError::MissingFarmRecord);
+ return Ok(errors);
+ }
+ let author = radroots_nostr_parse_pubkey(farm_pubkey)
+ .map_err(|_| TradeListingDvmError::InvalidPayload("invalid farm pubkey".to_string()))?;
+ let record_filter = RadrootsNostrFilter::new()
+ .kind(RadrootsNostrKind::Custom(KIND_FARM as u16))
+ .author(author)
+ .identifier(farm_d_tag.to_string());
+ let record_event = fetch_latest_event_by_kind(
+ client,
+ record_filter,
+ RadrootsNostrKind::Custom(KIND_FARM as u16),
+ )
+ .await?;
+ if record_event.is_none() {
errors.push(TradeListingValidationError::MissingFarmRecord);
}
-
Ok(errors)
}
-#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
-fn parse_payload<T: DeserializeOwned>(value: serde_json::Value) -> Result<T, TradeListingDvmError> {
- serde_json::from_value(value).map_err(|e| TradeListingDvmError::InvalidPayload(e.to_string()))
+fn required_tag_value(
+ tags: &[Vec<String>],
+ key: &'static str,
+) -> Result<String, TradeListingDvmError> {
+ tags.iter()
+ .find_map(|tag| {
+ if tag.first().map(String::as_str) == Some(key) {
+ tag.get(1).cloned()
+ } else {
+ None
+ }
+ })
+ .filter(|value| !value.trim().is_empty())
+ .ok_or(TradeListingDvmError::MissingTag(key))
}
-#[cfg(test)]
-fn tag_value(tags: &[Vec<String>], key: &str) -> Option<String> {
- tags.iter().find_map(|t| {
- if t.get(0).map(|k| k.as_str()) == Some(key) {
- t.get(1).cloned()
- } else {
- None
- }
+fn tag_has_value(tags: &[Vec<String>], key: &str, value: &str) -> bool {
+ tags.iter().any(|tag| {
+ tag.first().map(String::as_str) == Some(key)
+ && tag.get(1).map(String::as_str) == Some(value)
})
}
-fn tag_has_value(tags: &[Vec<String>], key: &str, value: &str) -> bool {
- tags.iter().any(|t| {
- t.get(0).map(|k| k.as_str()) == Some(key) && t.get(1).map(|v| v.as_str()) == Some(value)
- })
-}
-
-fn ensure_transition(
- from: TradeOrderStatus,
- to: TradeOrderStatus,
-) -> Result<(), TradeListingStateError> {
- if from == to {
- return Ok(());
- }
- let allowed = match from {
- TradeOrderStatus::Draft => matches!(to, TradeOrderStatus::Requested),
- TradeOrderStatus::Validated => matches!(to, TradeOrderStatus::Requested),
- TradeOrderStatus::Requested => matches!(
- to,
- TradeOrderStatus::Accepted
- | TradeOrderStatus::Declined
- | TradeOrderStatus::Questioned
- | TradeOrderStatus::Revised
- | TradeOrderStatus::Cancelled
- | TradeOrderStatus::Requested
- ),
- TradeOrderStatus::Questioned => matches!(
- to,
- TradeOrderStatus::Requested | TradeOrderStatus::Revised | TradeOrderStatus::Cancelled
- ),
- TradeOrderStatus::Revised => matches!(
- to,
- TradeOrderStatus::Accepted
- | TradeOrderStatus::Declined
- | TradeOrderStatus::Cancelled
- | TradeOrderStatus::Requested
- ),
- TradeOrderStatus::Accepted => {
- matches!(
- to,
- TradeOrderStatus::Fulfilled | TradeOrderStatus::Cancelled
- )
- }
- TradeOrderStatus::Declined => false,
- TradeOrderStatus::Cancelled => false,
- TradeOrderStatus::Fulfilled => {
- matches!(
- to,
- TradeOrderStatus::Completed
- | TradeOrderStatus::Fulfilled
- | TradeOrderStatus::Cancelled
- )
- }
- TradeOrderStatus::Completed => false,
- };
- if allowed {
- Ok(())
- } else {
- Err(TradeListingStateError::InvalidTransition { from, to })
- }
-}
-
-fn next_status_for_fulfillment_update(
- current: &TradeOrderStatus,
- fulfillment_status: &TradeFulfillmentStatus,
-) -> Result<Option<TradeOrderStatus>, TradeListingStateError> {
- match fulfillment_status {
- TradeFulfillmentStatus::Preparing
- | TradeFulfillmentStatus::Shipped
- | TradeFulfillmentStatus::ReadyForPickup => {
- if matches!(current, TradeOrderStatus::Accepted) {
- Ok(None)
- } else {
- Err(TradeListingStateError::InvalidTransition {
- from: current.clone(),
- to: TradeOrderStatus::Accepted,
- })
- }
- }
- TradeFulfillmentStatus::Delivered => {
- ensure_transition(current.clone(), TradeOrderStatus::Fulfilled)?;
- Ok(Some(TradeOrderStatus::Fulfilled))
- }
- TradeFulfillmentStatus::Cancelled => {
- ensure_transition(current.clone(), TradeOrderStatus::Cancelled)?;
- Ok(Some(TradeOrderStatus::Cancelled))
- }
- }
-}
-
-fn next_status_for_receipt(
- current: &TradeOrderStatus,
- acknowledged: bool,
-) -> Result<Option<TradeOrderStatus>, TradeListingStateError> {
- if acknowledged {
- ensure_transition(current.clone(), TradeOrderStatus::Completed)?;
- Ok(Some(TradeOrderStatus::Completed))
- } else if matches!(current, TradeOrderStatus::Fulfilled) {
- Ok(None)
- } else {
- Err(TradeListingStateError::InvalidTransition {
- from: current.clone(),
- to: TradeOrderStatus::Fulfilled,
- })
- }
-}
-
-pub async fn handle_error(
- error: TradeListingDvmError,
- event: &RadrootsNostrEvent,
- client: &RadrootsNostrClient,
-) -> Result<(), TradeListingDvmError> {
- let builder =
- radroots_nostr_build_event_job_feedback(event, "error", Some(error.to_string()), None)?;
- send_event_io(client, builder).await
+pub async fn handle_error(
+ error: TradeListingDvmError,
+ event: &RadrootsNostrEvent,
+ client: &RadrootsNostrClient,
+) -> Result<(), TradeListingDvmError> {
+ let builder =
+ radroots_nostr_build_event_job_feedback(event, "error", Some(error.to_string()), None)?;
+ send_event_io(client, builder).await
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::{
- DvmTestHooks, TradeListingDvmError, dvm_test_hooks, ensure_transition,
- fetch_event_by_id_io, fetch_events_io, fetch_latest_event_by_kind, fetch_listing_by_addr,
- handle_answer, handle_cancel, handle_discount_decision, handle_discount_offer,
- handle_discount_request, handle_error, handle_event, handle_fulfillment_update,
- handle_listing_validate_request, handle_order_request, handle_order_response,
- handle_order_revision, handle_order_revision_response, handle_question, handle_receipt,
- next_status_for_fulfillment_update, next_status_for_receipt, parse_payload, send_envelope,
- send_event_io, tag_has_value, tag_value, validate_farm_dependencies,
- validate_listing_event_io,
- };
- use crate::features::trade_listing::state::{
- TradeListingState, TradeListingStateError, TradeOrderState,
+ DvmTestHooks, TradeListingDvmError, dvm_test_hooks, ensure_transition, handle_error,
+ handle_event, tag_has_value,
};
+ use crate::features::trade_listing::state::{TradeListingState, TradeOrderStatus};
use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCoreMoney,
- RadrootsCoreUnit,
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
};
use radroots_events::RadrootsNostrEventPtr;
use radroots_events::farm::RadrootsFarmRef;
use radroots_events::kinds::{
- KIND_TRADE_LISTING_ANSWER_RES, KIND_TRADE_LISTING_CANCEL_REQ,
- KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ, KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ,
- KIND_TRADE_LISTING_DISCOUNT_OFFER_RES, KIND_TRADE_LISTING_DISCOUNT_REQ,
- KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ, KIND_TRADE_LISTING_ORDER_REQ,
- KIND_TRADE_LISTING_ORDER_RES, KIND_TRADE_LISTING_ORDER_REVISION_REQ,
- KIND_TRADE_LISTING_ORDER_REVISION_RES, KIND_TRADE_LISTING_QUESTION_REQ,
- KIND_TRADE_LISTING_RECEIPT_REQ, KIND_TRADE_LISTING_VALIDATE_REQ,
- KIND_TRADE_LISTING_VALIDATE_RES,
- };
- use radroots_events::trade::RadrootsTradeListingValidationError as TradeListingValidationError;
- use radroots_events::trade::{
- RadrootsTradeAnswer as TradeAnswer, RadrootsTradeDiscountDecision as TradeDiscountDecision,
- RadrootsTradeDiscountOffer as TradeDiscountOffer,
- RadrootsTradeDiscountRequest as TradeDiscountRequest,
- RadrootsTradeEnvelope as TradeListingEnvelope,
- RadrootsTradeFulfillmentStatus as TradeFulfillmentStatus,
- RadrootsTradeFulfillmentUpdate as TradeFulfillmentUpdate,
- RadrootsTradeListingCancel as TradeListingCancel,
- RadrootsTradeListingValidateRequest as TradeListingValidateRequest,
- RadrootsTradeListingValidateResult as TradeListingValidateResult,
- RadrootsTradeMessagePayload as TradeListingMessagePayload,
- RadrootsTradeMessageType as TradeListingMessageType,
- RadrootsTradeOrderEconomicItem as TradeOrderEconomicItem,
- RadrootsTradeOrderEconomics as TradeOrderEconomics,
- RadrootsTradeOrderItem as TradeOrderItem,
- RadrootsTradeOrderRequested as TradeOrderRequested,
- RadrootsTradeOrderResponse as TradeOrderResponse,
- RadrootsTradeOrderRevision as TradeOrderRevision,
- RadrootsTradeOrderRevisionResponse as TradeOrderRevisionResponse,
- RadrootsTradeOrderStatus as TradeOrderStatus,
- RadrootsTradePricingBasis as TradePricingBasis, RadrootsTradeQuestion as TradeQuestion,
- RadrootsTradeReceipt as TradeReceipt,
+ KIND_LISTING, KIND_ORDER_REQUEST, KIND_TRADE_LISTING_VALIDATION_REQUEST,
};
- use radroots_events_codec::trade::{
- RadrootsTradeListingAddress as TradeListingAddress, active_trade_order_request_event_build,
+ use radroots_events::order::{
+ RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics,
+ RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
};
- use radroots_nostr::error::RadrootsNostrError;
+ use radroots_events::trade_validation::RadrootsTradeValidationListingRequest;
+ use radroots_events_codec::order::order_request_event_build;
use radroots_nostr::prelude::{
- RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter,
- RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrTag, RadrootsNostrTagKind,
- RadrootsNostrTimestamp,
+ RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys,
+ RadrootsNostrKind, radroots_nostr_build_event,
};
- use serde_json::json;
- use std::collections::HashSet;
- use std::sync::atomic::{AtomicU64, Ordering};
- use std::sync::{Arc, Mutex, MutexGuard};
- use tokio::sync::Mutex as AsyncMutex;
+ use std::sync::Arc;
+ use tokio::sync::{Mutex, MutexGuard};
- static TEST_LOCK: Mutex<()> = Mutex::new(());
- static TEST_EVENT_TIMESTAMP: AtomicU64 = AtomicU64::new(1_700_000_000);
- const TEST_LISTING_EVENT_ID: &str = "listing-event";
+ static TEST_LOCK: Mutex<()> = Mutex::const_new(());
- fn test_guard() -> MutexGuard<'static, ()> {
- let guard = TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
- TEST_EVENT_TIMESTAMP.store(1_700_000_000, Ordering::Relaxed);
- *dvm_test_hooks()
- .lock()
- .unwrap_or_else(|err| err.into_inner()) = DvmTestHooks::default();
+ async fn test_guard() -> MutexGuard<'static, ()> {
+ let guard = TEST_LOCK.lock().await;
+ *dvm_test_hooks().lock().expect("hooks") = DvmTestHooks::default();
guard
}
- fn custom_trade_kind(kind: u32) -> RadrootsNostrKind {
- RadrootsNostrKind::Custom(
- kind.try_into()
- .expect("trade listing kinds fit in nostr custom range"),
- )
- }
-
- fn next_test_timestamp() -> RadrootsNostrTimestamp {
- RadrootsNostrTimestamp::from(TEST_EVENT_TIMESTAMP.fetch_add(1, Ordering::Relaxed))
- }
-
- fn push_send_ok() {
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .send_event_results
- .push_back(Ok(()));
- }
-
- fn push_fetch_events_ok(events: Vec<RadrootsNostrEvent>) {
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_events_results
- .push_back(Ok(events));
- }
-
- fn push_fetch_event_by_id_error_not_found() {
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Err(TradeListingDvmError::Nostr(
- RadrootsNostrError::EventNotFound("missing".to_string()),
- )));
- }
-
- fn push_validate_listing_ok(listing_addr: impl Into<String>, farm: RadrootsFarmRef) {
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .validate_listing_results
- .push_back(Ok((listing_addr.into(), farm)));
- }
-
- fn push_farm_validation_result(
- result: Result<Vec<TradeListingValidationError>, TradeListingDvmError>,
- ) {
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .farm_validation_results
- .push_back(result);
- }
-
- fn make_keys() -> (RadrootsNostrKeys, RadrootsNostrKeys, RadrootsNostrKeys) {
- (
- RadrootsNostrKeys::generate(),
- RadrootsNostrKeys::generate(),
- RadrootsNostrKeys::generate(),
- )
- }
-
- fn listing_addr_for_seller(seller: &RadrootsNostrKeys) -> String {
- format!(
- "30402:{}:AAAAAAAAAAAAAAAAAAAAAA",
- seller.public_key().to_hex()
- )
+ fn listing_id() -> &'static str {
+ "AAAAAAAAAAAAAAAAAAAAAg"
}
- fn make_client(keys: &RadrootsNostrKeys) -> RadrootsNostrClient {
- RadrootsNostrClient::new(keys.clone())
+ fn listing_addr(seller: &RadrootsNostrKeys) -> String {
+ format!("{}:{}:{}", KIND_LISTING, seller.public_key(), listing_id())
}
- fn make_order(
- order_id: &str,
- listing_addr: &str,
- buyer: &str,
- seller: &str,
- _status: TradeOrderStatus,
- ) -> TradeOrderRequested {
- TradeOrderRequested {
- order_id: order_id.to_string(),
- listing_addr: listing_addr.to_string(),
- buyer_pubkey: buyer.to_string(),
- seller_pubkey: seller.to_string(),
- items: vec![TradeOrderItem {
- bin_id: "bin-1".to_string(),
- bin_count: 1,
- }],
- economics: sample_order_economics(order_id, "bin-1", 1),
+ fn listing_event_ptr() -> RadrootsNostrEventPtr {
+ RadrootsNostrEventPtr {
+ id: "listing-event-1".to_string(),
+ relays: None,
}
}
- fn sample_order_economics(order_id: &str, bin_id: &str, bin_count: u32) -> TradeOrderEconomics {
- let currency = RadrootsCoreCurrency::USD;
- let quantity_amount = RadrootsCoreDecimal::from(1_u32);
- let unit_price_amount = RadrootsCoreDecimal::from(6_u32);
- let line_subtotal = RadrootsCoreMoney::new(
- unit_price_amount * quantity_amount * RadrootsCoreDecimal::from(bin_count),
- currency,
- );
- TradeOrderEconomics {
+ fn order_economics(order_id: &str) -> RadrootsOrderEconomics {
+ RadrootsOrderEconomics {
quote_id: format!("{order_id}-quote"),
quote_version: 1,
- pricing_basis: TradePricingBasis::ListingEvent,
- currency,
- items: vec![TradeOrderEconomicItem {
- bin_id: bin_id.to_string(),
- bin_count,
- quantity_amount,
+ pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
+ currency: RadrootsCoreCurrency::USD,
+ items: vec![RadrootsOrderEconomicItem {
+ bin_id: "bin-1".to_string(),
+ bin_count: 2,
+ quantity_amount: RadrootsCoreDecimal::from(1u32),
quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount,
- unit_price_currency: currency,
- line_subtotal: line_subtotal.clone(),
+ unit_price_amount: RadrootsCoreDecimal::from(5u32),
+ unit_price_currency: RadrootsCoreCurrency::USD,
+ line_subtotal: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(10u32),
+ RadrootsCoreCurrency::USD,
+ ),
}],
- discounts: Vec::new(),
- adjustments: Vec::new(),
- subtotal: line_subtotal.clone(),
- discount_total: RadrootsCoreMoney::zero(currency),
- adjustment_total: RadrootsCoreMoney::zero(currency),
- total: line_subtotal,
+ discounts: Vec::<RadrootsOrderEconomicLine>::new(),
+ adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
+ subtotal: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(10u32),
+ RadrootsCoreCurrency::USD,
+ ),
+ discount_total: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(0u32),
+ RadrootsCoreCurrency::USD,
+ ),
+ adjustment_total: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(0u32),
+ RadrootsCoreCurrency::USD,
+ ),
+ total: RadrootsCoreMoney::new(
+ RadrootsCoreDecimal::from(10u32),
+ RadrootsCoreCurrency::USD,
+ ),
}
}
- fn make_order_state(
+ fn order_request(
order_id: &str,
- listing_addr: &str,
- buyer: &str,
- seller: &str,
- status: TradeOrderStatus,
- ) -> TradeOrderState {
- TradeOrderState {
+ buyer: &RadrootsNostrKeys,
+ seller: &RadrootsNostrKeys,
+ ) -> RadrootsOrderRequest {
+ RadrootsOrderRequest {
order_id: order_id.to_string(),
- listing_addr: listing_addr.to_string(),
- buyer_pubkey: buyer.to_string(),
- seller_pubkey: seller.to_string(),
- status,
- listing_snapshot_event_id: Some("listing-event".to_string()),
- root_event_id: Some(format!("{order_id}:root")),
- last_event_id: Some(format!("{order_id}:root")),
- seen_event_ids: HashSet::new(),
- }
- }
-
- async fn state_with_order(
- listing_addr: &str,
- order_id: &str,
- buyer: &str,
- seller: &str,
- status: TradeOrderStatus,
- ) -> Arc<AsyncMutex<TradeListingState>> {
- let state = Arc::new(AsyncMutex::new(TradeListingState::default()));
- let mut locked = state.lock().await;
- locked.mark_listing_validated(listing_addr, "validated-listing-event");
- locked.upsert_listing_event(listing_addr, TEST_LISTING_EVENT_ID, 30402);
- locked.insert_order(make_order_state(
- order_id,
- listing_addr,
- buyer,
- seller,
- status,
- ));
- drop(locked);
- state
- }
-
- async fn set_order_status(
- state: &Arc<AsyncMutex<TradeListingState>>,
- order_id: &str,
- status: TradeOrderStatus,
- ) {
- let mut locked = state.lock().await;
- let order = locked.get_order_mut(order_id).expect("order");
- order.status = status;
- order.seen_event_ids.clear();
- }
-
- async fn mark_event_seen(
- state: &Arc<AsyncMutex<TradeListingState>>,
- order_id: &str,
- event_id: String,
- ) {
- let mut locked = state.lock().await;
- let order = locked.get_order_mut(order_id).expect("order");
- order.seen_event_ids.insert(event_id);
- }
-
- fn make_custom_tags(
- recipient: &str,
- listing_addr: &str,
- order_id: Option<&str>,
- ) -> Vec<RadrootsNostrTag> {
- make_workflow_tags(recipient, listing_addr, order_id, None, None, None)
- }
-
- fn make_workflow_tags(
- recipient: &str,
- listing_addr: &str,
- order_id: Option<&str>,
- listing_event_id: Option<&str>,
- root_event_id: Option<&str>,
- prev_event_id: Option<&str>,
- ) -> Vec<RadrootsNostrTag> {
- let mut tags = vec![
- RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("p"),
- vec![recipient.to_string()],
- ),
- RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("a"),
- vec![listing_addr.to_string()],
- ),
- ];
- if let Some(order_id) = order_id {
- tags.push(RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("d"),
- vec![order_id.to_string()],
- ));
- }
- if let Some(listing_event_id) = listing_event_id {
- tags.push(RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("listing_event"),
- vec![listing_event_id.to_string()],
- ));
- }
- if let Some(root_event_id) = root_event_id {
- tags.push(RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("e_root"),
- vec![root_event_id.to_string()],
- ));
- }
- if let Some(prev_event_id) = prev_event_id {
- tags.push(RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("e_prev"),
- vec![prev_event_id.to_string()],
- ));
+ listing_addr: listing_addr(seller),
+ buyer_pubkey: buyer.public_key().to_string(),
+ seller_pubkey: seller.public_key().to_string(),
+ items: vec![RadrootsOrderItem {
+ bin_id: "bin-1".to_string(),
+ bin_count: 2,
+ }],
+ economics: order_economics(order_id),
}
- tags
}
- fn make_event(
- sender: &RadrootsNostrKeys,
- kind: RadrootsNostrKind,
- content: String,
- tags: Vec<RadrootsNostrTag>,
+ fn signed_order_request_event(
+ buyer: &RadrootsNostrKeys,
+ seller: &RadrootsNostrKeys,
) -> RadrootsNostrEvent {
- RadrootsNostrEventBuilder::new(kind, content)
- .custom_created_at(next_test_timestamp())
- .tags(tags)
- .sign_with_keys(sender)
+ let payload = order_request("order-1", buyer, seller);
+ let wire = order_request_event_build(&listing_event_ptr(), &payload).expect("wire");
+ radroots_nostr_build_event(wire.kind, wire.content, wire.tags)
+ .expect("builder")
+ .sign_with_keys(buyer)
.expect("event")
}
- fn make_envelope_content(
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: Option<&str>,
- payload: serde_json::Value,
- ) -> String {
- serde_json::to_string(&TradeListingEnvelope::new(
- message_type,
- listing_addr.to_string(),
- order_id.map(|v| v.to_string()),
- payload,
- ))
- .expect("envelope")
- }
-
- fn make_canonical_envelope_content(
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: Option<&str>,
- payload: TradeListingMessagePayload,
- ) -> String {
- serde_json::to_string(&TradeListingEnvelope::new(
- message_type,
- listing_addr.to_string(),
- order_id.map(|value| value.to_string()),
- payload,
- ))
- .expect("canonical envelope")
- }
-
- fn payload_enum_for_message(
- message_type: TradeListingMessageType,
- order_id: &str,
- listing_addr: &str,
- buyer_pub: &str,
- seller_pub: &str,
- ) -> TradeListingMessagePayload {
- match message_type {
- TradeListingMessageType::ListingValidateRequest => {
- TradeListingMessagePayload::ListingValidateRequest(TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: TEST_LISTING_EVENT_ID.to_string(),
- relays: None,
- }),
- })
- }
- TradeListingMessageType::ListingValidateResult => {
- TradeListingMessagePayload::ListingValidateResult(TradeListingValidateResult {
- valid: true,
- errors: Vec::new(),
- })
- }
- TradeListingMessageType::OrderRequest => {
- TradeListingMessagePayload::TradeOrderRequested(make_order(
- order_id,
- listing_addr,
- buyer_pub,
- seller_pub,
- TradeOrderStatus::Requested,
- ))
- }
- TradeListingMessageType::OrderResponse => {
- TradeListingMessagePayload::OrderResponse(TradeOrderResponse {
- accepted: true,
- reason: None,
- })
- }
- TradeListingMessageType::OrderRevision => {
- TradeListingMessagePayload::OrderRevision(TradeOrderRevision {
- revision_id: "r-matrix".to_string(),
- changes: Vec::new(),
- })
- }
- TradeListingMessageType::OrderRevisionAccept => {
- TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse {
- accepted: true,
- reason: None,
- })
- }
- TradeListingMessageType::OrderRevisionDecline => {
- TradeListingMessagePayload::OrderRevisionDecline(TradeOrderRevisionResponse {
- accepted: false,
- reason: None,
- })
- }
- TradeListingMessageType::Question => {
- TradeListingMessagePayload::Question(TradeQuestion {
- question_id: "q-matrix".to_string(),
- })
- }
- TradeListingMessageType::Answer => TradeListingMessagePayload::Answer(TradeAnswer {
- question_id: "q-matrix".to_string(),
- }),
- TradeListingMessageType::DiscountRequest => {
- TradeListingMessagePayload::DiscountRequest(TradeDiscountRequest {
- discount_id: "d-matrix".to_string(),
- value: sample_discount_value(),
- })
- }
- TradeListingMessageType::DiscountOffer => {
- TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer {
- discount_id: "d-matrix".to_string(),
- value: sample_discount_value(),
- })
- }
- TradeListingMessageType::DiscountAccept => {
- TradeListingMessagePayload::DiscountAccept(TradeDiscountDecision::Accept {
- value: sample_discount_value(),
- })
- }
- TradeListingMessageType::DiscountDecline => {
- TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline {
- reason: None,
- })
- }
- TradeListingMessageType::Cancel => {
- TradeListingMessagePayload::Cancel(TradeListingCancel { reason: None })
- }
- TradeListingMessageType::FulfillmentUpdate => {
- TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Shipped,
- })
- }
- TradeListingMessageType::Receipt => TradeListingMessagePayload::Receipt(TradeReceipt {
- acknowledged: true,
- at: 1,
- }),
- }
- }
-
- fn recipient_for_message<'a>(
- message_type: TradeListingMessageType,
- buyer_pub: &'a str,
- seller_pub: &'a str,
- ) -> &'a str {
- match message_type {
- TradeListingMessageType::OrderResponse
- | TradeListingMessageType::OrderRevision
- | TradeListingMessageType::Answer
- | TradeListingMessageType::DiscountOffer
- | TradeListingMessageType::FulfillmentUpdate => buyer_pub,
- _ => seller_pub,
- }
- }
-
- async fn workflow_state_refs(
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: &str,
- state: &Arc<AsyncMutex<TradeListingState>>,
- ) -> (Option<String>, Option<String>, Option<String>) {
- let mut locked = state.lock().await;
- let listing_event_id = if message_type.requires_listing_snapshot() {
- Some(
- locked
- .listing_event_id(listing_addr)
- .unwrap_or(TEST_LISTING_EVENT_ID)
- .to_string(),
- )
- } else {
- None
- };
- let (root_event_id, prev_event_id) = if message_type.requires_trade_chain() {
- if let Some(order) = locked.get_order_mut(order_id) {
- (order.root_event_id.clone(), order.last_event_id.clone())
- } else {
- (
- Some(format!("{order_id}:root")),
- Some(format!("{order_id}:prev")),
- )
- }
- } else {
- (None, None)
- };
- (listing_event_id, root_event_id, prev_event_id)
+ fn listing_event(seller: &RadrootsNostrKeys) -> RadrootsNostrEvent {
+ RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(KIND_LISTING as u16), "{}")
+ .tags(vec![radroots_nostr::prelude::RadrootsNostrTag::identifier(
+ listing_id(),
+ )])
+ .sign_with_keys(seller)
+ .expect("listing event")
}
- async fn make_public_trade_event(
- sender: &RadrootsNostrKeys,
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: &str,
- buyer_pub: &str,
- seller_pub: &str,
- state: Option<&Arc<AsyncMutex<TradeListingState>>>,
- ) -> RadrootsNostrEvent {
- let (listing_event_id, root_event_id, prev_event_id) = if let Some(state) = state {
- workflow_state_refs(message_type, listing_addr, order_id, state).await
- } else {
- let listing_event_id = message_type
- .requires_listing_snapshot()
- .then(|| TEST_LISTING_EVENT_ID.to_string());
- (listing_event_id, None, None)
- };
-
- let payload =
- payload_enum_for_message(message_type, order_id, listing_addr, buyer_pub, seller_pub);
- make_public_trade_event_with_payload(
- sender,
- message_type,
- listing_addr,
- order_id,
- buyer_pub,
- seller_pub,
- payload,
- listing_event_id,
- root_event_id,
- prev_event_id,
+ #[tokio::test]
+ async fn order_request_inserts_canonical_order_state() {
+ let _guard = test_guard().await;
+ let worker = RadrootsNostrKeys::generate();
+ let buyer = RadrootsNostrKeys::generate();
+ let seller = RadrootsNostrKeys::generate();
+ let client = RadrootsNostrClient::new(worker.clone());
+ let state = Arc::new(Mutex::new(TradeListingState::default()));
+ state.lock().await.upsert_listing_event(
+ &listing_addr(&seller),
+ "listing-event-1",
+ KIND_LISTING,
+ );
+
+ handle_event(
+ signed_order_request_event(&buyer, &seller),
+ Vec::new(),
+ worker,
+ client,
+ state.clone(),
)
- }
+ .await
+ .expect("order request");
- fn make_public_trade_event_with_payload(
- sender: &RadrootsNostrKeys,
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: &str,
- buyer_pub: &str,
- seller_pub: &str,
- payload: TradeListingMessagePayload,
- listing_event_id: Option<String>,
- root_event_id: Option<String>,
- prev_event_id: Option<String>,
- ) -> RadrootsNostrEvent {
- let recipient = recipient_for_message(message_type, buyer_pub, seller_pub);
- make_trade_event_with_payload_and_recipient(
- sender,
- recipient,
- message_type,
- listing_addr,
- order_id,
- payload,
- listing_event_id,
- root_event_id,
- prev_event_id,
- )
+ let mut state = state.lock().await;
+ let order = state.get_order_mut("order-1").expect("order");
+ assert_eq!(order.status, TradeOrderStatus::Requested);
+ assert_eq!(order.buyer_pubkey, buyer.public_key().to_string());
+ assert_eq!(order.seller_pubkey, seller.public_key().to_string());
}
- fn make_trade_event_with_payload_and_recipient(
- sender: &RadrootsNostrKeys,
- recipient: &str,
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: &str,
- payload: TradeListingMessagePayload,
- listing_event_id: Option<String>,
- root_event_id: Option<String>,
- prev_event_id: Option<String>,
- ) -> RadrootsNostrEvent {
- let listing_event = listing_event_id.map(|id| RadrootsNostrEventPtr { id, relays: None });
- if message_type == TradeListingMessageType::OrderRequest {
- let TradeListingMessagePayload::TradeOrderRequested(payload) = payload else {
- panic!("order request helper requires active order-request payload");
- };
- let listing_event = listing_event.unwrap_or_else(|| RadrootsNostrEventPtr {
- id: TEST_LISTING_EVENT_ID.to_string(),
- relays: None,
- });
- let envelope_event = active_trade_order_request_event_build(&listing_event, &payload)
- .expect("build active order request event");
- let builder = radroots_nostr::prelude::radroots_nostr_build_event(
- envelope_event.kind,
- envelope_event.content,
- envelope_event.tags,
- )
- .expect("event builder");
- return builder
- .custom_created_at(next_test_timestamp())
- .sign_with_keys(sender)
- .expect("event");
+ #[tokio::test]
+ async fn listing_validation_request_sends_result_and_marks_listing_validated() {
+ let _guard = test_guard().await;
+ let worker = RadrootsNostrKeys::generate();
+ let seller = RadrootsNostrKeys::generate();
+ let requester = RadrootsNostrKeys::generate();
+ let client = RadrootsNostrClient::new(worker.clone());
+ let state = Arc::new(Mutex::new(TradeListingState::default()));
+ let listing_addr = listing_addr(&seller);
+ {
+ let mut hooks = dvm_test_hooks().lock().expect("hooks");
+ hooks
+ .fetch_event_by_id_results
+ .push_back(Ok(listing_event(&seller)));
+ hooks.validate_listing_results.push_back(Ok((
+ listing_addr.clone(),
+ RadrootsFarmRef {
+ pubkey: seller.public_key().to_string(),
+ d_tag: "farm-1".to_string(),
+ },
+ )));
+ hooks.farm_validation_results.push_back(Ok(Vec::new()));
+ hooks.send_event_results.push_back(Ok(()));
}
- let envelope_event = super::trade_listing_envelope_event_build(
- recipient.to_string(),
- message_type,
- listing_addr.to_string(),
- Some(order_id.to_string()),
- listing_event.as_ref(),
- root_event_id.as_deref(),
- prev_event_id.as_deref(),
- &payload,
- )
- .expect("build trade event");
- let builder = radroots_nostr::prelude::radroots_nostr_build_event(
- envelope_event.kind,
- envelope_event.content,
- envelope_event.tags,
- )
- .expect("event builder");
- builder
- .custom_created_at(next_test_timestamp())
- .sign_with_keys(sender)
- .expect("event")
- }
-
- async fn make_handle_event_trade_event(
- sender: &RadrootsNostrKeys,
- message_type: TradeListingMessageType,
- listing_addr: &str,
- order_id: &str,
- buyer_pub: &str,
- seller_pub: &str,
- state: Option<&Arc<AsyncMutex<TradeListingState>>>,
- ) -> (RadrootsNostrEvent, Vec<RadrootsNostrTag>) {
- let (listing_event_id, root_event_id, prev_event_id) = if let Some(state) = state {
- workflow_state_refs(message_type, listing_addr, order_id, state).await
- } else {
- let listing_event_id = message_type
- .requires_listing_snapshot()
- .then(|| TEST_LISTING_EVENT_ID.to_string());
- (listing_event_id, None, None)
+ let payload = RadrootsTradeValidationListingRequest {
+ listing_event: Some(listing_event_ptr()),
};
- let event = make_public_trade_event_with_payload(
- sender,
- message_type,
- listing_addr,
- order_id,
- buyer_pub,
- seller_pub,
- payload_enum_for_message(message_type, order_id, listing_addr, buyer_pub, seller_pub),
- listing_event_id,
- root_event_id,
- prev_event_id,
- );
- let tags = event.tags.iter().cloned().collect();
- (event, tags)
- }
+ let event = radroots_nostr_build_event(
+ KIND_TRADE_LISTING_VALIDATION_REQUEST,
+ serde_json::to_string(&payload).expect("payload"),
+ vec![
+ vec!["p".to_string(), worker.public_key().to_string()],
+ vec!["a".to_string(), listing_addr.clone()],
+ ],
+ )
+ .expect("builder")
+ .sign_with_keys(&requester)
+ .expect("event");
- fn sample_discount_value() -> RadrootsCoreDiscountValue {
- RadrootsCoreDiscountValue::MoneyPerBin(RadrootsCoreMoney::from_minor_units_u32(
- 100,
- RadrootsCoreCurrency::USD,
- ))
- }
+ handle_event(event, Vec::new(), worker, client, state.clone())
+ .await
+ .expect("validation request");
- fn sender_for_message<'a>(
- message_type: TradeListingMessageType,
- seller_keys: &'a RadrootsNostrKeys,
- buyer_keys: &'a RadrootsNostrKeys,
- ) -> &'a RadrootsNostrKeys {
- match message_type {
- TradeListingMessageType::OrderResponse
- | TradeListingMessageType::OrderRevision
- | TradeListingMessageType::Answer
- | TradeListingMessageType::DiscountOffer
- | TradeListingMessageType::FulfillmentUpdate => seller_keys,
- _ => buyer_keys,
- }
+ assert!(state.lock().await.is_listing_validated(&listing_addr));
}
- #[test]
- fn transition_matrix_and_tag_helpers_are_covered() {
- let _guard = test_guard();
-
- assert!(ensure_transition(TradeOrderStatus::Requested, TradeOrderStatus::Revised).is_ok());
- assert!(ensure_transition(TradeOrderStatus::Declined, TradeOrderStatus::Accepted).is_err());
- assert!(
- ensure_transition(TradeOrderStatus::Fulfilled, TradeOrderStatus::Completed).is_ok()
- );
- assert!(
- ensure_transition(TradeOrderStatus::Completed, TradeOrderStatus::Requested).is_err()
- );
- assert!(ensure_transition(TradeOrderStatus::Draft, TradeOrderStatus::Draft).is_ok());
- assert_eq!(
- next_status_for_fulfillment_update(
- &TradeOrderStatus::Accepted,
- &TradeFulfillmentStatus::Shipped
- )
- .expect("shipped keeps accepted"),
- None
- );
- assert_eq!(
- next_status_for_fulfillment_update(
- &TradeOrderStatus::Accepted,
- &TradeFulfillmentStatus::Delivered
- )
- .expect("delivered fulfills"),
- Some(TradeOrderStatus::Fulfilled)
- );
- assert_eq!(
- next_status_for_fulfillment_update(
- &TradeOrderStatus::Accepted,
- &TradeFulfillmentStatus::Cancelled
- )
- .expect("cancelled cancels"),
- Some(TradeOrderStatus::Cancelled)
- );
- assert!(
- next_status_for_fulfillment_update(
- &TradeOrderStatus::Requested,
- &TradeFulfillmentStatus::Shipped
- )
- .is_err()
- );
- assert_eq!(
- next_status_for_receipt(&TradeOrderStatus::Fulfilled, false)
- .expect("unacknowledged receipt keeps fulfilled"),
- None
- );
- assert_eq!(
- next_status_for_receipt(&TradeOrderStatus::Fulfilled, true)
- .expect("acknowledged receipt completes"),
- Some(TradeOrderStatus::Completed)
- );
- assert!(next_status_for_receipt(&TradeOrderStatus::Requested, false).is_err());
-
- let tags = vec![
- vec!["p".to_string(), "pk".to_string()],
- vec!["a".to_string(), "addr".to_string()],
- ];
- assert_eq!(tag_value(&tags, "a"), Some("addr".to_string()));
- assert_eq!(tag_value(&tags, "x"), None);
- assert!(tag_has_value(&tags, "p", "pk"));
- assert!(!tag_has_value(&tags, "p", "miss"));
-
- let parsed: Result<TradeOrderResponse, _> =
- parse_payload(json!({"accepted":true,"reason":null}));
- assert!(parsed.is_ok());
- let invalid: Result<TradeOrderResponse, _> = parse_payload(json!({"accepted":"true"}));
- assert!(invalid.is_err());
+ #[tokio::test]
+ async fn unsupported_kind_is_rejected() {
+ let _guard = test_guard().await;
+ let worker = RadrootsNostrKeys::generate();
+ let client = RadrootsNostrClient::new(worker.clone());
+ let state = Arc::new(Mutex::new(TradeListingState::default()));
+ let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(4999), "test")
+ .sign_with_keys(&RadrootsNostrKeys::generate())
+ .expect("event");
+ assert!(matches!(
+ handle_event(event, Vec::new(), worker, client, state).await,
+ Err(TradeListingDvmError::UnsupportedKind)
+ ));
}
#[test]
- fn transition_matrix_covers_all_from_arms() {
- let _guard = test_guard();
-
- assert!(ensure_transition(TradeOrderStatus::Draft, TradeOrderStatus::Requested).is_ok());
- assert!(ensure_transition(TradeOrderStatus::Draft, TradeOrderStatus::Accepted).is_err());
-
- assert!(
- ensure_transition(TradeOrderStatus::Validated, TradeOrderStatus::Requested).is_ok()
- );
- assert!(
- ensure_transition(TradeOrderStatus::Validated, TradeOrderStatus::Accepted).is_err()
- );
-
- assert!(ensure_transition(TradeOrderStatus::Requested, TradeOrderStatus::Accepted).is_ok());
- assert!(
- ensure_transition(TradeOrderStatus::Requested, TradeOrderStatus::Fulfilled).is_err()
- );
-
- assert!(
- ensure_transition(TradeOrderStatus::Questioned, TradeOrderStatus::Requested).is_ok()
- );
- assert!(
- ensure_transition(TradeOrderStatus::Questioned, TradeOrderStatus::Accepted).is_err()
- );
-
- assert!(ensure_transition(TradeOrderStatus::Revised, TradeOrderStatus::Declined).is_ok());
- assert!(ensure_transition(TradeOrderStatus::Revised, TradeOrderStatus::Fulfilled).is_err());
-
- assert!(ensure_transition(TradeOrderStatus::Accepted, TradeOrderStatus::Fulfilled).is_ok());
- assert!(
- ensure_transition(TradeOrderStatus::Accepted, TradeOrderStatus::Requested).is_err()
- );
-
- assert!(ensure_transition(TradeOrderStatus::Declined, TradeOrderStatus::Accepted).is_err());
- assert!(
- ensure_transition(TradeOrderStatus::Cancelled, TradeOrderStatus::Requested).is_err()
- );
-
- assert!(
- ensure_transition(TradeOrderStatus::Fulfilled, TradeOrderStatus::Completed).is_ok()
- );
+ fn transition_and_tag_helpers_cover_core_paths() {
assert!(
- ensure_transition(TradeOrderStatus::Fulfilled, TradeOrderStatus::Accepted).is_err()
+ ensure_transition(&TradeOrderStatus::Requested, &TradeOrderStatus::Accepted).is_ok()
);
-
assert!(
- ensure_transition(TradeOrderStatus::Completed, TradeOrderStatus::Cancelled).is_err()
+ ensure_transition(&TradeOrderStatus::Declined, &TradeOrderStatus::Accepted).is_err()
);
+ assert!(tag_has_value(
+ &[vec!["p".to_string(), "pubkey".to_string()]],
+ "p",
+ "pubkey"
+ ));
}
#[tokio::test]
- async fn io_hooks_cover_fetch_send_and_validate_wrappers() {
- let _guard = test_guard();
- let (rhi_keys, _, _) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&rhi_keys);
- let event = make_event(
- &rhi_keys,
- RadrootsNostrKind::Metadata,
- "meta".to_string(),
- vec![RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("t"),
- vec!["radroots:type:farm".to_string()],
- )],
- );
- push_fetch_events_ok(vec![event.clone()]);
- let fetched = fetch_events_io(
- &client,
- RadrootsNostrFilter::new(),
- std::time::Duration::from_secs(1),
- )
- .await
- .expect("fetch hook");
- assert_eq!(fetched.len(), 1);
-
+ async fn handle_error_uses_send_hook() {
+ let _guard = test_guard().await;
dvm_test_hooks()
.lock()
.expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(event.clone()));
- let by_id = super::fetch_event_by_id_io(&client, "id")
- .await
- .expect("by id");
- assert_eq!(by_id.id, event.id);
-
- push_send_ok();
- let builder = radroots_nostr::prelude::radroots_nostr_build_event(
- KIND_TRADE_LISTING_VALIDATE_RES as u32,
- "x",
- vec![vec!["p".to_string(), rhi_keys.public_key().to_hex()]],
+ .send_event_results
+ .push_back(Ok(()));
+ let keys = RadrootsNostrKeys::generate();
+ let client = RadrootsNostrClient::new(keys.clone());
+ let event = RadrootsNostrEventBuilder::new(
+ RadrootsNostrKind::Custom(KIND_ORDER_REQUEST as u16),
+ "bad",
)
- .expect("builder");
- assert!(send_event_io(&client, builder).await.is_ok());
-
- let farm = RadrootsFarmRef {
- pubkey: rhi_keys.public_key().to_hex(),
- d_tag: "farmtag".to_string(),
- };
- push_validate_listing_ok(listing_addr.clone(), farm.clone());
- let validated = validate_listing_event_io(&event).expect("validate hook");
- assert_eq!(validated.0, listing_addr);
- assert_eq!(validated.1.pubkey, farm.pubkey);
- assert_eq!(listing_addr.contains(':',), true);
- }
-
- #[tokio::test]
- async fn farm_dependency_validation_paths_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, _) = make_keys();
- let client = make_client(&rhi_keys);
-
- let invalid_farm = RadrootsFarmRef {
- pubkey: "bad".to_string(),
- d_tag: "farmtag".to_string(),
- };
- let errors = validate_farm_dependencies(&client, &invalid_farm)
- .await
- .expect("invalid farm result");
- assert!(errors.contains(&TradeListingValidationError::MissingFarmProfile));
- assert!(errors.contains(&TradeListingValidationError::MissingFarmRecord));
-
- let farm = RadrootsFarmRef {
- pubkey: seller_keys.public_key().to_hex(),
- d_tag: "farmtag".to_string(),
- };
- push_fetch_events_ok(Vec::new());
- push_fetch_events_ok(Vec::new());
- let missing = validate_farm_dependencies(&client, &farm)
- .await
- .expect("missing deps");
- assert!(missing.contains(&TradeListingValidationError::MissingFarmProfile));
- assert!(missing.contains(&TradeListingValidationError::MissingFarmRecord));
-
- let profile_event = make_event(
- &seller_keys,
- RadrootsNostrKind::Metadata,
- "profile".to_string(),
- vec![RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("t"),
- vec!["radroots:type:farm".to_string()],
- )],
- );
- let record_event = make_event(
- &seller_keys,
- RadrootsNostrKind::Custom(radroots_events::kinds::KIND_FARM as u16),
- "record".to_string(),
- Vec::new(),
+ .sign_with_keys(&keys)
+ .expect("event");
+ assert!(
+ handle_error(TradeListingDvmError::InvalidOrder, &event, &client)
+ .await
+ .is_ok()
);
- push_fetch_events_ok(vec![profile_event]);
- push_fetch_events_ok(vec![record_event]);
- let ok = validate_farm_dependencies(&client, &farm)
- .await
- .expect("ok deps");
- assert!(ok.is_empty());
- }
-
- #[tokio::test]
- async fn handle_listing_validate_request_paths_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let state = Arc::new(AsyncMutex::new(TradeListingState::default()));
- let missing_event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "missing".to_string(),
- Vec::new(),
- );
-
- push_fetch_event_by_id_error_not_found();
- push_send_ok();
- let payload = TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: "missing".to_string(),
- relays: None,
- }),
- };
- assert!(
- handle_listing_validate_request(
- &missing_event,
- payload,
- &listing_addr,
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let fetch_error_event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "fetch-error".to_string(),
- Vec::new(),
- );
- push_fetch_events_ok(Vec::new());
- push_send_ok();
- let payload = TradeListingValidateRequest {
- listing_event: None,
- };
- assert!(
- handle_listing_validate_request(
- &fetch_error_event,
- payload,
- &listing_addr,
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- let success_event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "success".to_string(),
- Vec::new(),
- );
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(success_event.clone()));
- push_validate_listing_ok(
- listing_addr.clone(),
- RadrootsFarmRef {
- pubkey: seller_keys.public_key().to_hex(),
- d_tag: "farmtag".to_string(),
- },
- );
- push_farm_validation_result(Ok(Vec::new()));
- push_send_ok();
- let payload = TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: success_event.id.to_hex(),
- relays: None,
- }),
- };
- assert!(
- handle_listing_validate_request(
- &success_event,
- payload,
- &listing_addr,
- &client,
- &state
- )
- .await
- .is_ok()
- );
- assert!(state.lock().await.is_listing_validated(&listing_addr));
- assert_eq!(
- state.lock().await.validated_listing_event_id(&listing_addr),
- Some(success_event.id.to_string().as_str())
- );
-
- let other_listing_addr = listing_addr_for_seller(&rhi_keys);
- let mismatch_event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "mismatch".to_string(),
- Vec::new(),
- );
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(mismatch_event.clone()));
- push_validate_listing_ok(
- other_listing_addr,
- RadrootsFarmRef {
- pubkey: seller_keys.public_key().to_hex(),
- d_tag: "farmtag".to_string(),
- },
- );
- push_send_ok();
- let payload = TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: mismatch_event.id.to_hex(),
- relays: None,
- }),
- };
- let mismatch_listing_addr = listing_addr_for_seller(&buyer_keys);
- assert!(
- handle_listing_validate_request(
- &mismatch_event,
- payload,
- &mismatch_listing_addr,
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert!(
- !state
- .lock()
- .await
- .is_listing_validated(&mismatch_listing_addr)
- );
-
- state
- .lock()
- .await
- .mark_listing_validated(&listing_addr, "stale-listing-event");
- let stale_event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "stale".to_string(),
- Vec::new(),
- );
- push_fetch_events_ok(Vec::new());
- push_send_ok();
- let payload = TradeListingValidateRequest {
- listing_event: None,
- };
- assert!(
- handle_listing_validate_request(&stale_event, payload, &listing_addr, &client, &state)
- .await
- .is_ok()
- );
- assert!(!state.lock().await.is_listing_validated(&listing_addr));
- }
-
- #[tokio::test]
- async fn handle_listing_validate_request_dedupes_replayed_request_event() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, _) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let state = Arc::new(AsyncMutex::new(TradeListingState::default()));
- let event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "content".to_string(),
- Vec::new(),
- );
- let payload = TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: event.id.to_hex(),
- relays: None,
- }),
- };
-
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(event.clone()));
- push_validate_listing_ok(
- listing_addr.clone(),
- RadrootsFarmRef {
- pubkey: seller_keys.public_key().to_hex(),
- d_tag: "farmtag".to_string(),
- },
- );
- push_farm_validation_result(Ok(Vec::new()));
- push_send_ok();
- assert!(
- handle_listing_validate_request(
- &event,
- payload.clone(),
- &listing_addr,
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert!(
- state
- .lock()
- .await
- .is_non_order_event_seen(&event.id.to_string())
- );
-
- assert!(
- handle_listing_validate_request(&event, payload, &listing_addr, &client, &state)
- .await
- .is_ok()
- );
- }
-
- #[tokio::test]
- async fn handler_paths_cover_state_transitions() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let listing_addr_parsed = TradeListingAddress::parse(&listing_addr).expect("addr");
- let order_id = "order-1";
- let seller_pub = seller_keys.public_key().to_hex();
- let buyer_pub = buyer_keys.public_key().to_hex();
- let state = Arc::new(AsyncMutex::new(TradeListingState::default()));
- state
- .lock()
- .await
- .upsert_listing_event(&listing_addr, TEST_LISTING_EVENT_ID, 30402);
-
- let order_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- let order_payload = make_order(
- order_id,
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- assert!(
- handle_order_request(
- &order_event,
- order_payload,
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let response_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderResponse,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_order_response(
- &response_event,
- TradeOrderResponse {
- accepted: true,
- reason: None,
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- state
- .lock()
- .await
- .get_order_mut(order_id)
- .expect("order")
- .status = TradeOrderStatus::Requested;
- let revision_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderRevision,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_order_revision(
- &revision_event,
- TradeOrderRevision {
- revision_id: "r1".to_string(),
- changes: Vec::new(),
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let revision_response_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRevisionAccept,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_order_revision_response(
- &revision_response_event,
- TradeListingMessageType::OrderRevisionAccept,
- TradeOrderRevisionResponse {
- accepted: true,
- reason: None,
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- state
- .lock()
- .await
- .get_order_mut(order_id)
- .expect("order")
- .status = TradeOrderStatus::Requested;
- let question_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Question,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_question(
- &question_event,
- TradeQuestion {
- question_id: "q1".to_string(),
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let answer_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::Answer,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_answer(
- &answer_event,
- TradeAnswer {
- question_id: "q1".to_string(),
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let discount_request_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountRequest,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_discount_request(
- &discount_request_event,
- TradeDiscountRequest {
- discount_id: "d1".to_string(),
- value: sample_discount_value(),
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let discount_offer_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::DiscountOffer,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_discount_offer(
- &discount_offer_event,
- TradeDiscountOffer {
- discount_id: "d1".to_string(),
- value: sample_discount_value(),
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let discount_accept_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountAccept,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_discount_decision(
- &discount_accept_event,
- TradeListingMessageType::DiscountAccept,
- TradeDiscountDecision::Accept {
- value: sample_discount_value(),
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- state
- .lock()
- .await
- .get_order_mut(order_id)
- .expect("order")
- .status = TradeOrderStatus::Requested;
- let cancel_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Cancel,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_cancel(
- &cancel_event,
- TradeListingCancel { reason: None },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- state
- .lock()
- .await
- .get_order_mut(order_id)
- .expect("order")
- .status = TradeOrderStatus::Accepted;
- let fulfill_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::FulfillmentUpdate,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_fulfillment_update(
- &fulfill_event,
- TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Delivered,
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
-
- let receipt_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Receipt,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_receipt(
- &receipt_event,
- TradeReceipt {
- acknowledged: true,
- at: 1,
- },
- &listing_addr_parsed,
- Some(order_id),
- &client,
- &state
- )
- .await
- .is_ok()
- );
- }
-
- #[tokio::test]
- async fn handle_event_covers_guard_and_dispatch_paths() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let rhi_pub = rhi_keys.public_key().to_hex();
- let buyer_pub = buyer_keys.public_key().to_hex();
- let seller_pub = seller_keys.public_key().to_hex();
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let order_id = "order-1";
- let tags = make_custom_tags(&rhi_pub, &listing_addr, Some(order_id));
- let state = state_with_order(
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )
- .await;
-
- let unsupported = make_event(
- &buyer_keys,
- RadrootsNostrKind::TextNote,
- "x".to_string(),
- tags.clone(),
- );
- assert!(matches!(
- handle_event(
- unsupported,
- tags.clone(),
- rhi_keys.clone(),
- client.clone(),
- state.clone()
- )
- .await,
- Err(TradeListingDvmError::UnsupportedKind)
- ));
-
- let missing_recipient = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::ListingValidateRequest,
- &listing_addr,
- None,
- TradeListingMessagePayload::ListingValidateRequest(TradeListingValidateRequest {
- listing_event: None,
- }),
- ),
- Vec::new(),
- );
- assert!(matches!(
- handle_event(
- missing_recipient,
- Vec::new(),
- rhi_keys.clone(),
- client.clone(),
- state.clone()
- )
- .await,
- Err(TradeListingDvmError::MissingRecipient)
- ));
-
- let unsupported_custom = make_event(
- &buyer_keys,
- RadrootsNostrKind::Custom(1),
- make_envelope_content(
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- Some(order_id),
- json!({}),
- ),
- tags.clone(),
- );
- assert!(matches!(
- handle_event(
- unsupported_custom,
- tags.clone(),
- rhi_keys.clone(),
- client.clone(),
- state.clone()
- )
- .await,
- Err(TradeListingDvmError::UnsupportedKind)
- ));
-
- let self_event = make_event(
- &rhi_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_envelope_content(
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- Some(order_id),
- json!({}),
- ),
- tags.clone(),
- );
- assert!(
- handle_event(
- self_event,
- tags.clone(),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await
- .is_ok()
- );
-
- let kind_mismatch = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_envelope_content(
- TradeListingMessageType::Question,
- &listing_addr,
- Some(order_id),
- json!({}),
- ),
- tags.clone(),
- );
- assert!(matches!(
- handle_event(
- kind_mismatch,
- tags.clone(),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let a_mismatch_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::OrderRequest,
- "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAA",
- Some(order_id),
- payload_enum_for_message(
- TradeListingMessageType::OrderRequest,
- order_id,
- "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAA",
- &buyer_pub,
- &seller_pub,
- ),
- ),
- tags.clone(),
- );
- assert!(matches!(
- handle_event(
- a_mismatch_event,
- tags.clone(),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let d_mismatch_tags = make_custom_tags(&rhi_pub, &listing_addr, Some("other-order"));
- let d_mismatch_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- Some(order_id),
- payload_enum_for_message(
- TradeListingMessageType::OrderRequest,
- order_id,
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- ),
- d_mismatch_tags.clone(),
- );
- assert!(matches!(
- handle_event(
- d_mismatch_event,
- d_mismatch_tags,
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let bad_addr = format!(
- "30404:{}:AAAAAAAAAAAAAAAAAAAAAA",
- seller_keys.public_key().to_hex()
- );
- let bad_addr_tags = make_workflow_tags(
- &rhi_pub,
- &bad_addr,
- Some(order_id),
- Some(TEST_LISTING_EVENT_ID),
- None,
- None,
- );
- let bad_addr_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::OrderRequest,
- &bad_addr,
- Some(order_id),
- payload_enum_for_message(
- TradeListingMessageType::OrderRequest,
- order_id,
- &bad_addr,
- &buyer_pub,
- &seller_pub,
- ),
- ),
- bad_addr_tags.clone(),
- );
- assert!(matches!(
- handle_event(
- bad_addr_event,
- bad_addr_tags,
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let cases = vec![
- (
- TradeListingMessageType::ListingValidateRequest,
- KIND_TRADE_LISTING_VALIDATE_REQ,
- ),
- (
- TradeListingMessageType::OrderRequest,
- KIND_TRADE_LISTING_ORDER_REQ,
- ),
- (
- TradeListingMessageType::OrderResponse,
- KIND_TRADE_LISTING_ORDER_RES,
- ),
- (
- TradeListingMessageType::OrderRevision,
- KIND_TRADE_LISTING_ORDER_REVISION_REQ,
- ),
- (
- TradeListingMessageType::OrderRevisionAccept,
- KIND_TRADE_LISTING_ORDER_REVISION_RES,
- ),
- (
- TradeListingMessageType::OrderRevisionDecline,
- KIND_TRADE_LISTING_ORDER_REVISION_RES,
- ),
- (
- TradeListingMessageType::Question,
- KIND_TRADE_LISTING_QUESTION_REQ,
- ),
- (
- TradeListingMessageType::Answer,
- KIND_TRADE_LISTING_ANSWER_RES,
- ),
- (
- TradeListingMessageType::DiscountRequest,
- KIND_TRADE_LISTING_DISCOUNT_REQ,
- ),
- (
- TradeListingMessageType::DiscountOffer,
- KIND_TRADE_LISTING_DISCOUNT_OFFER_RES,
- ),
- (
- TradeListingMessageType::DiscountAccept,
- KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ,
- ),
- (
- TradeListingMessageType::DiscountDecline,
- KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ,
- ),
- (
- TradeListingMessageType::Cancel,
- KIND_TRADE_LISTING_CANCEL_REQ,
- ),
- (
- TradeListingMessageType::FulfillmentUpdate,
- KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ,
- ),
- (
- TradeListingMessageType::Receipt,
- KIND_TRADE_LISTING_RECEIPT_REQ,
- ),
- (
- TradeListingMessageType::ListingValidateResult,
- KIND_TRADE_LISTING_VALIDATE_RES,
- ),
- ];
-
- for (message_type, kind) in cases {
- if message_type == TradeListingMessageType::ListingValidateRequest {
- push_fetch_events_ok(Vec::new());
- push_send_ok();
- }
- if message_type == TradeListingMessageType::Cancel {
- state
- .lock()
- .await
- .get_order_mut(order_id)
- .expect("order")
- .status = TradeOrderStatus::Requested;
- }
- let payload = if message_type == TradeListingMessageType::ListingValidateResult {
- json!({"valid": true, "errors": []})
- } else if message_type == TradeListingMessageType::ListingValidateRequest {
- json!({"listing_event": null})
- } else {
- json!({})
- };
- let content = make_envelope_content(
- message_type,
- &listing_addr,
- if message_type.requires_order_id() {
- Some(order_id)
- } else {
- None
- },
- payload,
- );
- let event = make_event(&buyer_keys, custom_trade_kind(kind), content, tags.clone());
- let _ = handle_event(
- event,
- tags.clone(),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
- }
- }
-
- #[tokio::test]
- async fn fetch_latest_send_envelope_and_handle_error_paths() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, _) = make_keys();
- let client = make_client(&rhi_keys);
- let older = make_event(
- &seller_keys,
- RadrootsNostrKind::Metadata,
- "old".to_string(),
- Vec::new(),
- );
- let newer = make_event(
- &seller_keys,
- RadrootsNostrKind::Metadata,
- "new".to_string(),
- Vec::new(),
- );
- push_fetch_events_ok(vec![older, newer.clone()]);
- let latest = fetch_latest_event_by_kind(
- &client,
- RadrootsNostrFilter::new(),
- RadrootsNostrKind::Metadata,
- )
- .await
- .expect("latest");
- assert!(latest.is_some());
-
- push_send_ok();
- assert!(
- send_envelope(
- &client,
- seller_keys.public_key().to_hex(),
- TradeListingMessageType::ListingValidateResult,
- &listing_addr_for_seller(&seller_keys),
- None,
- &TradeListingMessagePayload::ListingValidateResult(TradeListingValidateResult {
- valid: true,
- errors: Vec::new(),
- }),
- )
- .await
- .is_ok()
- );
-
- let event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- "x".to_string(),
- Vec::new(),
- );
- push_send_ok();
- assert!(
- handle_error(TradeListingDvmError::UnsupportedKind, &event, &client)
- .await
- .is_ok()
- );
- }
-
- #[tokio::test]
- async fn fetch_and_validation_guard_branches_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, _) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let listing_kind = TradeListingAddress::parse(&listing_addr)
- .expect("listing address")
- .kind;
-
- let wrong_kind = make_event(
- &seller_keys,
- RadrootsNostrKind::Metadata,
- "metadata".to_string(),
- Vec::new(),
- );
- let listing_event = make_event(
- &seller_keys,
- custom_trade_kind(listing_kind),
- "listing".to_string(),
- Vec::new(),
- );
- push_fetch_events_ok(vec![
- wrong_kind.clone(),
- listing_event.clone(),
- listing_event.clone(),
- ]);
- let fetched_listing = fetch_listing_by_addr(&client, &listing_addr)
- .await
- .expect("listing fetch");
- assert!(fetched_listing.is_some());
-
- let wrong_custom = make_event(
- &seller_keys,
- RadrootsNostrKind::Custom(9999),
- "other".to_string(),
- Vec::new(),
- );
- let metadata_event = make_event(
- &seller_keys,
- RadrootsNostrKind::Metadata,
- "profile".to_string(),
- Vec::new(),
- );
- push_fetch_events_ok(vec![
- wrong_custom,
- metadata_event.clone(),
- metadata_event.clone(),
- ]);
- let fetched_latest = fetch_latest_event_by_kind(
- &client,
- RadrootsNostrFilter::new(),
- RadrootsNostrKind::Metadata,
- )
- .await
- .expect("latest metadata");
- assert!(fetched_latest.is_some());
-
- let farm = RadrootsFarmRef {
- pubkey: seller_keys.public_key().to_hex(),
- d_tag: "farm".to_string(),
- };
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_events_results
- .push_back(Err(TradeListingDvmError::InvalidOrder));
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_events_results
- .push_back(Err(TradeListingDvmError::InvalidOrder));
- let errors = validate_farm_dependencies(&client, &farm)
- .await
- .expect("farm validation");
- assert!(errors.contains(&TradeListingValidationError::MissingFarmProfile));
- assert!(errors.contains(&TradeListingValidationError::MissingFarmRecord));
-
- let empty_farm_tag = RadrootsFarmRef {
- pubkey: seller_keys.public_key().to_hex(),
- d_tag: String::new(),
- };
- push_fetch_events_ok(Vec::new());
- let empty_tag_errors = validate_farm_dependencies(&client, &empty_farm_tag)
- .await
- .expect("empty farm tag");
- assert!(empty_tag_errors.contains(&TradeListingValidationError::MissingFarmRecord));
- }
-
- #[tokio::test]
- async fn io_wrapper_default_paths_cover_fallback_branches() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, _) = make_keys();
- let client = make_client(&rhi_keys);
- assert!(fetch_event_by_id_io(&client, "invalid-id").await.is_err());
- assert!(
- fetch_events_io(
- &client,
- RadrootsNostrFilter::new(),
- std::time::Duration::from_millis(1)
- )
- .await
- .is_err()
- );
- let builder = radroots_nostr::prelude::radroots_nostr_build_event(
- KIND_TRADE_LISTING_ORDER_REQ as u32,
- "x",
- vec![vec!["a".to_string(), listing_addr_for_seller(&seller_keys)]],
- )
- .expect("builder");
- assert!(send_event_io(&client, builder).await.is_err());
- let event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- "{}".to_string(),
- Vec::new(),
- );
- assert!(validate_listing_event_io(&event).is_err());
- }
-
- #[tokio::test]
- async fn handle_event_valid_dispatch_matrix_covers_arm_calls() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let rhi_pub = rhi_keys.public_key().to_hex();
- let order_id = "order-1";
- let buyer_pub = buyer_keys.public_key().to_hex();
- let seller_pub = seller_keys.public_key().to_hex();
- let state = state_with_order(
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )
- .await;
-
- push_fetch_events_ok(Vec::new());
- push_send_ok();
- let validate_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- make_envelope_content(
- TradeListingMessageType::ListingValidateRequest,
- &listing_addr,
- None,
- json!({"listing_event": null}),
- ),
- make_custom_tags(&rhi_pub, &listing_addr, None),
- );
- let _ = handle_event(
- validate_event,
- make_custom_tags(&rhi_pub, &listing_addr, None),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
-
- let cases: Vec<(
- TradeListingMessageType,
- u32,
- serde_json::Value,
- TradeOrderStatus,
- )> = vec![
- (
- TradeListingMessageType::OrderRequest,
- KIND_TRADE_LISTING_ORDER_REQ,
- serde_json::to_value(make_order(
- order_id,
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- ))
- .expect("order request"),
- TradeOrderStatus::Requested,
- ),
- (
- TradeListingMessageType::OrderResponse,
- KIND_TRADE_LISTING_ORDER_RES,
- serde_json::to_value(TradeOrderResponse {
- accepted: true,
- reason: None,
- })
- .expect("order response"),
- TradeOrderStatus::Requested,
- ),
- (
- TradeListingMessageType::OrderRevision,
- KIND_TRADE_LISTING_ORDER_REVISION_REQ,
- serde_json::to_value(TradeOrderRevision {
- revision_id: "r2".to_string(),
- changes: Vec::new(),
- })
- .expect("order revision"),
- TradeOrderStatus::Requested,
- ),
- (
- TradeListingMessageType::OrderRevisionAccept,
- KIND_TRADE_LISTING_ORDER_REVISION_RES,
- serde_json::to_value(TradeOrderRevisionResponse {
- accepted: true,
- reason: None,
- })
- .expect("order revision accept"),
- TradeOrderStatus::Revised,
- ),
- (
- TradeListingMessageType::OrderRevisionDecline,
- KIND_TRADE_LISTING_ORDER_REVISION_RES,
- serde_json::to_value(TradeOrderRevisionResponse {
- accepted: false,
- reason: None,
- })
- .expect("order revision decline"),
- TradeOrderStatus::Revised,
- ),
- (
- TradeListingMessageType::Question,
- KIND_TRADE_LISTING_QUESTION_REQ,
- serde_json::to_value(TradeQuestion {
- question_id: "qx".to_string(),
- })
- .expect("question"),
- TradeOrderStatus::Requested,
- ),
- (
- TradeListingMessageType::Answer,
- KIND_TRADE_LISTING_ANSWER_RES,
- serde_json::to_value(TradeAnswer {
- question_id: "qx".to_string(),
- })
- .expect("answer"),
- TradeOrderStatus::Questioned,
- ),
- (
- TradeListingMessageType::DiscountRequest,
- KIND_TRADE_LISTING_DISCOUNT_REQ,
- serde_json::to_value(TradeDiscountRequest {
- discount_id: "d2".to_string(),
- value: sample_discount_value(),
- })
- .expect("discount request"),
- TradeOrderStatus::Requested,
- ),
- (
- TradeListingMessageType::DiscountOffer,
- KIND_TRADE_LISTING_DISCOUNT_OFFER_RES,
- serde_json::to_value(TradeDiscountOffer {
- discount_id: "d2".to_string(),
- value: sample_discount_value(),
- })
- .expect("discount offer"),
- TradeOrderStatus::Requested,
- ),
- (
- TradeListingMessageType::DiscountAccept,
- KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ,
- serde_json::to_value(TradeDiscountDecision::Accept {
- value: sample_discount_value(),
- })
- .expect("discount accept"),
- TradeOrderStatus::Revised,
- ),
- (
- TradeListingMessageType::DiscountDecline,
- KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ,
- serde_json::to_value(TradeDiscountDecision::Decline { reason: None })
- .expect("discount decline"),
- TradeOrderStatus::Revised,
- ),
- (
- TradeListingMessageType::Cancel,
- KIND_TRADE_LISTING_CANCEL_REQ,
- serde_json::to_value(TradeListingCancel { reason: None }).expect("cancel"),
- TradeOrderStatus::Requested,
- ),
- (
- TradeListingMessageType::FulfillmentUpdate,
- KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ,
- serde_json::to_value(TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Shipped,
- })
- .expect("fulfillment"),
- TradeOrderStatus::Accepted,
- ),
- (
- TradeListingMessageType::Receipt,
- KIND_TRADE_LISTING_RECEIPT_REQ,
- serde_json::to_value(TradeReceipt {
- acknowledged: true,
- at: 1,
- })
- .expect("receipt"),
- TradeOrderStatus::Fulfilled,
- ),
- (
- TradeListingMessageType::ListingValidateResult,
- KIND_TRADE_LISTING_VALIDATE_RES,
- json!({"valid": true, "errors": []}),
- TradeOrderStatus::Requested,
- ),
- ];
-
- for (message_type, kind, payload, status_before) in cases {
- set_order_status(&state, order_id, status_before).await;
- if message_type != TradeListingMessageType::ListingValidateResult {
- push_send_ok();
- }
- let sender = match message_type {
- TradeListingMessageType::OrderResponse
- | TradeListingMessageType::OrderRevision
- | TradeListingMessageType::Answer
- | TradeListingMessageType::DiscountOffer
- | TradeListingMessageType::FulfillmentUpdate => &seller_keys,
- _ => &buyer_keys,
- };
- let content =
- make_envelope_content(message_type, &listing_addr, Some(order_id), payload);
- let tags = make_custom_tags(&rhi_pub, &listing_addr, Some(order_id));
- let event = make_event(sender, custom_trade_kind(kind), content, tags.clone());
- let _ =
- handle_event(event, tags, rhi_keys.clone(), client.clone(), state.clone()).await;
- }
- }
-
- #[tokio::test]
- async fn handler_error_branches_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let parsed = TradeListingAddress::parse(&listing_addr).expect("listing");
- let buyer_pub = buyer_keys.public_key().to_hex();
- let seller_pub = seller_keys.public_key().to_hex();
- let state = state_with_order(
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )
- .await;
-
- let bad_order = make_order(
- "bad",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- let event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(matches!(
- handle_order_request(&event, bad_order, &parsed, Some("order-1"), &client, &state)
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- let missing_state = Arc::new(AsyncMutex::new(TradeListingState::default()));
- let order = make_order(
- "order-2",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- let fetched_snapshot_event = make_event(
- &seller_keys,
- RadrootsNostrKind::Custom(30402),
- "listing-fetch".to_string(),
- Vec::new(),
- );
- let fetched_snapshot_id = fetched_snapshot_event.id.to_string();
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(fetched_snapshot_event));
- push_validate_listing_ok(
- listing_addr.clone(),
- RadrootsFarmRef {
- pubkey: seller_pub.clone(),
- d_tag: "farmtag".to_string(),
- },
- );
- let fetched_order_event = make_public_trade_event_with_payload(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- "order-2",
- &buyer_pub,
- &seller_pub,
- TradeListingMessagePayload::TradeOrderRequested(make_order(
- "order-2",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )),
- Some(fetched_snapshot_id),
- None,
- None,
- );
- assert!(
- handle_order_request(
- &fetched_order_event,
- order,
- &parsed,
- Some("order-2"),
- &client,
- &missing_state
- )
- .await
- .is_ok()
- );
- assert!(missing_state.lock().await.order_exists("order-2"));
-
- let mismatched_snapshot_state = Arc::new(AsyncMutex::new(TradeListingState::default()));
- let mismatched_snapshot_event = make_event(
- &seller_keys,
- RadrootsNostrKind::Custom(30402),
- "listing-mismatch".to_string(),
- Vec::new(),
- );
- let mismatched_snapshot_id = mismatched_snapshot_event.id.to_string();
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(mismatched_snapshot_event));
- push_validate_listing_ok(
- listing_addr_for_seller(&buyer_keys),
- RadrootsFarmRef {
- pubkey: buyer_pub.clone(),
- d_tag: "farmtag".to_string(),
- },
- );
- let mismatched_snapshot_order = make_order(
- "order-3",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- let mismatched_snapshot_event = make_public_trade_event_with_payload(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- "order-3",
- &buyer_pub,
- &seller_pub,
- TradeListingMessagePayload::TradeOrderRequested(make_order(
- "order-3",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )),
- Some(mismatched_snapshot_id),
- None,
- None,
- );
- assert!(matches!(
- handle_order_request(
- &mismatched_snapshot_event,
- mismatched_snapshot_order,
- &parsed,
- Some("order-3"),
- &client,
- &mismatched_snapshot_state
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- let seller_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderResponse,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- state
- .lock()
- .await
- .get_order_mut("order-1")
- .expect("order")
- .seen_event_ids
- .insert(seller_event.id.to_string());
- assert!(
- handle_order_response(
- &seller_event,
- TradeOrderResponse {
- accepted: true,
- reason: None,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- let wrong_buyer = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderRevisionAccept,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(matches!(
- handle_order_revision_response(
- &wrong_buyer,
- TradeListingMessageType::OrderRevisionAccept,
- TradeOrderRevisionResponse {
- accepted: false,
- reason: None,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized | TradeListingDvmError::InvalidOrder)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- let wrong_sender = make_public_trade_event(
- &rhi_keys,
- TradeListingMessageType::Cancel,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(matches!(
- handle_cancel(
- &wrong_sender,
- TradeListingCancel { reason: None },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- let validate_event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "x".to_string(),
- Vec::new(),
- );
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Err(TradeListingDvmError::InvalidOrder));
- push_send_ok();
- assert!(
- handle_listing_validate_request(
- &validate_event,
- TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: "x".to_string(),
- relays: None,
- }),
- },
- &listing_addr,
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- push_send_ok();
- assert!(
- handle_listing_validate_request(
- &validate_event,
- TradeListingValidateRequest {
- listing_event: None
- },
- "not-a-listing-addr",
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(validate_event.clone()));
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .validate_listing_results
- .push_back(Err(TradeListingValidationError::MissingInventory));
- push_send_ok();
- assert!(
- handle_listing_validate_request(
- &validate_event,
- TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: "x".to_string(),
- relays: None,
- }),
- },
- &listing_addr,
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- let duplicate_order = make_order(
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- let duplicate_order_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(
- handle_order_request(
- &duplicate_order_event,
- duplicate_order,
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- let unauthorized_order = make_order(
- "order-3",
- &listing_addr,
- "different-buyer",
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- let unauthorized_order_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- "order-3",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(matches!(
- handle_order_request(
- &unauthorized_order_event,
- unauthorized_order,
- &parsed,
- Some("order-3"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- let duplicate_order = make_order(
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- assert!(
- handle_order_request(
- &event,
- duplicate_order,
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- let buyer_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderResponse,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(matches!(
- handle_order_response(
- &buyer_event,
- TradeOrderResponse {
- accepted: false,
- reason: None,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- push_send_ok();
- assert!(
- handle_order_response(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderResponse,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeOrderResponse {
- accepted: false,
- reason: None,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- assert!(matches!(
- handle_order_revision(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRevision,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeOrderRevision {
- revision_id: "r-wrong-sender".to_string(),
- changes: Vec::new(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- assert!(matches!(
- handle_order_revision(
- &make_public_trade_event_with_payload(
- &seller_keys,
- TradeListingMessageType::OrderRevision,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- payload_enum_for_message(
- TradeListingMessageType::OrderRevision,
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- Some(TEST_LISTING_EVENT_ID.to_string()),
- Some("wrong-root".to_string()),
- Some("wrong-prev".to_string()),
- ),
- TradeOrderRevision {
- revision_id: "r3".to_string(),
- changes: Vec::new(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- let seen_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderRevision,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- state
- .lock()
- .await
- .get_order_mut("order-1")
- .expect("order")
- .seen_event_ids
- .insert(seen_event.id.to_string());
- assert!(
- handle_order_revision(
- &seen_event,
- TradeOrderRevision {
- revision_id: "r4".to_string(),
- changes: Vec::new(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- assert!(matches!(
- handle_question(
- &make_public_trade_event_with_payload(
- &buyer_keys,
- TradeListingMessageType::Question,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- payload_enum_for_message(
- TradeListingMessageType::Question,
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- None,
- Some("wrong-root".to_string()),
- Some("wrong-prev".to_string()),
- ),
- TradeQuestion {
- question_id: "q".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Questioned).await;
- assert!(matches!(
- handle_answer(
- &make_public_trade_event_with_payload(
- &seller_keys,
- TradeListingMessageType::Answer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- payload_enum_for_message(
- TradeListingMessageType::Answer,
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- None,
- Some("wrong-root".to_string()),
- Some("wrong-prev".to_string()),
- ),
- TradeAnswer {
- question_id: "q".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- assert!(matches!(
- handle_discount_request(
- &make_public_trade_event_with_payload(
- &buyer_keys,
- TradeListingMessageType::DiscountRequest,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- payload_enum_for_message(
- TradeListingMessageType::DiscountRequest,
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- Some(TEST_LISTING_EVENT_ID.to_string()),
- Some("wrong-root".to_string()),
- Some("wrong-prev".to_string()),
- ),
- TradeDiscountRequest {
- discount_id: "d".to_string(),
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- assert!(matches!(
- handle_discount_offer(
- &make_public_trade_event_with_payload(
- &seller_keys,
- TradeListingMessageType::DiscountOffer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- payload_enum_for_message(
- TradeListingMessageType::DiscountOffer,
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- Some(TEST_LISTING_EVENT_ID.to_string()),
- Some("wrong-root".to_string()),
- Some("wrong-prev".to_string()),
- ),
- TradeDiscountOffer {
- discount_id: "d".to_string(),
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Revised).await;
- assert!(matches!(
- handle_discount_decision(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountAccept,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeListingMessageType::DiscountAccept,
- TradeDiscountDecision::Decline { reason: None },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
- let result = handle_discount_decision(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountDecline,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeListingMessageType::DiscountDecline,
- TradeDiscountDecision::Accept {
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await;
- assert!(
- matches!(result, Err(TradeListingDvmError::InvalidPayload(ref message)) if message.contains("3431")),
- "unexpected discount decline mismatch result: {result:?}"
- );
-
- push_send_ok();
- assert!(
- handle_discount_decision(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountAccept,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeListingMessageType::Cancel,
- TradeDiscountDecision::Accept {
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- let (cancel_root_event_id, cancel_prev_event_id) = {
- let mut locked = state.lock().await;
- let order = locked.get_order_mut("order-1").expect("order");
- (
- order.root_event_id.clone().expect("root event"),
- order.last_event_id.clone().expect("prev event"),
- )
- };
- let cancel_by_seller = make_trade_event_with_payload_and_recipient(
- &seller_keys,
- &buyer_pub,
- TradeListingMessageType::Cancel,
- &listing_addr,
- "order-1",
- payload_enum_for_message(
- TradeListingMessageType::Cancel,
- "order-1",
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- None,
- Some(cancel_root_event_id),
- Some(cancel_prev_event_id),
- );
- push_send_ok();
- assert!(
- handle_cancel(
- &cancel_by_seller,
- TradeListingCancel { reason: None },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- set_order_status(&state, "order-1", TradeOrderStatus::Accepted).await;
- assert!(matches!(
- handle_fulfillment_update(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::FulfillmentUpdate,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Shipped,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Fulfilled).await;
- assert!(matches!(
- handle_receipt(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::Receipt,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeReceipt {
- acknowledged: true,
- at: 1,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
- }
-
- #[tokio::test]
- async fn handler_duplicate_optional_and_guard_branches_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let parsed = TradeListingAddress::parse(&listing_addr).expect("listing");
- let buyer_pub = buyer_keys.public_key().to_hex();
- let seller_pub = seller_keys.public_key().to_hex();
- let state = state_with_order(
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )
- .await;
-
- let mismatched_addr = listing_addr_for_seller(&buyer_keys);
- let mismatched_parsed =
- TradeListingAddress::parse(&mismatched_addr).expect("mismatched listing");
- let revision_event = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderRevision,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(matches!(
- handle_order_revision(
- &revision_event,
- TradeOrderRevision {
- revision_id: "r1".to_string(),
- changes: Vec::new(),
- },
- &mismatched_parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- let seen_revision_response = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRevisionAccept,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_revision_response.id.to_string()).await;
- assert!(
- handle_order_revision_response(
- &seen_revision_response,
- TradeListingMessageType::OrderRevisionAccept,
- TradeOrderRevisionResponse {
- accepted: true,
- reason: None,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- let (listing_event_id, root_event_id, prev_event_id) = workflow_state_refs(
- TradeListingMessageType::OrderRevisionAccept,
- &listing_addr,
- "order-1",
- &state,
- )
- .await;
- assert!(matches!(
- handle_order_revision_response(
- &make_public_trade_event_with_payload(
- &buyer_keys,
- TradeListingMessageType::OrderRevisionAccept,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse {
- accepted: false,
- reason: None,
- },),
- listing_event_id,
- root_event_id,
- prev_event_id,
- ),
- TradeListingMessageType::OrderRevisionAccept,
- TradeOrderRevisionResponse {
- accepted: false,
- reason: None,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
- let (listing_event_id, root_event_id, prev_event_id) = workflow_state_refs(
- TradeListingMessageType::OrderRevisionDecline,
- &listing_addr,
- "order-1",
- &state,
- )
- .await;
- assert!(matches!(
- handle_order_revision_response(
- &make_public_trade_event_with_payload(
- &buyer_keys,
- TradeListingMessageType::OrderRevisionDecline,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- TradeListingMessagePayload::OrderRevisionDecline(TradeOrderRevisionResponse {
- accepted: true,
- reason: None,
- },),
- listing_event_id,
- root_event_id,
- prev_event_id,
- ),
- TradeListingMessageType::OrderRevisionDecline,
- TradeOrderRevisionResponse {
- accepted: true,
- reason: None,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- assert!(
- handle_question(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Question,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeQuestion {
- question_id: "q1".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- let seen_question = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Question,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_question.id.to_string()).await;
- assert!(
- handle_question(
- &seen_question,
- TradeQuestion {
- question_id: "q2".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert!(matches!(
- handle_question(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::Question,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeQuestion {
- question_id: "q3".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Questioned).await;
- assert!(
- handle_answer(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::Answer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeAnswer {
- question_id: "q1".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- let seen_answer = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::Answer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_answer.id.to_string()).await;
- assert!(
- handle_answer(
- &seen_answer,
- TradeAnswer {
- question_id: "q1".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert!(matches!(
- handle_answer(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Answer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeAnswer {
- question_id: "q1".to_string(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- let seen_discount_request = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountRequest,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_discount_request.id.to_string()).await;
- assert!(
- handle_discount_request(
- &seen_discount_request,
- TradeDiscountRequest {
- discount_id: "d1".to_string(),
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert!(matches!(
- handle_discount_request(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::DiscountRequest,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeDiscountRequest {
- discount_id: "d2".to_string(),
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- let seen_discount_offer = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::DiscountOffer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_discount_offer.id.to_string()).await;
- assert!(
- handle_discount_offer(
- &seen_discount_offer,
- TradeDiscountOffer {
- discount_id: "d1".to_string(),
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert!(matches!(
- handle_discount_offer(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountOffer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeDiscountOffer {
- discount_id: "d2".to_string(),
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Revised).await;
- let seen_discount_decision = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::DiscountAccept,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_discount_decision.id.to_string()).await;
- assert!(
- handle_discount_decision(
- &seen_discount_decision,
- TradeListingMessageType::DiscountAccept,
- TradeDiscountDecision::Accept {
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert!(matches!(
- handle_discount_decision(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::DiscountAccept,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeListingMessageType::DiscountAccept,
- TradeDiscountDecision::Accept {
- value: sample_discount_value(),
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- let seen_cancel = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Cancel,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_cancel.id.to_string()).await;
- assert!(
- handle_cancel(
- &seen_cancel,
- TradeListingCancel { reason: None },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- set_order_status(&state, "order-1", TradeOrderStatus::Accepted).await;
- let seen_fulfillment = make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::FulfillmentUpdate,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_fulfillment.id.to_string()).await;
- assert!(
- handle_fulfillment_update(
- &seen_fulfillment,
- TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Shipped,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
-
- set_order_status(&state, "order-1", TradeOrderStatus::Fulfilled).await;
- let seen_receipt = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Receipt,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- mark_event_seen(&state, "order-1", seen_receipt.id.to_string()).await;
- assert!(
- handle_receipt(
- &seen_receipt,
- TradeReceipt {
- acknowledged: true,
- at: 1,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- }
-
- #[tokio::test]
- async fn fulfillment_and_receipt_handlers_follow_projection_semantics() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let parsed = TradeListingAddress::parse(&listing_addr).expect("listing");
- let buyer_pub = buyer_keys.public_key().to_hex();
- let seller_pub = seller_keys.public_key().to_hex();
- let state = state_with_order(
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Accepted,
- )
- .await;
-
- assert!(
- handle_fulfillment_update(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::FulfillmentUpdate,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Shipped,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert_eq!(
- state
- .lock()
- .await
- .get_order_mut("order-1")
- .expect("order")
- .status,
- TradeOrderStatus::Accepted
- );
-
- assert!(
- handle_fulfillment_update(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::FulfillmentUpdate,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Delivered,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert_eq!(
- state
- .lock()
- .await
- .get_order_mut("order-1")
- .expect("order")
- .status,
- TradeOrderStatus::Fulfilled
- );
-
- assert!(
- handle_receipt(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Receipt,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeReceipt {
- acknowledged: false,
- at: 1,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert_eq!(
- state
- .lock()
- .await
- .get_order_mut("order-1")
- .expect("order")
- .status,
- TradeOrderStatus::Fulfilled
- );
-
- assert!(
- handle_receipt(
- &make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::Receipt,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeReceipt {
- acknowledged: true,
- at: 2,
- },
- &parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await
- .is_ok()
- );
- assert_eq!(
- state
- .lock()
- .await
- .get_order_mut("order-1")
- .expect("order")
- .status,
- TradeOrderStatus::Completed
- );
-
- let cancelled_state = state_with_order(
- &listing_addr,
- "order-2",
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Accepted,
- )
- .await;
- assert!(
- handle_fulfillment_update(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::FulfillmentUpdate,
- &listing_addr,
- "order-2",
- &buyer_pub,
- &seller_pub,
- Some(&cancelled_state),
- )
- .await,
- TradeFulfillmentUpdate {
- status: TradeFulfillmentStatus::Cancelled,
- },
- &parsed,
- Some("order-2"),
- &client,
- &cancelled_state,
- )
- .await
- .is_ok()
- );
- assert_eq!(
- cancelled_state
- .lock()
- .await
- .get_order_mut("order-2")
- .expect("order")
- .status,
- TradeOrderStatus::Cancelled
- );
- }
-
- #[tokio::test]
- async fn dvm_remaining_edges_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let parsed = TradeListingAddress::parse(&listing_addr).expect("listing");
- let buyer_pub = buyer_keys.public_key().to_hex();
- let seller_pub = seller_keys.public_key().to_hex();
-
- let state_validate = Arc::new(AsyncMutex::new(TradeListingState::default()));
- let validate_event = make_event(
- &seller_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- "content".to_string(),
- Vec::new(),
- );
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_event_by_id_results
- .push_back(Ok(validate_event.clone()));
- push_validate_listing_ok(
- listing_addr.clone(),
- RadrootsFarmRef {
- pubkey: seller_keys.public_key().to_hex(),
- d_tag: "farmtag".to_string(),
- },
- );
- push_farm_validation_result(Ok(vec![TradeListingValidationError::MissingFarmRecord]));
- push_send_ok();
- assert!(
- handle_listing_validate_request(
- &validate_event,
- TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: "x".to_string(),
- relays: None,
- }),
- },
- &listing_addr,
- &client,
- &state_validate,
- )
- .await
- .is_ok()
- );
-
- let state = state_with_order(
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )
- .await;
- let order_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- "order-2",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
-
- let mismatch_payload = make_order(
- "order-2",
- "30402:deadbeef:AAAAAAAAAAAAAAAAAAAAAA",
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- );
- assert!(matches!(
- handle_order_request(
- &order_event,
- mismatch_payload,
- &parsed,
- Some("order-2"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::InvalidOrder)
- ));
-
- let unauthorized_payload = make_order(
- "order-3",
- &listing_addr,
- &buyer_pub,
- "not-seller",
- TradeOrderStatus::Requested,
- );
- let unauthorized_order_event = make_public_trade_event(
- &buyer_keys,
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- "order-3",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- assert!(matches!(
- handle_order_request(
- &unauthorized_order_event,
- unauthorized_payload,
- &parsed,
- Some("order-3"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- let mismatched_listing_addr = listing_addr_for_seller(&buyer_keys);
- let mismatched_parsed =
- TradeListingAddress::parse(&mismatched_listing_addr).expect("mismatched listing");
-
- set_order_status(&state, "order-1", TradeOrderStatus::Requested).await;
- assert!(matches!(
- handle_order_revision(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::OrderRevision,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeOrderRevision {
- revision_id: "r-edge".to_string(),
- changes: Vec::new(),
- },
- &mismatched_parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- set_order_status(&state, "order-1", TradeOrderStatus::Questioned).await;
- assert!(matches!(
- handle_answer(
- &make_public_trade_event(
- &seller_keys,
- TradeListingMessageType::Answer,
- &listing_addr,
- "order-1",
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await,
- TradeAnswer {
- question_id: "q-edge".to_string(),
- },
- &mismatched_parsed,
- Some("order-1"),
- &client,
- &state,
- )
- .await,
- Err(TradeListingDvmError::Unauthorized)
- ));
-
- let listing_event_new =
- RadrootsNostrEventBuilder::new(custom_trade_kind(parsed.kind), "listing-new")
- .custom_created_at(RadrootsNostrTimestamp::from(10_u64))
- .sign_with_keys(&seller_keys)
- .expect("listing new");
- let listing_event_old =
- RadrootsNostrEventBuilder::new(custom_trade_kind(parsed.kind), "listing-old")
- .custom_created_at(RadrootsNostrTimestamp::from(9_u64))
- .sign_with_keys(&seller_keys)
- .expect("listing old");
- push_fetch_events_ok(vec![listing_event_new, listing_event_old]);
- let fetched_listing = fetch_listing_by_addr(&client, &listing_addr)
- .await
- .expect("listing fetch");
- assert!(fetched_listing.is_some());
-
- let metadata_event_new =
- RadrootsNostrEventBuilder::new(RadrootsNostrKind::Metadata, "metadata-new")
- .custom_created_at(RadrootsNostrTimestamp::from(20_u64))
- .sign_with_keys(&seller_keys)
- .expect("metadata new");
- let metadata_event_old =
- RadrootsNostrEventBuilder::new(RadrootsNostrKind::Metadata, "metadata-old")
- .custom_created_at(RadrootsNostrTimestamp::from(19_u64))
- .sign_with_keys(&seller_keys)
- .expect("metadata old");
- push_fetch_events_ok(vec![metadata_event_new, metadata_event_old]);
- let latest_metadata = fetch_latest_event_by_kind(
- &client,
- RadrootsNostrFilter::new(),
- RadrootsNostrKind::Metadata,
- )
- .await
- .expect("latest metadata");
- assert!(latest_metadata.is_some());
- }
-
- #[tokio::test]
- async fn handle_event_guard_and_dispatch_error_paths_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, buyer_keys) = make_keys();
- let client = make_client(&rhi_keys);
- let listing_addr = listing_addr_for_seller(&seller_keys);
- let rhi_pub = rhi_keys.public_key().to_hex();
- let buyer_pub = buyer_keys.public_key().to_hex();
- let seller_pub = seller_keys.public_key().to_hex();
- let order_id = "order-1";
- let missing_order_id = "order-missing";
- let state = state_with_order(
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- TradeOrderStatus::Requested,
- )
- .await;
-
- let invalid_json_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- "{".to_string(),
- make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
- );
- let invalid_json_result = handle_event(
- invalid_json_event,
- make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
- assert!(matches!(
- invalid_json_result,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let invalid_envelope_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_envelope_content(
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- None,
- json!({}),
- ),
- make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
- );
- let invalid_envelope_result = handle_event(
- invalid_envelope_event,
- make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
- assert!(matches!(
- invalid_envelope_result,
- Err(TradeListingDvmError::InvalidEnvelope(_))
- | Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let missing_a_tags = vec![RadrootsNostrTag::custom(
- RadrootsNostrTagKind::custom("p"),
- vec![rhi_pub.clone()],
- )];
- let missing_a_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::OrderRequest,
- &listing_addr,
- Some(order_id),
- payload_enum_for_message(
- TradeListingMessageType::OrderRequest,
- order_id,
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- ),
- missing_a_tags.clone(),
- );
- let missing_a_result = handle_event(
- missing_a_event,
- missing_a_tags,
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
- assert!(matches!(
- missing_a_result,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let invalid_addr = "30402:badpubkey:id";
- let invalid_addr_tags = make_custom_tags(&rhi_pub, invalid_addr, Some(order_id));
- let invalid_addr_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_ORDER_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::OrderRequest,
- invalid_addr,
- Some(order_id),
- payload_enum_for_message(
- TradeListingMessageType::OrderRequest,
- order_id,
- invalid_addr,
- &buyer_pub,
- &seller_pub,
- ),
- ),
- invalid_addr_tags.clone(),
- );
- let invalid_addr_result = handle_event(
- invalid_addr_event,
- invalid_addr_tags,
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
- assert!(matches!(
- invalid_addr_result,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- let listing_validate_parse_error_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- make_envelope_content(
- TradeListingMessageType::ListingValidateRequest,
- &listing_addr,
- None,
- json!({"listing_event": 1}),
- ),
- make_custom_tags(&rhi_pub, &listing_addr, None),
- );
- let listing_validate_parse_error = handle_event(
- listing_validate_parse_error_event,
- make_custom_tags(&rhi_pub, &listing_addr, None),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
- assert!(matches!(
- listing_validate_parse_error,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
-
- push_fetch_event_by_id_error_not_found();
- let listing_validate_send_err_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::ListingValidateRequest,
- &listing_addr,
- None,
- TradeListingMessagePayload::ListingValidateRequest(TradeListingValidateRequest {
- listing_event: Some(RadrootsNostrEventPtr {
- id: "missing".to_string(),
- relays: None,
- }),
- }),
- ),
- make_custom_tags(&rhi_pub, &listing_addr, None),
- );
- assert!(matches!(
- handle_event(
- listing_validate_send_err_event,
- make_custom_tags(&rhi_pub, &listing_addr, None),
- rhi_keys.clone(),
- client.clone(),
- state.clone()
- )
- .await,
- Err(TradeListingDvmError::Nostr(_))
- ));
-
- let listing_validate_fetch_err_event = make_event(
- &buyer_keys,
- custom_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ),
- make_canonical_envelope_content(
- TradeListingMessageType::ListingValidateRequest,
- &listing_addr,
- None,
- TradeListingMessagePayload::ListingValidateRequest(TradeListingValidateRequest {
- listing_event: None,
- }),
- ),
- make_custom_tags(&rhi_pub, &listing_addr, None),
- );
- assert!(matches!(
- handle_event(
- listing_validate_fetch_err_event,
- make_custom_tags(&rhi_pub, &listing_addr, None),
- rhi_keys.clone(),
- client.clone(),
- state.clone()
- )
- .await,
- Err(TradeListingDvmError::Nostr(_))
- ));
-
- let missing_d_cases: Vec<(TradeListingMessageType, u32)> = vec![
- (
- TradeListingMessageType::OrderRequest,
- KIND_TRADE_LISTING_ORDER_REQ,
- ),
- (
- TradeListingMessageType::OrderResponse,
- KIND_TRADE_LISTING_ORDER_RES,
- ),
- (
- TradeListingMessageType::OrderRevision,
- KIND_TRADE_LISTING_ORDER_REVISION_REQ,
- ),
- (
- TradeListingMessageType::OrderRevisionAccept,
- KIND_TRADE_LISTING_ORDER_REVISION_RES,
- ),
- (
- TradeListingMessageType::OrderRevisionDecline,
- KIND_TRADE_LISTING_ORDER_REVISION_RES,
- ),
- (
- TradeListingMessageType::Question,
- KIND_TRADE_LISTING_QUESTION_REQ,
- ),
- (
- TradeListingMessageType::Answer,
- KIND_TRADE_LISTING_ANSWER_RES,
- ),
- (
- TradeListingMessageType::DiscountRequest,
- KIND_TRADE_LISTING_DISCOUNT_REQ,
- ),
- (
- TradeListingMessageType::DiscountOffer,
- KIND_TRADE_LISTING_DISCOUNT_OFFER_RES,
- ),
- (
- TradeListingMessageType::DiscountAccept,
- KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ,
- ),
- (
- TradeListingMessageType::Cancel,
- KIND_TRADE_LISTING_CANCEL_REQ,
- ),
- (
- TradeListingMessageType::FulfillmentUpdate,
- KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ,
- ),
- (
- TradeListingMessageType::Receipt,
- KIND_TRADE_LISTING_RECEIPT_REQ,
- ),
- ];
- for (message_type, kind) in missing_d_cases {
- let sender = sender_for_message(message_type, &seller_keys, &buyer_keys);
- let event = make_event(
- sender,
- custom_trade_kind(kind),
- make_canonical_envelope_content(
- message_type,
- &listing_addr,
- Some(order_id),
- payload_enum_for_message(
- message_type,
- order_id,
- &listing_addr,
- &buyer_pub,
- &seller_pub,
- ),
- ),
- make_custom_tags(&rhi_pub, &listing_addr, None),
- );
- let result = handle_event(
- event,
- make_custom_tags(&rhi_pub, &listing_addr, None),
- rhi_keys.clone(),
- client.clone(),
- state.clone(),
- )
- .await;
- if message_type == TradeListingMessageType::OrderRequest {
- assert!(matches!(
- result,
- Err(TradeListingDvmError::InvalidPayload(_))
- ));
- } else {
- assert!(matches!(result, Err(TradeListingDvmError::MissingTag("d"))));
- }
- }
-
- let missing_order_cases: Vec<TradeListingMessageType> = vec![
- TradeListingMessageType::OrderResponse,
- TradeListingMessageType::OrderRevision,
- TradeListingMessageType::OrderRevisionAccept,
- TradeListingMessageType::OrderRevisionDecline,
- TradeListingMessageType::Question,
- TradeListingMessageType::Answer,
- TradeListingMessageType::DiscountRequest,
- TradeListingMessageType::DiscountOffer,
- TradeListingMessageType::DiscountAccept,
- TradeListingMessageType::Cancel,
- TradeListingMessageType::FulfillmentUpdate,
- TradeListingMessageType::Receipt,
- ];
- for message_type in missing_order_cases {
- let sender = sender_for_message(message_type, &seller_keys, &buyer_keys);
- let (event, tags) = make_handle_event_trade_event(
- sender,
- message_type,
- &listing_addr,
- missing_order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- let result =
- handle_event(event, tags, rhi_keys.clone(), client.clone(), state.clone()).await;
- assert!(
- matches!(
- result,
- Err(TradeListingDvmError::State(
- TradeListingStateError::MissingOrder
- ))
- ),
- "{message_type:?}: {result:?}"
- );
- }
-
- let transition_cases: Vec<(TradeListingMessageType, TradeOrderStatus)> = vec![
- (
- TradeListingMessageType::OrderResponse,
- TradeOrderStatus::Completed,
- ),
- (
- TradeListingMessageType::OrderRevision,
- TradeOrderStatus::Completed,
- ),
- (
- TradeListingMessageType::OrderRevisionAccept,
- TradeOrderStatus::Completed,
- ),
- (
- TradeListingMessageType::OrderRevisionDecline,
- TradeOrderStatus::Completed,
- ),
- (
- TradeListingMessageType::Question,
- TradeOrderStatus::Completed,
- ),
- (TradeListingMessageType::Answer, TradeOrderStatus::Completed),
- (
- TradeListingMessageType::DiscountOffer,
- TradeOrderStatus::Completed,
- ),
- (
- TradeListingMessageType::DiscountAccept,
- TradeOrderStatus::Completed,
- ),
- (TradeListingMessageType::Cancel, TradeOrderStatus::Completed),
- (
- TradeListingMessageType::FulfillmentUpdate,
- TradeOrderStatus::Completed,
- ),
- (
- TradeListingMessageType::Receipt,
- TradeOrderStatus::Requested,
- ),
- ];
- for (message_type, status_before) in transition_cases {
- set_order_status(&state, order_id, status_before).await;
- let sender = sender_for_message(message_type, &seller_keys, &buyer_keys);
- let (event, tags) = make_handle_event_trade_event(
- sender,
- message_type,
- &listing_addr,
- order_id,
- &buyer_pub,
- &seller_pub,
- Some(&state),
- )
- .await;
- let result =
- handle_event(event, tags, rhi_keys.clone(), client.clone(), state.clone()).await;
- assert!(matches!(
- result,
- Err(TradeListingDvmError::State(
- TradeListingStateError::InvalidTransition { .. }
- ))
- ));
- }
- }
-
- #[tokio::test]
- async fn fetch_listing_by_addr_error_regions_are_covered() {
- let _guard = test_guard();
- let (rhi_keys, seller_keys, _) = make_keys();
- let client = make_client(&rhi_keys);
-
- let invalid_author_result = fetch_listing_by_addr(&client, "30402:not_a_pubkey:list");
- assert!(matches!(
- invalid_author_result.await,
- Err(TradeListingDvmError::InvalidListingAddr)
- ));
-
- let listing_addr = listing_addr_for_seller(&seller_keys);
- dvm_test_hooks()
- .lock()
- .expect("hooks")
- .fetch_events_results
- .push_back(Err(TradeListingDvmError::InvalidOrder));
- let fetch_error_result = fetch_listing_by_addr(&client, &listing_addr).await;
- assert!(matches!(
- fetch_error_result,
- Err(TradeListingDvmError::InvalidOrder)
- ));
}
}
diff --git a/src/features/trade_listing/state.rs b/src/features/trade_listing/state.rs
@@ -4,7 +4,6 @@ use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
-use radroots_events::trade::RadrootsTradeOrderStatus as TradeOrderStatus;
use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrTimestamp};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -14,6 +13,17 @@ pub type SharedTradeListingState = Arc<Mutex<TradeListingState>>;
const TRADE_LISTING_STATE_VERSION: u32 = 1;
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum TradeOrderStatus {
+ Requested,
+ Accepted,
+ Declined,
+ Cancelled,
+ Completed,
+ Disputed,
+ Invalid,
+}
+
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TradeOrderState {
pub order_id: String,
@@ -341,9 +351,8 @@ mod tests {
use super::{
ListingEventState, PersistedTradeListingState, TradeListingRuntime,
TradeListingRuntimeConfig, TradeListingRuntimeError, TradeListingState,
- TradeListingStateError, TradeOrderState, ValidatedListingState,
+ TradeListingStateError, TradeOrderState, TradeOrderStatus, ValidatedListingState,
};
- use radroots_events::trade::RadrootsTradeOrderStatus as TradeOrderStatus;
use std::collections::{HashMap, HashSet};
fn unique_state_path(suffix: &str) -> std::path::PathBuf {
diff --git a/src/features/trade_listing/subscriber.rs b/src/features/trade_listing/subscriber.rs
@@ -6,7 +6,8 @@ use std::time::Duration;
use anyhow::{Result, anyhow};
use radroots_events::kinds::{
- KIND_LISTING, KIND_LISTING_DRAFT, TRADE_LISTING_KINDS, is_trade_service_kind,
+ KIND_LISTING, KIND_LISTING_DRAFT, ORDER_EVENT_KINDS, TRADE_VALIDATION_EVENT_KINDS,
+ is_trade_validation_service_event_kind,
};
use radroots_nostr::prelude::{
RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKeys,
@@ -230,7 +231,7 @@ async fn process_event_notification(
match err {
TradeListingDvmError::MissingRecipient | TradeListingDvmError::UnsupportedKind => {}
other => {
- if event_kind.is_some_and(is_trade_service_kind) {
+ if event_kind.is_some_and(is_trade_validation_service_event_kind) {
if let Err(err) = handle_error_io(other, &event, &client).await {
warn!("trade_listing: failed to send error feedback: {err}");
}
@@ -266,10 +267,11 @@ pub async fn subscriber(
) -> Result<()> {
let subscribed_kinds = [KIND_LISTING, KIND_LISTING_DRAFT]
.into_iter()
- .chain(TRADE_LISTING_KINDS)
+ .chain(ORDER_EVENT_KINDS)
+ .chain(TRADE_VALIDATION_EVENT_KINDS)
.collect::<Vec<_>>();
info!(
- "Starting subscriber for trade listing and public trade kinds: {:?}",
+ "Starting subscriber for trade listing, order, and trade validation kinds: {:?}",
subscribed_kinds
);
diff --git a/src/features/trade_validation_receipt.rs b/src/features/trade_validation_receipt.rs
@@ -2,15 +2,15 @@
#![cfg_attr(coverage_nightly, coverage(off))]
use radroots_events::kinds::{
- KIND_TRADE_VALIDATION_RECEIPT, KIND_WORKER_TRADE_TRANSITION_PROOF_REQ,
- KIND_WORKER_TRADE_TRANSITION_PROOF_RES, is_listing_kind,
+ KIND_TRADE_TRANSITION_PROOF_REQUEST, KIND_TRADE_TRANSITION_PROOF_RESULT,
+ KIND_TRADE_VALIDATION_RECEIPT, is_listing_kind,
};
-use radroots_events::trade::{
- RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderRequested,
+use radroots_events::order::{
+ RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderRequest,
};
-use radroots_events_codec::trade::{
- active_trade_order_decision_from_event, active_trade_order_request_from_event,
- parse_trade_listing_event_tag, parse_trade_prev_tag, parse_trade_root_tag,
+use radroots_events_codec::order::{
+ order_decision_from_event, order_request_from_event, parse_order_listing_event_tag,
+ parse_order_prev_tag, parse_order_root_tag,
};
use radroots_nostr::prelude::{
RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys,
@@ -470,7 +470,7 @@ pub async fn handle_trade_validation_receipt_job_request(
prover_policy: &TradeValidationReceiptProverPolicy,
) -> Result<(), TradeValidationReceiptJobError> {
let kind = event_kind_u32(event)?;
- if kind != KIND_WORKER_TRADE_TRANSITION_PROOF_REQ {
+ if kind != KIND_TRADE_TRANSITION_PROOF_REQUEST {
return Err(TradeValidationReceiptJobError::UnsupportedKind);
}
@@ -503,15 +503,14 @@ pub async fn handle_trade_validation_receipt_job_request(
let request_rr = radroots_event_from_nostr(&order_request_event);
let decision_rr = radroots_event_from_nostr(&order_decision_event);
- let request_envelope = active_trade_order_request_from_event(&request_rr).map_err(|error| {
+ let request_envelope = order_request_from_event(&request_rr).map_err(|error| {
+ TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
+ })?;
+ let decision_envelope = order_decision_from_event(&decision_rr).map_err(|error| {
TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
})?;
- let decision_envelope =
- active_trade_order_decision_from_event(&decision_rr).map_err(|error| {
- TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
- })?;
- let listing_event_ptr = parse_trade_listing_event_tag(&request_rr.tags)
+ let listing_event_ptr = parse_order_listing_event_tag(&request_rr.tags)
.map_err(|error| {
TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
})?
@@ -520,10 +519,10 @@ pub async fn handle_trade_validation_receipt_job_request(
return Err(TradeValidationReceiptJobError::EventSetMismatch);
}
- let root_event_id = parse_trade_root_tag(&decision_rr.tags).map_err(|error| {
+ let root_event_id = parse_order_root_tag(&decision_rr.tags).map_err(|error| {
TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
})?;
- let prev_event_id = parse_trade_prev_tag(&decision_rr.tags).map_err(|error| {
+ let prev_event_id = parse_order_prev_tag(&decision_rr.tags).map_err(|error| {
TradeValidationReceiptJobError::InvalidActiveTradeEvent(error.to_string())
})?;
if root_event_id.as_deref() != Some(request.request_event_id.as_str())
@@ -613,7 +612,7 @@ pub async fn handle_trade_validation_receipt_job_request(
let result_tags = result_tags(event, &receipt_event_id, &result);
publish_event_parts_io(
client,
- KIND_WORKER_TRADE_TRANSITION_PROOF_RES,
+ KIND_TRADE_TRANSITION_PROOF_RESULT,
result_content,
result_tags,
)
@@ -711,7 +710,7 @@ fn hex_lower(bytes: &[u8]) -> String {
}
fn order_request_witness_from_payload(
- payload: RadrootsTradeOrderRequested,
+ payload: RadrootsOrderRequest,
) -> RadrootsSp1TradeOrderRequestWitness {
RadrootsSp1TradeOrderRequestWitness {
order_id: payload.order_id,
@@ -730,7 +729,7 @@ fn order_request_witness_from_payload(
}
fn order_decision_witness_from_payload(
- payload: RadrootsTradeOrderDecisionEvent,
+ payload: RadrootsOrderDecision,
) -> RadrootsSp1TradeOrderDecisionEventWitness {
RadrootsSp1TradeOrderDecisionEventWitness {
order_id: payload.order_id,
@@ -738,7 +737,7 @@ fn order_decision_witness_from_payload(
buyer_pubkey: payload.buyer_pubkey,
seller_pubkey: payload.seller_pubkey,
decision: match payload.decision {
- RadrootsTradeOrderDecision::Accepted {
+ RadrootsOrderDecisionOutcome::Accepted {
inventory_commitments,
} => RadrootsSp1TradeOrderDecisionWitness::Accepted {
inventory_commitments: inventory_commitments
@@ -749,7 +748,7 @@ fn order_decision_witness_from_payload(
})
.collect(),
},
- RadrootsTradeOrderDecision::Declined { reason } => {
+ RadrootsOrderDecisionOutcome::Declined { reason } => {
RadrootsSp1TradeOrderDecisionWitness::Declined { reason }
}
},
@@ -1512,18 +1511,15 @@ mod tests {
};
use radroots_events::RadrootsNostrEventPtr;
use radroots_events::kinds::{
- KIND_LISTING, KIND_TRADE_VALIDATION_RECEIPT, KIND_WORKER_TRADE_TRANSITION_PROOF_REQ,
- KIND_WORKER_TRADE_TRANSITION_PROOF_RES,
- };
- use radroots_events::trade::{
- RadrootsTradeInventoryCommitment, RadrootsTradeOrderDecision,
- RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem,
- RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem,
- RadrootsTradeOrderRequested, RadrootsTradePricingBasis,
+ KIND_LISTING, KIND_TRADE_TRANSITION_PROOF_REQUEST, KIND_TRADE_TRANSITION_PROOF_RESULT,
+ KIND_TRADE_VALIDATION_RECEIPT,
};
- use radroots_events_codec::trade::{
- active_trade_order_decision_event_build, active_trade_order_request_event_build,
+ use radroots_events::order::{
+ RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem,
+ RadrootsOrderEconomicLine, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment,
+ RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
};
+ use radroots_events_codec::order::{order_decision_event_build, order_request_event_build};
use radroots_nostr::prelude::{
RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKeys,
RadrootsNostrKind, RadrootsNostrTag, RadrootsNostrTagKind, radroots_event_from_nostr,
@@ -1597,13 +1593,13 @@ mod tests {
listing_addr: &str,
buyer: &RadrootsNostrKeys,
seller: &RadrootsNostrKeys,
- ) -> RadrootsTradeOrderRequested {
- RadrootsTradeOrderRequested {
+ ) -> RadrootsOrderRequest {
+ RadrootsOrderRequest {
order_id: order_id.to_string(),
listing_addr: listing_addr.to_string(),
buyer_pubkey: buyer.public_key().to_hex(),
seller_pubkey: seller.public_key().to_hex(),
- items: vec![RadrootsTradeOrderItem {
+ items: vec![RadrootsOrderItem {
bin_id: "bin-1".to_string(),
bin_count: 2,
}],
@@ -1616,14 +1612,14 @@ mod tests {
listing_addr: &str,
buyer: &RadrootsNostrKeys,
seller: &RadrootsNostrKeys,
- ) -> RadrootsTradeOrderDecisionEvent {
- RadrootsTradeOrderDecisionEvent {
+ ) -> RadrootsOrderDecision {
+ RadrootsOrderDecision {
order_id: order_id.to_string(),
listing_addr: listing_addr.to_string(),
buyer_pubkey: buyer.public_key().to_hex(),
seller_pubkey: seller.public_key().to_hex(),
- decision: RadrootsTradeOrderDecision::Accepted {
- inventory_commitments: vec![RadrootsTradeInventoryCommitment {
+ decision: RadrootsOrderDecisionOutcome::Accepted {
+ inventory_commitments: vec![RadrootsOrderInventoryCommitment {
bin_id: "bin-1".to_string(),
bin_count: 2,
}],
@@ -1631,15 +1627,15 @@ mod tests {
}
}
- fn economics(order_id: &str, bin_count: u32) -> RadrootsTradeOrderEconomics {
+ fn economics(order_id: &str, bin_count: u32) -> RadrootsOrderEconomics {
let subtotal = RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count);
let money = RadrootsCoreMoney::new(subtotal, RadrootsCoreCurrency::USD);
- RadrootsTradeOrderEconomics {
+ RadrootsOrderEconomics {
quote_id: format!("{order_id}-quote"),
quote_version: 1,
- pricing_basis: RadrootsTradePricingBasis::ListingEvent,
+ pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
currency: RadrootsCoreCurrency::USD,
- items: vec![RadrootsTradeOrderEconomicItem {
+ items: vec![RadrootsOrderEconomicItem {
bin_id: "bin-1".to_string(),
bin_count,
quantity_amount: RadrootsCoreDecimal::from(1u32),
@@ -1648,8 +1644,8 @@ mod tests {
unit_price_currency: RadrootsCoreCurrency::USD,
line_subtotal: money.clone(),
}],
- discounts: Vec::<RadrootsTradeOrderEconomicLine>::new(),
- adjustments: Vec::<RadrootsTradeOrderEconomicLine>::new(),
+ discounts: Vec::<RadrootsOrderEconomicLine>::new(),
+ adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
subtotal: money.clone(),
discount_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
adjustment_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
@@ -1668,7 +1664,7 @@ mod tests {
id: listing_event.id.to_hex(),
relays: None,
};
- let request_wire = active_trade_order_request_event_build(
+ let request_wire = order_request_event_build(
&listing_ptr,
&request_payload(order_id, &listing_addr, buyer, seller),
)
@@ -1679,7 +1675,7 @@ mod tests {
request_wire.content,
request_wire.tags,
);
- let decision_wire = active_trade_order_decision_event_build(
+ let decision_wire = order_decision_event_build(
&request_event.id.to_hex(),
&request_event.id.to_hex(),
&decision_payload(order_id, &listing_addr, buyer, seller),
@@ -1727,7 +1723,7 @@ mod tests {
};
signed_event(
requester,
- KIND_WORKER_TRADE_TRANSITION_PROOF_REQ,
+ KIND_TRADE_TRANSITION_PROOF_REQUEST,
serde_json::to_string(&request).expect("job json"),
vec![vec!["p".to_string(), worker.public_key().to_string()]],
)
@@ -2060,7 +2056,7 @@ mod tests {
assert_eq!(published.len(), 2);
assert_eq!(published[0].kind, KIND_TRADE_VALIDATION_RECEIPT);
- assert_eq!(published[1].kind, KIND_WORKER_TRADE_TRANSITION_PROOF_RES);
+ assert_eq!(published[1].kind, KIND_TRADE_TRANSITION_PROOF_RESULT);
let result: TradeValidationReceiptJobResult =
serde_json::from_str(&published[1].content).expect("result json");
assert_eq!(
@@ -2585,7 +2581,7 @@ mod tests {
.clone();
assert_eq!(published.len(), 2);
assert_eq!(published[0].kind, KIND_TRADE_VALIDATION_RECEIPT);
- assert_eq!(published[1].kind, KIND_WORKER_TRADE_TRANSITION_PROOF_RES);
+ assert_eq!(published[1].kind, KIND_TRADE_TRANSITION_PROOF_RESULT);
let receipt_event = radroots_events::RadrootsNostrEvent {
id: publish_result_id(1),
@@ -2721,7 +2717,7 @@ mod tests {
request_json["prover_backend"] = serde_json::Value::String("local_cpu_prove".to_string());
let job = signed_event(
&requester,
- KIND_WORKER_TRADE_TRANSITION_PROOF_REQ,
+ KIND_TRADE_TRANSITION_PROOF_REQUEST,
serde_json::to_string(&request_json).expect("request json"),
vec![vec!["p".to_string(), worker.public_key().to_string()]],
);
@@ -2882,7 +2878,7 @@ mod tests {
"0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string();
let job = signed_event(
&requester,
- KIND_WORKER_TRADE_TRANSITION_PROOF_REQ,
+ KIND_TRADE_TRANSITION_PROOF_REQUEST,
serde_json::to_string(&request).expect("job json"),
vec![vec!["p".to_string(), worker.public_key().to_string()]],
);
@@ -2991,7 +2987,7 @@ mod tests {
let worker = RadrootsNostrKeys::generate();
let requester = RadrootsNostrKeys::generate();
let job = RadrootsNostrEventBuilder::new(
- RadrootsNostrKind::Custom(KIND_WORKER_TRADE_TRANSITION_PROOF_REQ as u16),
+ RadrootsNostrKind::Custom(KIND_TRADE_TRANSITION_PROOF_REQUEST as u16),
"{}",
)
.tags(vec![RadrootsNostrTag::custom(
@@ -3030,7 +3026,7 @@ mod tests {
}
#[test]
- fn signed_events_are_canonical_active_trade_events() {
+ fn signed_events_are_canonical_order_events() {
let _guard = test_guard();
let buyer = RadrootsNostrKeys::generate();
let seller = RadrootsNostrKeys::generate();
@@ -3039,7 +3035,7 @@ mod tests {
let request_rr = radroots_event_from_nostr(&request_event);
let decision_rr = radroots_event_from_nostr(&decision_event);
assert!(
- active_trade_order_request_event_build(
+ order_request_event_build(
&RadrootsNostrEventPtr {
id: listing_event.id.to_hex(),
relays: None,
diff --git a/src/lib.rs b/src/lib.rs
@@ -13,7 +13,9 @@ pub mod rhi;
pub use cli::Args as cli_args;
use anyhow::Result;
-use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT, TRADE_LISTING_KINDS};
+use radroots_events::kinds::{
+ KIND_LISTING, KIND_LISTING_DRAFT, ORDER_EVENT_KINDS, TRADE_VALIDATION_EVENT_KINDS,
+};
use std::time::Duration;
use crate::features::trade_listing::state::{TradeListingRuntime, TradeListingRuntimeConfig};
@@ -141,7 +143,8 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()>
if !relays.is_empty() {
let handler_kinds = [KIND_LISTING, KIND_LISTING_DRAFT]
.into_iter()
- .chain(TRADE_LISTING_KINDS)
+ .chain(ORDER_EVENT_KINDS)
+ .chain(TRADE_VALIDATION_EVENT_KINDS)
.collect();
let handler_spec = RadrootsNostrApplicationHandlerSpec {
kinds: handler_kinds,