lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 33449d17fffd2a73b4b1ab02bb1369a62b3c6ebd
parent bf4ac52aaaca6e23430633fe53b22ac459f10117
Author: triesap <tyson@radroots.org>
Date:   Fri, 12 Jun 2026 18:55:05 -0700

events: refactor trade events into order contracts

- split public event APIs into listing, order, order economics, trade validation, and contract modules
- move codecs, SDK facades, conformance vectors, and export metadata onto order and trade-validation domains
- delete broad public trade envelopes, alias modules, and legacy active-trade source names
- update coverage policy and CI guardrails for the heavy-development first-pass refactor

Diffstat:
Acrates/events/src/contract.rs | 620+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events/src/kinds.rs | 592+++++++++++++++++++++++++------------------------------------------------------
Mcrates/events/src/lib.rs | 5++++-
Acrates/events/src/order.rs | 2459++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/events/src/order_economics.rs | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/events/src/trade.rs | 3568-------------------------------------------------------------------------------
Acrates/events/src/trade_validation.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events_codec/src/lib.rs | 2+-
Acrates/events_codec/src/order/decode.rs | 1521+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/events_codec/src/order/encode.rs | 329+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/events_codec/src/order/mod.rs | 25+++++++++++++++++++++++++
Acrates/events_codec/src/order/tags.rs | 671+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/events_codec/src/trade/decode.rs | 1932-------------------------------------------------------------------------------
Dcrates/events_codec/src/trade/encode.rs | 403-------------------------------------------------------------------------------
Dcrates/events_codec/src/trade/mod.rs | 29-----------------------------
Dcrates/events_codec/src/trade/tags.rs | 671-------------------------------------------------------------------------------
Mcrates/sdk/src/adapters/radrootsd.rs | 740+------------------------------------------------------------------------------
Mcrates/sdk/src/client.rs | 505++++++++++++++-----------------------------------------------------------------
Mcrates/sdk/src/lib.rs | 15+++++----------
Mcrates/sdk/src/listing.rs | 7+++----
Acrates/sdk/src/order.rs | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/sdk/src/trade.rs | 333-------------------------------------------------------------------------------
Mcrates/sdk/tests/client.rs | 227++++++++++++++++++++++++++++++++++++-------------------------------------------
Mcrates/sdk/tests/facade.rs | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mcrates/sdk/tests/radrootsd.rs | 305+++++++++++--------------------------------------------------------------------
Mcrates/sdk/tests/relay_direct.rs | 143++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mcrates/trade/src/lib.rs | 1-
Mcrates/trade/src/listing/codec.rs | 257++++++++++++++++++++++++++++++++++++-------------------------------------------
Dcrates/trade/src/listing/contract.rs | 70----------------------------------------------------------------------
Dcrates/trade/src/listing/dvm.rs | 1208------------------------------------------------------------------------------
Dcrates/trade/src/listing/kinds.rs | 210-------------------------------------------------------------------------------
Mcrates/trade/src/listing/mod.rs | 17+++++------------
Dcrates/trade/src/listing/order.rs | 127-------------------------------------------------------------------------------
Dcrates/trade/src/listing/overlay.rs | 1194-------------------------------------------------------------------------------
Dcrates/trade/src/listing/projection.rs | 5766-------------------------------------------------------------------------------
Dcrates/trade/src/listing/tags.rs | 330-------------------------------------------------------------------------------
Mcrates/trade/src/listing/validation.rs | 10+++++-----
Mcrates/trade/src/order.rs | 2595++++++++++++++++++++++++++++++++++++-------------------------------------------
Mcrates/trade/src/prelude.rs | 1-
Dcrates/trade/src/public_trade.rs | 182-------------------------------------------------------------------------------
Mcrates/trade/src/validation_receipt.rs | 12++++++------
Mcrates/xtask/src/contract.rs | 191+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpolicy/coverage/policy.toml | 4++--
Mscripts/ci/guard_no_legacy_identifiers.sh | 44++++++++++++++++++++++++++++++++------------
Mspec/RCLD.md | 62+++++++++++++++++++++++++++++++-------------------------------
Aspec/conformance/vectors/order/build_order_decision_draft.v1.json | 36++++++++++++++++++++++++++++++++++++
Aspec/conformance/vectors/order/build_order_request_draft.v1.json | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspec/conformance/vectors/order/parse_listing_address.v1.json | 16++++++++++++++++
Aspec/conformance/vectors/order/parse_order_decision.v1.json | 22++++++++++++++++++++++
Aspec/conformance/vectors/order/parse_order_request.v1.json | 31+++++++++++++++++++++++++++++++
Dspec/conformance/vectors/trade/.gitkeep | 1-
Dspec/conformance/vectors/trade/build_envelope_draft.v1.json | 23-----------------------
Dspec/conformance/vectors/trade/build_order_decision_draft.v1.json | 36------------------------------------
Dspec/conformance/vectors/trade/build_order_request_draft.v1.json | 74--------------------------------------------------------------------------
Dspec/conformance/vectors/trade/parse_envelope.v1.json | 20--------------------
Dspec/conformance/vectors/trade/parse_listing_address.v1.json | 16----------------
Dspec/conformance/vectors/trade/parse_order_decision.v1.json | 22----------------------
Dspec/conformance/vectors/trade/parse_order_request.v1.json | 31-------------------------------
Dspec/conformance/vectors/trade/validate_listing_event.v1.json | 20--------------------
Aspec/conformance/vectors/trade_validation/validate_listing_event.v1.json | 20++++++++++++++++++++
Mspec/operations.toml | 191+++++++++++++++++++++++++++++--------------------------------------------------
Mspec/sdk-exports/go.toml | 47++++++++++++++++++++++-------------------------
Mspec/sdk-exports/kotlin.toml | 47++++++++++++++++++++++-------------------------
Mspec/sdk-exports/py.toml | 47++++++++++++++++++++++-------------------------
Mspec/sdk-exports/swift.toml | 47++++++++++++++++++++++-------------------------
Mspec/sdk-exports/ts.toml | 47++++++++++++++++++++++-------------------------
66 files changed, 8535 insertions(+), 20291 deletions(-)

diff --git a/crates/events/src/contract.rs b/crates/events/src/contract.rs @@ -0,0 +1,620 @@ +#![forbid(unsafe_code)] + +use crate::kinds::{ + KIND_LISTING, KIND_LISTING_DRAFT, 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, KIND_TRADE_VALIDATION_RECEIPT, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsEventClass { + Regular, + Replaceable, + Addressable, + Ephemeral, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsEventPrivacy { + Public, + Encrypted, + LocalOnly, + Secret, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsEventStability { + Stable, + Experimental, + Deprecated, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsActorRole { + Any, + Farmer, + Seller, + Buyer, + Service, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsReducer { + ListingProjection, + MarketProjection, + OrderProjection, + ListingInventoryAccounting, + TradeValidation, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsTagCardinality { + RequiredOne, + OptionalOne, + OptionalMany, + RequiredMany, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsTagSemantic { + Identifier, + Counterparty, + ListingAddress, + RootEvent, + PreviousEvent, + ListingSnapshot, + Title, + Summary, + PublishedAt, + Location, + Price, + Status, + Category, + Image, + ServiceInput, + ServiceOutput, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RadrootsTagContract { + pub name: &'static str, + pub cardinality: RadrootsTagCardinality, + pub semantic: RadrootsTagSemantic, + pub relay_indexed: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RadrootsEventContract { + pub id: &'static str, + pub kind: u32, + pub name: &'static str, + pub payload_type: &'static str, + pub class: RadrootsEventClass, + pub stability: RadrootsEventStability, + pub privacy: RadrootsEventPrivacy, + pub author_role: RadrootsActorRole, + pub tags: &'static [RadrootsTagContract], + pub reducers: &'static [RadrootsReducer], +} + +const LISTING_REDUCERS: &[RadrootsReducer] = &[ + RadrootsReducer::ListingProjection, + RadrootsReducer::MarketProjection, + RadrootsReducer::ListingInventoryAccounting, +]; +const ORDER_REDUCERS: &[RadrootsReducer] = &[ + RadrootsReducer::OrderProjection, + RadrootsReducer::ListingInventoryAccounting, +]; +const TRADE_VALIDATION_REDUCERS: &[RadrootsReducer] = &[RadrootsReducer::TradeValidation]; + +const LISTING_TAGS: &[RadrootsTagContract] = &[ + RadrootsTagContract { + name: "d", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::Identifier, + relay_indexed: true, + }, + RadrootsTagContract { + name: "title", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::Title, + relay_indexed: true, + }, + RadrootsTagContract { + name: "summary", + cardinality: RadrootsTagCardinality::OptionalOne, + semantic: RadrootsTagSemantic::Summary, + relay_indexed: true, + }, + RadrootsTagContract { + name: "published_at", + cardinality: RadrootsTagCardinality::OptionalOne, + semantic: RadrootsTagSemantic::PublishedAt, + relay_indexed: true, + }, + RadrootsTagContract { + name: "location", + cardinality: RadrootsTagCardinality::OptionalMany, + semantic: RadrootsTagSemantic::Location, + relay_indexed: true, + }, + RadrootsTagContract { + name: "price", + cardinality: RadrootsTagCardinality::OptionalMany, + semantic: RadrootsTagSemantic::Price, + relay_indexed: true, + }, + RadrootsTagContract { + name: "status", + cardinality: RadrootsTagCardinality::OptionalOne, + semantic: RadrootsTagSemantic::Status, + relay_indexed: true, + }, + RadrootsTagContract { + name: "category", + cardinality: RadrootsTagCardinality::OptionalMany, + semantic: RadrootsTagSemantic::Category, + relay_indexed: true, + }, + RadrootsTagContract { + name: "image", + cardinality: RadrootsTagCardinality::OptionalMany, + semantic: RadrootsTagSemantic::Image, + relay_indexed: true, + }, +]; + +const ORDER_REQUEST_TAGS: &[RadrootsTagContract] = &[ + RadrootsTagContract { + name: "d", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::Identifier, + relay_indexed: true, + }, + RadrootsTagContract { + name: "p", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::Counterparty, + relay_indexed: true, + }, + RadrootsTagContract { + name: "a", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ListingAddress, + relay_indexed: true, + }, + RadrootsTagContract { + name: "listing_event", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ListingSnapshot, + relay_indexed: false, + }, +]; + +const CHAINED_ORDER_TAGS: &[RadrootsTagContract] = &[ + RadrootsTagContract { + name: "d", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::Identifier, + relay_indexed: true, + }, + RadrootsTagContract { + name: "p", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::Counterparty, + relay_indexed: true, + }, + RadrootsTagContract { + name: "a", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ListingAddress, + relay_indexed: true, + }, + RadrootsTagContract { + name: "e", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::RootEvent, + relay_indexed: true, + }, + RadrootsTagContract { + name: "e", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::PreviousEvent, + relay_indexed: true, + }, +]; + +const TRADE_VALIDATION_REQUEST_TAGS: &[RadrootsTagContract] = &[ + RadrootsTagContract { + name: "i", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ServiceInput, + relay_indexed: true, + }, + RadrootsTagContract { + name: "a", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ListingAddress, + relay_indexed: true, + }, +]; + +const TRADE_VALIDATION_RESULT_TAGS: &[RadrootsTagContract] = &[ + RadrootsTagContract { + name: "request", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ServiceInput, + relay_indexed: true, + }, + RadrootsTagContract { + name: "output", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ServiceOutput, + relay_indexed: false, + }, +]; + +const TRADE_VALIDATION_RECEIPT_TAGS: &[RadrootsTagContract] = &[ + RadrootsTagContract { + name: "e", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::RootEvent, + relay_indexed: true, + }, + RadrootsTagContract { + name: "a", + cardinality: RadrootsTagCardinality::OptionalOne, + semantic: RadrootsTagSemantic::ListingAddress, + relay_indexed: true, + }, + RadrootsTagContract { + name: "output", + cardinality: RadrootsTagCardinality::RequiredOne, + semantic: RadrootsTagSemantic::ServiceOutput, + relay_indexed: false, + }, +]; + +const fn contract( + id: &'static str, + kind: u32, + name: &'static str, + payload_type: &'static str, + class: RadrootsEventClass, + author_role: RadrootsActorRole, + tags: &'static [RadrootsTagContract], + reducers: &'static [RadrootsReducer], +) -> RadrootsEventContract { + RadrootsEventContract { + id, + kind, + name, + payload_type, + class, + stability: RadrootsEventStability::Stable, + privacy: RadrootsEventPrivacy::Public, + author_role, + tags, + reducers, + } +} + +const LISTING_CONTRACT: RadrootsEventContract = contract( + "listing", + KIND_LISTING, + "Listing", + "RadrootsListing", + RadrootsEventClass::Addressable, + RadrootsActorRole::Seller, + LISTING_TAGS, + LISTING_REDUCERS, +); +const LISTING_DRAFT_CONTRACT: RadrootsEventContract = contract( + "listing_draft", + KIND_LISTING_DRAFT, + "Listing Draft", + "RadrootsListing", + RadrootsEventClass::Addressable, + RadrootsActorRole::Seller, + LISTING_TAGS, + LISTING_REDUCERS, +); +const ORDER_REQUEST_CONTRACT: RadrootsEventContract = contract( + "order_request", + KIND_ORDER_REQUEST, + "Order Request", + "RadrootsOrderRequest", + RadrootsEventClass::Regular, + RadrootsActorRole::Buyer, + ORDER_REQUEST_TAGS, + ORDER_REDUCERS, +); +const ORDER_DECISION_CONTRACT: RadrootsEventContract = contract( + "order_decision", + KIND_ORDER_DECISION, + "Order Decision", + "RadrootsOrderDecision", + RadrootsEventClass::Regular, + RadrootsActorRole::Seller, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const ORDER_REVISION_PROPOSAL_CONTRACT: RadrootsEventContract = contract( + "order_revision_proposal", + KIND_ORDER_REVISION_PROPOSAL, + "Order Revision Proposal", + "RadrootsOrderRevisionProposal", + RadrootsEventClass::Regular, + RadrootsActorRole::Buyer, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const ORDER_REVISION_DECISION_CONTRACT: RadrootsEventContract = contract( + "order_revision_decision", + KIND_ORDER_REVISION_DECISION, + "Order Revision Decision", + "RadrootsOrderRevisionDecision", + RadrootsEventClass::Regular, + RadrootsActorRole::Seller, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const ORDER_CANCELLATION_CONTRACT: RadrootsEventContract = contract( + "order_cancellation", + KIND_ORDER_CANCELLATION, + "Order Cancellation", + "RadrootsOrderCancellation", + RadrootsEventClass::Regular, + RadrootsActorRole::Buyer, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const ORDER_FULFILLMENT_UPDATE_CONTRACT: RadrootsEventContract = contract( + "order_fulfillment_update", + KIND_ORDER_FULFILLMENT_UPDATE, + "Order Fulfillment Update", + "RadrootsOrderFulfillmentUpdate", + RadrootsEventClass::Regular, + RadrootsActorRole::Seller, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const ORDER_RECEIPT_CONTRACT: RadrootsEventContract = contract( + "order_receipt", + KIND_ORDER_RECEIPT, + "Order Receipt", + "RadrootsOrderReceipt", + RadrootsEventClass::Regular, + RadrootsActorRole::Buyer, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const ORDER_PAYMENT_RECORD_CONTRACT: RadrootsEventContract = contract( + "order_payment_record", + KIND_ORDER_PAYMENT_RECORD, + "Order Payment Record", + "RadrootsOrderPaymentRecord", + RadrootsEventClass::Regular, + RadrootsActorRole::Buyer, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const ORDER_SETTLEMENT_DECISION_CONTRACT: RadrootsEventContract = contract( + "order_settlement_decision", + KIND_ORDER_SETTLEMENT_DECISION, + "Order Settlement Decision", + "RadrootsOrderSettlementDecision", + RadrootsEventClass::Regular, + RadrootsActorRole::Seller, + CHAINED_ORDER_TAGS, + ORDER_REDUCERS, +); +const TRADE_LISTING_VALIDATION_REQUEST_CONTRACT: RadrootsEventContract = contract( + "trade_listing_validation_request", + KIND_TRADE_LISTING_VALIDATION_REQUEST, + "Trade Listing Validation Request", + "RadrootsTradeValidationListingRequest", + RadrootsEventClass::Regular, + RadrootsActorRole::Service, + TRADE_VALIDATION_REQUEST_TAGS, + TRADE_VALIDATION_REDUCERS, +); +const TRADE_LISTING_VALIDATION_RESULT_CONTRACT: RadrootsEventContract = contract( + "trade_listing_validation_result", + KIND_TRADE_LISTING_VALIDATION_RESULT, + "Trade Listing Validation Result", + "RadrootsTradeValidationListingResult", + RadrootsEventClass::Regular, + RadrootsActorRole::Service, + TRADE_VALIDATION_RESULT_TAGS, + TRADE_VALIDATION_REDUCERS, +); +const TRADE_TRANSITION_PROOF_REQUEST_CONTRACT: RadrootsEventContract = contract( + "trade_transition_proof_request", + KIND_TRADE_TRANSITION_PROOF_REQUEST, + "Trade Transition Proof Request", + "RadrootsTradeTransitionProofRequest", + RadrootsEventClass::Regular, + RadrootsActorRole::Service, + TRADE_VALIDATION_REQUEST_TAGS, + TRADE_VALIDATION_REDUCERS, +); +const TRADE_TRANSITION_PROOF_RESULT_CONTRACT: RadrootsEventContract = contract( + "trade_transition_proof_result", + KIND_TRADE_TRANSITION_PROOF_RESULT, + "Trade Transition Proof Result", + "RadrootsTradeTransitionProofResult", + RadrootsEventClass::Regular, + RadrootsActorRole::Service, + TRADE_VALIDATION_RESULT_TAGS, + TRADE_VALIDATION_REDUCERS, +); +const TRADE_VALIDATION_RECEIPT_CONTRACT: RadrootsEventContract = contract( + "trade_validation_receipt", + KIND_TRADE_VALIDATION_RECEIPT, + "Trade Validation Receipt", + "RadrootsTradeValidationReceipt", + RadrootsEventClass::Regular, + RadrootsActorRole::Service, + TRADE_VALIDATION_RECEIPT_TAGS, + TRADE_VALIDATION_REDUCERS, +); + +static LISTING_EVENT_CONTRACTS: [RadrootsEventContract; 2] = + [LISTING_CONTRACT, LISTING_DRAFT_CONTRACT]; +static ORDER_EVENT_CONTRACTS: [RadrootsEventContract; 9] = [ + ORDER_REQUEST_CONTRACT, + ORDER_DECISION_CONTRACT, + ORDER_REVISION_PROPOSAL_CONTRACT, + ORDER_REVISION_DECISION_CONTRACT, + ORDER_CANCELLATION_CONTRACT, + ORDER_FULFILLMENT_UPDATE_CONTRACT, + ORDER_RECEIPT_CONTRACT, + ORDER_PAYMENT_RECORD_CONTRACT, + ORDER_SETTLEMENT_DECISION_CONTRACT, +]; +static TRADE_VALIDATION_CONTRACTS: [RadrootsEventContract; 5] = [ + TRADE_LISTING_VALIDATION_REQUEST_CONTRACT, + TRADE_LISTING_VALIDATION_RESULT_CONTRACT, + TRADE_TRANSITION_PROOF_REQUEST_CONTRACT, + TRADE_TRANSITION_PROOF_RESULT_CONTRACT, + TRADE_VALIDATION_RECEIPT_CONTRACT, +]; +static ALL_EVENT_CONTRACTS: [RadrootsEventContract; 16] = [ + LISTING_CONTRACT, + LISTING_DRAFT_CONTRACT, + ORDER_REQUEST_CONTRACT, + ORDER_DECISION_CONTRACT, + ORDER_REVISION_PROPOSAL_CONTRACT, + ORDER_REVISION_DECISION_CONTRACT, + ORDER_CANCELLATION_CONTRACT, + ORDER_FULFILLMENT_UPDATE_CONTRACT, + ORDER_RECEIPT_CONTRACT, + ORDER_PAYMENT_RECORD_CONTRACT, + ORDER_SETTLEMENT_DECISION_CONTRACT, + TRADE_LISTING_VALIDATION_REQUEST_CONTRACT, + TRADE_LISTING_VALIDATION_RESULT_CONTRACT, + TRADE_TRANSITION_PROOF_REQUEST_CONTRACT, + TRADE_TRANSITION_PROOF_RESULT_CONTRACT, + TRADE_VALIDATION_RECEIPT_CONTRACT, +]; + +pub fn contract_for_kind(kind: u32) -> Option<&'static RadrootsEventContract> { + ALL_EVENT_CONTRACTS + .iter() + .find(|contract| contract.kind == kind) +} + +pub fn all_contracts() -> &'static [RadrootsEventContract] { + &ALL_EVENT_CONTRACTS +} + +pub fn order_event_contracts() -> &'static [RadrootsEventContract] { + &ORDER_EVENT_CONTRACTS +} + +pub fn listing_event_contracts() -> &'static [RadrootsEventContract] { + &LISTING_EVENT_CONTRACTS +} + +pub fn trade_validation_contracts() -> &'static [RadrootsEventContract] { + &TRADE_VALIDATION_CONTRACTS +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::kinds::{COMMERCIAL_EVENT_KINDS, ORDER_EVENT_KINDS, TRADE_VALIDATION_EVENT_KINDS}; + + #[test] + fn exposes_scoped_contract_sets() { + assert_eq!(all_contracts().len(), COMMERCIAL_EVENT_KINDS.len()); + assert_eq!(listing_event_contracts().len(), 2); + assert_eq!(order_event_contracts().len(), ORDER_EVENT_KINDS.len()); + assert_eq!( + trade_validation_contracts().len(), + TRADE_VALIDATION_EVENT_KINDS.len() + ); + } + + #[test] + fn every_commercial_kind_has_one_contract() { + for kind in COMMERCIAL_EVENT_KINDS { + let matches = all_contracts() + .iter() + .filter(|contract| contract.kind == kind) + .count(); + assert_eq!(matches, 1, "kind {kind}"); + assert_eq!( + contract_for_kind(kind).map(|contract| contract.kind), + Some(kind) + ); + } + } + + #[test] + fn order_request_contract_requires_listing_snapshot_without_relay_indexing() { + let contract = contract_for_kind(KIND_ORDER_REQUEST).expect("order request contract"); + assert_eq!(contract.class, RadrootsEventClass::Regular); + assert_eq!(contract.author_role, RadrootsActorRole::Buyer); + assert!(contract.tags.iter().any(|tag| { + tag.name == "p" + && tag.cardinality == RadrootsTagCardinality::RequiredOne + && tag.semantic == RadrootsTagSemantic::Counterparty + })); + assert!(contract.tags.iter().any(|tag| { + tag.name == "a" + && tag.cardinality == RadrootsTagCardinality::RequiredOne + && tag.semantic == RadrootsTagSemantic::ListingAddress + })); + assert!(contract.tags.iter().any(|tag| { + tag.name == "d" + && tag.cardinality == RadrootsTagCardinality::RequiredOne + && tag.semantic == RadrootsTagSemantic::Identifier + })); + assert!(contract.tags.iter().any(|tag| { + tag.name == "listing_event" + && tag.cardinality == RadrootsTagCardinality::RequiredOne + && tag.semantic == RadrootsTagSemantic::ListingSnapshot + && !tag.relay_indexed + })); + } + + #[test] + fn chained_order_contract_requires_root_and_previous_event_tags() { + let contract = contract_for_kind(KIND_ORDER_DECISION).expect("order decision contract"); + assert_eq!(contract.class, RadrootsEventClass::Regular); + assert!(contract.tags.iter().any(|tag| { + tag.name == "e" + && tag.cardinality == RadrootsTagCardinality::RequiredOne + && tag.semantic == RadrootsTagSemantic::RootEvent + })); + assert!(contract.tags.iter().any(|tag| { + tag.name == "e" + && tag.cardinality == RadrootsTagCardinality::RequiredOne + && tag.semantic == RadrootsTagSemantic::PreviousEvent + })); + } + + #[test] + fn validation_receipt_is_trade_validation_contract() { + let contract = + contract_for_kind(KIND_TRADE_VALIDATION_RECEIPT).expect("validation receipt contract"); + assert_eq!(contract.id, "trade_validation_receipt"); + assert_eq!(contract.author_role, RadrootsActorRole::Service); + assert!( + contract + .reducers + .contains(&RadrootsReducer::TradeValidation) + ); + assert!( + trade_validation_contracts() + .iter() + .any(|contract| contract.kind == KIND_TRADE_VALIDATION_RECEIPT) + ); + } +} diff --git a/crates/events/src/kinds.rs b/crates/events/src/kinds.rs @@ -80,120 +80,69 @@ pub const KIND_GROUP_ADMINS: u32 = 39001; pub const KIND_GROUP_MEMBERS: u32 = 39002; pub const KIND_GROUP_ROLES: u32 = 39003; -pub const KIND_TRADE_LISTING_VALIDATE_REQ: u32 = 5321; -pub const KIND_TRADE_LISTING_VALIDATE_RES: u32 = 6321; -pub const KIND_WORKER_TRADE_TRANSITION_PROOF_REQ: u32 = 5322; -pub const KIND_WORKER_TRADE_TRANSITION_PROOF_RES: u32 = 6322; -pub const KIND_TRADE_ORDER_REQUEST: u32 = 3422; -pub const KIND_TRADE_ORDER_RESPONSE: u32 = 3423; -pub const KIND_TRADE_ORDER_DECISION: u32 = KIND_TRADE_ORDER_RESPONSE; -pub const KIND_TRADE_ORDER_REVISION: u32 = 3424; -pub const KIND_TRADE_ORDER_REVISION_RESPONSE: u32 = 3425; -pub const KIND_TRADE_QUESTION: u32 = 3426; -pub const KIND_TRADE_ANSWER: u32 = 3427; -pub const KIND_TRADE_DISCOUNT_REQUEST: u32 = 3428; -pub const KIND_TRADE_DISCOUNT_OFFER: u32 = 3429; -pub const KIND_TRADE_DISCOUNT_ACCEPT: u32 = 3430; -pub const KIND_TRADE_FORBIDDEN_3431: u32 = 3431; -pub const KIND_TRADE_DISCOUNT_DECLINE: u32 = KIND_TRADE_FORBIDDEN_3431; -pub const KIND_TRADE_CANCEL: u32 = 3432; -pub const KIND_TRADE_FULFILLMENT_UPDATE: u32 = 3433; -pub const KIND_TRADE_RECEIPT: u32 = 3434; +pub const KIND_TRADE_LISTING_VALIDATION_REQUEST: u32 = 5321; +pub const KIND_TRADE_LISTING_VALIDATION_RESULT: u32 = 6321; +pub const KIND_TRADE_TRANSITION_PROOF_REQUEST: u32 = 5322; +pub const KIND_TRADE_TRANSITION_PROOF_RESULT: u32 = 6322; +pub const KIND_ORDER_REQUEST: u32 = 3422; +pub const KIND_ORDER_DECISION: u32 = 3423; +pub const KIND_ORDER_REVISION_PROPOSAL: u32 = 3424; +pub const KIND_ORDER_REVISION_DECISION: u32 = 3425; +pub const KIND_ORDER_CANCELLATION: u32 = 3432; +pub const KIND_ORDER_FULFILLMENT_UPDATE: u32 = 3433; +pub const KIND_ORDER_RECEIPT: u32 = 3434; +pub const KIND_ORDER_PAYMENT_RECORD: u32 = 3435; +pub const KIND_ORDER_SETTLEMENT_DECISION: u32 = 3436; pub const KIND_TRADE_VALIDATION_RECEIPT: u32 = 3440; -pub const KIND_TRADE_PAYMENT_RECORDED: u32 = 3435; -pub const KIND_TRADE_SETTLEMENT_DECISION: u32 = 3436; - -pub const KIND_TRADE_LISTING_ORDER_REQ: u32 = KIND_TRADE_ORDER_REQUEST; -pub const KIND_TRADE_LISTING_ORDER_RES: u32 = KIND_TRADE_ORDER_RESPONSE; -pub const KIND_TRADE_LISTING_ORDER_REVISION_REQ: u32 = KIND_TRADE_ORDER_REVISION; -pub const KIND_TRADE_LISTING_ORDER_REVISION_RES: u32 = KIND_TRADE_ORDER_REVISION_RESPONSE; -pub const KIND_TRADE_LISTING_QUESTION_REQ: u32 = KIND_TRADE_QUESTION; -pub const KIND_TRADE_LISTING_ANSWER_RES: u32 = KIND_TRADE_ANSWER; -pub const KIND_TRADE_LISTING_DISCOUNT_REQ: u32 = KIND_TRADE_DISCOUNT_REQUEST; -pub const KIND_TRADE_LISTING_DISCOUNT_OFFER_RES: u32 = KIND_TRADE_DISCOUNT_OFFER; -pub const KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ: u32 = KIND_TRADE_DISCOUNT_ACCEPT; -pub const KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ: u32 = KIND_TRADE_FORBIDDEN_3431; -pub const KIND_TRADE_LISTING_CANCEL_REQ: u32 = KIND_TRADE_CANCEL; -pub const KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ: u32 = KIND_TRADE_FULFILLMENT_UPDATE; -pub const KIND_TRADE_LISTING_RECEIPT_REQ: u32 = KIND_TRADE_RECEIPT; - -pub const TRADE_SERVICE_KINDS: [u32; 4] = [ - KIND_TRADE_LISTING_VALIDATE_REQ, - KIND_TRADE_LISTING_VALIDATE_RES, - KIND_WORKER_TRADE_TRANSITION_PROOF_REQ, - KIND_WORKER_TRADE_TRANSITION_PROOF_RES, -]; -pub const TRADE_PUBLIC_KINDS: [u32; 14] = [ - KIND_TRADE_ORDER_REQUEST, - KIND_TRADE_ORDER_RESPONSE, - KIND_TRADE_ORDER_REVISION, - KIND_TRADE_ORDER_REVISION_RESPONSE, - KIND_TRADE_QUESTION, - KIND_TRADE_ANSWER, - KIND_TRADE_DISCOUNT_REQUEST, - KIND_TRADE_DISCOUNT_OFFER, - KIND_TRADE_DISCOUNT_ACCEPT, - KIND_TRADE_CANCEL, - KIND_TRADE_FULFILLMENT_UPDATE, - KIND_TRADE_RECEIPT, - KIND_TRADE_PAYMENT_RECORDED, - KIND_TRADE_SETTLEMENT_DECISION, +pub const LISTING_EVENT_KINDS: [u32; 2] = [KIND_LISTING, KIND_LISTING_DRAFT]; + +pub const ORDER_EVENT_KINDS: [u32; 9] = [ + KIND_ORDER_REQUEST, + KIND_ORDER_DECISION, + KIND_ORDER_REVISION_PROPOSAL, + KIND_ORDER_REVISION_DECISION, + KIND_ORDER_CANCELLATION, + KIND_ORDER_FULFILLMENT_UPDATE, + KIND_ORDER_RECEIPT, + KIND_ORDER_PAYMENT_RECORD, + KIND_ORDER_SETTLEMENT_DECISION, ]; -pub const TRADE_KINDS: [u32; 18] = [ - KIND_TRADE_LISTING_VALIDATE_REQ, - KIND_TRADE_LISTING_VALIDATE_RES, - KIND_WORKER_TRADE_TRANSITION_PROOF_REQ, - KIND_WORKER_TRADE_TRANSITION_PROOF_RES, - KIND_TRADE_ORDER_REQUEST, - KIND_TRADE_ORDER_RESPONSE, - KIND_TRADE_ORDER_REVISION, - KIND_TRADE_ORDER_REVISION_RESPONSE, - KIND_TRADE_QUESTION, - KIND_TRADE_ANSWER, - KIND_TRADE_DISCOUNT_REQUEST, - KIND_TRADE_DISCOUNT_OFFER, - KIND_TRADE_DISCOUNT_ACCEPT, - KIND_TRADE_CANCEL, - KIND_TRADE_FULFILLMENT_UPDATE, - KIND_TRADE_RECEIPT, - KIND_TRADE_PAYMENT_RECORDED, - KIND_TRADE_SETTLEMENT_DECISION, +pub const TRADE_VALIDATION_SERVICE_EVENT_KINDS: [u32; 4] = [ + KIND_TRADE_LISTING_VALIDATION_REQUEST, + KIND_TRADE_LISTING_VALIDATION_RESULT, + KIND_TRADE_TRANSITION_PROOF_REQUEST, + KIND_TRADE_TRANSITION_PROOF_RESULT, ]; -pub const TRADE_LISTING_KINDS: [u32; 18] = TRADE_KINDS; - -pub const ACTIVE_TRADE_LISTING_KINDS: [u32; 2] = [KIND_LISTING, KIND_LISTING_DRAFT]; - -pub const ACTIVE_TRADE_PUBLIC_KINDS: [u32; 9] = [ - KIND_TRADE_ORDER_REQUEST, - KIND_TRADE_ORDER_DECISION, - KIND_TRADE_ORDER_REVISION, - KIND_TRADE_ORDER_REVISION_RESPONSE, - KIND_TRADE_CANCEL, - KIND_TRADE_FULFILLMENT_UPDATE, - KIND_TRADE_RECEIPT, - KIND_TRADE_PAYMENT_RECORDED, - KIND_TRADE_SETTLEMENT_DECISION, +pub const TRADE_VALIDATION_EVENT_KINDS: [u32; 5] = [ + KIND_TRADE_LISTING_VALIDATION_REQUEST, + KIND_TRADE_LISTING_VALIDATION_RESULT, + KIND_TRADE_TRANSITION_PROOF_REQUEST, + KIND_TRADE_TRANSITION_PROOF_RESULT, + KIND_TRADE_VALIDATION_RECEIPT, ]; -pub const ACTIVE_TRADE_KINDS: [u32; 11] = [ +pub const COMMERCIAL_EVENT_KINDS: [u32; 16] = [ KIND_LISTING, KIND_LISTING_DRAFT, - KIND_TRADE_ORDER_REQUEST, - KIND_TRADE_ORDER_DECISION, - KIND_TRADE_ORDER_REVISION, - KIND_TRADE_ORDER_REVISION_RESPONSE, - KIND_TRADE_CANCEL, - KIND_TRADE_FULFILLMENT_UPDATE, - KIND_TRADE_RECEIPT, - KIND_TRADE_PAYMENT_RECORDED, - KIND_TRADE_SETTLEMENT_DECISION, + KIND_ORDER_REQUEST, + KIND_ORDER_DECISION, + KIND_ORDER_REVISION_PROPOSAL, + KIND_ORDER_REVISION_DECISION, + KIND_ORDER_CANCELLATION, + KIND_ORDER_FULFILLMENT_UPDATE, + KIND_ORDER_RECEIPT, + KIND_ORDER_PAYMENT_RECORD, + KIND_ORDER_SETTLEMENT_DECISION, + KIND_TRADE_LISTING_VALIDATION_REQUEST, + KIND_TRADE_LISTING_VALIDATION_RESULT, + KIND_TRADE_TRANSITION_PROOF_REQUEST, + KIND_TRADE_TRANSITION_PROOF_RESULT, + KIND_TRADE_VALIDATION_RECEIPT, ]; -pub const TRADE_VALIDATION_RECEIPT_KINDS: [u32; 1] = [KIND_TRADE_VALIDATION_RECEIPT]; - pub const KIND_JOB_REQUEST_MIN: u32 = 5000; pub const KIND_JOB_REQUEST_MAX: u32 = 5999; pub const KIND_JOB_RESULT_MIN: u32 = 6000; @@ -329,6 +278,11 @@ pub const fn is_listing_kind(kind: u32) -> bool { } #[inline] +pub const fn is_listing_event_kind(kind: u32) -> bool { + is_listing_kind(kind) +} + +#[inline] pub const fn is_public_file_metadata_kind(kind: u32) -> bool { kind == KIND_PUBLIC_FILE_METADATA } @@ -480,154 +434,71 @@ pub const fn is_private_farm_ops_kind(kind: u32) -> bool { } #[inline] -pub const fn is_trade_service_request_kind(kind: u32) -> bool { +pub const fn is_trade_validation_service_request_kind(kind: u32) -> bool { matches!( kind, - KIND_TRADE_LISTING_VALIDATE_REQ | KIND_WORKER_TRADE_TRANSITION_PROOF_REQ + KIND_TRADE_LISTING_VALIDATION_REQUEST | KIND_TRADE_TRANSITION_PROOF_REQUEST ) } #[inline] -pub const fn is_trade_service_result_kind(kind: u32) -> bool { +pub const fn is_trade_validation_service_result_kind(kind: u32) -> bool { matches!( kind, - KIND_TRADE_LISTING_VALIDATE_RES | KIND_WORKER_TRADE_TRANSITION_PROOF_RES + KIND_TRADE_LISTING_VALIDATION_RESULT | KIND_TRADE_TRANSITION_PROOF_RESULT ) } #[inline] -pub const fn is_trade_service_kind(kind: u32) -> bool { - is_trade_service_request_kind(kind) || is_trade_service_result_kind(kind) +pub const fn is_trade_validation_service_event_kind(kind: u32) -> bool { + is_trade_validation_service_request_kind(kind) || is_trade_validation_service_result_kind(kind) } #[inline] -pub const fn is_trade_public_kind(kind: u32) -> bool { +pub const fn is_order_event_kind(kind: u32) -> bool { matches!( kind, - KIND_TRADE_ORDER_REQUEST - | KIND_TRADE_ORDER_RESPONSE - | KIND_TRADE_ORDER_REVISION - | KIND_TRADE_ORDER_REVISION_RESPONSE - | KIND_TRADE_QUESTION - | KIND_TRADE_ANSWER - | KIND_TRADE_DISCOUNT_REQUEST - | KIND_TRADE_DISCOUNT_OFFER - | KIND_TRADE_DISCOUNT_ACCEPT - | KIND_TRADE_CANCEL - | KIND_TRADE_FULFILLMENT_UPDATE - | KIND_TRADE_RECEIPT - | KIND_TRADE_PAYMENT_RECORDED - | KIND_TRADE_SETTLEMENT_DECISION + KIND_ORDER_REQUEST + | KIND_ORDER_DECISION + | KIND_ORDER_REVISION_PROPOSAL + | KIND_ORDER_REVISION_DECISION + | KIND_ORDER_CANCELLATION + | KIND_ORDER_FULFILLMENT_UPDATE + | KIND_ORDER_RECEIPT + | KIND_ORDER_PAYMENT_RECORD + | KIND_ORDER_SETTLEMENT_DECISION ) } #[inline] -pub const fn is_trade_kind(kind: u32) -> bool { - is_trade_service_kind(kind) || is_trade_public_kind(kind) -} - -#[inline] -pub const fn is_active_trade_listing_kind(kind: u32) -> bool { - matches!(kind, KIND_LISTING | KIND_LISTING_DRAFT) -} - -#[inline] -pub const fn is_active_trade_public_kind(kind: u32) -> bool { - matches!( - kind, - KIND_TRADE_ORDER_REQUEST - | KIND_TRADE_ORDER_DECISION - | KIND_TRADE_ORDER_REVISION - | KIND_TRADE_ORDER_REVISION_RESPONSE - | KIND_TRADE_CANCEL - | KIND_TRADE_FULFILLMENT_UPDATE - | KIND_TRADE_RECEIPT - | KIND_TRADE_PAYMENT_RECORDED - | KIND_TRADE_SETTLEMENT_DECISION - ) -} - -#[inline] -pub const fn is_active_trade_kind(kind: u32) -> bool { - is_active_trade_listing_kind(kind) || is_active_trade_public_kind(kind) -} - -#[inline] pub const fn is_trade_validation_receipt_kind(kind: u32) -> bool { kind == KIND_TRADE_VALIDATION_RECEIPT } #[inline] -pub const fn is_trade_listing_request_kind(kind: u32) -> bool { - matches!( - kind, - KIND_TRADE_LISTING_VALIDATE_REQ - | KIND_TRADE_ORDER_REQUEST - | KIND_TRADE_ORDER_REVISION - | KIND_TRADE_QUESTION - | KIND_TRADE_DISCOUNT_REQUEST - | KIND_TRADE_DISCOUNT_ACCEPT - | KIND_TRADE_CANCEL - | KIND_TRADE_FULFILLMENT_UPDATE - | KIND_TRADE_RECEIPT - ) +pub const fn is_trade_validation_event_kind(kind: u32) -> bool { + is_trade_validation_service_event_kind(kind) || is_trade_validation_receipt_kind(kind) } #[inline] -pub const fn is_trade_listing_result_kind(kind: u32) -> bool { - matches!( - kind, - KIND_TRADE_LISTING_VALIDATE_RES - | KIND_TRADE_ORDER_RESPONSE - | KIND_TRADE_ORDER_REVISION_RESPONSE - | KIND_TRADE_ANSWER - | KIND_TRADE_DISCOUNT_OFFER - ) +pub const fn is_commercial_event_kind(kind: u32) -> bool { + is_listing_event_kind(kind) || is_order_event_kind(kind) || is_trade_validation_event_kind(kind) } #[inline] -pub const fn is_trade_listing_kind(kind: u32) -> bool { - is_trade_kind(kind) -} - -#[inline] -pub const fn trade_service_result_kind_for_request(kind: u32) -> Option<u32> { - match kind { - KIND_TRADE_LISTING_VALIDATE_REQ => Some(KIND_TRADE_LISTING_VALIDATE_RES), - KIND_WORKER_TRADE_TRANSITION_PROOF_REQ => Some(KIND_WORKER_TRADE_TRANSITION_PROOF_RES), - _ => None, - } -} - -#[inline] -pub const fn trade_service_request_kind_for_result(kind: u32) -> Option<u32> { - match kind { - KIND_TRADE_LISTING_VALIDATE_RES => Some(KIND_TRADE_LISTING_VALIDATE_REQ), - KIND_WORKER_TRADE_TRANSITION_PROOF_RES => Some(KIND_WORKER_TRADE_TRANSITION_PROOF_REQ), - _ => None, - } -} - -#[inline] -pub const fn trade_listing_result_kind_for_request(kind: u32) -> Option<u32> { +pub const fn trade_validation_service_result_kind_for_request(kind: u32) -> Option<u32> { match kind { - KIND_TRADE_LISTING_VALIDATE_REQ => Some(KIND_TRADE_LISTING_VALIDATE_RES), - KIND_TRADE_ORDER_REQUEST => Some(KIND_TRADE_ORDER_RESPONSE), - KIND_TRADE_ORDER_REVISION => Some(KIND_TRADE_ORDER_REVISION_RESPONSE), - KIND_TRADE_QUESTION => Some(KIND_TRADE_ANSWER), - KIND_TRADE_DISCOUNT_REQUEST => Some(KIND_TRADE_DISCOUNT_OFFER), + KIND_TRADE_LISTING_VALIDATION_REQUEST => Some(KIND_TRADE_LISTING_VALIDATION_RESULT), + KIND_TRADE_TRANSITION_PROOF_REQUEST => Some(KIND_TRADE_TRANSITION_PROOF_RESULT), _ => None, } } #[inline] -pub const fn trade_listing_request_kind_for_result(kind: u32) -> Option<u32> { +pub const fn trade_validation_service_request_kind_for_result(kind: u32) -> Option<u32> { match kind { - KIND_TRADE_LISTING_VALIDATE_RES => Some(KIND_TRADE_LISTING_VALIDATE_REQ), - KIND_TRADE_ORDER_RESPONSE => Some(KIND_TRADE_ORDER_REQUEST), - KIND_TRADE_ORDER_REVISION_RESPONSE => Some(KIND_TRADE_ORDER_REVISION), - KIND_TRADE_ANSWER => Some(KIND_TRADE_QUESTION), - KIND_TRADE_DISCOUNT_OFFER => Some(KIND_TRADE_DISCOUNT_REQUEST), + KIND_TRADE_LISTING_VALIDATION_RESULT => Some(KIND_TRADE_LISTING_VALIDATION_REQUEST), + KIND_TRADE_TRANSITION_PROOF_RESULT => Some(KIND_TRADE_TRANSITION_PROOF_REQUEST), _ => None, } } @@ -910,231 +781,134 @@ mod tests { } #[test] - fn classifies_trade_listing_kinds() { - assert!(is_listing_kind(KIND_LISTING)); - assert!(is_listing_kind(KIND_LISTING_DRAFT)); - assert!(!is_listing_kind(KIND_PROFILE)); - - assert!(is_trade_service_request_kind( - KIND_TRADE_LISTING_VALIDATE_REQ - )); - assert!(is_trade_service_request_kind( - KIND_WORKER_TRADE_TRANSITION_PROOF_REQ - )); - assert!(!is_trade_service_request_kind( - KIND_TRADE_LISTING_VALIDATE_RES + fn classifies_commercial_event_kinds() { + assert_eq!(LISTING_EVENT_KINDS, [KIND_LISTING, KIND_LISTING_DRAFT]); + assert_eq!( + ORDER_EVENT_KINDS, + [ + KIND_ORDER_REQUEST, + KIND_ORDER_DECISION, + KIND_ORDER_REVISION_PROPOSAL, + KIND_ORDER_REVISION_DECISION, + KIND_ORDER_CANCELLATION, + KIND_ORDER_FULFILLMENT_UPDATE, + KIND_ORDER_RECEIPT, + KIND_ORDER_PAYMENT_RECORD, + KIND_ORDER_SETTLEMENT_DECISION, + ] + ); + assert_eq!( + TRADE_VALIDATION_SERVICE_EVENT_KINDS, + [ + KIND_TRADE_LISTING_VALIDATION_REQUEST, + KIND_TRADE_LISTING_VALIDATION_RESULT, + KIND_TRADE_TRANSITION_PROOF_REQUEST, + KIND_TRADE_TRANSITION_PROOF_RESULT, + ] + ); + assert_eq!( + TRADE_VALIDATION_EVENT_KINDS, + [ + KIND_TRADE_LISTING_VALIDATION_REQUEST, + KIND_TRADE_LISTING_VALIDATION_RESULT, + KIND_TRADE_TRANSITION_PROOF_REQUEST, + KIND_TRADE_TRANSITION_PROOF_RESULT, + KIND_TRADE_VALIDATION_RECEIPT, + ] + ); + assert_eq!(COMMERCIAL_EVENT_KINDS.len(), 16); + + assert!(is_listing_event_kind(KIND_LISTING)); + assert!(is_listing_event_kind(KIND_LISTING_DRAFT)); + assert!(!is_listing_event_kind(KIND_PROFILE)); + + assert!(is_order_event_kind(KIND_ORDER_REQUEST)); + assert!(is_order_event_kind(KIND_ORDER_DECISION)); + assert!(is_order_event_kind(KIND_ORDER_REVISION_PROPOSAL)); + assert!(is_order_event_kind(KIND_ORDER_REVISION_DECISION)); + assert!(is_order_event_kind(KIND_ORDER_CANCELLATION)); + assert!(is_order_event_kind(KIND_ORDER_FULFILLMENT_UPDATE)); + assert!(is_order_event_kind(KIND_ORDER_RECEIPT)); + assert!(is_order_event_kind(KIND_ORDER_PAYMENT_RECORD)); + assert!(is_order_event_kind(KIND_ORDER_SETTLEMENT_DECISION)); + assert!(!is_order_event_kind(KIND_TRADE_LISTING_VALIDATION_REQUEST)); + assert!(!is_order_event_kind(KIND_TRADE_VALIDATION_RECEIPT)); + assert!(!is_order_event_kind(3431)); + + assert!(is_trade_validation_service_request_kind( + KIND_TRADE_LISTING_VALIDATION_REQUEST )); - assert!(is_trade_service_result_kind( - KIND_TRADE_LISTING_VALIDATE_RES + assert!(is_trade_validation_service_request_kind( + KIND_TRADE_TRANSITION_PROOF_REQUEST )); - assert!(is_trade_service_result_kind( - KIND_WORKER_TRADE_TRANSITION_PROOF_RES + assert!(!is_trade_validation_service_request_kind( + KIND_TRADE_LISTING_VALIDATION_RESULT )); - assert!(!is_trade_service_result_kind( - KIND_TRADE_LISTING_VALIDATE_REQ + assert!(is_trade_validation_service_result_kind( + KIND_TRADE_LISTING_VALIDATION_RESULT )); - assert!(is_trade_service_kind(KIND_TRADE_LISTING_VALIDATE_REQ)); - assert!(is_trade_service_kind(KIND_TRADE_LISTING_VALIDATE_RES)); - assert!(is_trade_service_kind( - KIND_WORKER_TRADE_TRANSITION_PROOF_REQ + assert!(is_trade_validation_service_result_kind( + KIND_TRADE_TRANSITION_PROOF_RESULT )); - assert!(is_trade_service_kind( - KIND_WORKER_TRADE_TRANSITION_PROOF_RES + assert!(!is_trade_validation_service_result_kind( + KIND_TRADE_LISTING_VALIDATION_REQUEST )); - assert!(!is_trade_service_kind(KIND_TRADE_ORDER_REQUEST)); - assert!(is_trade_public_kind(KIND_TRADE_ORDER_REQUEST)); - assert!(is_trade_public_kind(KIND_TRADE_ORDER_RESPONSE)); - assert!(is_trade_public_kind(KIND_TRADE_RECEIPT)); - assert!(!is_trade_public_kind(KIND_TRADE_LISTING_VALIDATE_REQ)); - assert!(is_trade_kind(KIND_TRADE_ORDER_REQUEST)); - assert!(is_trade_kind(KIND_TRADE_LISTING_VALIDATE_REQ)); - assert!(!is_trade_kind(KIND_LISTING)); - assert!(is_trade_listing_request_kind(KIND_TRADE_LISTING_ORDER_REQ)); - assert!(is_trade_listing_request_kind( - KIND_TRADE_LISTING_ORDER_REVISION_REQ + assert!(is_trade_validation_service_event_kind( + KIND_TRADE_LISTING_VALIDATION_REQUEST )); - assert!(is_trade_listing_request_kind( - KIND_TRADE_LISTING_QUESTION_REQ + assert!(is_trade_validation_service_event_kind( + KIND_TRADE_LISTING_VALIDATION_RESULT )); - assert!(is_trade_listing_request_kind( - KIND_TRADE_LISTING_DISCOUNT_REQ + assert!(is_trade_validation_service_event_kind( + KIND_TRADE_TRANSITION_PROOF_REQUEST )); - assert!(is_trade_listing_request_kind( - KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ + assert!(is_trade_validation_service_event_kind( + KIND_TRADE_TRANSITION_PROOF_RESULT )); - assert!(is_trade_listing_request_kind(KIND_TRADE_LISTING_CANCEL_REQ)); - assert!(is_trade_listing_request_kind( - KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ + assert!(!is_trade_validation_service_event_kind(KIND_ORDER_REQUEST)); + assert!(is_trade_validation_receipt_kind( + KIND_TRADE_VALIDATION_RECEIPT )); - assert!(is_trade_listing_request_kind( - KIND_TRADE_LISTING_RECEIPT_REQ + assert!(!is_trade_validation_receipt_kind(KIND_ORDER_RECEIPT)); + assert!(is_trade_validation_event_kind( + KIND_TRADE_VALIDATION_RECEIPT )); - assert!(!is_trade_listing_request_kind(KIND_TRADE_LISTING_ORDER_RES)); - assert!(is_trade_listing_result_kind(KIND_TRADE_LISTING_ORDER_RES)); - assert!(is_trade_listing_result_kind( - KIND_TRADE_LISTING_ORDER_REVISION_RES + assert!(is_trade_validation_event_kind( + KIND_TRADE_TRANSITION_PROOF_RESULT )); - assert!(is_trade_listing_result_kind(KIND_TRADE_LISTING_ANSWER_RES)); - assert!(is_trade_listing_result_kind( - KIND_TRADE_LISTING_DISCOUNT_OFFER_RES + assert!(!is_trade_validation_event_kind(KIND_ORDER_RECEIPT)); + + assert!(is_commercial_event_kind(KIND_LISTING)); + assert!(is_commercial_event_kind(KIND_ORDER_REQUEST)); + assert!(is_commercial_event_kind( + KIND_TRADE_LISTING_VALIDATION_REQUEST )); - assert!(!is_trade_listing_result_kind(KIND_TRADE_LISTING_CANCEL_REQ)); - assert!(is_trade_listing_kind(KIND_TRADE_LISTING_RECEIPT_REQ)); - assert!(!is_trade_listing_kind(KIND_LISTING)); - assert!(!is_trade_public_kind(KIND_TRADE_FORBIDDEN_3431)); - assert!(!is_trade_kind(KIND_TRADE_FORBIDDEN_3431)); - assert!(!is_trade_listing_request_kind(KIND_TRADE_FORBIDDEN_3431)); + assert!(is_commercial_event_kind(KIND_TRADE_VALIDATION_RECEIPT)); + assert!(!is_commercial_event_kind(KIND_PROFILE)); + assert_eq!( - trade_service_result_kind_for_request(KIND_TRADE_LISTING_VALIDATE_REQ), - Some(KIND_TRADE_LISTING_VALIDATE_RES) + trade_validation_service_result_kind_for_request(KIND_TRADE_LISTING_VALIDATION_REQUEST), + Some(KIND_TRADE_LISTING_VALIDATION_RESULT) ); assert_eq!( - trade_service_result_kind_for_request(KIND_WORKER_TRADE_TRANSITION_PROOF_REQ), - Some(KIND_WORKER_TRADE_TRANSITION_PROOF_RES) + trade_validation_service_result_kind_for_request(KIND_TRADE_TRANSITION_PROOF_REQUEST), + Some(KIND_TRADE_TRANSITION_PROOF_RESULT) ); assert_eq!( - trade_service_result_kind_for_request(KIND_TRADE_ORDER_REQUEST), + trade_validation_service_result_kind_for_request(KIND_ORDER_REQUEST), None ); assert_eq!( - trade_service_request_kind_for_result(KIND_TRADE_LISTING_VALIDATE_RES), - Some(KIND_TRADE_LISTING_VALIDATE_REQ) + trade_validation_service_request_kind_for_result(KIND_TRADE_LISTING_VALIDATION_RESULT), + Some(KIND_TRADE_LISTING_VALIDATION_REQUEST) ); assert_eq!( - trade_service_request_kind_for_result(KIND_WORKER_TRADE_TRANSITION_PROOF_RES), - Some(KIND_WORKER_TRADE_TRANSITION_PROOF_REQ) + trade_validation_service_request_kind_for_result(KIND_TRADE_TRANSITION_PROOF_RESULT), + Some(KIND_TRADE_TRANSITION_PROOF_REQUEST) ); assert_eq!( - trade_service_request_kind_for_result(KIND_TRADE_ORDER_RESPONSE), + trade_validation_service_request_kind_for_result(KIND_ORDER_DECISION), None ); - assert_eq!( - trade_listing_result_kind_for_request(KIND_TRADE_LISTING_VALIDATE_REQ), - Some(KIND_TRADE_LISTING_VALIDATE_RES) - ); - assert_eq!( - trade_listing_result_kind_for_request(KIND_TRADE_LISTING_ORDER_REQ), - Some(KIND_TRADE_LISTING_ORDER_RES) - ); - assert_eq!( - trade_listing_result_kind_for_request(KIND_TRADE_LISTING_ORDER_REVISION_REQ), - Some(KIND_TRADE_LISTING_ORDER_REVISION_RES) - ); - assert_eq!( - trade_listing_result_kind_for_request(KIND_TRADE_LISTING_QUESTION_REQ), - Some(KIND_TRADE_LISTING_ANSWER_RES) - ); - assert_eq!( - trade_listing_result_kind_for_request(KIND_TRADE_LISTING_DISCOUNT_REQ), - Some(KIND_TRADE_LISTING_DISCOUNT_OFFER_RES) - ); - assert_eq!( - trade_listing_result_kind_for_request(KIND_TRADE_LISTING_CANCEL_REQ), - None - ); - assert_eq!( - trade_listing_request_kind_for_result(KIND_TRADE_LISTING_VALIDATE_RES), - Some(KIND_TRADE_LISTING_VALIDATE_REQ) - ); - assert_eq!( - trade_listing_request_kind_for_result(KIND_TRADE_LISTING_ORDER_RES), - Some(KIND_TRADE_LISTING_ORDER_REQ) - ); - assert_eq!( - trade_listing_request_kind_for_result(KIND_TRADE_LISTING_ORDER_REVISION_RES), - Some(KIND_TRADE_LISTING_ORDER_REVISION_REQ) - ); - assert_eq!( - trade_listing_request_kind_for_result(KIND_TRADE_LISTING_ANSWER_RES), - Some(KIND_TRADE_LISTING_QUESTION_REQ) - ); - assert_eq!( - trade_listing_request_kind_for_result(KIND_TRADE_LISTING_DISCOUNT_OFFER_RES), - Some(KIND_TRADE_LISTING_DISCOUNT_REQ) - ); - assert_eq!( - trade_listing_request_kind_for_result(KIND_TRADE_LISTING_RECEIPT_REQ), - None - ); - } - - #[test] - fn active_trade_kind_set_contains_listing_order_revision_decision_fulfillment_cancellation_and_receipt() - { - assert_eq!( - ACTIVE_TRADE_LISTING_KINDS, - [KIND_LISTING, KIND_LISTING_DRAFT] - ); - assert_eq!( - ACTIVE_TRADE_PUBLIC_KINDS, - [ - KIND_TRADE_ORDER_REQUEST, - KIND_TRADE_ORDER_DECISION, - KIND_TRADE_ORDER_REVISION, - KIND_TRADE_ORDER_REVISION_RESPONSE, - KIND_TRADE_CANCEL, - KIND_TRADE_FULFILLMENT_UPDATE, - KIND_TRADE_RECEIPT, - KIND_TRADE_PAYMENT_RECORDED, - KIND_TRADE_SETTLEMENT_DECISION, - ] - ); - assert_eq!( - ACTIVE_TRADE_KINDS, - [ - KIND_LISTING, - KIND_LISTING_DRAFT, - KIND_TRADE_ORDER_REQUEST, - KIND_TRADE_ORDER_DECISION, - KIND_TRADE_ORDER_REVISION, - KIND_TRADE_ORDER_REVISION_RESPONSE, - KIND_TRADE_CANCEL, - KIND_TRADE_FULFILLMENT_UPDATE, - KIND_TRADE_RECEIPT, - KIND_TRADE_PAYMENT_RECORDED, - KIND_TRADE_SETTLEMENT_DECISION, - ] - ); - - assert!(is_active_trade_kind(KIND_LISTING)); - assert!(is_active_trade_kind(KIND_LISTING_DRAFT)); - assert!(is_active_trade_public_kind(KIND_TRADE_ORDER_REQUEST)); - assert!(is_active_trade_public_kind(KIND_TRADE_ORDER_DECISION)); - assert!(is_active_trade_public_kind(KIND_TRADE_ORDER_REVISION)); - assert!(is_active_trade_public_kind( - KIND_TRADE_ORDER_REVISION_RESPONSE - )); - assert!(is_active_trade_public_kind(KIND_TRADE_CANCEL)); - assert!(is_active_trade_public_kind(KIND_TRADE_FULFILLMENT_UPDATE)); - assert!(is_active_trade_public_kind(KIND_TRADE_RECEIPT)); - assert!(is_active_trade_public_kind(KIND_TRADE_PAYMENT_RECORDED)); - assert!(is_active_trade_public_kind(KIND_TRADE_SETTLEMENT_DECISION)); - assert!(!is_active_trade_public_kind( - KIND_TRADE_LISTING_VALIDATE_REQ - )); - assert!(!is_active_trade_public_kind(KIND_TRADE_QUESTION)); - assert!(!is_active_trade_public_kind(KIND_TRADE_ANSWER)); - assert!(!is_active_trade_public_kind(KIND_TRADE_DISCOUNT_REQUEST)); - assert!(!is_active_trade_public_kind(KIND_TRADE_DISCOUNT_OFFER)); - assert!(!is_active_trade_public_kind(KIND_TRADE_DISCOUNT_ACCEPT)); - assert!(!is_active_trade_public_kind(KIND_TRADE_FORBIDDEN_3431)); - } - - #[test] - fn validation_receipt_kind_is_registered_outside_buyer_receipt_lifecycle() { - assert_eq!(KIND_TRADE_RECEIPT, 3434); - assert_eq!(KIND_TRADE_VALIDATION_RECEIPT, 3440); - assert_ne!(KIND_TRADE_VALIDATION_RECEIPT, KIND_TRADE_RECEIPT); - assert_eq!( - TRADE_VALIDATION_RECEIPT_KINDS, - [KIND_TRADE_VALIDATION_RECEIPT] - ); - assert!(is_trade_validation_receipt_kind( - KIND_TRADE_VALIDATION_RECEIPT - )); - assert!(!is_trade_validation_receipt_kind(KIND_TRADE_RECEIPT)); - assert!(!is_trade_public_kind(KIND_TRADE_VALIDATION_RECEIPT)); - assert!(!is_active_trade_public_kind(KIND_TRADE_VALIDATION_RECEIPT)); - assert!(!is_active_trade_kind(KIND_TRADE_VALIDATION_RECEIPT)); } } diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs @@ -11,6 +11,7 @@ pub mod app_data; pub mod article; pub mod calendar; pub mod comment; +pub mod contract; pub mod coop; pub mod document; pub mod farm; @@ -33,6 +34,8 @@ pub mod list_set; pub mod listing; pub mod message; pub mod message_file; +pub mod order; +pub mod order_economics; pub mod plot; pub mod post; pub mod profile; @@ -46,7 +49,7 @@ pub mod resource_cap; pub mod seal; pub mod social; pub mod tags; -pub mod trade; +pub mod trade_validation; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/events/src/order.rs b/crates/events/src/order.rs @@ -0,0 +1,2459 @@ +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use crate::kinds::*; +pub use crate::order_economics::*; +#[cfg(test)] +use crate::trade_validation::RadrootsTradeValidationListingError; +use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney}; + +pub const RADROOTS_COMMERCIAL_LISTING_DOMAIN: &str = "trade:listing"; +pub const RADROOTS_ORDER_ENVELOPE_VERSION: u16 = 1; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsListingParseError { + InvalidKind(u32), + MissingTag(String), + InvalidTag(String), + InvalidNumber(String), + InvalidUnit, + InvalidCurrency, + InvalidJson(String), + InvalidDiscount(String), +} + +impl core::fmt::Display for RadrootsListingParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidKind(kind) => write!(f, "invalid listing kind: {kind}"), + Self::MissingTag(tag) => write!(f, "missing required tag: {tag}"), + Self::InvalidTag(tag) => write!(f, "invalid tag: {tag}"), + Self::InvalidNumber(field) => write!(f, "invalid number: {field}"), + Self::InvalidUnit => write!(f, "invalid unit"), + Self::InvalidCurrency => write!(f, "invalid currency"), + Self::InvalidJson(field) => write!(f, "invalid json: {field}"), + Self::InvalidDiscount(kind) => write!(f, "invalid discount data for {kind}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsListingParseError {} + +impl RadrootsOrderEconomics { + pub fn canonicalize(&mut self) { + self.items + .sort_by(|left, right| left.bin_id.cmp(&right.bin_id)); + self.discounts.sort_by(|left, right| left.id.cmp(&right.id)); + self.adjustments + .sort_by(|left, right| left.id.cmp(&right.id)); + if let Ok(totals) = self.derived_totals() { + self.subtotal = totals.subtotal; + self.discount_total = totals.discount_total; + self.adjustment_total = totals.adjustment_total; + self.total = totals.total; + } + } + + pub fn canonicalized(&self) -> Self { + let mut economics = self.clone(); + economics.canonicalize(); + economics + } + + pub fn derived_totals(&self) -> Result<RadrootsOrderEconomicTotals, RadrootsOrderPayloadError> { + if self.items.is_empty() { + return Err(RadrootsOrderPayloadError::MissingEconomicItems); + } + + let mut subtotal = RadrootsCoreMoney::zero(self.currency); + for (index, item) in self.items.iter().enumerate() { + let line_subtotal = validate_economic_item(item, self.currency, index)?; + subtotal = checked_money_add(&subtotal, &line_subtotal, "subtotal")?; + } + + let mut discount_total = RadrootsCoreMoney::zero(self.currency); + for (index, line) in self.discounts.iter().enumerate() { + validate_economic_line(line, self.currency, "discounts", index)?; + if line.kind != RadrootsOrderEconomicLineKind::ListingDiscount { + return Err(RadrootsOrderPayloadError::InvalidEconomicLineKind { + field: "discounts", + index, + }); + } + if line.effect != RadrootsOrderEconomicEffect::Decrease { + return Err(RadrootsOrderPayloadError::InvalidEconomicLineEffect { + field: "discounts", + index, + }); + } + discount_total = checked_money_add(&discount_total, &line.amount, "discount_total")?; + } + + let mut adjustment_total = RadrootsCoreMoney::zero(self.currency); + let mut total = checked_money_sub_non_negative(&subtotal, &discount_total, "total")?; + for (index, line) in self.adjustments.iter().enumerate() { + validate_economic_line(line, self.currency, "adjustments", index)?; + if line.kind == RadrootsOrderEconomicLineKind::ListingDiscount { + return Err(RadrootsOrderPayloadError::InvalidEconomicLineKind { + field: "adjustments", + index, + }); + } + adjustment_total = + checked_money_add(&adjustment_total, &line.amount, "adjustment_total")?; + total = match line.effect { + RadrootsOrderEconomicEffect::Increase => { + checked_money_add(&total, &line.amount, "total")? + } + RadrootsOrderEconomicEffect::Decrease => { + checked_money_sub_non_negative(&total, &line.amount, "total")? + } + }; + } + + Ok(RadrootsOrderEconomicTotals { + subtotal, + discount_total, + adjustment_total, + total, + }) + } + + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.quote_id, "quote_id")?; + if self.quote_version == 0 { + return Err(RadrootsOrderPayloadError::InvalidQuoteVersion); + } + + let totals = self.derived_totals()?; + validate_economic_item_order(&self.items)?; + validate_economic_line_order(&self.discounts, "discounts")?; + validate_economic_line_order(&self.adjustments, "adjustments")?; + validate_total_money(&self.subtotal, self.currency, "subtotal")?; + validate_total_money(&self.discount_total, self.currency, "discount_total")?; + validate_total_money(&self.adjustment_total, self.currency, "adjustment_total")?; + validate_total_money(&self.total, self.currency, "total")?; + validate_total_matches(&self.subtotal, &totals.subtotal, "subtotal")?; + validate_total_matches( + &self.discount_total, + &totals.discount_total, + "discount_total", + )?; + validate_total_matches( + &self.adjustment_total, + &totals.adjustment_total, + "adjustment_total", + )?; + validate_total_matches(&self.total, &totals.total, "total") + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderRequest { + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub items: Vec<RadrootsOrderItem>, + pub economics: RadrootsOrderEconomics, +} + +impl RadrootsOrderRequest { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + validate_order_items(&self.items)?; + self.economics.validate()?; + validate_order_economics_binding(&self.items, &self.economics) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderRevisionProposal { + pub revision_id: String, + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub root_event_id: String, + pub prev_event_id: String, + pub items: Vec<RadrootsOrderItem>, + pub economics: RadrootsOrderEconomics, + pub reason: String, +} + +impl RadrootsOrderRevisionProposal { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.revision_id, "revision_id")?; + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + validate_required_field(&self.root_event_id, "root_event_id")?; + validate_required_field(&self.prev_event_id, "prev_event_id")?; + validate_required_field(&self.reason, "reason")?; + validate_order_items(&self.items)?; + self.economics.validate()?; + validate_order_economics_binding(&self.items, &self.economics) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "decision"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsOrderRevisionOutcome { + Accepted, + Declined { reason: String }, +} + +impl RadrootsOrderRevisionOutcome { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + match self { + Self::Accepted => Ok(()), + Self::Declined { reason } => validate_required_field(reason, "reason"), + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderRevisionDecision { + pub revision_id: String, + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub root_event_id: String, + pub prev_event_id: String, + pub decision: RadrootsOrderRevisionOutcome, +} + +impl RadrootsOrderRevisionDecision { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.revision_id, "revision_id")?; + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + validate_required_field(&self.root_event_id, "root_event_id")?; + validate_required_field(&self.prev_event_id, "prev_event_id")?; + self.decision.validate() + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderInventoryCommitment { + pub bin_id: String, + pub bin_count: u32, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "decision"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsOrderDecisionOutcome { + Accepted { + inventory_commitments: Vec<RadrootsOrderInventoryCommitment>, + }, + Declined { + reason: String, + }, +} + +impl RadrootsOrderDecisionOutcome { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + match self { + Self::Accepted { + inventory_commitments, + } => validate_inventory_commitments(inventory_commitments), + Self::Declined { reason } => validate_required_field(reason, "reason"), + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderDecision { + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub decision: RadrootsOrderDecisionOutcome, +} + +impl RadrootsOrderDecision { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + self.decision.validate() + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderFulfillmentState { + AcceptedNotFulfilled, + Preparing, + ReadyForPickup, + OutForDelivery, + Delivered, + SellerCancelled, +} + +impl RadrootsOrderFulfillmentState { + #[inline] + pub const fn is_publishable_update(self) -> bool { + !matches!(self, Self::AcceptedNotFulfilled) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderFulfillmentUpdate { + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub status: RadrootsOrderFulfillmentState, +} + +impl RadrootsOrderFulfillmentUpdate { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + if self.status.is_publishable_update() { + Ok(()) + } else { + Err(RadrootsOrderPayloadError::InvalidFulfillmentStatus) + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderCancellation { + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub reason: String, +} + +impl RadrootsOrderCancellation { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + validate_required_field(&self.reason, "reason") + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderReceipt { + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub received: bool, + pub issue: Option<String>, + pub received_at: u64, +} + +impl RadrootsOrderReceipt { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + if self.received { + if self.issue.is_some() { + return Err(RadrootsOrderPayloadError::UnexpectedReceiptIssue); + } + } else { + match self.issue.as_deref() { + Some(issue) => validate_required_field(issue, "issue")?, + None => return Err(RadrootsOrderPayloadError::MissingReceiptIssue), + } + } + Ok(()) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderPaymentMethod { + Cash, + ManualTransfer, + Other, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderPaymentRecord { + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub root_event_id: String, + pub previous_event_id: String, + pub agreement_event_id: String, + pub quote_id: String, + pub quote_version: u32, + pub economics_digest: String, + pub amount: RadrootsCoreDecimal, + pub currency: RadrootsCoreCurrency, + pub method: RadrootsOrderPaymentMethod, + pub reference: Option<String>, + pub paid_at: Option<u64>, +} + +impl RadrootsOrderPaymentRecord { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + validate_required_field(&self.root_event_id, "root_event_id")?; + validate_required_field(&self.previous_event_id, "previous_event_id")?; + validate_required_field(&self.agreement_event_id, "agreement_event_id")?; + validate_required_field(&self.quote_id, "quote_id")?; + validate_required_field(&self.economics_digest, "economics_digest")?; + if self.quote_version == 0 { + return Err(RadrootsOrderPayloadError::InvalidQuoteVersion); + } + if self.amount.is_zero() || self.amount.is_sign_negative() { + return Err(RadrootsOrderPayloadError::InvalidPaymentAmount); + } + if let Some(reference) = self.reference.as_deref() { + validate_required_field(reference, "reference")?; + } + Ok(()) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderSettlementOutcome { + Accepted, + Rejected, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderSettlementDecision { + pub order_id: String, + pub listing_addr: String, + pub seller_pubkey: String, + pub buyer_pubkey: String, + pub root_event_id: String, + pub previous_event_id: String, + pub agreement_event_id: String, + pub payment_event_id: String, + pub quote_id: String, + pub quote_version: u32, + pub economics_digest: String, + pub amount: RadrootsCoreDecimal, + pub currency: RadrootsCoreCurrency, + pub decision: RadrootsOrderSettlementOutcome, + pub reason: Option<String>, +} + +impl RadrootsOrderSettlementDecision { + pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.root_event_id, "root_event_id")?; + validate_required_field(&self.previous_event_id, "previous_event_id")?; + validate_required_field(&self.agreement_event_id, "agreement_event_id")?; + validate_required_field(&self.payment_event_id, "payment_event_id")?; + validate_required_field(&self.quote_id, "quote_id")?; + validate_required_field(&self.economics_digest, "economics_digest")?; + if self.quote_version == 0 { + return Err(RadrootsOrderPayloadError::InvalidQuoteVersion); + } + if self.amount.is_zero() || self.amount.is_sign_negative() { + return Err(RadrootsOrderPayloadError::InvalidPaymentAmount); + } + match self.decision { + RadrootsOrderSettlementOutcome::Accepted => { + if self.reason.is_some() { + return Err(RadrootsOrderPayloadError::UnexpectedSettlementReason); + } + } + RadrootsOrderSettlementOutcome::Rejected => match self.reason.as_deref() { + Some(reason) => validate_required_field(reason, "reason")?, + None => return Err(RadrootsOrderPayloadError::MissingSettlementReason), + }, + } + Ok(()) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsCommercialDomain { + #[cfg_attr(feature = "serde", serde(rename = "trade:listing"))] + Listing, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderEventType { + #[cfg_attr(feature = "serde", serde(rename = "TradeOrderRequested"))] + OrderRequested, + #[cfg_attr(feature = "serde", serde(rename = "TradeOrderDecision"))] + OrderDecision, + #[cfg_attr(feature = "serde", serde(rename = "TradeOrderRevisionProposed"))] + OrderRevisionProposed, + #[cfg_attr(feature = "serde", serde(rename = "TradeOrderRevisionDecision"))] + OrderRevisionDecision, + #[cfg_attr(feature = "serde", serde(rename = "TradeOrderCancelled"))] + OrderCancelled, + #[cfg_attr(feature = "serde", serde(rename = "TradeFulfillmentUpdated"))] + FulfillmentUpdated, + #[cfg_attr(feature = "serde", serde(rename = "TradeBuyerReceipt"))] + BuyerReceipt, + #[cfg_attr(feature = "serde", serde(rename = "TradePaymentRecorded"))] + PaymentRecorded, + #[cfg_attr(feature = "serde", serde(rename = "TradeSettlementDecision"))] + SettlementDecision, +} + +impl RadrootsOrderEventType { + #[inline] + pub const fn from_kind(kind: u32) -> Option<Self> { + match kind { + KIND_ORDER_REQUEST => Some(Self::OrderRequested), + KIND_ORDER_DECISION => Some(Self::OrderDecision), + KIND_ORDER_REVISION_PROPOSAL => Some(Self::OrderRevisionProposed), + KIND_ORDER_REVISION_DECISION => Some(Self::OrderRevisionDecision), + KIND_ORDER_CANCELLATION => Some(Self::OrderCancelled), + KIND_ORDER_FULFILLMENT_UPDATE => Some(Self::FulfillmentUpdated), + KIND_ORDER_RECEIPT => Some(Self::BuyerReceipt), + KIND_ORDER_PAYMENT_RECORD => Some(Self::PaymentRecorded), + KIND_ORDER_SETTLEMENT_DECISION => Some(Self::SettlementDecision), + _ => None, + } + } + + #[inline] + pub const fn kind(self) -> u32 { + match self { + Self::OrderRequested => KIND_ORDER_REQUEST, + Self::OrderDecision => KIND_ORDER_DECISION, + Self::OrderRevisionProposed => KIND_ORDER_REVISION_PROPOSAL, + Self::OrderRevisionDecision => KIND_ORDER_REVISION_DECISION, + Self::OrderCancelled => KIND_ORDER_CANCELLATION, + Self::FulfillmentUpdated => KIND_ORDER_FULFILLMENT_UPDATE, + Self::BuyerReceipt => KIND_ORDER_RECEIPT, + Self::PaymentRecorded => KIND_ORDER_PAYMENT_RECORD, + Self::SettlementDecision => KIND_ORDER_SETTLEMENT_DECISION, + } + } + + #[inline] + pub const fn name(self) -> &'static str { + match self { + Self::OrderRequested => "TradeOrderRequested", + Self::OrderDecision => "TradeOrderDecision", + Self::OrderRevisionProposed => "TradeOrderRevisionProposed", + Self::OrderRevisionDecision => "TradeOrderRevisionDecision", + Self::OrderCancelled => "TradeOrderCancelled", + Self::FulfillmentUpdated => "TradeFulfillmentUpdated", + Self::BuyerReceipt => "TradeBuyerReceipt", + Self::PaymentRecorded => "TradePaymentRecorded", + Self::SettlementDecision => "TradeSettlementDecision", + } + } + + #[inline] + pub const fn requires_listing_snapshot(self) -> bool { + matches!(self, Self::OrderRequested) + } + + #[inline] + pub const fn requires_order_chain(self) -> bool { + matches!( + self, + Self::OrderDecision + | Self::OrderRevisionProposed + | Self::OrderRevisionDecision + | Self::OrderCancelled + | Self::FulfillmentUpdated + | Self::BuyerReceipt + | Self::PaymentRecorded + | Self::SettlementDecision + ) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderEnvelope<T> { + pub version: u16, + pub domain: RadrootsCommercialDomain, + #[cfg_attr(feature = "serde", serde(rename = "type"))] + pub message_type: RadrootsOrderEventType, + pub order_id: String, + pub listing_addr: String, + pub payload: T, +} + +impl<T> RadrootsOrderEnvelope<T> { + #[inline] + pub fn new( + message_type: RadrootsOrderEventType, + listing_addr: impl Into<String>, + order_id: impl Into<String>, + payload: T, + ) -> Self { + Self { + version: RADROOTS_ORDER_ENVELOPE_VERSION, + domain: RadrootsCommercialDomain::Listing, + message_type, + order_id: order_id.into(), + listing_addr: listing_addr.into(), + payload, + } + } + + pub fn validate(&self) -> Result<(), RadrootsOrderEnvelopeError> { + if self.version != RADROOTS_ORDER_ENVELOPE_VERSION { + return Err(RadrootsOrderEnvelopeError::InvalidVersion { + expected: RADROOTS_ORDER_ENVELOPE_VERSION, + got: self.version, + }); + } + if self.order_id.trim().is_empty() { + return Err(RadrootsOrderEnvelopeError::MissingOrderId); + } + if self.listing_addr.trim().is_empty() { + return Err(RadrootsOrderEnvelopeError::MissingListingAddr); + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsOrderEnvelopeError { + InvalidVersion { expected: u16, got: u16 }, + MissingOrderId, + MissingListingAddr, +} + +impl core::fmt::Display for RadrootsOrderEnvelopeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidVersion { expected, got } => { + write!( + f, + "invalid order envelope version: expected {expected}, got {got}" + ) + } + Self::MissingOrderId => write!(f, "missing order_id for order message"), + Self::MissingListingAddr => write!(f, "missing listing_addr"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsOrderEnvelopeError {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsOrderPayloadError { + EmptyField(&'static str), + MissingItems, + InvalidItemBinCount { index: usize }, + MissingEconomicItems, + InvalidEconomicItemBinCount { index: usize }, + InvalidEconomicItemQuantity { index: usize }, + InvalidEconomicItemPrice { index: usize }, + InvalidEconomicItemSubtotal { index: usize }, + InvalidEconomicLineAmount { field: &'static str, index: usize }, + InvalidEconomicLineKind { field: &'static str, index: usize }, + InvalidEconomicLineEffect { field: &'static str, index: usize }, + InvalidEconomicCurrency { field: &'static str }, + InvalidEconomicOrdering { field: &'static str }, + InvalidEconomicTotal { field: &'static str }, + InvalidOrderEconomicsBinding { field: &'static str }, + InvalidQuoteVersion, + MissingInventoryCommitments, + InvalidInventoryCommitmentCount { index: usize }, + InvalidFulfillmentStatus, + MissingReceiptIssue, + UnexpectedReceiptIssue, + InvalidPaymentAmount, + MissingSettlementReason, + UnexpectedSettlementReason, +} + +impl core::fmt::Display for RadrootsOrderPayloadError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::EmptyField(field) => write!(f, "{field} cannot be empty"), + Self::MissingItems => write!(f, "items must contain at least one item"), + Self::InvalidItemBinCount { index } => { + write!(f, "items[{index}].bin_count must be greater than zero") + } + Self::MissingEconomicItems => { + write!(f, "economics.items must contain at least one item") + } + Self::InvalidEconomicItemBinCount { index } => write!( + f, + "economics.items[{index}].bin_count must be greater than zero" + ), + Self::InvalidEconomicItemQuantity { index } => write!( + f, + "economics.items[{index}].quantity_amount must be greater than zero" + ), + Self::InvalidEconomicItemPrice { index } => write!( + f, + "economics.items[{index}].unit_price_amount must not be negative" + ), + Self::InvalidEconomicItemSubtotal { index } => { + write!(f, "economics.items[{index}].line_subtotal is invalid") + } + Self::InvalidEconomicLineAmount { field, index } => { + write!( + f, + "economics.{field}[{index}].amount must be greater than zero" + ) + } + Self::InvalidEconomicLineKind { field, index } => { + write!(f, "economics.{field}[{index}].kind is invalid") + } + Self::InvalidEconomicLineEffect { field, index } => { + write!(f, "economics.{field}[{index}].effect is invalid") + } + Self::InvalidEconomicCurrency { field } => { + write!(f, "economics.{field} currency is invalid") + } + Self::InvalidEconomicOrdering { field } => { + write!(f, "economics.{field} is not in canonical order") + } + Self::InvalidEconomicTotal { field } => { + write!(f, "economics.{field} total is invalid") + } + Self::InvalidOrderEconomicsBinding { field } => { + write!(f, "order {field} does not match economics") + } + Self::InvalidQuoteVersion => { + write!(f, "economics.quote_version must be greater than zero") + } + Self::MissingInventoryCommitments => { + write!( + f, + "accepted decisions must contain at least one inventory commitment" + ) + } + Self::InvalidInventoryCommitmentCount { index } => write!( + f, + "inventory_commitments[{index}].bin_count must be greater than zero" + ), + Self::InvalidFulfillmentStatus => { + write!(f, "fulfillment status is not publishable") + } + Self::MissingReceiptIssue => { + write!(f, "receipt issue is required when received is false") + } + Self::UnexpectedReceiptIssue => { + write!(f, "receipt issue must be absent when received is true") + } + Self::InvalidPaymentAmount => { + write!(f, "payment amount must be greater than zero") + } + Self::MissingSettlementReason => { + write!(f, "settlement reason is required when decision is rejected") + } + Self::UnexpectedSettlementReason => { + write!( + f, + "settlement reason must be absent when decision is accepted" + ) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsOrderPayloadError {} + +fn validate_required_field( + value: &str, + field: &'static str, +) -> Result<(), RadrootsOrderPayloadError> { + if value.trim().is_empty() { + Err(RadrootsOrderPayloadError::EmptyField(field)) + } else { + Ok(()) + } +} + +fn validate_order_items(items: &[RadrootsOrderItem]) -> Result<(), RadrootsOrderPayloadError> { + if items.is_empty() { + return Err(RadrootsOrderPayloadError::MissingItems); + } + for (index, item) in items.iter().enumerate() { + validate_required_field(&item.bin_id, "bin_id")?; + if item.bin_count == 0 { + return Err(RadrootsOrderPayloadError::InvalidItemBinCount { index }); + } + } + Ok(()) +} + +fn validate_economic_item( + item: &RadrootsOrderEconomicItem, + expected_currency: RadrootsCoreCurrency, + index: usize, +) -> Result<RadrootsCoreMoney, RadrootsOrderPayloadError> { + validate_required_field(&item.bin_id, "economics.items.bin_id")?; + if item.bin_count == 0 { + return Err(RadrootsOrderPayloadError::InvalidEconomicItemBinCount { index }); + } + if item.quantity_amount.is_zero() || item.quantity_amount.is_sign_negative() { + return Err(RadrootsOrderPayloadError::InvalidEconomicItemQuantity { index }); + } + if item.unit_price_amount.is_sign_negative() { + return Err(RadrootsOrderPayloadError::InvalidEconomicItemPrice { index }); + } + if item.unit_price_currency != expected_currency { + return Err(RadrootsOrderPayloadError::InvalidEconomicCurrency { + field: "items.unit_price_currency", + }); + } + validate_total_money( + &item.line_subtotal, + expected_currency, + "items.line_subtotal", + )?; + + let quantity_total = checked_decimal_mul( + item.quantity_amount, + RadrootsCoreDecimal::from(item.bin_count), + ) + .ok_or(RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { index })?; + let expected_subtotal = checked_decimal_mul(item.unit_price_amount, quantity_total) + .ok_or(RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { index })?; + if item.line_subtotal.amount != expected_subtotal { + return Err(RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { index }); + } + Ok(item.line_subtotal.clone()) +} + +fn validate_order_economics_binding( + items: &[RadrootsOrderItem], + economics: &RadrootsOrderEconomics, +) -> Result<(), RadrootsOrderPayloadError> { + let order_items = normalized_order_item_counts(items).ok_or( + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { + field: "items.bin_count", + }, + )?; + if order_items.len() != economics.items.len() { + return Err(RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { field: "items" }); + } + for (item, economic_item) in order_items.iter().zip(economics.items.iter()) { + if item.bin_id != economic_item.bin_id { + return Err(RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { + field: "items.bin_id", + }); + } + if item.bin_count != u64::from(economic_item.bin_count) { + return Err(RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { + field: "items.bin_count", + }); + } + } + Ok(()) +} + +#[derive(Debug, PartialEq, Eq)] +struct NormalizedOrderItemCount { + bin_id: String, + bin_count: u64, +} + +fn normalized_order_item_counts( + items: &[RadrootsOrderItem], +) -> Option<Vec<NormalizedOrderItemCount>> { + let mut counts: Vec<NormalizedOrderItemCount> = Vec::new(); + for item in items { + let bin_id = item.bin_id.trim(); + if bin_id.is_empty() || item.bin_count == 0 { + return None; + } + if let Some(existing) = counts.iter_mut().find(|count| count.bin_id == bin_id) { + existing.bin_count = existing.bin_count.checked_add(u64::from(item.bin_count))?; + } else { + counts.push(NormalizedOrderItemCount { + bin_id: bin_id.to_string(), + bin_count: u64::from(item.bin_count), + }); + } + } + counts.sort_by(|left, right| left.bin_id.cmp(&right.bin_id)); + Some(counts) +} + +fn validate_economic_line( + line: &RadrootsOrderEconomicLine, + expected_currency: RadrootsCoreCurrency, + field: &'static str, + index: usize, +) -> Result<(), RadrootsOrderPayloadError> { + validate_required_field(&line.id, "economics.line.id")?; + validate_required_field(&line.reason, "economics.line.reason")?; + if line.amount.currency != expected_currency { + return Err(RadrootsOrderPayloadError::InvalidEconomicCurrency { field }); + } + if line.amount.amount.is_zero() || line.amount.amount.is_sign_negative() { + return Err(RadrootsOrderPayloadError::InvalidEconomicLineAmount { field, index }); + } + Ok(()) +} + +fn validate_economic_item_order( + items: &[RadrootsOrderEconomicItem], +) -> Result<(), RadrootsOrderPayloadError> { + for pair in items.windows(2) { + if pair[0].bin_id >= pair[1].bin_id { + return Err(RadrootsOrderPayloadError::InvalidEconomicOrdering { + field: "items.bin_id", + }); + } + } + Ok(()) +} + +fn validate_economic_line_order( + lines: &[RadrootsOrderEconomicLine], + field: &'static str, +) -> Result<(), RadrootsOrderPayloadError> { + for pair in lines.windows(2) { + if pair[0].id >= pair[1].id { + return Err(RadrootsOrderPayloadError::InvalidEconomicOrdering { field }); + } + } + Ok(()) +} + +fn validate_total_money( + money: &RadrootsCoreMoney, + expected_currency: RadrootsCoreCurrency, + field: &'static str, +) -> Result<(), RadrootsOrderPayloadError> { + if money.currency != expected_currency { + return Err(RadrootsOrderPayloadError::InvalidEconomicCurrency { field }); + } + if money.amount.is_sign_negative() { + return Err(RadrootsOrderPayloadError::InvalidEconomicTotal { field }); + } + Ok(()) +} + +fn validate_total_matches( + actual: &RadrootsCoreMoney, + expected: &RadrootsCoreMoney, + field: &'static str, +) -> Result<(), RadrootsOrderPayloadError> { + if actual.currency != expected.currency { + return Err(RadrootsOrderPayloadError::InvalidEconomicCurrency { field }); + } + if actual.amount != expected.amount { + return Err(RadrootsOrderPayloadError::InvalidEconomicTotal { field }); + } + Ok(()) +} + +fn checked_decimal_add( + left: RadrootsCoreDecimal, + right: RadrootsCoreDecimal, +) -> Option<RadrootsCoreDecimal> { + left.0.checked_add(right.0).map(RadrootsCoreDecimal) +} + +fn checked_decimal_sub( + left: RadrootsCoreDecimal, + right: RadrootsCoreDecimal, +) -> Option<RadrootsCoreDecimal> { + left.0.checked_sub(right.0).map(RadrootsCoreDecimal) +} + +fn checked_decimal_mul( + left: RadrootsCoreDecimal, + right: RadrootsCoreDecimal, +) -> Option<RadrootsCoreDecimal> { + left.0.checked_mul(right.0).map(RadrootsCoreDecimal) +} + +fn checked_money_add( + left: &RadrootsCoreMoney, + right: &RadrootsCoreMoney, + field: &'static str, +) -> Result<RadrootsCoreMoney, RadrootsOrderPayloadError> { + if left.currency != right.currency { + return Err(RadrootsOrderPayloadError::InvalidEconomicCurrency { field }); + } + let amount = checked_decimal_add(left.amount, right.amount) + .ok_or(RadrootsOrderPayloadError::InvalidEconomicTotal { field })?; + Ok(RadrootsCoreMoney::new(amount, left.currency)) +} + +fn checked_money_sub_non_negative( + left: &RadrootsCoreMoney, + right: &RadrootsCoreMoney, + field: &'static str, +) -> Result<RadrootsCoreMoney, RadrootsOrderPayloadError> { + if left.currency != right.currency { + return Err(RadrootsOrderPayloadError::InvalidEconomicCurrency { field }); + } + let amount = checked_decimal_sub(left.amount, right.amount) + .ok_or(RadrootsOrderPayloadError::InvalidEconomicTotal { field })?; + if amount.is_sign_negative() { + return Err(RadrootsOrderPayloadError::InvalidEconomicTotal { field }); + } + Ok(RadrootsCoreMoney::new(amount, left.currency)) +} + +fn validate_inventory_commitments( + commitments: &[RadrootsOrderInventoryCommitment], +) -> Result<(), RadrootsOrderPayloadError> { + if commitments.is_empty() { + return Err(RadrootsOrderPayloadError::MissingInventoryCommitments); + } + for (index, commitment) in commitments.iter().enumerate() { + validate_required_field(&commitment.bin_id, "bin_id")?; + if commitment.bin_count == 0 { + return Err(RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { index }); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, + }; + + fn sample_listing_addr() -> String { + "30402:pubkey:AAAAAAAAAAAAAAAAAAAAAg".into() + } + + fn sample_order_request() -> RadrootsOrderRequest { + RadrootsOrderRequest { + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + items: vec![RadrootsOrderItem { + bin_id: "bin-1".into(), + bin_count: 2, + }], + economics: sample_bound_order_economics(), + } + } + + fn decimal(raw: &str) -> RadrootsCoreDecimal { + raw.parse().unwrap() + } + + fn usd(raw: &str) -> RadrootsCoreMoney { + RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) + } + + fn sample_order_economics() -> RadrootsOrderEconomics { + RadrootsOrderEconomics { + quote_id: "quote-1".into(), + quote_version: 1, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![ + RadrootsOrderEconomicItem { + bin_id: "bin-a".into(), + bin_count: 2, + quantity_amount: decimal("1.5"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("4"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd("12"), + }, + RadrootsOrderEconomicItem { + bin_id: "bin-b".into(), + bin_count: 1, + quantity_amount: decimal("2"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("3"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd("6"), + }, + ], + discounts: vec![RadrootsOrderEconomicLine { + id: "discount-a".into(), + kind: RadrootsOrderEconomicLineKind::ListingDiscount, + actor: RadrootsOrderEconomicActor::Seller, + effect: RadrootsOrderEconomicEffect::Decrease, + amount: usd("3"), + reason: "farmstand pickup".into(), + }], + adjustments: vec![ + RadrootsOrderEconomicLine { + id: "adjustment-a".into(), + kind: RadrootsOrderEconomicLineKind::BasketAdjustment, + actor: RadrootsOrderEconomicActor::Buyer, + effect: RadrootsOrderEconomicEffect::Increase, + amount: usd("2"), + reason: "special handling".into(), + }, + RadrootsOrderEconomicLine { + id: "adjustment-b".into(), + kind: RadrootsOrderEconomicLineKind::BasketAdjustment, + actor: RadrootsOrderEconomicActor::Buyer, + effect: RadrootsOrderEconomicEffect::Decrease, + amount: usd("1"), + reason: "local pickup credit".into(), + }, + ], + subtotal: usd("18"), + discount_total: usd("3"), + adjustment_total: usd("3"), + total: usd("16"), + } + } + + fn sample_bound_order_economics() -> RadrootsOrderEconomics { + RadrootsOrderEconomics { + quote_id: "quote-bound-1".into(), + quote_version: 1, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsOrderEconomicItem { + bin_id: "bin-1".into(), + bin_count: 2, + quantity_amount: decimal("1"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("5"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd("10"), + }], + discounts: Vec::new(), + adjustments: Vec::new(), + subtotal: usd("10"), + discount_total: usd("0"), + adjustment_total: usd("0"), + total: usd("10"), + } + } + + fn sample_inventory_commitment() -> RadrootsOrderInventoryCommitment { + RadrootsOrderInventoryCommitment { + bin_id: "bin-1".into(), + bin_count: 2, + } + } + + fn sample_order_decision() -> RadrootsOrderDecision { + RadrootsOrderDecision { + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![sample_inventory_commitment()], + }, + } + } + + fn sample_order_fulfillment_update() -> RadrootsOrderFulfillmentUpdate { + RadrootsOrderFulfillmentUpdate { + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + status: RadrootsOrderFulfillmentState::ReadyForPickup, + } + } + + fn sample_order_cancellation() -> RadrootsOrderCancellation { + RadrootsOrderCancellation { + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + reason: "changed plans".into(), + } + } + + fn sample_order_buyer_receipt(received: bool) -> RadrootsOrderReceipt { + RadrootsOrderReceipt { + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + received, + issue: (!received).then(|| "damaged items".into()), + received_at: 1_777_665_600, + } + } + + fn sample_order_revision_proposal() -> RadrootsOrderRevisionProposal { + RadrootsOrderRevisionProposal { + revision_id: "rev-1".into(), + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + root_event_id: "root-event".into(), + prev_event_id: "previous-event".into(), + items: vec![RadrootsOrderItem { + bin_id: "bin-1".into(), + bin_count: 2, + }], + economics: sample_bound_order_economics(), + reason: "update quantity".into(), + } + } + + fn sample_order_revision_decision( + decision: RadrootsOrderRevisionOutcome, + ) -> RadrootsOrderRevisionDecision { + RadrootsOrderRevisionDecision { + revision_id: "rev-1".into(), + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + root_event_id: "root-event".into(), + prev_event_id: "previous-event".into(), + decision, + } + } + + fn sample_payment_recorded() -> RadrootsOrderPaymentRecord { + RadrootsOrderPaymentRecord { + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + root_event_id: "root-event".into(), + previous_event_id: "previous-event".into(), + agreement_event_id: "agreement-event".into(), + quote_id: "quote-1".into(), + quote_version: 1, + economics_digest: "economics-digest".into(), + amount: decimal("16"), + currency: RadrootsCoreCurrency::USD, + method: RadrootsOrderPaymentMethod::ManualTransfer, + reference: Some("bank-ref".into()), + paid_at: Some(1_777_665_600), + } + } + + fn sample_settlement_decision( + decision: RadrootsOrderSettlementOutcome, + reason: Option<&str>, + ) -> RadrootsOrderSettlementDecision { + RadrootsOrderSettlementDecision { + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + seller_pubkey: "seller".into(), + buyer_pubkey: "buyer".into(), + root_event_id: "root-event".into(), + previous_event_id: "previous-event".into(), + agreement_event_id: "agreement-event".into(), + payment_event_id: "payment-event".into(), + quote_id: "quote-1".into(), + quote_version: 1, + economics_digest: "economics-digest".into(), + amount: decimal("16"), + currency: RadrootsCoreCurrency::USD, + decision, + reason: reason.map(Into::into), + } + } + + #[test] + fn order_message_type_uses_canonical_names_and_kinds() { + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_REQUEST), + Some(RadrootsOrderEventType::OrderRequested) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_DECISION), + Some(RadrootsOrderEventType::OrderDecision) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_REVISION_PROPOSAL), + Some(RadrootsOrderEventType::OrderRevisionProposed) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_REVISION_DECISION), + Some(RadrootsOrderEventType::OrderRevisionDecision) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_FULFILLMENT_UPDATE), + Some(RadrootsOrderEventType::FulfillmentUpdated) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_CANCELLATION), + Some(RadrootsOrderEventType::OrderCancelled) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_RECEIPT), + Some(RadrootsOrderEventType::BuyerReceipt) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_PAYMENT_RECORD), + Some(RadrootsOrderEventType::PaymentRecorded) + ); + assert_eq!( + RadrootsOrderEventType::from_kind(KIND_ORDER_SETTLEMENT_DECISION), + Some(RadrootsOrderEventType::SettlementDecision) + ); + assert_eq!(RadrootsOrderEventType::from_kind(3431), None); + assert_eq!( + RadrootsOrderEventType::OrderRequested.kind(), + KIND_ORDER_REQUEST + ); + assert_eq!( + RadrootsOrderEventType::OrderDecision.kind(), + KIND_ORDER_DECISION + ); + assert_eq!( + RadrootsOrderEventType::OrderRevisionProposed.kind(), + KIND_ORDER_REVISION_PROPOSAL + ); + assert_eq!( + RadrootsOrderEventType::OrderRevisionDecision.kind(), + KIND_ORDER_REVISION_DECISION + ); + assert_eq!( + RadrootsOrderEventType::FulfillmentUpdated.kind(), + KIND_ORDER_FULFILLMENT_UPDATE + ); + assert_eq!( + RadrootsOrderEventType::OrderCancelled.kind(), + KIND_ORDER_CANCELLATION + ); + assert_eq!( + RadrootsOrderEventType::BuyerReceipt.kind(), + KIND_ORDER_RECEIPT + ); + assert_eq!( + RadrootsOrderEventType::PaymentRecorded.kind(), + KIND_ORDER_PAYMENT_RECORD + ); + assert_eq!( + RadrootsOrderEventType::SettlementDecision.kind(), + KIND_ORDER_SETTLEMENT_DECISION + ); + assert_eq!( + RadrootsOrderEventType::OrderRequested.name(), + "TradeOrderRequested" + ); + assert_eq!( + RadrootsOrderEventType::OrderDecision.name(), + "TradeOrderDecision" + ); + assert_eq!( + RadrootsOrderEventType::OrderRevisionProposed.name(), + "TradeOrderRevisionProposed" + ); + assert_eq!( + RadrootsOrderEventType::OrderRevisionDecision.name(), + "TradeOrderRevisionDecision" + ); + assert_eq!( + RadrootsOrderEventType::FulfillmentUpdated.name(), + "TradeFulfillmentUpdated" + ); + assert_eq!( + RadrootsOrderEventType::OrderCancelled.name(), + "TradeOrderCancelled" + ); + assert_eq!( + RadrootsOrderEventType::BuyerReceipt.name(), + "TradeBuyerReceipt" + ); + assert_eq!( + RadrootsOrderEventType::PaymentRecorded.name(), + "TradePaymentRecorded" + ); + assert_eq!( + RadrootsOrderEventType::SettlementDecision.name(), + "TradeSettlementDecision" + ); + assert!(RadrootsOrderEventType::OrderRequested.requires_listing_snapshot()); + assert!(RadrootsOrderEventType::OrderDecision.requires_order_chain()); + assert!(RadrootsOrderEventType::OrderRevisionProposed.requires_order_chain()); + assert!(RadrootsOrderEventType::OrderRevisionDecision.requires_order_chain()); + assert!(RadrootsOrderEventType::FulfillmentUpdated.requires_order_chain()); + assert!(RadrootsOrderEventType::OrderCancelled.requires_order_chain()); + assert!(RadrootsOrderEventType::BuyerReceipt.requires_order_chain()); + assert!(RadrootsOrderEventType::PaymentRecorded.requires_order_chain()); + assert!(RadrootsOrderEventType::SettlementDecision.requires_order_chain()); + assert!(!RadrootsOrderEventType::OrderRequested.requires_order_chain()); + assert!(!RadrootsOrderEventType::PaymentRecorded.requires_listing_snapshot()); + + let request_name = serde_json::to_value(RadrootsOrderEventType::OrderRequested).unwrap(); + let decision_name = serde_json::to_value(RadrootsOrderEventType::OrderDecision).unwrap(); + let revision_proposed_name = + serde_json::to_value(RadrootsOrderEventType::OrderRevisionProposed).unwrap(); + let revision_decision_name = + serde_json::to_value(RadrootsOrderEventType::OrderRevisionDecision).unwrap(); + let fulfillment_name = + serde_json::to_value(RadrootsOrderEventType::FulfillmentUpdated).unwrap(); + let cancellation_name = + serde_json::to_value(RadrootsOrderEventType::OrderCancelled).unwrap(); + let receipt_name = serde_json::to_value(RadrootsOrderEventType::BuyerReceipt).unwrap(); + let payment_name = serde_json::to_value(RadrootsOrderEventType::PaymentRecorded).unwrap(); + let settlement_name = + serde_json::to_value(RadrootsOrderEventType::SettlementDecision).unwrap(); + assert_eq!(request_name, serde_json::json!("TradeOrderRequested")); + assert_eq!(decision_name, serde_json::json!("TradeOrderDecision")); + assert_eq!( + revision_proposed_name, + serde_json::json!("TradeOrderRevisionProposed") + ); + assert_eq!( + revision_decision_name, + serde_json::json!("TradeOrderRevisionDecision") + ); + assert_eq!( + fulfillment_name, + serde_json::json!("TradeFulfillmentUpdated") + ); + assert_eq!(cancellation_name, serde_json::json!("TradeOrderCancelled")); + assert_eq!(receipt_name, serde_json::json!("TradeBuyerReceipt")); + assert_eq!(payment_name, serde_json::json!("TradePaymentRecorded")); + assert_eq!( + settlement_name, + serde_json::json!("TradeSettlementDecision") + ); + } + + #[test] + fn order_request_validation_rejects_invalid_fields() { + assert_eq!(sample_order_request().validate(), Ok(())); + + let mut missing_order_id = sample_order_request(); + missing_order_id.order_id = " ".into(); + assert_eq!( + missing_order_id.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("order_id") + ); + + let mut missing_items = sample_order_request(); + missing_items.items.clear(); + assert_eq!( + missing_items.validate().unwrap_err(), + RadrootsOrderPayloadError::MissingItems + ); + + let mut invalid_count = sample_order_request(); + invalid_count.items[0].bin_count = 0; + assert_eq!( + invalid_count.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidItemBinCount { index: 0 } + ); + + let mut missing_bin_id = sample_order_request(); + missing_bin_id.items[0].bin_id = " ".into(); + assert_eq!( + missing_bin_id.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("bin_id") + ); + + let mut mismatched_economic_item = sample_order_request(); + mismatched_economic_item.economics.items[0].bin_id = "bin-other".into(); + assert_eq!( + mismatched_economic_item.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { + field: "items.bin_id" + } + ); + + let mut mismatched_economic_count = sample_order_request(); + mismatched_economic_count.economics.items[0].bin_count = 3; + mismatched_economic_count.economics.items[0].line_subtotal = usd("15"); + mismatched_economic_count.economics.subtotal = usd("15"); + mismatched_economic_count.economics.total = usd("15"); + assert_eq!( + mismatched_economic_count.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { + field: "items.bin_count" + } + ); + } + + #[test] + fn order_economics_validation_accepts_canonical_totals() { + let economics = sample_order_economics(); + assert_eq!(economics.validate(), Ok(())); + + let totals = economics.derived_totals().unwrap(); + assert_eq!(totals.subtotal, usd("18")); + assert_eq!(totals.discount_total, usd("3")); + assert_eq!(totals.adjustment_total, usd("3")); + assert_eq!(totals.total, usd("16")); + + let json = serde_json::to_value(&economics).unwrap(); + assert_eq!(json["pricing_basis"], serde_json::json!("listing_event")); + assert_eq!( + json["discounts"][0]["kind"], + serde_json::json!("listing_discount") + ); + assert_eq!( + json["adjustments"][0]["effect"], + serde_json::json!("increase") + ); + } + + #[test] + fn order_economics_canonicalized_sorts_items_and_lines() { + let mut economics = sample_order_economics(); + economics.items.reverse(); + economics.adjustments.reverse(); + economics.discounts.push(RadrootsOrderEconomicLine { + id: "discount-b".into(), + kind: RadrootsOrderEconomicLineKind::ListingDiscount, + actor: RadrootsOrderEconomicActor::Seller, + effect: RadrootsOrderEconomicEffect::Decrease, + amount: usd("1"), + reason: "market credit".into(), + }); + economics.discounts.reverse(); + economics.subtotal = usd("19"); + economics.total = usd("17"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicOrdering { + field: "items.bin_id" + } + ); + + let canonical = economics.canonicalized(); + assert_eq!(canonical.items[0].bin_id, "bin-a"); + assert_eq!(canonical.discounts[0].id, "discount-a"); + assert_eq!(canonical.adjustments[0].id, "adjustment-a"); + assert_eq!(canonical.subtotal, usd("18")); + assert_eq!(canonical.discount_total, usd("4")); + assert_eq!(canonical.total, usd("15")); + assert_eq!(canonical.validate(), Ok(())); + + let mut uncanonicalizable = sample_order_economics(); + uncanonicalizable.items.clear(); + uncanonicalizable.subtotal = usd("88"); + uncanonicalizable.canonicalize(); + assert_eq!(uncanonicalizable.subtotal, usd("88")); + } + + #[test] + fn order_economics_validation_rejects_mixed_currency() { + let mut economics = sample_order_economics(); + economics.items[0].unit_price_currency = RadrootsCoreCurrency::EUR; + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicCurrency { + field: "items.unit_price_currency" + } + ); + + let mut economics = sample_order_economics(); + economics.adjustments[0].amount = + RadrootsCoreMoney::new(decimal("2"), RadrootsCoreCurrency::EUR); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicCurrency { + field: "adjustments" + } + ); + } + + #[test] + fn order_economics_validation_rejects_bad_subtotal() { + let mut economics = sample_order_economics(); + economics.items[0].bin_count = 0; + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicItemBinCount { index: 0 } + ); + + let mut economics = sample_order_economics(); + economics.items[0].line_subtotal = usd("11.99"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { index: 0 } + ); + + let mut economics = sample_order_economics(); + economics.items[0].line_subtotal = + RadrootsCoreMoney::new(decimal("12"), RadrootsCoreCurrency::EUR); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicCurrency { + field: "items.line_subtotal" + } + ); + } + + #[test] + fn order_economics_validation_covers_remaining_error_paths() { + let mut economics = sample_order_economics(); + economics.items.clear(); + assert_eq!( + economics.derived_totals().unwrap_err(), + RadrootsOrderPayloadError::MissingEconomicItems + ); + + let mut economics = sample_order_economics(); + economics.quote_version = 0; + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidQuoteVersion + ); + + let mut economics = sample_order_economics(); + economics.items[0].quantity_amount = decimal("0"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicItemQuantity { index: 0 } + ); + + let mut economics = sample_order_economics(); + economics.items[0].quantity_amount = decimal("-1"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicItemQuantity { index: 0 } + ); + + let mut economics = sample_order_economics(); + economics.items[0].unit_price_amount = decimal("-1"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicItemPrice { index: 0 } + ); + + let mut economics = sample_order_economics(); + economics.discounts[0].kind = RadrootsOrderEconomicLineKind::BasketAdjustment; + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicLineKind { + field: "discounts", + index: 0 + } + ); + + let mut economics = sample_order_economics(); + economics.subtotal = RadrootsCoreMoney::new(decimal("18"), RadrootsCoreCurrency::EUR); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicCurrency { field: "subtotal" } + ); + + let mut economics = sample_order_economics(); + economics.subtotal = usd("-1"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicTotal { field: "subtotal" } + ); + + let mut economics = sample_order_economics(); + economics.discount_total = usd("4"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicTotal { + field: "discount_total" + } + ); + + let mut economics = sample_order_economics(); + economics.adjustment_total = usd("4"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicTotal { + field: "adjustment_total" + } + ); + + let economics = sample_bound_order_economics(); + assert_eq!( + validate_order_economics_binding(&[], &economics).unwrap_err(), + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { field: "items" } + ); + + let invalid_order_items = [RadrootsOrderItem { + bin_id: " ".into(), + bin_count: 1, + }]; + assert_eq!( + validate_order_economics_binding(&invalid_order_items, &economics).unwrap_err(), + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { + field: "items.bin_count" + } + ); + + let duplicate_counts = normalized_order_item_counts(&[ + RadrootsOrderItem { + bin_id: "bin-1".into(), + bin_count: 1, + }, + RadrootsOrderItem { + bin_id: "bin-1".into(), + bin_count: 2, + }, + ]) + .unwrap(); + assert_eq!(duplicate_counts[0].bin_count, 3); + + assert!( + normalized_order_item_counts(&[RadrootsOrderItem { + bin_id: " ".into(), + bin_count: 1, + }]) + .is_none() + ); + assert!( + normalized_order_item_counts(&[RadrootsOrderItem { + bin_id: "bin-1".into(), + bin_count: 0, + }]) + .is_none() + ); + let sorted_counts = normalized_order_item_counts(&[ + RadrootsOrderItem { + bin_id: "bin-b".into(), + bin_count: 1, + }, + RadrootsOrderItem { + bin_id: "bin-a".into(), + bin_count: 1, + }, + ]) + .unwrap(); + assert_eq!(sorted_counts[0].bin_id, "bin-a"); + } + + #[test] + fn order_economics_validation_rejects_bad_line_semantics() { + let mut economics = sample_order_economics(); + economics.discounts[0].effect = RadrootsOrderEconomicEffect::Increase; + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicLineEffect { + field: "discounts", + index: 0 + } + ); + + let mut economics = sample_order_economics(); + economics.adjustments[0].kind = RadrootsOrderEconomicLineKind::ListingDiscount; + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicLineKind { + field: "adjustments", + index: 0 + } + ); + + let mut economics = sample_order_economics(); + economics.adjustments[0].amount = usd("0"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicLineAmount { + field: "adjustments", + index: 0 + } + ); + + let mut economics = sample_order_economics(); + economics.adjustments[0].amount = usd("-1"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicLineAmount { + field: "adjustments", + index: 0 + } + ); + } + + #[test] + fn order_economics_helpers_cover_currency_error_paths() { + assert_eq!( + validate_total_money(&usd("-1"), RadrootsCoreCurrency::USD, "subtotal").unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicTotal { field: "subtotal" } + ); + assert_eq!( + validate_total_matches( + &usd("1"), + &RadrootsCoreMoney::new(decimal("1"), RadrootsCoreCurrency::EUR), + "total" + ) + .unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicCurrency { field: "total" } + ); + assert_eq!( + checked_money_add( + &usd("1"), + &RadrootsCoreMoney::new(decimal("1"), RadrootsCoreCurrency::EUR), + "subtotal" + ) + .unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicCurrency { field: "subtotal" } + ); + assert_eq!( + checked_money_sub_non_negative( + &usd("1"), + &RadrootsCoreMoney::new(decimal("1"), RadrootsCoreCurrency::EUR), + "total" + ) + .unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicCurrency { field: "total" } + ); + } + + #[test] + fn order_economics_validation_rejects_duplicate_line_ids() { + let mut economics = sample_order_economics(); + economics.adjustments[1].id = "adjustment-a".into(); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicOrdering { + field: "adjustments" + } + ); + } + + #[test] + fn order_economics_validation_rejects_negative_derived_total() { + let mut economics = sample_order_economics(); + economics.adjustments[1].amount = usd("20"); + economics.adjustment_total = usd("22"); + economics.total = usd("0"); + assert_eq!( + economics.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidEconomicTotal { field: "total" } + ); + } + + #[test] + fn order_decision_validation_enforces_commitment_invariants() { + assert_eq!(sample_order_decision().validate(), Ok(())); + + let declined = RadrootsOrderDecision { + decision: RadrootsOrderDecisionOutcome::Declined { + reason: "out_of_stock".into(), + }, + ..sample_order_decision() + }; + assert_eq!(declined.validate(), Ok(())); + + let accepted_without_commitments = RadrootsOrderDecision { + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: Vec::new(), + }, + ..sample_order_decision() + }; + assert_eq!( + accepted_without_commitments.validate().unwrap_err(), + RadrootsOrderPayloadError::MissingInventoryCommitments + ); + + let accepted_with_zero_count = RadrootsOrderDecision { + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { + bin_id: "bin-1".into(), + bin_count: 0, + }], + }, + ..sample_order_decision() + }; + assert_eq!( + accepted_with_zero_count.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { index: 0 } + ); + + let declined_without_reason = RadrootsOrderDecision { + decision: RadrootsOrderDecisionOutcome::Declined { reason: " ".into() }, + ..sample_order_decision() + }; + assert_eq!( + declined_without_reason.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("reason") + ); + } + + #[test] + fn order_revision_validation_covers_proposed_and_decision_paths() { + assert_eq!(sample_order_revision_proposal().validate(), Ok(())); + + let missing_prev = RadrootsOrderRevisionProposal { + prev_event_id: " ".into(), + ..sample_order_revision_proposal() + }; + assert_eq!( + missing_prev.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("prev_event_id") + ); + + assert_eq!( + sample_order_revision_decision(RadrootsOrderRevisionOutcome::Accepted).validate(), + Ok(()) + ); + assert_eq!( + sample_order_revision_decision(RadrootsOrderRevisionOutcome::Declined { + reason: "out of stock".into(), + }) + .validate(), + Ok(()) + ); + + let declined_without_reason = + sample_order_revision_decision(RadrootsOrderRevisionOutcome::Declined { + reason: " ".into(), + }); + assert_eq!( + declined_without_reason.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("reason") + ); + + let missing_root = RadrootsOrderRevisionDecision { + root_event_id: " ".into(), + ..sample_order_revision_decision(RadrootsOrderRevisionOutcome::Accepted) + }; + assert_eq!( + missing_root.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("root_event_id") + ); + } + + #[test] + fn order_fulfillment_update_validation_rejects_derived_state() { + assert_eq!(sample_order_fulfillment_update().validate(), Ok(())); + + let derived = RadrootsOrderFulfillmentUpdate { + status: RadrootsOrderFulfillmentState::AcceptedNotFulfilled, + ..sample_order_fulfillment_update() + }; + assert_eq!( + derived.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidFulfillmentStatus + ); + + let missing_seller = RadrootsOrderFulfillmentUpdate { + seller_pubkey: " ".into(), + ..sample_order_fulfillment_update() + }; + assert_eq!( + missing_seller.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("seller_pubkey") + ); + } + + #[test] + fn order_cancellation_validation_requires_buyer_bindings_and_reason() { + assert_eq!(sample_order_cancellation().validate(), Ok(())); + + let missing_reason = RadrootsOrderCancellation { + reason: " ".into(), + ..sample_order_cancellation() + }; + assert_eq!( + missing_reason.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("reason") + ); + + let missing_buyer = RadrootsOrderCancellation { + buyer_pubkey: " ".into(), + ..sample_order_cancellation() + }; + assert_eq!( + missing_buyer.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("buyer_pubkey") + ); + } + + #[test] + fn order_buyer_receipt_validation_requires_consistent_received_and_issue() { + assert_eq!(sample_order_buyer_receipt(true).validate(), Ok(())); + assert_eq!(sample_order_buyer_receipt(false).validate(), Ok(())); + + let received_with_issue = RadrootsOrderReceipt { + issue: Some("damaged".into()), + ..sample_order_buyer_receipt(true) + }; + assert_eq!( + received_with_issue.validate().unwrap_err(), + RadrootsOrderPayloadError::UnexpectedReceiptIssue + ); + + let not_received_without_issue = RadrootsOrderReceipt { + issue: None, + ..sample_order_buyer_receipt(false) + }; + assert_eq!( + not_received_without_issue.validate().unwrap_err(), + RadrootsOrderPayloadError::MissingReceiptIssue + ); + + let not_received_blank_issue = RadrootsOrderReceipt { + issue: Some(" ".into()), + ..sample_order_buyer_receipt(false) + }; + assert_eq!( + not_received_blank_issue.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("issue") + ); + } + + #[test] + fn order_payment_and_settlement_validation_covers_amount_and_reason_paths() { + assert_eq!(sample_payment_recorded().validate(), Ok(())); + + let unreferenced_payment = RadrootsOrderPaymentRecord { + reference: None, + ..sample_payment_recorded() + }; + assert_eq!(unreferenced_payment.validate(), Ok(())); + + let invalid_quote_version = RadrootsOrderPaymentRecord { + quote_version: 0, + ..sample_payment_recorded() + }; + assert_eq!( + invalid_quote_version.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidQuoteVersion + ); + + let invalid_amount = RadrootsOrderPaymentRecord { + amount: decimal("0"), + ..sample_payment_recorded() + }; + assert_eq!( + invalid_amount.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidPaymentAmount + ); + + let negative_amount = RadrootsOrderPaymentRecord { + amount: decimal("-1"), + ..sample_payment_recorded() + }; + assert_eq!( + negative_amount.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidPaymentAmount + ); + + let blank_reference = RadrootsOrderPaymentRecord { + reference: Some(" ".into()), + ..sample_payment_recorded() + }; + assert_eq!( + blank_reference.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("reference") + ); + + assert_eq!( + sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None).validate(), + Ok(()) + ); + assert_eq!( + sample_settlement_decision(RadrootsOrderSettlementOutcome::Rejected, Some("damaged")) + .validate(), + Ok(()) + ); + + let accepted_with_reason = + sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, Some("extra")); + assert_eq!( + accepted_with_reason.validate().unwrap_err(), + RadrootsOrderPayloadError::UnexpectedSettlementReason + ); + + let rejected_without_reason = + sample_settlement_decision(RadrootsOrderSettlementOutcome::Rejected, None); + assert_eq!( + rejected_without_reason.validate().unwrap_err(), + RadrootsOrderPayloadError::MissingSettlementReason + ); + + let rejected_blank_reason = + sample_settlement_decision(RadrootsOrderSettlementOutcome::Rejected, Some(" ")); + assert_eq!( + rejected_blank_reason.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("reason") + ); + + let invalid_quote_version = RadrootsOrderSettlementDecision { + quote_version: 0, + ..sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None) + }; + assert_eq!( + invalid_quote_version.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidQuoteVersion + ); + + let zero_amount = RadrootsOrderSettlementDecision { + amount: decimal("0"), + ..sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None) + }; + assert_eq!( + zero_amount.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidPaymentAmount + ); + + let invalid_amount = RadrootsOrderSettlementDecision { + amount: decimal("-1"), + ..sample_settlement_decision(RadrootsOrderSettlementOutcome::Accepted, None) + }; + assert_eq!( + invalid_amount.validate().unwrap_err(), + RadrootsOrderPayloadError::InvalidPaymentAmount + ); + } + + #[test] + fn order_envelope_serializes_canonical_type_name() { + let envelope = RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + sample_listing_addr(), + "order-1", + sample_order_request(), + ); + assert_eq!(envelope.validate(), Ok(())); + + let json = serde_json::to_value(&envelope).unwrap(); + assert_eq!(json["type"], serde_json::json!("TradeOrderRequested")); + assert_eq!(json["order_id"], serde_json::json!("order-1")); + assert_eq!( + json["listing_addr"], + serde_json::json!("30402:pubkey:AAAAAAAAAAAAAAAAAAAAAg") + ); + assert_eq!(json["payload"]["items"][0]["bin_id"], "bin-1"); + } + + #[test] + fn order_envelope_validation_and_display_cover_error_paths() { + let invalid_version = RadrootsOrderEnvelope { + version: RADROOTS_ORDER_ENVELOPE_VERSION + 1, + domain: RadrootsCommercialDomain::Listing, + message_type: RadrootsOrderEventType::OrderRequested, + order_id: "order-1".into(), + listing_addr: sample_listing_addr(), + payload: sample_order_request(), + }; + let invalid_version_err = invalid_version.validate().unwrap_err(); + assert_eq!( + invalid_version_err, + RadrootsOrderEnvelopeError::InvalidVersion { + expected: RADROOTS_ORDER_ENVELOPE_VERSION, + got: RADROOTS_ORDER_ENVELOPE_VERSION + 1, + } + ); + assert_eq!( + invalid_version_err.to_string(), + "invalid order envelope version: expected 1, got 2" + ); + + let missing_order = RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + sample_listing_addr(), + " ", + sample_order_request(), + ); + let missing_order_err = missing_order.validate().unwrap_err(); + assert_eq!( + missing_order_err, + RadrootsOrderEnvelopeError::MissingOrderId + ); + assert_eq!( + missing_order_err.to_string(), + "missing order_id for order message" + ); + + let missing_listing = RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + " ", + "order-1", + sample_order_request(), + ); + let missing_listing_err = missing_listing.validate().unwrap_err(); + assert_eq!( + missing_listing_err, + RadrootsOrderEnvelopeError::MissingListingAddr + ); + assert_eq!(missing_listing_err.to_string(), "missing listing_addr"); + } + + #[test] + fn listing_parse_error_display_variants() { + assert_eq!( + RadrootsListingParseError::InvalidKind(KIND_PROFILE).to_string(), + "invalid listing kind: 0" + ); + assert_eq!( + RadrootsListingParseError::MissingTag("price".into()).to_string(), + "missing required tag: price" + ); + assert_eq!( + RadrootsListingParseError::InvalidTag("farm".into()).to_string(), + "invalid tag: farm" + ); + assert_eq!( + RadrootsListingParseError::InvalidNumber("inventory".into()).to_string(), + "invalid number: inventory" + ); + assert_eq!( + RadrootsListingParseError::InvalidUnit.to_string(), + "invalid unit" + ); + assert_eq!( + RadrootsListingParseError::InvalidCurrency.to_string(), + "invalid currency" + ); + assert_eq!( + RadrootsListingParseError::InvalidJson("bins".into()).to_string(), + "invalid json: bins" + ); + assert_eq!( + RadrootsListingParseError::InvalidDiscount("offer".into()).to_string(), + "invalid discount data for offer" + ); + } + + #[test] + fn listing_validation_error_display_variants() { + assert_eq!( + (RadrootsTradeValidationListingError::InvalidKind { kind: KIND_PROFILE }).to_string(), + "invalid listing kind: 0" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingListingId.to_string(), + "missing listing id" + ); + assert_eq!( + RadrootsTradeValidationListingError::ListingEventNotFound { + listing_addr: "listing-1".into(), + } + .to_string(), + "listing event not found: listing-1" + ); + assert_eq!( + RadrootsTradeValidationListingError::ListingEventFetchFailed { + listing_addr: "listing-2".into(), + } + .to_string(), + "listing event fetch failed: listing-2" + ); + assert_eq!( + RadrootsTradeValidationListingError::ParseError { + error: RadrootsListingParseError::InvalidJson("payload".into()), + } + .to_string(), + "invalid listing data: invalid json: payload" + ); + assert_eq!( + RadrootsTradeValidationListingError::InvalidSeller.to_string(), + "listing author does not match farm pubkey" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingFarmProfile.to_string(), + "missing farm profile" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingFarmRecord.to_string(), + "missing farm record" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingTitle.to_string(), + "missing listing title" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingDescription.to_string(), + "missing listing description" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingProductType.to_string(), + "missing listing product type" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingBins.to_string(), + "missing listing bins" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingPrimaryBin.to_string(), + "missing primary listing bin" + ); + assert_eq!( + RadrootsTradeValidationListingError::InvalidBin.to_string(), + "invalid listing bin" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingPrice.to_string(), + "missing listing price" + ); + assert_eq!( + RadrootsTradeValidationListingError::InvalidPrice.to_string(), + "invalid listing price" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingInventory.to_string(), + "missing listing inventory" + ); + assert_eq!( + RadrootsTradeValidationListingError::InvalidInventory.to_string(), + "invalid listing inventory" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingAvailability.to_string(), + "missing listing availability" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingLocation.to_string(), + "missing listing location" + ); + assert_eq!( + RadrootsTradeValidationListingError::MissingDeliveryMethod.to_string(), + "missing listing delivery method" + ); + } + + #[test] + fn order_payload_error_display_variants_cover_all_messages() { + let cases = [ + ( + RadrootsOrderPayloadError::EmptyField("field"), + "field cannot be empty", + ), + ( + RadrootsOrderPayloadError::MissingItems, + "items must contain at least one item", + ), + ( + RadrootsOrderPayloadError::InvalidItemBinCount { index: 2 }, + "items[2].bin_count must be greater than zero", + ), + ( + RadrootsOrderPayloadError::MissingEconomicItems, + "economics.items must contain at least one item", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicItemBinCount { index: 3 }, + "economics.items[3].bin_count must be greater than zero", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicItemQuantity { index: 4 }, + "economics.items[4].quantity_amount must be greater than zero", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicItemPrice { index: 5 }, + "economics.items[5].unit_price_amount must not be negative", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { index: 6 }, + "economics.items[6].line_subtotal is invalid", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicLineAmount { + field: "adjustments", + index: 7, + }, + "economics.adjustments[7].amount must be greater than zero", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicLineKind { + field: "discounts", + index: 8, + }, + "economics.discounts[8].kind is invalid", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicLineEffect { + field: "discounts", + index: 9, + }, + "economics.discounts[9].effect is invalid", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicCurrency { field: "total" }, + "economics.total currency is invalid", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicOrdering { field: "items" }, + "economics.items is not in canonical order", + ), + ( + RadrootsOrderPayloadError::InvalidEconomicTotal { field: "subtotal" }, + "economics.subtotal total is invalid", + ), + ( + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { field: "items" }, + "order items does not match economics", + ), + ( + RadrootsOrderPayloadError::InvalidQuoteVersion, + "economics.quote_version must be greater than zero", + ), + ( + RadrootsOrderPayloadError::MissingInventoryCommitments, + "accepted decisions must contain at least one inventory commitment", + ), + ( + RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { index: 1 }, + "inventory_commitments[1].bin_count must be greater than zero", + ), + ( + RadrootsOrderPayloadError::InvalidFulfillmentStatus, + "fulfillment status is not publishable", + ), + ( + RadrootsOrderPayloadError::MissingReceiptIssue, + "receipt issue is required when received is false", + ), + ( + RadrootsOrderPayloadError::UnexpectedReceiptIssue, + "receipt issue must be absent when received is true", + ), + ( + RadrootsOrderPayloadError::InvalidPaymentAmount, + "payment amount must be greater than zero", + ), + ( + RadrootsOrderPayloadError::MissingSettlementReason, + "settlement reason is required when decision is rejected", + ), + ( + RadrootsOrderPayloadError::UnexpectedSettlementReason, + "settlement reason must be absent when decision is accepted", + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + } + } +} diff --git a/crates/events/src/order_economics.rs b/crates/events/src/order_economics.rs @@ -0,0 +1,95 @@ +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, +}; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderItem { + pub bin_id: String, + pub bin_count: u32, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderPricingBasis { + ListingEvent, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderEconomicLineKind { + ListingDiscount, + BasketAdjustment, + RevisionAdjustment, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderEconomicActor { + Buyer, + Seller, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOrderEconomicEffect { + Increase, + Decrease, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderEconomicItem { + pub bin_id: String, + pub bin_count: u32, + pub quantity_amount: RadrootsCoreDecimal, + pub quantity_unit: RadrootsCoreUnit, + pub unit_price_amount: RadrootsCoreDecimal, + pub unit_price_currency: RadrootsCoreCurrency, + pub line_subtotal: RadrootsCoreMoney, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderEconomicLine { + pub id: String, + pub kind: RadrootsOrderEconomicLineKind, + pub actor: RadrootsOrderEconomicActor, + pub effect: RadrootsOrderEconomicEffect, + pub amount: RadrootsCoreMoney, + pub reason: String, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderEconomicTotals { + pub subtotal: RadrootsCoreMoney, + pub discount_total: RadrootsCoreMoney, + pub adjustment_total: RadrootsCoreMoney, + pub total: RadrootsCoreMoney, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderEconomics { + pub quote_id: String, + pub quote_version: u32, + pub pricing_basis: RadrootsOrderPricingBasis, + pub currency: RadrootsCoreCurrency, + pub items: Vec<RadrootsOrderEconomicItem>, + pub discounts: Vec<RadrootsOrderEconomicLine>, + pub adjustments: Vec<RadrootsOrderEconomicLine>, + pub subtotal: RadrootsCoreMoney, + pub discount_total: RadrootsCoreMoney, + pub adjustment_total: RadrootsCoreMoney, + pub total: RadrootsCoreMoney, +} diff --git a/crates/events/src/trade.rs b/crates/events/src/trade.rs @@ -1,3568 +0,0 @@ -#![forbid(unsafe_code)] - -#[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; - -use crate::{RadrootsNostrEventPtr, kinds::*}; -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCoreMoney, - RadrootsCoreUnit, -}; - -pub const RADROOTS_TRADE_LISTING_DOMAIN: &str = "trade:listing"; -pub const RADROOTS_TRADE_ENVELOPE_VERSION: u16 = 1; - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeListingParseError { - InvalidKind(u32), - MissingTag(String), - InvalidTag(String), - InvalidNumber(String), - InvalidUnit, - InvalidCurrency, - InvalidJson(String), - InvalidDiscount(String), -} - -impl core::fmt::Display for RadrootsTradeListingParseError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidKind(kind) => write!(f, "invalid listing kind: {kind}"), - Self::MissingTag(tag) => write!(f, "missing required tag: {tag}"), - Self::InvalidTag(tag) => write!(f, "invalid tag: {tag}"), - Self::InvalidNumber(field) => write!(f, "invalid number: {field}"), - Self::InvalidUnit => write!(f, "invalid unit"), - Self::InvalidCurrency => write!(f, "invalid currency"), - Self::InvalidJson(field) => write!(f, "invalid json: {field}"), - Self::InvalidDiscount(kind) => write!(f, "invalid discount data for {kind}"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsTradeListingParseError {} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeListingValidationError { - InvalidKind { - kind: u32, - }, - MissingListingId, - ListingEventNotFound { - listing_addr: String, - }, - ListingEventFetchFailed { - listing_addr: String, - }, - ParseError { - error: RadrootsTradeListingParseError, - }, - InvalidSeller, - MissingFarmProfile, - MissingFarmRecord, - MissingTitle, - MissingDescription, - MissingProductType, - MissingBins, - MissingPrimaryBin, - InvalidBin, - MissingPrice, - InvalidPrice, - MissingInventory, - InvalidInventory, - MissingAvailability, - MissingLocation, - MissingDeliveryMethod, -} - -impl core::fmt::Display for RadrootsTradeListingValidationError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidKind { kind } => write!(f, "invalid listing kind: {kind}"), - Self::MissingListingId => write!(f, "missing listing id"), - Self::ListingEventNotFound { listing_addr } => { - write!(f, "listing event not found: {listing_addr}") - } - Self::ListingEventFetchFailed { listing_addr } => { - write!(f, "listing event fetch failed: {listing_addr}") - } - Self::ParseError { error } => write!(f, "invalid listing data: {error}"), - Self::InvalidSeller => write!(f, "listing author does not match farm pubkey"), - Self::MissingFarmProfile => write!(f, "missing farm profile"), - Self::MissingFarmRecord => write!(f, "missing farm record"), - Self::MissingTitle => write!(f, "missing listing title"), - Self::MissingDescription => write!(f, "missing listing description"), - Self::MissingProductType => write!(f, "missing listing product type"), - Self::MissingBins => write!(f, "missing listing bins"), - Self::MissingPrimaryBin => write!(f, "missing primary listing bin"), - Self::InvalidBin => write!(f, "invalid listing bin"), - Self::MissingPrice => write!(f, "missing listing price"), - Self::InvalidPrice => write!(f, "invalid listing price"), - Self::MissingInventory => write!(f, "missing listing inventory"), - Self::InvalidInventory => write!(f, "invalid listing inventory"), - Self::MissingAvailability => write!(f, "missing listing availability"), - Self::MissingLocation => write!(f, "missing listing location"), - Self::MissingDeliveryMethod => write!(f, "missing listing delivery method"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsTradeListingValidationError {} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderItem { - pub bin_id: String, - pub bin_count: u32, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradePricingBasis { - ListingEvent, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeEconomicLineKind { - ListingDiscount, - BasketAdjustment, - RevisionAdjustment, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeEconomicActor { - Buyer, - Seller, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeEconomicEffect { - Increase, - Decrease, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderEconomicItem { - pub bin_id: String, - pub bin_count: u32, - pub quantity_amount: RadrootsCoreDecimal, - pub quantity_unit: RadrootsCoreUnit, - pub unit_price_amount: RadrootsCoreDecimal, - pub unit_price_currency: RadrootsCoreCurrency, - pub line_subtotal: RadrootsCoreMoney, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderEconomicLine { - pub id: String, - pub kind: RadrootsTradeEconomicLineKind, - pub actor: RadrootsTradeEconomicActor, - pub effect: RadrootsTradeEconomicEffect, - pub amount: RadrootsCoreMoney, - pub reason: String, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderEconomicTotals { - pub subtotal: RadrootsCoreMoney, - pub discount_total: RadrootsCoreMoney, - pub adjustment_total: RadrootsCoreMoney, - pub total: RadrootsCoreMoney, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderEconomics { - pub quote_id: String, - pub quote_version: u32, - pub pricing_basis: RadrootsTradePricingBasis, - pub currency: RadrootsCoreCurrency, - pub items: Vec<RadrootsTradeOrderEconomicItem>, - pub discounts: Vec<RadrootsTradeOrderEconomicLine>, - pub adjustments: Vec<RadrootsTradeOrderEconomicLine>, - pub subtotal: RadrootsCoreMoney, - pub discount_total: RadrootsCoreMoney, - pub adjustment_total: RadrootsCoreMoney, - pub total: RadrootsCoreMoney, -} - -impl RadrootsTradeOrderEconomics { - pub fn canonicalize(&mut self) { - self.items - .sort_by(|left, right| left.bin_id.cmp(&right.bin_id)); - self.discounts.sort_by(|left, right| left.id.cmp(&right.id)); - self.adjustments - .sort_by(|left, right| left.id.cmp(&right.id)); - if let Ok(totals) = self.derived_totals() { - self.subtotal = totals.subtotal; - self.discount_total = totals.discount_total; - self.adjustment_total = totals.adjustment_total; - self.total = totals.total; - } - } - - pub fn canonicalized(&self) -> Self { - let mut economics = self.clone(); - economics.canonicalize(); - economics - } - - pub fn derived_totals( - &self, - ) -> Result<RadrootsTradeOrderEconomicTotals, RadrootsActiveTradePayloadError> { - if self.items.is_empty() { - return Err(RadrootsActiveTradePayloadError::MissingEconomicItems); - } - - let mut subtotal = RadrootsCoreMoney::zero(self.currency); - for (index, item) in self.items.iter().enumerate() { - let line_subtotal = validate_economic_item(item, self.currency, index)?; - subtotal = checked_money_add(&subtotal, &line_subtotal, "subtotal")?; - } - - let mut discount_total = RadrootsCoreMoney::zero(self.currency); - for (index, line) in self.discounts.iter().enumerate() { - validate_economic_line(line, self.currency, "discounts", index)?; - if line.kind != RadrootsTradeEconomicLineKind::ListingDiscount { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicLineKind { - field: "discounts", - index, - }); - } - if line.effect != RadrootsTradeEconomicEffect::Decrease { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicLineEffect { - field: "discounts", - index, - }); - } - discount_total = checked_money_add(&discount_total, &line.amount, "discount_total")?; - } - - let mut adjustment_total = RadrootsCoreMoney::zero(self.currency); - let mut total = checked_money_sub_non_negative(&subtotal, &discount_total, "total")?; - for (index, line) in self.adjustments.iter().enumerate() { - validate_economic_line(line, self.currency, "adjustments", index)?; - if line.kind == RadrootsTradeEconomicLineKind::ListingDiscount { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicLineKind { - field: "adjustments", - index, - }); - } - adjustment_total = - checked_money_add(&adjustment_total, &line.amount, "adjustment_total")?; - total = match line.effect { - RadrootsTradeEconomicEffect::Increase => { - checked_money_add(&total, &line.amount, "total")? - } - RadrootsTradeEconomicEffect::Decrease => { - checked_money_sub_non_negative(&total, &line.amount, "total")? - } - }; - } - - Ok(RadrootsTradeOrderEconomicTotals { - subtotal, - discount_total, - adjustment_total, - total, - }) - } - - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.quote_id, "quote_id")?; - if self.quote_version == 0 { - return Err(RadrootsActiveTradePayloadError::InvalidQuoteVersion); - } - - let totals = self.derived_totals()?; - validate_economic_item_order(&self.items)?; - validate_economic_line_order(&self.discounts, "discounts")?; - validate_economic_line_order(&self.adjustments, "adjustments")?; - validate_total_money(&self.subtotal, self.currency, "subtotal")?; - validate_total_money(&self.discount_total, self.currency, "discount_total")?; - validate_total_money(&self.adjustment_total, self.currency, "adjustment_total")?; - validate_total_money(&self.total, self.currency, "total")?; - validate_total_matches(&self.subtotal, &totals.subtotal, "subtotal")?; - validate_total_matches( - &self.discount_total, - &totals.discount_total, - "discount_total", - )?; - validate_total_matches( - &self.adjustment_total, - &totals.adjustment_total, - "adjustment_total", - )?; - validate_total_matches(&self.total, &totals.total, "total") - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeOrderChange { - BinCount { item_index: u32, bin_count: u32 }, - ItemAdd { item: RadrootsTradeOrderItem }, - ItemRemove { item_index: u32 }, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderRevision { - pub revision_id: String, - pub changes: Vec<RadrootsTradeOrderChange>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeOrderStatus { - Draft, - Validated, - Requested, - Questioned, - Revised, - Accepted, - Declined, - Cancelled, - Fulfilled, - Completed, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderRequested { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub items: Vec<RadrootsTradeOrderItem>, - pub economics: RadrootsTradeOrderEconomics, -} - -impl RadrootsTradeOrderRequested { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - validate_order_items(&self.items)?; - self.economics.validate()?; - validate_order_economics_binding(&self.items, &self.economics) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderRevisionProposed { - pub revision_id: String, - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub root_event_id: String, - pub prev_event_id: String, - pub items: Vec<RadrootsTradeOrderItem>, - pub economics: RadrootsTradeOrderEconomics, - pub reason: String, -} - -impl RadrootsTradeOrderRevisionProposed { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.revision_id, "revision_id")?; - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - validate_required_field(&self.root_event_id, "root_event_id")?; - validate_required_field(&self.prev_event_id, "prev_event_id")?; - validate_required_field(&self.reason, "reason")?; - validate_order_items(&self.items)?; - self.economics.validate()?; - validate_order_economics_binding(&self.items, &self.economics) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "decision"))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeOrderRevisionDecision { - Accepted, - Declined { reason: String }, -} - -impl RadrootsTradeOrderRevisionDecision { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - match self { - Self::Accepted => Ok(()), - Self::Declined { reason } => validate_required_field(reason, "reason"), - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderRevisionDecisionEvent { - pub revision_id: String, - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub root_event_id: String, - pub prev_event_id: String, - pub decision: RadrootsTradeOrderRevisionDecision, -} - -impl RadrootsTradeOrderRevisionDecisionEvent { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.revision_id, "revision_id")?; - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - validate_required_field(&self.root_event_id, "root_event_id")?; - validate_required_field(&self.prev_event_id, "prev_event_id")?; - self.decision.validate() - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeInventoryCommitment { - pub bin_id: String, - pub bin_count: u32, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "decision"))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeOrderDecision { - Accepted { - inventory_commitments: Vec<RadrootsTradeInventoryCommitment>, - }, - Declined { - reason: String, - }, -} - -impl RadrootsTradeOrderDecision { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - match self { - Self::Accepted { - inventory_commitments, - } => validate_inventory_commitments(inventory_commitments), - Self::Declined { reason } => validate_required_field(reason, "reason"), - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderDecisionEvent { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub decision: RadrootsTradeOrderDecision, -} - -impl RadrootsTradeOrderDecisionEvent { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - self.decision.validate() - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsActiveTradeFulfillmentState { - AcceptedNotFulfilled, - Preparing, - ReadyForPickup, - OutForDelivery, - Delivered, - SellerCancelled, -} - -impl RadrootsActiveTradeFulfillmentState { - #[inline] - pub const fn is_publishable_update(self) -> bool { - !matches!(self, Self::AcceptedNotFulfilled) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeFulfillmentUpdated { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub status: RadrootsActiveTradeFulfillmentState, -} - -impl RadrootsTradeFulfillmentUpdated { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - if self.status.is_publishable_update() { - Ok(()) - } else { - Err(RadrootsActiveTradePayloadError::InvalidFulfillmentStatus) - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderCancelled { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub reason: String, -} - -impl RadrootsTradeOrderCancelled { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - validate_required_field(&self.reason, "reason") - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeBuyerReceipt { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub received: bool, - pub issue: Option<String>, - pub received_at: u64, -} - -impl RadrootsTradeBuyerReceipt { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - if self.received { - if self.issue.is_some() { - return Err(RadrootsActiveTradePayloadError::UnexpectedReceiptIssue); - } - } else { - match self.issue.as_deref() { - Some(issue) => validate_required_field(issue, "issue")?, - None => return Err(RadrootsActiveTradePayloadError::MissingReceiptIssue), - } - } - Ok(()) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradePaymentMethod { - Cash, - ManualTransfer, - Other, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradePaymentRecorded { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub root_event_id: String, - pub previous_event_id: String, - pub agreement_event_id: String, - pub quote_id: String, - pub quote_version: u32, - pub economics_digest: String, - pub amount: RadrootsCoreDecimal, - pub currency: RadrootsCoreCurrency, - pub method: RadrootsTradePaymentMethod, - pub reference: Option<String>, - pub paid_at: Option<u64>, -} - -impl RadrootsTradePaymentRecorded { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - validate_required_field(&self.root_event_id, "root_event_id")?; - validate_required_field(&self.previous_event_id, "previous_event_id")?; - validate_required_field(&self.agreement_event_id, "agreement_event_id")?; - validate_required_field(&self.quote_id, "quote_id")?; - validate_required_field(&self.economics_digest, "economics_digest")?; - if self.quote_version == 0 { - return Err(RadrootsActiveTradePayloadError::InvalidQuoteVersion); - } - if self.amount.is_zero() || self.amount.is_sign_negative() { - return Err(RadrootsActiveTradePayloadError::InvalidPaymentAmount); - } - if let Some(reference) = self.reference.as_deref() { - validate_required_field(reference, "reference")?; - } - Ok(()) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeSettlementDecision { - Accepted, - Rejected, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeSettlementDecisionEvent { - pub order_id: String, - pub listing_addr: String, - pub seller_pubkey: String, - pub buyer_pubkey: String, - pub root_event_id: String, - pub previous_event_id: String, - pub agreement_event_id: String, - pub payment_event_id: String, - pub quote_id: String, - pub quote_version: u32, - pub economics_digest: String, - pub amount: RadrootsCoreDecimal, - pub currency: RadrootsCoreCurrency, - pub decision: RadrootsTradeSettlementDecision, - pub reason: Option<String>, -} - -impl RadrootsTradeSettlementDecisionEvent { - pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&self.order_id, "order_id")?; - validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.root_event_id, "root_event_id")?; - validate_required_field(&self.previous_event_id, "previous_event_id")?; - validate_required_field(&self.agreement_event_id, "agreement_event_id")?; - validate_required_field(&self.payment_event_id, "payment_event_id")?; - validate_required_field(&self.quote_id, "quote_id")?; - validate_required_field(&self.economics_digest, "economics_digest")?; - if self.quote_version == 0 { - return Err(RadrootsActiveTradePayloadError::InvalidQuoteVersion); - } - if self.amount.is_zero() || self.amount.is_sign_negative() { - return Err(RadrootsActiveTradePayloadError::InvalidPaymentAmount); - } - match self.decision { - RadrootsTradeSettlementDecision::Accepted => { - if self.reason.is_some() { - return Err(RadrootsActiveTradePayloadError::UnexpectedSettlementReason); - } - } - RadrootsTradeSettlementDecision::Rejected => match self.reason.as_deref() { - Some(reason) => validate_required_field(reason, "reason")?, - None => return Err(RadrootsActiveTradePayloadError::MissingSettlementReason), - }, - } - Ok(()) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeQuestion { - pub question_id: String, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeAnswer { - pub question_id: String, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeDiscountRequest { - pub discount_id: String, - pub value: RadrootsCoreDiscountValue, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeDiscountOffer { - pub discount_id: String, - pub value: RadrootsCoreDiscountValue, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeDiscountDecision { - Accept { value: RadrootsCoreDiscountValue }, - Decline { reason: Option<String> }, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeFulfillmentStatus { - Preparing, - Shipped, - ReadyForPickup, - Delivered, - Cancelled, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeFulfillmentUpdate { - pub status: RadrootsTradeFulfillmentStatus, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeReceipt { - pub acknowledged: bool, - pub at: u64, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeListingValidateRequest { - pub listing_event: Option<RadrootsNostrEventPtr>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeListingValidateResult { - pub valid: bool, - pub errors: Vec<RadrootsTradeListingValidationError>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderResponse { - pub accepted: bool, - pub reason: Option<String>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderRevisionResponse { - pub accepted: bool, - pub reason: Option<String>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeListingCancel { - pub reason: Option<String>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeDomain { - #[cfg_attr(feature = "serde", serde(rename = "trade:listing"))] - TradeListing, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeTransportLane { - Service, - Public, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsActiveTradeMessageType { - #[cfg_attr(feature = "serde", serde(rename = "TradeOrderRequested"))] - TradeOrderRequested, - #[cfg_attr(feature = "serde", serde(rename = "TradeOrderDecision"))] - TradeOrderDecision, - #[cfg_attr(feature = "serde", serde(rename = "TradeOrderRevisionProposed"))] - TradeOrderRevisionProposed, - #[cfg_attr(feature = "serde", serde(rename = "TradeOrderRevisionDecision"))] - TradeOrderRevisionDecision, - #[cfg_attr(feature = "serde", serde(rename = "TradeOrderCancelled"))] - TradeOrderCancelled, - #[cfg_attr(feature = "serde", serde(rename = "TradeFulfillmentUpdated"))] - TradeFulfillmentUpdated, - #[cfg_attr(feature = "serde", serde(rename = "TradeBuyerReceipt"))] - TradeBuyerReceipt, - #[cfg_attr(feature = "serde", serde(rename = "TradePaymentRecorded"))] - TradePaymentRecorded, - #[cfg_attr(feature = "serde", serde(rename = "TradeSettlementDecision"))] - TradeSettlementDecision, -} - -impl RadrootsActiveTradeMessageType { - #[inline] - pub const fn from_kind(kind: u32) -> Option<Self> { - match kind { - KIND_TRADE_ORDER_REQUEST => Some(Self::TradeOrderRequested), - KIND_TRADE_ORDER_DECISION => Some(Self::TradeOrderDecision), - KIND_TRADE_ORDER_REVISION => Some(Self::TradeOrderRevisionProposed), - KIND_TRADE_ORDER_REVISION_RESPONSE => Some(Self::TradeOrderRevisionDecision), - KIND_TRADE_CANCEL => Some(Self::TradeOrderCancelled), - KIND_TRADE_FULFILLMENT_UPDATE => Some(Self::TradeFulfillmentUpdated), - KIND_TRADE_RECEIPT => Some(Self::TradeBuyerReceipt), - KIND_TRADE_PAYMENT_RECORDED => Some(Self::TradePaymentRecorded), - KIND_TRADE_SETTLEMENT_DECISION => Some(Self::TradeSettlementDecision), - _ => None, - } - } - - #[inline] - pub const fn kind(self) -> u32 { - match self { - Self::TradeOrderRequested => KIND_TRADE_ORDER_REQUEST, - Self::TradeOrderDecision => KIND_TRADE_ORDER_DECISION, - Self::TradeOrderRevisionProposed => KIND_TRADE_ORDER_REVISION, - Self::TradeOrderRevisionDecision => KIND_TRADE_ORDER_REVISION_RESPONSE, - Self::TradeOrderCancelled => KIND_TRADE_CANCEL, - Self::TradeFulfillmentUpdated => KIND_TRADE_FULFILLMENT_UPDATE, - Self::TradeBuyerReceipt => KIND_TRADE_RECEIPT, - Self::TradePaymentRecorded => KIND_TRADE_PAYMENT_RECORDED, - Self::TradeSettlementDecision => KIND_TRADE_SETTLEMENT_DECISION, - } - } - - #[inline] - pub const fn name(self) -> &'static str { - match self { - Self::TradeOrderRequested => "TradeOrderRequested", - Self::TradeOrderDecision => "TradeOrderDecision", - Self::TradeOrderRevisionProposed => "TradeOrderRevisionProposed", - Self::TradeOrderRevisionDecision => "TradeOrderRevisionDecision", - Self::TradeOrderCancelled => "TradeOrderCancelled", - Self::TradeFulfillmentUpdated => "TradeFulfillmentUpdated", - Self::TradeBuyerReceipt => "TradeBuyerReceipt", - Self::TradePaymentRecorded => "TradePaymentRecorded", - Self::TradeSettlementDecision => "TradeSettlementDecision", - } - } - - #[inline] - pub const fn requires_listing_snapshot(self) -> bool { - matches!(self, Self::TradeOrderRequested) - } - - #[inline] - pub const fn requires_trade_chain(self) -> bool { - matches!( - self, - Self::TradeOrderDecision - | Self::TradeOrderRevisionProposed - | Self::TradeOrderRevisionDecision - | Self::TradeOrderCancelled - | Self::TradeFulfillmentUpdated - | Self::TradeBuyerReceipt - | Self::TradePaymentRecorded - | Self::TradeSettlementDecision - ) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeMessageType { - ListingValidateRequest, - ListingValidateResult, - OrderRequest, - OrderResponse, - OrderRevision, - OrderRevisionAccept, - OrderRevisionDecline, - Question, - Answer, - DiscountRequest, - DiscountOffer, - DiscountAccept, - DiscountDecline, - Cancel, - FulfillmentUpdate, - Receipt, -} - -impl RadrootsTradeMessageType { - #[inline] - pub const fn from_kind(kind: u32) -> Option<Self> { - match kind { - KIND_TRADE_LISTING_VALIDATE_REQ => Some(Self::ListingValidateRequest), - KIND_TRADE_LISTING_VALIDATE_RES => Some(Self::ListingValidateResult), - KIND_TRADE_ORDER_REQUEST => None, - KIND_TRADE_ORDER_RESPONSE => Some(Self::OrderResponse), - KIND_TRADE_ORDER_REVISION => Some(Self::OrderRevision), - KIND_TRADE_ORDER_REVISION_RESPONSE => None, - KIND_TRADE_QUESTION => Some(Self::Question), - KIND_TRADE_ANSWER => Some(Self::Answer), - KIND_TRADE_DISCOUNT_REQUEST => Some(Self::DiscountRequest), - KIND_TRADE_DISCOUNT_OFFER => Some(Self::DiscountOffer), - KIND_TRADE_DISCOUNT_ACCEPT => Some(Self::DiscountAccept), - KIND_TRADE_CANCEL => Some(Self::Cancel), - KIND_TRADE_FULFILLMENT_UPDATE => Some(Self::FulfillmentUpdate), - KIND_TRADE_RECEIPT => Some(Self::Receipt), - _ => None, - } - } - - #[inline] - pub const fn kind(self) -> u32 { - match self { - Self::ListingValidateRequest => KIND_TRADE_LISTING_VALIDATE_REQ, - Self::ListingValidateResult => KIND_TRADE_LISTING_VALIDATE_RES, - Self::OrderRequest => KIND_TRADE_ORDER_REQUEST, - Self::OrderResponse => KIND_TRADE_ORDER_RESPONSE, - Self::OrderRevision => KIND_TRADE_ORDER_REVISION, - Self::OrderRevisionAccept => KIND_TRADE_ORDER_REVISION_RESPONSE, - Self::OrderRevisionDecline => KIND_TRADE_ORDER_REVISION_RESPONSE, - Self::Question => KIND_TRADE_QUESTION, - Self::Answer => KIND_TRADE_ANSWER, - Self::DiscountRequest => KIND_TRADE_DISCOUNT_REQUEST, - Self::DiscountOffer => KIND_TRADE_DISCOUNT_OFFER, - Self::DiscountAccept => KIND_TRADE_DISCOUNT_ACCEPT, - Self::DiscountDecline => KIND_TRADE_FORBIDDEN_3431, - Self::Cancel => KIND_TRADE_CANCEL, - Self::FulfillmentUpdate => KIND_TRADE_FULFILLMENT_UPDATE, - Self::Receipt => KIND_TRADE_RECEIPT, - } - } - - #[inline] - pub const fn lane(self) -> RadrootsTradeTransportLane { - match self { - Self::ListingValidateRequest | Self::ListingValidateResult => { - RadrootsTradeTransportLane::Service - } - Self::OrderRequest - | Self::OrderResponse - | Self::OrderRevision - | Self::OrderRevisionAccept - | Self::OrderRevisionDecline - | Self::Question - | Self::Answer - | Self::DiscountRequest - | Self::DiscountOffer - | Self::DiscountAccept - | Self::DiscountDecline - | Self::Cancel - | Self::FulfillmentUpdate - | Self::Receipt => RadrootsTradeTransportLane::Public, - } - } - - #[inline] - pub const fn is_service(self) -> bool { - matches!(self.lane(), RadrootsTradeTransportLane::Service) - } - - #[inline] - pub const fn is_public(self) -> bool { - matches!(self.lane(), RadrootsTradeTransportLane::Public) - } - - #[inline] - pub const fn requires_order_id(self) -> bool { - !matches!( - self, - Self::ListingValidateRequest | Self::ListingValidateResult - ) - } - - #[inline] - pub const fn requires_listing_snapshot(self) -> bool { - matches!( - self, - Self::OrderRequest | Self::OrderRevision | Self::DiscountRequest | Self::DiscountOffer - ) - } - - #[inline] - pub const fn requires_trade_chain(self) -> bool { - self.is_public() && !matches!(self, Self::OrderRequest) - } - - #[inline] - pub const fn is_request(self) -> bool { - matches!( - self, - Self::ListingValidateRequest - | Self::OrderRequest - | Self::OrderRevision - | Self::Question - | Self::DiscountRequest - | Self::DiscountAccept - | Self::DiscountDecline - | Self::Cancel - | Self::FulfillmentUpdate - | Self::Receipt - ) - } - - #[inline] - pub const fn is_result(self) -> bool { - !self.is_request() - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveTradeEnvelope<T> { - pub version: u16, - pub domain: RadrootsTradeDomain, - #[cfg_attr(feature = "serde", serde(rename = "type"))] - pub message_type: RadrootsActiveTradeMessageType, - pub order_id: String, - pub listing_addr: String, - pub payload: T, -} - -impl<T> RadrootsActiveTradeEnvelope<T> { - #[inline] - pub fn new( - message_type: RadrootsActiveTradeMessageType, - listing_addr: impl Into<String>, - order_id: impl Into<String>, - payload: T, - ) -> Self { - Self { - version: RADROOTS_TRADE_ENVELOPE_VERSION, - domain: RadrootsTradeDomain::TradeListing, - message_type, - order_id: order_id.into(), - listing_addr: listing_addr.into(), - payload, - } - } - - pub fn validate(&self) -> Result<(), RadrootsActiveTradeEnvelopeError> { - if self.version != RADROOTS_TRADE_ENVELOPE_VERSION { - return Err(RadrootsActiveTradeEnvelopeError::InvalidVersion { - expected: RADROOTS_TRADE_ENVELOPE_VERSION, - got: self.version, - }); - } - if self.order_id.trim().is_empty() { - return Err(RadrootsActiveTradeEnvelopeError::MissingOrderId); - } - if self.listing_addr.trim().is_empty() { - return Err(RadrootsActiveTradeEnvelopeError::MissingListingAddr); - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsActiveTradeEnvelopeError { - InvalidVersion { expected: u16, got: u16 }, - MissingOrderId, - MissingListingAddr, -} - -impl core::fmt::Display for RadrootsActiveTradeEnvelopeError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidVersion { expected, got } => { - write!( - f, - "invalid active trade envelope version: expected {expected}, got {got}" - ) - } - Self::MissingOrderId => write!(f, "missing order_id for active trade message"), - Self::MissingListingAddr => write!(f, "missing listing_addr"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsActiveTradeEnvelopeError {} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeEnvelope<T> { - pub version: u16, - pub domain: RadrootsTradeDomain, - #[cfg_attr(feature = "serde", serde(rename = "type"))] - pub message_type: RadrootsTradeMessageType, - pub order_id: Option<String>, - pub listing_addr: String, - pub payload: T, -} - -impl<T> RadrootsTradeEnvelope<T> { - #[inline] - pub fn new( - message_type: RadrootsTradeMessageType, - listing_addr: impl Into<String>, - order_id: Option<String>, - payload: T, - ) -> Self { - Self { - version: RADROOTS_TRADE_ENVELOPE_VERSION, - domain: RadrootsTradeDomain::TradeListing, - message_type, - order_id, - listing_addr: listing_addr.into(), - payload, - } - } - - pub fn validate(&self) -> Result<(), RadrootsTradeEnvelopeError> { - if self.version != RADROOTS_TRADE_ENVELOPE_VERSION { - return Err(RadrootsTradeEnvelopeError::InvalidVersion { - expected: RADROOTS_TRADE_ENVELOPE_VERSION, - got: self.version, - }); - } - if self.listing_addr.trim().is_empty() { - return Err(RadrootsTradeEnvelopeError::MissingListingAddr); - } - if self.message_type.requires_order_id() { - match self.order_id.as_deref() { - Some(id) if !id.trim().is_empty() => {} - _ => return Err(RadrootsTradeEnvelopeError::MissingOrderId), - } - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsActiveTradePayloadError { - EmptyField(&'static str), - MissingItems, - InvalidItemBinCount { index: usize }, - MissingEconomicItems, - InvalidEconomicItemBinCount { index: usize }, - InvalidEconomicItemQuantity { index: usize }, - InvalidEconomicItemPrice { index: usize }, - InvalidEconomicItemSubtotal { index: usize }, - InvalidEconomicLineAmount { field: &'static str, index: usize }, - InvalidEconomicLineKind { field: &'static str, index: usize }, - InvalidEconomicLineEffect { field: &'static str, index: usize }, - InvalidEconomicCurrency { field: &'static str }, - InvalidEconomicOrdering { field: &'static str }, - InvalidEconomicTotal { field: &'static str }, - InvalidOrderEconomicsBinding { field: &'static str }, - InvalidQuoteVersion, - MissingInventoryCommitments, - InvalidInventoryCommitmentCount { index: usize }, - InvalidFulfillmentStatus, - MissingReceiptIssue, - UnexpectedReceiptIssue, - InvalidPaymentAmount, - MissingSettlementReason, - UnexpectedSettlementReason, -} - -impl core::fmt::Display for RadrootsActiveTradePayloadError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::EmptyField(field) => write!(f, "{field} cannot be empty"), - Self::MissingItems => write!(f, "items must contain at least one item"), - Self::InvalidItemBinCount { index } => { - write!(f, "items[{index}].bin_count must be greater than zero") - } - Self::MissingEconomicItems => { - write!(f, "economics.items must contain at least one item") - } - Self::InvalidEconomicItemBinCount { index } => write!( - f, - "economics.items[{index}].bin_count must be greater than zero" - ), - Self::InvalidEconomicItemQuantity { index } => write!( - f, - "economics.items[{index}].quantity_amount must be greater than zero" - ), - Self::InvalidEconomicItemPrice { index } => write!( - f, - "economics.items[{index}].unit_price_amount must not be negative" - ), - Self::InvalidEconomicItemSubtotal { index } => { - write!(f, "economics.items[{index}].line_subtotal is invalid") - } - Self::InvalidEconomicLineAmount { field, index } => { - write!( - f, - "economics.{field}[{index}].amount must be greater than zero" - ) - } - Self::InvalidEconomicLineKind { field, index } => { - write!(f, "economics.{field}[{index}].kind is invalid") - } - Self::InvalidEconomicLineEffect { field, index } => { - write!(f, "economics.{field}[{index}].effect is invalid") - } - Self::InvalidEconomicCurrency { field } => { - write!(f, "economics.{field} currency is invalid") - } - Self::InvalidEconomicOrdering { field } => { - write!(f, "economics.{field} is not in canonical order") - } - Self::InvalidEconomicTotal { field } => { - write!(f, "economics.{field} total is invalid") - } - Self::InvalidOrderEconomicsBinding { field } => { - write!(f, "order {field} does not match economics") - } - Self::InvalidQuoteVersion => { - write!(f, "economics.quote_version must be greater than zero") - } - Self::MissingInventoryCommitments => { - write!( - f, - "accepted decisions must contain at least one inventory commitment" - ) - } - Self::InvalidInventoryCommitmentCount { index } => write!( - f, - "inventory_commitments[{index}].bin_count must be greater than zero" - ), - Self::InvalidFulfillmentStatus => { - write!(f, "fulfillment status is not publishable") - } - Self::MissingReceiptIssue => { - write!(f, "receipt issue is required when received is false") - } - Self::UnexpectedReceiptIssue => { - write!(f, "receipt issue must be absent when received is true") - } - Self::InvalidPaymentAmount => { - write!(f, "payment amount must be greater than zero") - } - Self::MissingSettlementReason => { - write!(f, "settlement reason is required when decision is rejected") - } - Self::UnexpectedSettlementReason => { - write!( - f, - "settlement reason must be absent when decision is accepted" - ) - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsActiveTradePayloadError {} - -fn validate_required_field( - value: &str, - field: &'static str, -) -> Result<(), RadrootsActiveTradePayloadError> { - if value.trim().is_empty() { - Err(RadrootsActiveTradePayloadError::EmptyField(field)) - } else { - Ok(()) - } -} - -fn validate_order_items( - items: &[RadrootsTradeOrderItem], -) -> Result<(), RadrootsActiveTradePayloadError> { - if items.is_empty() { - return Err(RadrootsActiveTradePayloadError::MissingItems); - } - for (index, item) in items.iter().enumerate() { - validate_required_field(&item.bin_id, "bin_id")?; - if item.bin_count == 0 { - return Err(RadrootsActiveTradePayloadError::InvalidItemBinCount { index }); - } - } - Ok(()) -} - -fn validate_economic_item( - item: &RadrootsTradeOrderEconomicItem, - expected_currency: RadrootsCoreCurrency, - index: usize, -) -> Result<RadrootsCoreMoney, RadrootsActiveTradePayloadError> { - validate_required_field(&item.bin_id, "economics.items.bin_id")?; - if item.bin_count == 0 { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicItemBinCount { index }); - } - if item.quantity_amount.is_zero() || item.quantity_amount.is_sign_negative() { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicItemQuantity { index }); - } - if item.unit_price_amount.is_sign_negative() { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicItemPrice { index }); - } - if item.unit_price_currency != expected_currency { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicCurrency { - field: "items.unit_price_currency", - }); - } - validate_total_money( - &item.line_subtotal, - expected_currency, - "items.line_subtotal", - )?; - - let quantity_total = checked_decimal_mul( - item.quantity_amount, - RadrootsCoreDecimal::from(item.bin_count), - ) - .ok_or(RadrootsActiveTradePayloadError::InvalidEconomicItemSubtotal { index })?; - let expected_subtotal = checked_decimal_mul(item.unit_price_amount, quantity_total) - .ok_or(RadrootsActiveTradePayloadError::InvalidEconomicItemSubtotal { index })?; - if item.line_subtotal.amount != expected_subtotal { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicItemSubtotal { index }); - } - Ok(item.line_subtotal.clone()) -} - -fn validate_order_economics_binding( - items: &[RadrootsTradeOrderItem], - economics: &RadrootsTradeOrderEconomics, -) -> Result<(), RadrootsActiveTradePayloadError> { - let order_items = normalized_order_item_counts(items).ok_or( - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { - field: "items.bin_count", - }, - )?; - if order_items.len() != economics.items.len() { - return Err( - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { field: "items" }, - ); - } - for (item, economic_item) in order_items.iter().zip(economics.items.iter()) { - if item.bin_id != economic_item.bin_id { - return Err( - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { - field: "items.bin_id", - }, - ); - } - if item.bin_count != u64::from(economic_item.bin_count) { - return Err( - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { - field: "items.bin_count", - }, - ); - } - } - Ok(()) -} - -#[derive(Debug, PartialEq, Eq)] -struct NormalizedOrderItemCount { - bin_id: String, - bin_count: u64, -} - -fn normalized_order_item_counts( - items: &[RadrootsTradeOrderItem], -) -> Option<Vec<NormalizedOrderItemCount>> { - let mut counts: Vec<NormalizedOrderItemCount> = Vec::new(); - for item in items { - let bin_id = item.bin_id.trim(); - if bin_id.is_empty() || item.bin_count == 0 { - return None; - } - if let Some(existing) = counts.iter_mut().find(|count| count.bin_id == bin_id) { - existing.bin_count = existing.bin_count.checked_add(u64::from(item.bin_count))?; - } else { - counts.push(NormalizedOrderItemCount { - bin_id: bin_id.to_string(), - bin_count: u64::from(item.bin_count), - }); - } - } - counts.sort_by(|left, right| left.bin_id.cmp(&right.bin_id)); - Some(counts) -} - -fn validate_economic_line( - line: &RadrootsTradeOrderEconomicLine, - expected_currency: RadrootsCoreCurrency, - field: &'static str, - index: usize, -) -> Result<(), RadrootsActiveTradePayloadError> { - validate_required_field(&line.id, "economics.line.id")?; - validate_required_field(&line.reason, "economics.line.reason")?; - if line.amount.currency != expected_currency { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field }); - } - if line.amount.amount.is_zero() || line.amount.amount.is_sign_negative() { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicLineAmount { field, index }); - } - Ok(()) -} - -fn validate_economic_item_order( - items: &[RadrootsTradeOrderEconomicItem], -) -> Result<(), RadrootsActiveTradePayloadError> { - for pair in items.windows(2) { - if pair[0].bin_id >= pair[1].bin_id { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicOrdering { - field: "items.bin_id", - }); - } - } - Ok(()) -} - -fn validate_economic_line_order( - lines: &[RadrootsTradeOrderEconomicLine], - field: &'static str, -) -> Result<(), RadrootsActiveTradePayloadError> { - for pair in lines.windows(2) { - if pair[0].id >= pair[1].id { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicOrdering { field }); - } - } - Ok(()) -} - -fn validate_total_money( - money: &RadrootsCoreMoney, - expected_currency: RadrootsCoreCurrency, - field: &'static str, -) -> Result<(), RadrootsActiveTradePayloadError> { - if money.currency != expected_currency { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field }); - } - if money.amount.is_sign_negative() { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicTotal { field }); - } - Ok(()) -} - -fn validate_total_matches( - actual: &RadrootsCoreMoney, - expected: &RadrootsCoreMoney, - field: &'static str, -) -> Result<(), RadrootsActiveTradePayloadError> { - if actual.currency != expected.currency { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field }); - } - if actual.amount != expected.amount { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicTotal { field }); - } - Ok(()) -} - -fn checked_decimal_add( - left: RadrootsCoreDecimal, - right: RadrootsCoreDecimal, -) -> Option<RadrootsCoreDecimal> { - left.0.checked_add(right.0).map(RadrootsCoreDecimal) -} - -fn checked_decimal_sub( - left: RadrootsCoreDecimal, - right: RadrootsCoreDecimal, -) -> Option<RadrootsCoreDecimal> { - left.0.checked_sub(right.0).map(RadrootsCoreDecimal) -} - -fn checked_decimal_mul( - left: RadrootsCoreDecimal, - right: RadrootsCoreDecimal, -) -> Option<RadrootsCoreDecimal> { - left.0.checked_mul(right.0).map(RadrootsCoreDecimal) -} - -fn checked_money_add( - left: &RadrootsCoreMoney, - right: &RadrootsCoreMoney, - field: &'static str, -) -> Result<RadrootsCoreMoney, RadrootsActiveTradePayloadError> { - if left.currency != right.currency { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field }); - } - let amount = checked_decimal_add(left.amount, right.amount) - .ok_or(RadrootsActiveTradePayloadError::InvalidEconomicTotal { field })?; - Ok(RadrootsCoreMoney::new(amount, left.currency)) -} - -fn checked_money_sub_non_negative( - left: &RadrootsCoreMoney, - right: &RadrootsCoreMoney, - field: &'static str, -) -> Result<RadrootsCoreMoney, RadrootsActiveTradePayloadError> { - if left.currency != right.currency { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field }); - } - let amount = checked_decimal_sub(left.amount, right.amount) - .ok_or(RadrootsActiveTradePayloadError::InvalidEconomicTotal { field })?; - if amount.is_sign_negative() { - return Err(RadrootsActiveTradePayloadError::InvalidEconomicTotal { field }); - } - Ok(RadrootsCoreMoney::new(amount, left.currency)) -} - -fn validate_inventory_commitments( - commitments: &[RadrootsTradeInventoryCommitment], -) -> Result<(), RadrootsActiveTradePayloadError> { - if commitments.is_empty() { - return Err(RadrootsActiveTradePayloadError::MissingInventoryCommitments); - } - for (index, commitment) in commitments.iter().enumerate() { - validate_required_field(&commitment.bin_id, "bin_id")?; - if commitment.bin_count == 0 { - return Err(RadrootsActiveTradePayloadError::InvalidInventoryCommitmentCount { index }); - } - } - Ok(()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsTradeEnvelopeError { - InvalidVersion { expected: u16, got: u16 }, - MissingOrderId, - MissingListingAddr, -} - -impl core::fmt::Display for RadrootsTradeEnvelopeError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidVersion { expected, got } => { - write!( - f, - "invalid envelope version: expected {expected}, got {got}" - ) - } - Self::MissingOrderId => write!(f, "missing order_id for order-scoped message"), - Self::MissingListingAddr => write!(f, "missing listing_addr"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsTradeEnvelopeError {} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeMessagePayload { - ListingValidateRequest(RadrootsTradeListingValidateRequest), - ListingValidateResult(RadrootsTradeListingValidateResult), - TradeOrderRequested(RadrootsTradeOrderRequested), - OrderResponse(RadrootsTradeOrderResponse), - OrderRevision(RadrootsTradeOrderRevision), - OrderRevisionAccept(RadrootsTradeOrderRevisionResponse), - OrderRevisionDecline(RadrootsTradeOrderRevisionResponse), - Question(RadrootsTradeQuestion), - Answer(RadrootsTradeAnswer), - DiscountRequest(RadrootsTradeDiscountRequest), - DiscountOffer(RadrootsTradeDiscountOffer), - DiscountAccept(RadrootsTradeDiscountDecision), - DiscountDecline(RadrootsTradeDiscountDecision), - Cancel(RadrootsTradeListingCancel), - FulfillmentUpdate(RadrootsTradeFulfillmentUpdate), - Receipt(RadrootsTradeReceipt), -} - -impl RadrootsTradeMessagePayload { - #[inline] - pub const fn message_type(&self) -> RadrootsTradeMessageType { - match self { - Self::ListingValidateRequest(_) => RadrootsTradeMessageType::ListingValidateRequest, - Self::ListingValidateResult(_) => RadrootsTradeMessageType::ListingValidateResult, - Self::TradeOrderRequested(_) => RadrootsTradeMessageType::OrderRequest, - Self::OrderResponse(_) => RadrootsTradeMessageType::OrderResponse, - Self::OrderRevision(_) => RadrootsTradeMessageType::OrderRevision, - Self::OrderRevisionAccept(_) => RadrootsTradeMessageType::OrderRevisionAccept, - Self::OrderRevisionDecline(_) => RadrootsTradeMessageType::OrderRevisionDecline, - Self::Question(_) => RadrootsTradeMessageType::Question, - Self::Answer(_) => RadrootsTradeMessageType::Answer, - Self::DiscountRequest(_) => RadrootsTradeMessageType::DiscountRequest, - Self::DiscountOffer(_) => RadrootsTradeMessageType::DiscountOffer, - Self::DiscountAccept(_) => RadrootsTradeMessageType::DiscountAccept, - Self::DiscountDecline(_) => RadrootsTradeMessageType::DiscountDecline, - Self::Cancel(_) => RadrootsTradeMessageType::Cancel, - Self::FulfillmentUpdate(_) => RadrootsTradeMessageType::FulfillmentUpdate, - Self::Receipt(_) => RadrootsTradeMessageType::Receipt, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCoreMoney, - RadrootsCorePercent, RadrootsCoreUnit, - }; - - fn sample_listing_addr() -> String { - "30402:pubkey:AAAAAAAAAAAAAAAAAAAAAg".into() - } - - fn sample_discount_value() -> RadrootsCoreDiscountValue { - RadrootsCoreDiscountValue::MoneyPerBin(RadrootsCoreMoney::from_minor_units_u64( - 250, - RadrootsCoreCurrency::USD, - )) - } - - fn sample_percent_discount() -> RadrootsCoreDiscountValue { - RadrootsCoreDiscountValue::Percent(RadrootsCorePercent::new(RadrootsCoreDecimal::from( - 10u32, - ))) - } - - fn sample_active_order_request() -> RadrootsTradeOrderRequested { - RadrootsTradeOrderRequested { - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - items: vec![RadrootsTradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 2, - }], - economics: sample_bound_order_economics(), - } - } - - fn decimal(raw: &str) -> RadrootsCoreDecimal { - raw.parse().unwrap() - } - - fn usd(raw: &str) -> RadrootsCoreMoney { - RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) - } - - fn sample_active_order_economics() -> RadrootsTradeOrderEconomics { - RadrootsTradeOrderEconomics { - quote_id: "quote-1".into(), - quote_version: 1, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![ - RadrootsTradeOrderEconomicItem { - bin_id: "bin-a".into(), - bin_count: 2, - quantity_amount: decimal("1.5"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("4"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("12"), - }, - RadrootsTradeOrderEconomicItem { - bin_id: "bin-b".into(), - bin_count: 1, - quantity_amount: decimal("2"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("3"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("6"), - }, - ], - discounts: vec![RadrootsTradeOrderEconomicLine { - id: "discount-a".into(), - kind: RadrootsTradeEconomicLineKind::ListingDiscount, - actor: RadrootsTradeEconomicActor::Seller, - effect: RadrootsTradeEconomicEffect::Decrease, - amount: usd("3"), - reason: "farmstand pickup".into(), - }], - adjustments: vec![ - RadrootsTradeOrderEconomicLine { - id: "adjustment-a".into(), - kind: RadrootsTradeEconomicLineKind::BasketAdjustment, - actor: RadrootsTradeEconomicActor::Buyer, - effect: RadrootsTradeEconomicEffect::Increase, - amount: usd("2"), - reason: "special handling".into(), - }, - RadrootsTradeOrderEconomicLine { - id: "adjustment-b".into(), - kind: RadrootsTradeEconomicLineKind::BasketAdjustment, - actor: RadrootsTradeEconomicActor::Buyer, - effect: RadrootsTradeEconomicEffect::Decrease, - amount: usd("1"), - reason: "local pickup credit".into(), - }, - ], - subtotal: usd("18"), - discount_total: usd("3"), - adjustment_total: usd("3"), - total: usd("16"), - } - } - - fn sample_bound_order_economics() -> RadrootsTradeOrderEconomics { - RadrootsTradeOrderEconomics { - quote_id: "quote-bound-1".into(), - quote_version: 1, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsTradeOrderEconomicItem { - bin_id: "bin-1".into(), - bin_count: 2, - quantity_amount: decimal("1"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("5"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("10"), - }], - discounts: Vec::new(), - adjustments: Vec::new(), - subtotal: usd("10"), - discount_total: usd("0"), - adjustment_total: usd("0"), - total: usd("10"), - } - } - - fn sample_inventory_commitment() -> RadrootsTradeInventoryCommitment { - RadrootsTradeInventoryCommitment { - bin_id: "bin-1".into(), - bin_count: 2, - } - } - - fn sample_active_order_decision() -> RadrootsTradeOrderDecisionEvent { - RadrootsTradeOrderDecisionEvent { - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - decision: RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![sample_inventory_commitment()], - }, - } - } - - fn sample_active_fulfillment_update() -> RadrootsTradeFulfillmentUpdated { - RadrootsTradeFulfillmentUpdated { - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - status: RadrootsActiveTradeFulfillmentState::ReadyForPickup, - } - } - - fn sample_active_order_cancelled() -> RadrootsTradeOrderCancelled { - RadrootsTradeOrderCancelled { - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - reason: "changed plans".into(), - } - } - - fn sample_active_buyer_receipt(received: bool) -> RadrootsTradeBuyerReceipt { - RadrootsTradeBuyerReceipt { - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - received, - issue: (!received).then(|| "damaged items".into()), - received_at: 1_777_665_600, - } - } - - fn sample_active_revision_proposed() -> RadrootsTradeOrderRevisionProposed { - RadrootsTradeOrderRevisionProposed { - revision_id: "rev-1".into(), - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - root_event_id: "root-event".into(), - prev_event_id: "previous-event".into(), - items: vec![RadrootsTradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 2, - }], - economics: sample_bound_order_economics(), - reason: "update quantity".into(), - } - } - - fn sample_active_revision_decision_event( - decision: RadrootsTradeOrderRevisionDecision, - ) -> RadrootsTradeOrderRevisionDecisionEvent { - RadrootsTradeOrderRevisionDecisionEvent { - revision_id: "rev-1".into(), - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - root_event_id: "root-event".into(), - prev_event_id: "previous-event".into(), - decision, - } - } - - fn sample_payment_recorded() -> RadrootsTradePaymentRecorded { - RadrootsTradePaymentRecorded { - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - root_event_id: "root-event".into(), - previous_event_id: "previous-event".into(), - agreement_event_id: "agreement-event".into(), - quote_id: "quote-1".into(), - quote_version: 1, - economics_digest: "economics-digest".into(), - amount: decimal("16"), - currency: RadrootsCoreCurrency::USD, - method: RadrootsTradePaymentMethod::ManualTransfer, - reference: Some("bank-ref".into()), - paid_at: Some(1_777_665_600), - } - } - - fn sample_settlement_decision( - decision: RadrootsTradeSettlementDecision, - reason: Option<&str>, - ) -> RadrootsTradeSettlementDecisionEvent { - RadrootsTradeSettlementDecisionEvent { - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - seller_pubkey: "seller".into(), - buyer_pubkey: "buyer".into(), - root_event_id: "root-event".into(), - previous_event_id: "previous-event".into(), - agreement_event_id: "agreement-event".into(), - payment_event_id: "payment-event".into(), - quote_id: "quote-1".into(), - quote_version: 1, - economics_digest: "economics-digest".into(), - amount: decimal("16"), - currency: RadrootsCoreCurrency::USD, - decision, - reason: reason.map(Into::into), - } - } - - fn sample_order_revision() -> RadrootsTradeOrderRevision { - RadrootsTradeOrderRevision { - revision_id: "rev-1".into(), - changes: vec![ - RadrootsTradeOrderChange::BinCount { - item_index: 0, - bin_count: 3, - }, - RadrootsTradeOrderChange::ItemAdd { - item: RadrootsTradeOrderItem { - bin_id: "bin-2".into(), - bin_count: 1, - }, - }, - RadrootsTradeOrderChange::ItemRemove { item_index: 1 }, - ], - } - } - - fn sample_order_response(accepted: bool) -> RadrootsTradeOrderResponse { - RadrootsTradeOrderResponse { - accepted, - reason: (!accepted).then(|| "not today".into()), - } - } - - fn sample_order_revision_response(accepted: bool) -> RadrootsTradeOrderRevisionResponse { - RadrootsTradeOrderRevisionResponse { - accepted, - reason: (!accepted).then(|| "needs changes".into()), - } - } - - fn sample_validate_request() -> RadrootsTradeListingValidateRequest { - RadrootsTradeListingValidateRequest { - listing_event: Some(RadrootsNostrEventPtr { - id: "listing-event".into(), - relays: Some("wss://relay.example.com".into()), - }), - } - } - - fn sample_validate_result() -> RadrootsTradeListingValidateResult { - RadrootsTradeListingValidateResult { - valid: false, - errors: vec![RadrootsTradeListingValidationError::MissingDeliveryMethod], - } - } - - #[test] - fn message_type_classifies_request_and_result_kinds() { - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_LISTING_ORDER_REQ), - None - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_LISTING_ORDER_RES), - Some(RadrootsTradeMessageType::OrderResponse) - ); - assert!(RadrootsTradeMessageType::ListingValidateRequest.is_service()); - assert!(RadrootsTradeMessageType::OrderRequest.is_public()); - assert!(RadrootsTradeMessageType::OrderRequest.is_request()); - assert!(RadrootsTradeMessageType::OrderResponse.is_result()); - } - - #[test] - fn active_message_type_uses_canonical_names_and_kinds() { - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_ORDER_REQUEST), - Some(RadrootsActiveTradeMessageType::TradeOrderRequested) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_ORDER_DECISION), - Some(RadrootsActiveTradeMessageType::TradeOrderDecision) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_ORDER_REVISION), - Some(RadrootsActiveTradeMessageType::TradeOrderRevisionProposed) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_ORDER_REVISION_RESPONSE), - Some(RadrootsActiveTradeMessageType::TradeOrderRevisionDecision) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_FULFILLMENT_UPDATE), - Some(RadrootsActiveTradeMessageType::TradeFulfillmentUpdated) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_CANCEL), - Some(RadrootsActiveTradeMessageType::TradeOrderCancelled) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_RECEIPT), - Some(RadrootsActiveTradeMessageType::TradeBuyerReceipt) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_PAYMENT_RECORDED), - Some(RadrootsActiveTradeMessageType::TradePaymentRecorded) - ); - assert_eq!( - RadrootsActiveTradeMessageType::from_kind(KIND_TRADE_SETTLEMENT_DECISION), - Some(RadrootsActiveTradeMessageType::TradeSettlementDecision) - ); - assert_eq!(RadrootsActiveTradeMessageType::from_kind(3431), None); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderRequested.kind(), - KIND_TRADE_ORDER_REQUEST - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderDecision.kind(), - KIND_TRADE_ORDER_DECISION - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderRevisionProposed.kind(), - KIND_TRADE_ORDER_REVISION - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderRevisionDecision.kind(), - KIND_TRADE_ORDER_REVISION_RESPONSE - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeFulfillmentUpdated.kind(), - KIND_TRADE_FULFILLMENT_UPDATE - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderCancelled.kind(), - KIND_TRADE_CANCEL - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeBuyerReceipt.kind(), - KIND_TRADE_RECEIPT - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradePaymentRecorded.kind(), - KIND_TRADE_PAYMENT_RECORDED - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeSettlementDecision.kind(), - KIND_TRADE_SETTLEMENT_DECISION - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderRequested.name(), - "TradeOrderRequested" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderDecision.name(), - "TradeOrderDecision" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderRevisionProposed.name(), - "TradeOrderRevisionProposed" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderRevisionDecision.name(), - "TradeOrderRevisionDecision" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeFulfillmentUpdated.name(), - "TradeFulfillmentUpdated" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeOrderCancelled.name(), - "TradeOrderCancelled" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeBuyerReceipt.name(), - "TradeBuyerReceipt" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradePaymentRecorded.name(), - "TradePaymentRecorded" - ); - assert_eq!( - RadrootsActiveTradeMessageType::TradeSettlementDecision.name(), - "TradeSettlementDecision" - ); - assert!(RadrootsActiveTradeMessageType::TradeOrderRequested.requires_listing_snapshot()); - assert!(RadrootsActiveTradeMessageType::TradeOrderDecision.requires_trade_chain()); - assert!(RadrootsActiveTradeMessageType::TradeOrderRevisionProposed.requires_trade_chain()); - assert!(RadrootsActiveTradeMessageType::TradeOrderRevisionDecision.requires_trade_chain()); - assert!(RadrootsActiveTradeMessageType::TradeFulfillmentUpdated.requires_trade_chain()); - assert!(RadrootsActiveTradeMessageType::TradeOrderCancelled.requires_trade_chain()); - assert!(RadrootsActiveTradeMessageType::TradeBuyerReceipt.requires_trade_chain()); - assert!(RadrootsActiveTradeMessageType::TradePaymentRecorded.requires_trade_chain()); - assert!(RadrootsActiveTradeMessageType::TradeSettlementDecision.requires_trade_chain()); - assert!(!RadrootsActiveTradeMessageType::TradeOrderRequested.requires_trade_chain()); - assert!(!RadrootsActiveTradeMessageType::TradePaymentRecorded.requires_listing_snapshot()); - - let request_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeOrderRequested).unwrap(); - let decision_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeOrderDecision).unwrap(); - let revision_proposed_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeOrderRevisionProposed) - .unwrap(); - let revision_decision_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeOrderRevisionDecision) - .unwrap(); - let fulfillment_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeFulfillmentUpdated).unwrap(); - let cancellation_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeOrderCancelled).unwrap(); - let receipt_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeBuyerReceipt).unwrap(); - let payment_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradePaymentRecorded).unwrap(); - let settlement_name = - serde_json::to_value(RadrootsActiveTradeMessageType::TradeSettlementDecision).unwrap(); - assert_eq!(request_name, serde_json::json!("TradeOrderRequested")); - assert_eq!(decision_name, serde_json::json!("TradeOrderDecision")); - assert_eq!( - revision_proposed_name, - serde_json::json!("TradeOrderRevisionProposed") - ); - assert_eq!( - revision_decision_name, - serde_json::json!("TradeOrderRevisionDecision") - ); - assert_eq!( - fulfillment_name, - serde_json::json!("TradeFulfillmentUpdated") - ); - assert_eq!(cancellation_name, serde_json::json!("TradeOrderCancelled")); - assert_eq!(receipt_name, serde_json::json!("TradeBuyerReceipt")); - assert_eq!(payment_name, serde_json::json!("TradePaymentRecorded")); - assert_eq!( - settlement_name, - serde_json::json!("TradeSettlementDecision") - ); - } - - #[test] - fn active_order_request_validation_rejects_invalid_fields() { - assert_eq!(sample_active_order_request().validate(), Ok(())); - - let mut missing_order_id = sample_active_order_request(); - missing_order_id.order_id = " ".into(); - assert_eq!( - missing_order_id.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("order_id") - ); - - let mut missing_items = sample_active_order_request(); - missing_items.items.clear(); - assert_eq!( - missing_items.validate().unwrap_err(), - RadrootsActiveTradePayloadError::MissingItems - ); - - let mut invalid_count = sample_active_order_request(); - invalid_count.items[0].bin_count = 0; - assert_eq!( - invalid_count.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidItemBinCount { index: 0 } - ); - - let mut missing_bin_id = sample_active_order_request(); - missing_bin_id.items[0].bin_id = " ".into(); - assert_eq!( - missing_bin_id.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("bin_id") - ); - - let mut mismatched_economic_item = sample_active_order_request(); - mismatched_economic_item.economics.items[0].bin_id = "bin-other".into(); - assert_eq!( - mismatched_economic_item.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { - field: "items.bin_id" - } - ); - - let mut mismatched_economic_count = sample_active_order_request(); - mismatched_economic_count.economics.items[0].bin_count = 3; - mismatched_economic_count.economics.items[0].line_subtotal = usd("15"); - mismatched_economic_count.economics.subtotal = usd("15"); - mismatched_economic_count.economics.total = usd("15"); - assert_eq!( - mismatched_economic_count.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { - field: "items.bin_count" - } - ); - } - - #[test] - fn active_order_economics_validation_accepts_canonical_totals() { - let economics = sample_active_order_economics(); - assert_eq!(economics.validate(), Ok(())); - - let totals = economics.derived_totals().unwrap(); - assert_eq!(totals.subtotal, usd("18")); - assert_eq!(totals.discount_total, usd("3")); - assert_eq!(totals.adjustment_total, usd("3")); - assert_eq!(totals.total, usd("16")); - - let json = serde_json::to_value(&economics).unwrap(); - assert_eq!(json["pricing_basis"], serde_json::json!("listing_event")); - assert_eq!( - json["discounts"][0]["kind"], - serde_json::json!("listing_discount") - ); - assert_eq!( - json["adjustments"][0]["effect"], - serde_json::json!("increase") - ); - } - - #[test] - fn active_order_economics_canonicalized_sorts_items_and_lines() { - let mut economics = sample_active_order_economics(); - economics.items.reverse(); - economics.adjustments.reverse(); - economics.discounts.push(RadrootsTradeOrderEconomicLine { - id: "discount-b".into(), - kind: RadrootsTradeEconomicLineKind::ListingDiscount, - actor: RadrootsTradeEconomicActor::Seller, - effect: RadrootsTradeEconomicEffect::Decrease, - amount: usd("1"), - reason: "market credit".into(), - }); - economics.discounts.reverse(); - economics.subtotal = usd("19"); - economics.total = usd("17"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicOrdering { - field: "items.bin_id" - } - ); - - let canonical = economics.canonicalized(); - assert_eq!(canonical.items[0].bin_id, "bin-a"); - assert_eq!(canonical.discounts[0].id, "discount-a"); - assert_eq!(canonical.adjustments[0].id, "adjustment-a"); - assert_eq!(canonical.subtotal, usd("18")); - assert_eq!(canonical.discount_total, usd("4")); - assert_eq!(canonical.total, usd("15")); - assert_eq!(canonical.validate(), Ok(())); - - let mut uncanonicalizable = sample_active_order_economics(); - uncanonicalizable.items.clear(); - uncanonicalizable.subtotal = usd("88"); - uncanonicalizable.canonicalize(); - assert_eq!(uncanonicalizable.subtotal, usd("88")); - } - - #[test] - fn active_order_economics_validation_rejects_mixed_currency() { - let mut economics = sample_active_order_economics(); - economics.items[0].unit_price_currency = RadrootsCoreCurrency::EUR; - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { - field: "items.unit_price_currency" - } - ); - - let mut economics = sample_active_order_economics(); - economics.adjustments[0].amount = - RadrootsCoreMoney::new(decimal("2"), RadrootsCoreCurrency::EUR); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { - field: "adjustments" - } - ); - } - - #[test] - fn active_order_economics_validation_rejects_bad_subtotal() { - let mut economics = sample_active_order_economics(); - economics.items[0].bin_count = 0; - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicItemBinCount { index: 0 } - ); - - let mut economics = sample_active_order_economics(); - economics.items[0].line_subtotal = usd("11.99"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicItemSubtotal { index: 0 } - ); - - let mut economics = sample_active_order_economics(); - economics.items[0].line_subtotal = - RadrootsCoreMoney::new(decimal("12"), RadrootsCoreCurrency::EUR); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { - field: "items.line_subtotal" - } - ); - } - - #[test] - fn active_order_economics_validation_covers_remaining_error_paths() { - let mut economics = sample_active_order_economics(); - economics.items.clear(); - assert_eq!( - economics.derived_totals().unwrap_err(), - RadrootsActiveTradePayloadError::MissingEconomicItems - ); - - let mut economics = sample_active_order_economics(); - economics.quote_version = 0; - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidQuoteVersion - ); - - let mut economics = sample_active_order_economics(); - economics.items[0].quantity_amount = decimal("0"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicItemQuantity { index: 0 } - ); - - let mut economics = sample_active_order_economics(); - economics.items[0].quantity_amount = decimal("-1"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicItemQuantity { index: 0 } - ); - - let mut economics = sample_active_order_economics(); - economics.items[0].unit_price_amount = decimal("-1"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicItemPrice { index: 0 } - ); - - let mut economics = sample_active_order_economics(); - economics.discounts[0].kind = RadrootsTradeEconomicLineKind::BasketAdjustment; - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicLineKind { - field: "discounts", - index: 0 - } - ); - - let mut economics = sample_active_order_economics(); - economics.subtotal = RadrootsCoreMoney::new(decimal("18"), RadrootsCoreCurrency::EUR); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field: "subtotal" } - ); - - let mut economics = sample_active_order_economics(); - economics.subtotal = usd("-1"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicTotal { field: "subtotal" } - ); - - let mut economics = sample_active_order_economics(); - economics.discount_total = usd("4"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicTotal { - field: "discount_total" - } - ); - - let mut economics = sample_active_order_economics(); - economics.adjustment_total = usd("4"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicTotal { - field: "adjustment_total" - } - ); - - let economics = sample_bound_order_economics(); - assert_eq!( - validate_order_economics_binding(&[], &economics).unwrap_err(), - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { field: "items" } - ); - - let invalid_order_items = [RadrootsTradeOrderItem { - bin_id: " ".into(), - bin_count: 1, - }]; - assert_eq!( - validate_order_economics_binding(&invalid_order_items, &economics).unwrap_err(), - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { - field: "items.bin_count" - } - ); - - let duplicate_counts = normalized_order_item_counts(&[ - RadrootsTradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 1, - }, - RadrootsTradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 2, - }, - ]) - .unwrap(); - assert_eq!(duplicate_counts[0].bin_count, 3); - - assert!( - normalized_order_item_counts(&[RadrootsTradeOrderItem { - bin_id: " ".into(), - bin_count: 1, - }]) - .is_none() - ); - assert!( - normalized_order_item_counts(&[RadrootsTradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 0, - }]) - .is_none() - ); - let sorted_counts = normalized_order_item_counts(&[ - RadrootsTradeOrderItem { - bin_id: "bin-b".into(), - bin_count: 1, - }, - RadrootsTradeOrderItem { - bin_id: "bin-a".into(), - bin_count: 1, - }, - ]) - .unwrap(); - assert_eq!(sorted_counts[0].bin_id, "bin-a"); - } - - #[test] - fn active_order_economics_validation_rejects_bad_line_semantics() { - let mut economics = sample_active_order_economics(); - economics.discounts[0].effect = RadrootsTradeEconomicEffect::Increase; - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicLineEffect { - field: "discounts", - index: 0 - } - ); - - let mut economics = sample_active_order_economics(); - economics.adjustments[0].kind = RadrootsTradeEconomicLineKind::ListingDiscount; - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicLineKind { - field: "adjustments", - index: 0 - } - ); - - let mut economics = sample_active_order_economics(); - economics.adjustments[0].amount = usd("0"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicLineAmount { - field: "adjustments", - index: 0 - } - ); - - let mut economics = sample_active_order_economics(); - economics.adjustments[0].amount = usd("-1"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicLineAmount { - field: "adjustments", - index: 0 - } - ); - } - - #[test] - fn active_order_economics_helpers_cover_currency_error_paths() { - assert_eq!( - validate_total_money(&usd("-1"), RadrootsCoreCurrency::USD, "subtotal").unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicTotal { field: "subtotal" } - ); - assert_eq!( - validate_total_matches( - &usd("1"), - &RadrootsCoreMoney::new(decimal("1"), RadrootsCoreCurrency::EUR), - "total" - ) - .unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field: "total" } - ); - assert_eq!( - checked_money_add( - &usd("1"), - &RadrootsCoreMoney::new(decimal("1"), RadrootsCoreCurrency::EUR), - "subtotal" - ) - .unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field: "subtotal" } - ); - assert_eq!( - checked_money_sub_non_negative( - &usd("1"), - &RadrootsCoreMoney::new(decimal("1"), RadrootsCoreCurrency::EUR), - "total" - ) - .unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field: "total" } - ); - } - - #[test] - fn active_order_economics_validation_rejects_duplicate_line_ids() { - let mut economics = sample_active_order_economics(); - economics.adjustments[1].id = "adjustment-a".into(); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicOrdering { - field: "adjustments" - } - ); - } - - #[test] - fn active_order_economics_validation_rejects_negative_derived_total() { - let mut economics = sample_active_order_economics(); - economics.adjustments[1].amount = usd("20"); - economics.adjustment_total = usd("22"); - economics.total = usd("0"); - assert_eq!( - economics.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidEconomicTotal { field: "total" } - ); - } - - #[test] - fn active_order_decision_validation_enforces_commitment_invariants() { - assert_eq!(sample_active_order_decision().validate(), Ok(())); - - let declined = RadrootsTradeOrderDecisionEvent { - decision: RadrootsTradeOrderDecision::Declined { - reason: "out_of_stock".into(), - }, - ..sample_active_order_decision() - }; - assert_eq!(declined.validate(), Ok(())); - - let accepted_without_commitments = RadrootsTradeOrderDecisionEvent { - decision: RadrootsTradeOrderDecision::Accepted { - inventory_commitments: Vec::new(), - }, - ..sample_active_order_decision() - }; - assert_eq!( - accepted_without_commitments.validate().unwrap_err(), - RadrootsActiveTradePayloadError::MissingInventoryCommitments - ); - - let accepted_with_zero_count = RadrootsTradeOrderDecisionEvent { - decision: RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { - bin_id: "bin-1".into(), - bin_count: 0, - }], - }, - ..sample_active_order_decision() - }; - assert_eq!( - accepted_with_zero_count.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidInventoryCommitmentCount { index: 0 } - ); - - let declined_without_reason = RadrootsTradeOrderDecisionEvent { - decision: RadrootsTradeOrderDecision::Declined { reason: " ".into() }, - ..sample_active_order_decision() - }; - assert_eq!( - declined_without_reason.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("reason") - ); - } - - #[test] - fn active_revision_validation_covers_proposed_and_decision_paths() { - assert_eq!(sample_active_revision_proposed().validate(), Ok(())); - - let missing_prev = RadrootsTradeOrderRevisionProposed { - prev_event_id: " ".into(), - ..sample_active_revision_proposed() - }; - assert_eq!( - missing_prev.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("prev_event_id") - ); - - assert_eq!( - sample_active_revision_decision_event(RadrootsTradeOrderRevisionDecision::Accepted) - .validate(), - Ok(()) - ); - assert_eq!( - sample_active_revision_decision_event(RadrootsTradeOrderRevisionDecision::Declined { - reason: "out of stock".into(), - }) - .validate(), - Ok(()) - ); - - let declined_without_reason = - sample_active_revision_decision_event(RadrootsTradeOrderRevisionDecision::Declined { - reason: " ".into(), - }); - assert_eq!( - declined_without_reason.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("reason") - ); - - let missing_root = RadrootsTradeOrderRevisionDecisionEvent { - root_event_id: " ".into(), - ..sample_active_revision_decision_event(RadrootsTradeOrderRevisionDecision::Accepted) - }; - assert_eq!( - missing_root.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("root_event_id") - ); - } - - #[test] - fn active_fulfillment_update_validation_rejects_derived_state() { - assert_eq!(sample_active_fulfillment_update().validate(), Ok(())); - - let derived = RadrootsTradeFulfillmentUpdated { - status: RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled, - ..sample_active_fulfillment_update() - }; - assert_eq!( - derived.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidFulfillmentStatus - ); - - let missing_seller = RadrootsTradeFulfillmentUpdated { - seller_pubkey: " ".into(), - ..sample_active_fulfillment_update() - }; - assert_eq!( - missing_seller.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("seller_pubkey") - ); - } - - #[test] - fn active_cancellation_validation_requires_buyer_bindings_and_reason() { - assert_eq!(sample_active_order_cancelled().validate(), Ok(())); - - let missing_reason = RadrootsTradeOrderCancelled { - reason: " ".into(), - ..sample_active_order_cancelled() - }; - assert_eq!( - missing_reason.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("reason") - ); - - let missing_buyer = RadrootsTradeOrderCancelled { - buyer_pubkey: " ".into(), - ..sample_active_order_cancelled() - }; - assert_eq!( - missing_buyer.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("buyer_pubkey") - ); - } - - #[test] - fn active_buyer_receipt_validation_requires_consistent_received_and_issue() { - assert_eq!(sample_active_buyer_receipt(true).validate(), Ok(())); - assert_eq!(sample_active_buyer_receipt(false).validate(), Ok(())); - - let received_with_issue = RadrootsTradeBuyerReceipt { - issue: Some("damaged".into()), - ..sample_active_buyer_receipt(true) - }; - assert_eq!( - received_with_issue.validate().unwrap_err(), - RadrootsActiveTradePayloadError::UnexpectedReceiptIssue - ); - - let not_received_without_issue = RadrootsTradeBuyerReceipt { - issue: None, - ..sample_active_buyer_receipt(false) - }; - assert_eq!( - not_received_without_issue.validate().unwrap_err(), - RadrootsActiveTradePayloadError::MissingReceiptIssue - ); - - let not_received_blank_issue = RadrootsTradeBuyerReceipt { - issue: Some(" ".into()), - ..sample_active_buyer_receipt(false) - }; - assert_eq!( - not_received_blank_issue.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("issue") - ); - } - - #[test] - fn active_payment_and_settlement_validation_covers_amount_and_reason_paths() { - assert_eq!(sample_payment_recorded().validate(), Ok(())); - - let unreferenced_payment = RadrootsTradePaymentRecorded { - reference: None, - ..sample_payment_recorded() - }; - assert_eq!(unreferenced_payment.validate(), Ok(())); - - let invalid_quote_version = RadrootsTradePaymentRecorded { - quote_version: 0, - ..sample_payment_recorded() - }; - assert_eq!( - invalid_quote_version.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidQuoteVersion - ); - - let invalid_amount = RadrootsTradePaymentRecorded { - amount: decimal("0"), - ..sample_payment_recorded() - }; - assert_eq!( - invalid_amount.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidPaymentAmount - ); - - let negative_amount = RadrootsTradePaymentRecorded { - amount: decimal("-1"), - ..sample_payment_recorded() - }; - assert_eq!( - negative_amount.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidPaymentAmount - ); - - let blank_reference = RadrootsTradePaymentRecorded { - reference: Some(" ".into()), - ..sample_payment_recorded() - }; - assert_eq!( - blank_reference.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("reference") - ); - - assert_eq!( - sample_settlement_decision(RadrootsTradeSettlementDecision::Accepted, None).validate(), - Ok(()) - ); - assert_eq!( - sample_settlement_decision(RadrootsTradeSettlementDecision::Rejected, Some("damaged")) - .validate(), - Ok(()) - ); - - let accepted_with_reason = - sample_settlement_decision(RadrootsTradeSettlementDecision::Accepted, Some("extra")); - assert_eq!( - accepted_with_reason.validate().unwrap_err(), - RadrootsActiveTradePayloadError::UnexpectedSettlementReason - ); - - let rejected_without_reason = - sample_settlement_decision(RadrootsTradeSettlementDecision::Rejected, None); - assert_eq!( - rejected_without_reason.validate().unwrap_err(), - RadrootsActiveTradePayloadError::MissingSettlementReason - ); - - let rejected_blank_reason = - sample_settlement_decision(RadrootsTradeSettlementDecision::Rejected, Some(" ")); - assert_eq!( - rejected_blank_reason.validate().unwrap_err(), - RadrootsActiveTradePayloadError::EmptyField("reason") - ); - - let invalid_quote_version = RadrootsTradeSettlementDecisionEvent { - quote_version: 0, - ..sample_settlement_decision(RadrootsTradeSettlementDecision::Accepted, None) - }; - assert_eq!( - invalid_quote_version.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidQuoteVersion - ); - - let zero_amount = RadrootsTradeSettlementDecisionEvent { - amount: decimal("0"), - ..sample_settlement_decision(RadrootsTradeSettlementDecision::Accepted, None) - }; - assert_eq!( - zero_amount.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidPaymentAmount - ); - - let invalid_amount = RadrootsTradeSettlementDecisionEvent { - amount: decimal("-1"), - ..sample_settlement_decision(RadrootsTradeSettlementDecision::Accepted, None) - }; - assert_eq!( - invalid_amount.validate().unwrap_err(), - RadrootsActiveTradePayloadError::InvalidPaymentAmount - ); - } - - #[test] - fn active_envelope_serializes_canonical_type_name() { - let envelope = RadrootsActiveTradeEnvelope::new( - RadrootsActiveTradeMessageType::TradeOrderRequested, - sample_listing_addr(), - "order-1", - sample_active_order_request(), - ); - assert_eq!(envelope.validate(), Ok(())); - - let json = serde_json::to_value(&envelope).unwrap(); - assert_eq!(json["type"], serde_json::json!("TradeOrderRequested")); - assert_eq!(json["order_id"], serde_json::json!("order-1")); - assert_eq!( - json["listing_addr"], - serde_json::json!("30402:pubkey:AAAAAAAAAAAAAAAAAAAAAg") - ); - assert_eq!(json["payload"]["items"][0]["bin_id"], "bin-1"); - } - - #[test] - fn active_envelope_validation_and_display_cover_error_paths() { - let invalid_version = RadrootsActiveTradeEnvelope { - version: RADROOTS_TRADE_ENVELOPE_VERSION + 1, - domain: RadrootsTradeDomain::TradeListing, - message_type: RadrootsActiveTradeMessageType::TradeOrderRequested, - order_id: "order-1".into(), - listing_addr: sample_listing_addr(), - payload: sample_active_order_request(), - }; - let invalid_version_err = invalid_version.validate().unwrap_err(); - assert_eq!( - invalid_version_err, - RadrootsActiveTradeEnvelopeError::InvalidVersion { - expected: RADROOTS_TRADE_ENVELOPE_VERSION, - got: RADROOTS_TRADE_ENVELOPE_VERSION + 1, - } - ); - assert_eq!( - invalid_version_err.to_string(), - "invalid active trade envelope version: expected 1, got 2" - ); - - let missing_order = RadrootsActiveTradeEnvelope::new( - RadrootsActiveTradeMessageType::TradeOrderRequested, - sample_listing_addr(), - " ", - sample_active_order_request(), - ); - let missing_order_err = missing_order.validate().unwrap_err(); - assert_eq!( - missing_order_err, - RadrootsActiveTradeEnvelopeError::MissingOrderId - ); - assert_eq!( - missing_order_err.to_string(), - "missing order_id for active trade message" - ); - - let missing_listing = RadrootsActiveTradeEnvelope::new( - RadrootsActiveTradeMessageType::TradeOrderRequested, - " ", - "order-1", - sample_active_order_request(), - ); - let missing_listing_err = missing_listing.validate().unwrap_err(); - assert_eq!( - missing_listing_err, - RadrootsActiveTradeEnvelopeError::MissingListingAddr - ); - assert_eq!(missing_listing_err.to_string(), "missing listing_addr"); - } - - #[test] - fn listing_parse_error_display_variants() { - assert_eq!( - RadrootsTradeListingParseError::InvalidKind(KIND_PROFILE).to_string(), - "invalid listing kind: 0" - ); - assert_eq!( - RadrootsTradeListingParseError::MissingTag("price".into()).to_string(), - "missing required tag: price" - ); - assert_eq!( - RadrootsTradeListingParseError::InvalidTag("farm".into()).to_string(), - "invalid tag: farm" - ); - assert_eq!( - RadrootsTradeListingParseError::InvalidNumber("inventory".into()).to_string(), - "invalid number: inventory" - ); - assert_eq!( - RadrootsTradeListingParseError::InvalidUnit.to_string(), - "invalid unit" - ); - assert_eq!( - RadrootsTradeListingParseError::InvalidCurrency.to_string(), - "invalid currency" - ); - assert_eq!( - RadrootsTradeListingParseError::InvalidJson("bins".into()).to_string(), - "invalid json: bins" - ); - assert_eq!( - RadrootsTradeListingParseError::InvalidDiscount("offer".into()).to_string(), - "invalid discount data for offer" - ); - } - - #[test] - fn listing_validation_error_display_variants() { - assert_eq!( - (RadrootsTradeListingValidationError::InvalidKind { kind: KIND_PROFILE }).to_string(), - "invalid listing kind: 0" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingListingId.to_string(), - "missing listing id" - ); - assert_eq!( - RadrootsTradeListingValidationError::ListingEventNotFound { - listing_addr: "listing-1".into(), - } - .to_string(), - "listing event not found: listing-1" - ); - assert_eq!( - RadrootsTradeListingValidationError::ListingEventFetchFailed { - listing_addr: "listing-2".into(), - } - .to_string(), - "listing event fetch failed: listing-2" - ); - assert_eq!( - RadrootsTradeListingValidationError::ParseError { - error: RadrootsTradeListingParseError::InvalidJson("payload".into()), - } - .to_string(), - "invalid listing data: invalid json: payload" - ); - assert_eq!( - RadrootsTradeListingValidationError::InvalidSeller.to_string(), - "listing author does not match farm pubkey" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingFarmProfile.to_string(), - "missing farm profile" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingFarmRecord.to_string(), - "missing farm record" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingTitle.to_string(), - "missing listing title" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingDescription.to_string(), - "missing listing description" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingProductType.to_string(), - "missing listing product type" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingBins.to_string(), - "missing listing bins" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingPrimaryBin.to_string(), - "missing primary listing bin" - ); - assert_eq!( - RadrootsTradeListingValidationError::InvalidBin.to_string(), - "invalid listing bin" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingPrice.to_string(), - "missing listing price" - ); - assert_eq!( - RadrootsTradeListingValidationError::InvalidPrice.to_string(), - "invalid listing price" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingInventory.to_string(), - "missing listing inventory" - ); - assert_eq!( - RadrootsTradeListingValidationError::InvalidInventory.to_string(), - "invalid listing inventory" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingAvailability.to_string(), - "missing listing availability" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingLocation.to_string(), - "missing listing location" - ); - assert_eq!( - RadrootsTradeListingValidationError::MissingDeliveryMethod.to_string(), - "missing listing delivery method" - ); - } - - #[test] - fn message_type_maps_all_supported_kinds_and_helpers() { - let cases = [ - ( - RadrootsTradeMessageType::ListingValidateRequest, - KIND_TRADE_LISTING_VALIDATE_REQ, - true, - false, - false, - false, - false, - true, - ), - ( - RadrootsTradeMessageType::ListingValidateResult, - KIND_TRADE_LISTING_VALIDATE_RES, - true, - false, - false, - false, - false, - false, - ), - ( - RadrootsTradeMessageType::OrderRequest, - KIND_TRADE_ORDER_REQUEST, - false, - true, - true, - true, - false, - true, - ), - ( - RadrootsTradeMessageType::OrderResponse, - KIND_TRADE_ORDER_RESPONSE, - false, - true, - true, - false, - true, - false, - ), - ( - RadrootsTradeMessageType::OrderRevision, - KIND_TRADE_ORDER_REVISION, - false, - true, - true, - true, - true, - true, - ), - ( - RadrootsTradeMessageType::OrderRevisionAccept, - KIND_TRADE_ORDER_REVISION_RESPONSE, - false, - true, - true, - false, - true, - false, - ), - ( - RadrootsTradeMessageType::OrderRevisionDecline, - KIND_TRADE_ORDER_REVISION_RESPONSE, - false, - true, - true, - false, - true, - false, - ), - ( - RadrootsTradeMessageType::Question, - KIND_TRADE_QUESTION, - false, - true, - true, - false, - true, - true, - ), - ( - RadrootsTradeMessageType::Answer, - KIND_TRADE_ANSWER, - false, - true, - true, - false, - true, - false, - ), - ( - RadrootsTradeMessageType::DiscountRequest, - KIND_TRADE_DISCOUNT_REQUEST, - false, - true, - true, - true, - true, - true, - ), - ( - RadrootsTradeMessageType::DiscountOffer, - KIND_TRADE_DISCOUNT_OFFER, - false, - true, - true, - true, - true, - false, - ), - ( - RadrootsTradeMessageType::DiscountAccept, - KIND_TRADE_DISCOUNT_ACCEPT, - false, - true, - true, - false, - true, - true, - ), - ( - RadrootsTradeMessageType::DiscountDecline, - KIND_TRADE_FORBIDDEN_3431, - false, - true, - true, - false, - true, - true, - ), - ( - RadrootsTradeMessageType::Cancel, - KIND_TRADE_CANCEL, - false, - true, - true, - false, - true, - true, - ), - ( - RadrootsTradeMessageType::FulfillmentUpdate, - KIND_TRADE_FULFILLMENT_UPDATE, - false, - true, - true, - false, - true, - true, - ), - ( - RadrootsTradeMessageType::Receipt, - KIND_TRADE_RECEIPT, - false, - true, - true, - false, - true, - true, - ), - ]; - - for ( - message_type, - kind, - service, - public, - requires_order_id, - requires_listing_snapshot, - requires_trade_chain, - is_request, - ) in cases - { - assert_eq!(message_type.kind(), kind); - assert_eq!(message_type.is_service(), service); - assert_eq!(message_type.is_public(), public); - assert_eq!(message_type.requires_order_id(), requires_order_id); - assert_eq!( - message_type.requires_listing_snapshot(), - requires_listing_snapshot - ); - assert_eq!(message_type.requires_trade_chain(), requires_trade_chain); - assert_eq!(message_type.is_request(), is_request); - assert_eq!(message_type.is_result(), !is_request); - } - - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_LISTING_VALIDATE_REQ), - Some(RadrootsTradeMessageType::ListingValidateRequest) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_LISTING_VALIDATE_RES), - Some(RadrootsTradeMessageType::ListingValidateResult) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_ORDER_REQUEST), - None - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_ORDER_RESPONSE), - Some(RadrootsTradeMessageType::OrderResponse) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_ORDER_REVISION), - Some(RadrootsTradeMessageType::OrderRevision) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_ORDER_REVISION_RESPONSE), - None - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_QUESTION), - Some(RadrootsTradeMessageType::Question) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_ANSWER), - Some(RadrootsTradeMessageType::Answer) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_DISCOUNT_REQUEST), - Some(RadrootsTradeMessageType::DiscountRequest) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_DISCOUNT_OFFER), - Some(RadrootsTradeMessageType::DiscountOffer) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_DISCOUNT_ACCEPT), - Some(RadrootsTradeMessageType::DiscountAccept) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_FORBIDDEN_3431), - None - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_CANCEL), - Some(RadrootsTradeMessageType::Cancel) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_FULFILLMENT_UPDATE), - Some(RadrootsTradeMessageType::FulfillmentUpdate) - ); - assert_eq!( - RadrootsTradeMessageType::from_kind(KIND_TRADE_RECEIPT), - Some(RadrootsTradeMessageType::Receipt) - ); - assert_eq!(RadrootsTradeMessageType::from_kind(KIND_PROFILE), None); - } - - #[test] - fn envelope_requires_order_id_for_order_scoped_messages() { - let envelope = RadrootsTradeEnvelope::new( - RadrootsTradeMessageType::OrderResponse, - sample_listing_addr(), - None, - RadrootsTradeMessagePayload::OrderResponse(sample_order_response(true)), - ); - assert_eq!( - envelope.validate().unwrap_err(), - RadrootsTradeEnvelopeError::MissingOrderId - ); - } - - #[test] - fn envelope_validation_covers_success_and_error_paths() { - let service_envelope = RadrootsTradeEnvelope::new( - RadrootsTradeMessageType::ListingValidateRequest, - sample_listing_addr(), - None, - RadrootsTradeMessagePayload::ListingValidateRequest(sample_validate_request()), - ); - assert_eq!(service_envelope.validate(), Ok(())); - - let public_envelope = RadrootsTradeEnvelope::new( - RadrootsTradeMessageType::OrderResponse, - sample_listing_addr(), - Some("order-1".into()), - RadrootsTradeMessagePayload::OrderResponse(sample_order_response(true)), - ); - assert_eq!(public_envelope.validate(), Ok(())); - - let invalid_version = RadrootsTradeEnvelope { - version: RADROOTS_TRADE_ENVELOPE_VERSION + 1, - domain: RadrootsTradeDomain::TradeListing, - message_type: RadrootsTradeMessageType::OrderResponse, - order_id: Some("order-1".into()), - listing_addr: sample_listing_addr(), - payload: RadrootsTradeMessagePayload::OrderResponse(sample_order_response(true)), - }; - assert_eq!( - invalid_version.validate().unwrap_err(), - RadrootsTradeEnvelopeError::InvalidVersion { - expected: RADROOTS_TRADE_ENVELOPE_VERSION, - got: RADROOTS_TRADE_ENVELOPE_VERSION + 1, - } - ); - - let missing_listing_addr = RadrootsTradeEnvelope::new( - RadrootsTradeMessageType::ListingValidateRequest, - " ", - None, - RadrootsTradeMessagePayload::ListingValidateRequest(sample_validate_request()), - ); - assert_eq!( - missing_listing_addr.validate().unwrap_err(), - RadrootsTradeEnvelopeError::MissingListingAddr - ); - - let blank_order_id = RadrootsTradeEnvelope::new( - RadrootsTradeMessageType::OrderResponse, - sample_listing_addr(), - Some(" ".into()), - RadrootsTradeMessagePayload::OrderResponse(sample_order_response(true)), - ); - assert_eq!( - blank_order_id.validate().unwrap_err(), - RadrootsTradeEnvelopeError::MissingOrderId - ); - } - - #[test] - fn envelope_error_display_variants() { - assert_eq!( - (RadrootsTradeEnvelopeError::InvalidVersion { - expected: 1, - got: 2, - }) - .to_string(), - "invalid envelope version: expected 1, got 2" - ); - assert_eq!( - RadrootsTradeEnvelopeError::MissingOrderId.to_string(), - "missing order_id for order-scoped message" - ); - assert_eq!( - RadrootsTradeEnvelopeError::MissingListingAddr.to_string(), - "missing listing_addr" - ); - } - - #[test] - fn active_payload_error_display_variants_cover_all_messages() { - let cases = [ - ( - RadrootsActiveTradePayloadError::EmptyField("field"), - "field cannot be empty", - ), - ( - RadrootsActiveTradePayloadError::MissingItems, - "items must contain at least one item", - ), - ( - RadrootsActiveTradePayloadError::InvalidItemBinCount { index: 2 }, - "items[2].bin_count must be greater than zero", - ), - ( - RadrootsActiveTradePayloadError::MissingEconomicItems, - "economics.items must contain at least one item", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicItemBinCount { index: 3 }, - "economics.items[3].bin_count must be greater than zero", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicItemQuantity { index: 4 }, - "economics.items[4].quantity_amount must be greater than zero", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicItemPrice { index: 5 }, - "economics.items[5].unit_price_amount must not be negative", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicItemSubtotal { index: 6 }, - "economics.items[6].line_subtotal is invalid", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicLineAmount { - field: "adjustments", - index: 7, - }, - "economics.adjustments[7].amount must be greater than zero", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicLineKind { - field: "discounts", - index: 8, - }, - "economics.discounts[8].kind is invalid", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicLineEffect { - field: "discounts", - index: 9, - }, - "economics.discounts[9].effect is invalid", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field: "total" }, - "economics.total currency is invalid", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicOrdering { field: "items" }, - "economics.items is not in canonical order", - ), - ( - RadrootsActiveTradePayloadError::InvalidEconomicTotal { field: "subtotal" }, - "economics.subtotal total is invalid", - ), - ( - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { field: "items" }, - "order items does not match economics", - ), - ( - RadrootsActiveTradePayloadError::InvalidQuoteVersion, - "economics.quote_version must be greater than zero", - ), - ( - RadrootsActiveTradePayloadError::MissingInventoryCommitments, - "accepted decisions must contain at least one inventory commitment", - ), - ( - RadrootsActiveTradePayloadError::InvalidInventoryCommitmentCount { index: 1 }, - "inventory_commitments[1].bin_count must be greater than zero", - ), - ( - RadrootsActiveTradePayloadError::InvalidFulfillmentStatus, - "fulfillment status is not publishable", - ), - ( - RadrootsActiveTradePayloadError::MissingReceiptIssue, - "receipt issue is required when received is false", - ), - ( - RadrootsActiveTradePayloadError::UnexpectedReceiptIssue, - "receipt issue must be absent when received is true", - ), - ( - RadrootsActiveTradePayloadError::InvalidPaymentAmount, - "payment amount must be greater than zero", - ), - ( - RadrootsActiveTradePayloadError::MissingSettlementReason, - "settlement reason is required when decision is rejected", - ), - ( - RadrootsActiveTradePayloadError::UnexpectedSettlementReason, - "settlement reason must be absent when decision is accepted", - ), - ]; - - for (error, expected) in cases { - assert_eq!(error.to_string(), expected); - } - } - - #[test] - fn payload_message_type_covers_all_variants() { - let payloads = [ - ( - RadrootsTradeMessagePayload::ListingValidateRequest(sample_validate_request()), - RadrootsTradeMessageType::ListingValidateRequest, - ), - ( - RadrootsTradeMessagePayload::ListingValidateResult(sample_validate_result()), - RadrootsTradeMessageType::ListingValidateResult, - ), - ( - RadrootsTradeMessagePayload::TradeOrderRequested(sample_active_order_request()), - RadrootsTradeMessageType::OrderRequest, - ), - ( - RadrootsTradeMessagePayload::OrderResponse(sample_order_response(false)), - RadrootsTradeMessageType::OrderResponse, - ), - ( - RadrootsTradeMessagePayload::OrderRevision(sample_order_revision()), - RadrootsTradeMessageType::OrderRevision, - ), - ( - RadrootsTradeMessagePayload::OrderRevisionAccept(sample_order_revision_response( - true, - )), - RadrootsTradeMessageType::OrderRevisionAccept, - ), - ( - RadrootsTradeMessagePayload::OrderRevisionDecline(sample_order_revision_response( - false, - )), - RadrootsTradeMessageType::OrderRevisionDecline, - ), - ( - RadrootsTradeMessagePayload::Question(RadrootsTradeQuestion { - question_id: "question-1".into(), - }), - RadrootsTradeMessageType::Question, - ), - ( - RadrootsTradeMessagePayload::Answer(RadrootsTradeAnswer { - question_id: "question-1".into(), - }), - RadrootsTradeMessageType::Answer, - ), - ( - RadrootsTradeMessagePayload::DiscountRequest(RadrootsTradeDiscountRequest { - discount_id: "discount-1".into(), - value: sample_discount_value(), - }), - RadrootsTradeMessageType::DiscountRequest, - ), - ( - RadrootsTradeMessagePayload::DiscountOffer(RadrootsTradeDiscountOffer { - discount_id: "discount-1".into(), - value: sample_percent_discount(), - }), - RadrootsTradeMessageType::DiscountOffer, - ), - ( - RadrootsTradeMessagePayload::DiscountAccept( - RadrootsTradeDiscountDecision::Accept { - value: sample_discount_value(), - }, - ), - RadrootsTradeMessageType::DiscountAccept, - ), - ( - RadrootsTradeMessagePayload::DiscountDecline( - RadrootsTradeDiscountDecision::Decline { - reason: Some("no thanks".into()), - }, - ), - RadrootsTradeMessageType::DiscountDecline, - ), - ( - RadrootsTradeMessagePayload::Cancel(RadrootsTradeListingCancel { - reason: Some("out of stock".into()), - }), - RadrootsTradeMessageType::Cancel, - ), - ( - RadrootsTradeMessagePayload::FulfillmentUpdate(RadrootsTradeFulfillmentUpdate { - status: RadrootsTradeFulfillmentStatus::Delivered, - }), - RadrootsTradeMessageType::FulfillmentUpdate, - ), - ( - RadrootsTradeMessagePayload::Receipt(RadrootsTradeReceipt { - acknowledged: true, - at: 42, - }), - RadrootsTradeMessageType::Receipt, - ), - ]; - - for (payload, message_type) in payloads { - assert_eq!(payload.message_type(), message_type); - } - } -} diff --git a/crates/events/src/trade_validation.rs b/crates/events/src/trade_validation.rs @@ -0,0 +1,84 @@ +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use crate::{RadrootsNostrEventPtr, order::RadrootsListingParseError}; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde(rename_all = "snake_case", tag = "kind", content = "amount") +)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsTradeValidationListingError { + InvalidKind { kind: u32 }, + MissingListingId, + ListingEventNotFound { listing_addr: String }, + ListingEventFetchFailed { listing_addr: String }, + ParseError { error: RadrootsListingParseError }, + InvalidSeller, + MissingFarmProfile, + MissingFarmRecord, + MissingTitle, + MissingDescription, + MissingProductType, + MissingBins, + MissingPrimaryBin, + InvalidBin, + MissingPrice, + InvalidPrice, + MissingInventory, + InvalidInventory, + MissingAvailability, + MissingLocation, + MissingDeliveryMethod, +} + +impl core::fmt::Display for RadrootsTradeValidationListingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidKind { kind } => write!(f, "invalid listing kind: {kind}"), + Self::MissingListingId => write!(f, "missing listing id"), + Self::ListingEventNotFound { listing_addr } => { + write!(f, "listing event not found: {listing_addr}") + } + Self::ListingEventFetchFailed { listing_addr } => { + write!(f, "listing event fetch failed: {listing_addr}") + } + Self::ParseError { error } => write!(f, "invalid listing data: {error}"), + Self::InvalidSeller => write!(f, "listing author does not match farm pubkey"), + Self::MissingFarmProfile => write!(f, "missing farm profile"), + Self::MissingFarmRecord => write!(f, "missing farm record"), + Self::MissingTitle => write!(f, "missing listing title"), + Self::MissingDescription => write!(f, "missing listing description"), + Self::MissingProductType => write!(f, "missing listing product type"), + Self::MissingBins => write!(f, "missing listing bins"), + Self::MissingPrimaryBin => write!(f, "missing primary listing bin"), + Self::InvalidBin => write!(f, "invalid listing bin"), + Self::MissingPrice => write!(f, "missing listing price"), + Self::InvalidPrice => write!(f, "invalid listing price"), + Self::MissingInventory => write!(f, "missing listing inventory"), + Self::InvalidInventory => write!(f, "invalid listing inventory"), + Self::MissingAvailability => write!(f, "missing listing availability"), + Self::MissingLocation => write!(f, "missing listing location"), + Self::MissingDeliveryMethod => write!(f, "missing listing delivery method"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsTradeValidationListingError {} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsTradeValidationListingRequest { + pub listing_event: Option<RadrootsNostrEventPtr>, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsTradeValidationListingResult { + pub valid: bool, + pub errors: Vec<RadrootsTradeValidationListingError>, +} diff --git a/crates/events_codec/src/lib.rs b/crates/events_codec/src/lib.rs @@ -45,7 +45,7 @@ pub mod seal; pub mod list; pub mod list_set; pub mod listing; -pub mod trade; +pub mod order; #[cfg(test)] mod test_fixtures; diff --git a/crates/events_codec/src/order/decode.rs b/crates/events_codec/src/order/decode.rs @@ -0,0 +1,1521 @@ +#[cfg(all(not(feature = "std"), feature = "serde_json"))] +use alloc::{borrow::ToOwned, format, string::String, vec::Vec}; + +#[cfg(feature = "serde_json")] +use radroots_events::{ + RadrootsNostrEvent, RadrootsNostrEventPtr, + kinds::{KIND_PROFILE, is_order_event_kind}, + order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderEnvelope, + RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderFulfillmentUpdate, + RadrootsOrderPayloadError, RadrootsOrderPaymentRecord, RadrootsOrderReceipt, + RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal, + RadrootsOrderSettlementDecision, + }, + tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, +}; +#[cfg(feature = "serde_json")] +use serde::de::DeserializeOwned; + +#[cfg(feature = "serde_json")] +use crate::d_tag::is_d_tag_base64url; +#[cfg(feature = "serde_json")] +use crate::order::tags::{ + TAG_LISTING_EVENT, parse_order_counterparty_tag, parse_order_listing_event_tag, + parse_order_prev_tag, parse_order_root_tag, +}; + +#[cfg(feature = "serde_json")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsOrderEnvelopeParseError { + InvalidKind(u32), + InvalidJson, + InvalidEnvelope(RadrootsOrderEnvelopeError), + InvalidPayload(RadrootsOrderPayloadError), + MessageTypeKindMismatch { + event_kind: u32, + message_type: RadrootsOrderEventType, + }, + MissingTag(&'static str), + InvalidTag(&'static str), + ListingAddrTagMismatch, + OrderIdTagMismatch, + PayloadBindingMismatch(&'static str), + AuthorMismatch, + CounterpartyTagMismatch, + InvalidListingAddr(RadrootsOrderListingAddressError), +} + +#[cfg(feature = "serde_json")] +impl core::fmt::Display for RadrootsOrderEnvelopeParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidKind(kind) => write!(f, "invalid order event kind: {kind}"), + Self::InvalidJson => write!(f, "invalid order envelope json"), + Self::InvalidEnvelope(error) => write!(f, "{error}"), + Self::InvalidPayload(error) => write!(f, "{error}"), + Self::MessageTypeKindMismatch { + event_kind, + message_type, + } => write!( + f, + "order envelope type {message_type:?} does not match event kind {event_kind}" + ), + Self::MissingTag(tag) => write!(f, "missing required order tag: {tag}"), + Self::InvalidTag(tag) => write!(f, "invalid order tag: {tag}"), + Self::ListingAddrTagMismatch => { + write!(f, "order listing address tag does not match envelope") + } + Self::OrderIdTagMismatch => { + write!(f, "order order id tag does not match envelope") + } + Self::PayloadBindingMismatch(field) => { + write!(f, "order payload {field} does not match envelope") + } + Self::AuthorMismatch => write!(f, "order event author does not match payload"), + Self::CounterpartyTagMismatch => { + write!(f, "order counterparty tag does not match payload") + } + Self::InvalidListingAddr(error) => write!(f, "{error}"), + } + } +} + +#[cfg(all(feature = "std", feature = "serde_json"))] +impl std::error::Error for RadrootsOrderEnvelopeParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidEnvelope(error) => Some(error), + Self::InvalidPayload(error) => Some(error), + Self::InvalidListingAddr(error) => Some(error), + _ => None, + } + } +} + +#[cfg(feature = "serde_json")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderEventContext { + pub counterparty_pubkey: String, + pub listing_event: Option<RadrootsNostrEventPtr>, + pub root_event_id: Option<String>, + pub prev_event_id: Option<String>, +} + +#[cfg(feature = "serde_json")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOrderListingAddress { + pub kind: u32, + pub seller_pubkey: String, + pub listing_id: String, +} + +#[cfg(feature = "serde_json")] +impl RadrootsOrderListingAddress { + pub fn parse(addr: &str) -> Result<Self, RadrootsOrderListingAddressError> { + let (kind_raw, seller_and_listing) = addr + .split_once(':') + .ok_or(RadrootsOrderListingAddressError::InvalidFormat)?; + let (seller_pubkey_raw, listing_id_raw) = seller_and_listing + .split_once(':') + .ok_or(RadrootsOrderListingAddressError::InvalidFormat)?; + if listing_id_raw.contains(':') { + return Err(RadrootsOrderListingAddressError::InvalidFormat); + } + let kind = kind_raw + .parse::<u32>() + .map_err(|_| RadrootsOrderListingAddressError::InvalidFormat)?; + let seller_pubkey = seller_pubkey_raw.to_owned(); + let listing_id = listing_id_raw.to_owned(); + if kind == KIND_PROFILE + || seller_pubkey.trim().is_empty() + || listing_id.trim().is_empty() + || !is_d_tag_base64url(&listing_id) + { + return Err(RadrootsOrderListingAddressError::InvalidFormat); + } + Ok(Self { + kind, + seller_pubkey, + listing_id, + }) + } + + #[inline] + pub fn as_str(&self) -> String { + format!("{}:{}:{}", self.kind, self.seller_pubkey, self.listing_id) + } +} + +#[cfg(feature = "serde_json")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsOrderListingAddressError { + InvalidFormat, +} + +#[cfg(feature = "serde_json")] +impl core::fmt::Display for RadrootsOrderListingAddressError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidFormat => write!(f, "invalid listing address format"), + } + } +} + +#[cfg(all(feature = "std", feature = "serde_json"))] +impl std::error::Error for RadrootsOrderListingAddressError {} + +#[cfg(feature = "serde_json")] +pub fn order_envelope_from_event<T: DeserializeOwned>( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<T>, RadrootsOrderEnvelopeParseError> { + if !is_order_event_kind(event.kind) { + return Err(RadrootsOrderEnvelopeParseError::InvalidKind(event.kind)); + } + let envelope = serde_json::from_str::<RadrootsOrderEnvelope<T>>(&event.content) + .map_err(|_| RadrootsOrderEnvelopeParseError::InvalidJson)?; + envelope + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidEnvelope)?; + if envelope.message_type.kind() != event.kind { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + + let listing_addr = required_order_tag_value(&event.tags, "a")?; + if envelope.listing_addr != listing_addr { + return Err(RadrootsOrderEnvelopeParseError::ListingAddrTagMismatch); + } + RadrootsOrderListingAddress::parse(&envelope.listing_addr) + .map_err(RadrootsOrderEnvelopeParseError::InvalidListingAddr)?; + + let tag_order_id = required_order_tag_value(&event.tags, TAG_D)?; + if tag_order_id != envelope.order_id { + return Err(RadrootsOrderEnvelopeParseError::OrderIdTagMismatch); + } + + order_event_context_from_tags(envelope.message_type, &event.tags)?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_request_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderRequest>, RadrootsOrderEnvelopeParseError> { + let envelope = order_envelope_from_event::<RadrootsOrderRequest>(event)?; + if envelope.message_type != RadrootsOrderEventType::OrderRequested { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.buyer_pubkey, + &envelope.payload.seller_pubkey, + )?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_decision_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderDecision>, RadrootsOrderEnvelopeParseError> { + let envelope = order_envelope_from_event::<RadrootsOrderDecision>(event)?; + if envelope.message_type != RadrootsOrderEventType::OrderDecision { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.seller_pubkey, + &envelope.payload.buyer_pubkey, + )?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_revision_proposal_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderRevisionProposal>, RadrootsOrderEnvelopeParseError> { + let envelope = order_envelope_from_event::<RadrootsOrderRevisionProposal>(event)?; + if envelope.message_type != RadrootsOrderEventType::OrderRevisionProposed { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.seller_pubkey, + &envelope.payload.buyer_pubkey, + )?; + let context = order_event_context_from_tags(envelope.message_type, &event.tags)?; + if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "root_event_id", + )); + } + if context.prev_event_id.as_deref() != Some(envelope.payload.prev_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "prev_event_id", + )); + } + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_revision_decision_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderRevisionDecision>, RadrootsOrderEnvelopeParseError> { + let envelope = order_envelope_from_event::<RadrootsOrderRevisionDecision>(event)?; + if envelope.message_type != RadrootsOrderEventType::OrderRevisionDecision { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.buyer_pubkey, + &envelope.payload.seller_pubkey, + )?; + let context = order_event_context_from_tags(envelope.message_type, &event.tags)?; + if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "root_event_id", + )); + } + if context.prev_event_id.as_deref() != Some(envelope.payload.prev_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "prev_event_id", + )); + } + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_fulfillment_update_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderFulfillmentUpdate>, RadrootsOrderEnvelopeParseError> +{ + let envelope = order_envelope_from_event::<RadrootsOrderFulfillmentUpdate>(event)?; + if envelope.message_type != RadrootsOrderEventType::FulfillmentUpdated { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.seller_pubkey, + &envelope.payload.buyer_pubkey, + )?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_cancellation_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderCancellation>, RadrootsOrderEnvelopeParseError> { + let envelope = order_envelope_from_event::<RadrootsOrderCancellation>(event)?; + if envelope.message_type != RadrootsOrderEventType::OrderCancelled { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.buyer_pubkey, + &envelope.payload.seller_pubkey, + )?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_receipt_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderReceipt>, RadrootsOrderEnvelopeParseError> { + let envelope = order_envelope_from_event::<RadrootsOrderReceipt>(event)?; + if envelope.message_type != RadrootsOrderEventType::BuyerReceipt { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.buyer_pubkey, + &envelope.payload.seller_pubkey, + )?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_payment_record_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderPaymentRecord>, RadrootsOrderEnvelopeParseError> { + let envelope = order_envelope_from_event::<RadrootsOrderPaymentRecord>(event)?; + if envelope.message_type != RadrootsOrderEventType::PaymentRecorded { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.buyer_pubkey, + &envelope.payload.seller_pubkey, + )?; + let context = order_event_context_from_tags(envelope.message_type, &event.tags)?; + if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "root_event_id", + )); + } + if context.prev_event_id.as_deref() != Some(envelope.payload.previous_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "previous_event_id", + )); + } + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_settlement_decision_from_event( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderSettlementDecision>, RadrootsOrderEnvelopeParseError> +{ + let envelope = order_envelope_from_event::<RadrootsOrderSettlementDecision>(event)?; + if envelope.message_type != RadrootsOrderEventType::SettlementDecision { + return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }); + } + envelope + .payload + .validate() + .map_err(RadrootsOrderEnvelopeParseError::InvalidPayload)?; + validate_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.seller_pubkey, + &envelope.payload.buyer_pubkey, + )?; + let context = order_event_context_from_tags(envelope.message_type, &event.tags)?; + if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "root_event_id", + )); + } + if context.prev_event_id.as_deref() != Some(envelope.payload.previous_event_id.as_str()) { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "previous_event_id", + )); + } + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn order_event_context_from_tags( + message_type: RadrootsOrderEventType, + tags: &[Vec<String>], +) -> Result<RadrootsOrderEventContext, RadrootsOrderEnvelopeParseError> { + let counterparty_pubkey = + parse_order_counterparty_tag(tags).map_err(map_tag_parse_error_for_order_envelope)?; + let listing_event = + parse_order_listing_event_tag(tags).map_err(map_tag_parse_error_for_order_envelope)?; + let root_event_id = + parse_order_root_tag(tags).map_err(map_tag_parse_error_for_order_envelope)?; + let prev_event_id = + parse_order_prev_tag(tags).map_err(map_tag_parse_error_for_order_envelope)?; + + if message_type.requires_listing_snapshot() && listing_event.is_none() { + return Err(RadrootsOrderEnvelopeParseError::MissingTag( + TAG_LISTING_EVENT, + )); + } + if message_type.requires_order_chain() { + if root_event_id.is_none() { + return Err(RadrootsOrderEnvelopeParseError::MissingTag(TAG_E_ROOT)); + } + if prev_event_id.is_none() { + return Err(RadrootsOrderEnvelopeParseError::MissingTag(TAG_E_PREV)); + } + } + + Ok(RadrootsOrderEventContext { + counterparty_pubkey, + listing_event, + root_event_id, + prev_event_id, + }) +} + +#[cfg(feature = "serde_json")] +fn required_order_tag_value<'a>( + tags: &'a [Vec<String>], + key: &'static str, +) -> Result<&'a str, RadrootsOrderEnvelopeParseError> { + let tag = tags + .iter() + .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) + .ok_or(RadrootsOrderEnvelopeParseError::MissingTag(key))?; + let value = tag + .get(1) + .map(|value| value.as_str()) + .ok_or(RadrootsOrderEnvelopeParseError::InvalidTag(key))?; + if value.trim().is_empty() { + return Err(RadrootsOrderEnvelopeParseError::InvalidTag(key)); + } + Ok(value) +} + +#[cfg(feature = "serde_json")] +fn map_tag_parse_error_for_order_envelope( + error: crate::error::EventParseError, +) -> RadrootsOrderEnvelopeParseError { + match error { + crate::error::EventParseError::MissingTag(tag) => { + RadrootsOrderEnvelopeParseError::MissingTag(tag) + } + crate::error::EventParseError::InvalidTag(tag) => { + RadrootsOrderEnvelopeParseError::InvalidTag(tag) + } + crate::error::EventParseError::InvalidKind { expected: _, got } => { + RadrootsOrderEnvelopeParseError::InvalidKind(got) + } + crate::error::EventParseError::InvalidNumber(tag, _) + | crate::error::EventParseError::InvalidJson(tag) => { + RadrootsOrderEnvelopeParseError::InvalidTag(tag) + } + } +} + +#[cfg(feature = "serde_json")] +fn validate_order_binding<T>( + event: &RadrootsNostrEvent, + envelope: &RadrootsOrderEnvelope<T>, + payload_order_id: &str, + payload_listing_addr: &str, + expected_author: &str, + expected_counterparty: &str, +) -> Result<(), RadrootsOrderEnvelopeParseError> { + if envelope.order_id != payload_order_id { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "order_id", + )); + } + if envelope.listing_addr != payload_listing_addr { + return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( + "listing_addr", + )); + } + if event.author != expected_author { + return Err(RadrootsOrderEnvelopeParseError::AuthorMismatch); + } + let counterparty = parse_order_counterparty_tag(&event.tags) + .map_err(map_tag_parse_error_for_order_envelope)?; + if counterparty != expected_counterparty { + return Err(RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch); + } + Ok(()) +} + +#[cfg(all(test, feature = "serde_json"))] +mod tests { + use super::{ + RadrootsOrderEnvelopeParseError, RadrootsOrderListingAddress, + order_cancellation_from_event, order_decision_from_event, order_envelope_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, + }; + use crate::order::encode::{ + order_cancellation_event_build, order_decision_event_build, + order_fulfillment_update_event_build, order_payment_record_event_build, + order_receipt_event_build, order_request_event_build, order_revision_decision_event_build, + order_revision_proposal_event_build, order_settlement_decision_event_build, + }; + use crate::order::tags::TAG_LISTING_EVENT; + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, + }; + use radroots_events::{ + RadrootsNostrEvent, RadrootsNostrEventPtr, + kinds::{ + 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, + }, + order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, + RadrootsOrderEnvelope, RadrootsOrderEventType, RadrootsOrderFulfillmentState, + RadrootsOrderFulfillmentUpdate, RadrootsOrderInventoryCommitment, RadrootsOrderItem, + RadrootsOrderPayloadError, RadrootsOrderPaymentMethod, RadrootsOrderPaymentRecord, + RadrootsOrderPricingBasis, RadrootsOrderReceipt, RadrootsOrderRequest, + RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, + RadrootsOrderRevisionProposal, RadrootsOrderSettlementDecision, + RadrootsOrderSettlementOutcome, + }, + tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, + }; + + fn order_request() -> RadrootsOrderRequest { + RadrootsOrderRequest { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + items: vec![RadrootsOrderItem { + bin_id: "lb".into(), + bin_count: 3, + }], + economics: request_economics(), + } + } + + fn decimal(raw: &str) -> RadrootsCoreDecimal { + raw.parse().unwrap() + } + + fn usd(raw: &str) -> RadrootsCoreMoney { + RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) + } + + fn request_economics() -> RadrootsOrderEconomics { + RadrootsOrderEconomics { + quote_id: "quote-1".into(), + quote_version: 1, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsOrderEconomicItem { + bin_id: "lb".into(), + bin_count: 3, + quantity_amount: decimal("1"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("5"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd("15"), + }], + discounts: Vec::<RadrootsOrderEconomicLine>::new(), + adjustments: Vec::<RadrootsOrderEconomicLine>::new(), + subtotal: usd("15"), + discount_total: usd("0"), + adjustment_total: usd("0"), + total: usd("15"), + } + } + + fn order_decision() -> RadrootsOrderDecision { + RadrootsOrderDecision { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { + bin_id: "lb".into(), + bin_count: 3, + }], + }, + } + } + + fn order_revision_proposal() -> RadrootsOrderRevisionProposal { + let mut economics = request_economics(); + economics.quote_id = "revision-quote-1".into(); + economics.quote_version = 2; + economics.items[0].bin_count = 4; + economics.items[0].line_subtotal = usd("20"); + economics.subtotal = usd("20"); + economics.total = usd("20"); + economics.canonicalize(); + RadrootsOrderRevisionProposal { + revision_id: "rev-1".into(), + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + root_event_id: "root-event".into(), + prev_event_id: "decision-event".into(), + items: vec![RadrootsOrderItem { + bin_id: "lb".into(), + bin_count: 4, + }], + economics, + reason: "update count".into(), + } + } + + fn order_revision_decision( + decision: RadrootsOrderRevisionOutcome, + ) -> RadrootsOrderRevisionDecision { + RadrootsOrderRevisionDecision { + revision_id: "rev-1".into(), + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + root_event_id: "root-event".into(), + prev_event_id: "revision-event".into(), + decision, + } + } + + fn order_fulfillment_update() -> RadrootsOrderFulfillmentUpdate { + RadrootsOrderFulfillmentUpdate { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + status: RadrootsOrderFulfillmentState::ReadyForPickup, + } + } + + fn order_cancelled() -> RadrootsOrderCancellation { + RadrootsOrderCancellation { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + reason: "changed plans".into(), + } + } + + fn order_buyer_receipt(received: bool) -> RadrootsOrderReceipt { + RadrootsOrderReceipt { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + received, + issue: (!received).then(|| "damaged items".into()), + received_at: 1_777_665_600, + } + } + + fn order_payment_recorded() -> RadrootsOrderPaymentRecord { + RadrootsOrderPaymentRecord { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + root_event_id: "root-event".into(), + previous_event_id: "agreement-event".into(), + agreement_event_id: "agreement-event".into(), + quote_id: "quote-1".into(), + quote_version: 1, + economics_digest: "digest-1".into(), + amount: decimal("15"), + currency: RadrootsCoreCurrency::USD, + method: RadrootsOrderPaymentMethod::Cash, + reference: Some("cash drawer".into()), + paid_at: Some(1_777_665_600), + } + } + + fn order_settlement_decision( + decision: RadrootsOrderSettlementOutcome, + ) -> RadrootsOrderSettlementDecision { + RadrootsOrderSettlementDecision { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + seller_pubkey: "seller".into(), + buyer_pubkey: "buyer".into(), + root_event_id: "root-event".into(), + previous_event_id: "payment-event".into(), + agreement_event_id: "agreement-event".into(), + payment_event_id: "payment-event".into(), + quote_id: "quote-1".into(), + quote_version: 1, + economics_digest: "digest-1".into(), + amount: decimal("15"), + currency: RadrootsCoreCurrency::USD, + decision, + reason: (decision == RadrootsOrderSettlementOutcome::Rejected) + .then(|| "reference mismatch".into()), + } + } + + fn listing_event_ptr() -> RadrootsNostrEventPtr { + RadrootsNostrEventPtr { + id: "listing-snapshot".into(), + relays: Some("wss://relay.example.com".into()), + } + } + + #[test] + fn listing_address_roundtrips() { + let addr = RadrootsOrderListingAddress::parse("30402:seller:AAAAAAAAAAAAAAAAAAAAAg") + .expect("parse listing address"); + assert_eq!(addr.as_str(), "30402:seller:AAAAAAAAAAAAAAAAAAAAAg"); + } + + #[test] + fn order_request_builder_emits_canonical_shape() { + let payload = order_request(); + let built = order_request_event_build(&listing_event_ptr(), &payload).unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderRequest> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_REQUEST); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::OrderRequested + ); + assert_eq!(envelope.order_id, "order-1"); + assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); + assert_eq!( + built.tags[1], + vec![ + "a".to_string(), + "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".to_string() + ] + ); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert_eq!(envelope.payload.economics.quote_id, "quote-1"); + assert_eq!(envelope.payload.economics.total, usd("15")); + assert!( + built + .tags + .iter() + .any(|tag| tag.first().map(String::as_str) == Some(TAG_LISTING_EVENT)) + ); + assert!( + !built + .tags + .iter() + .any(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) + ); + } + + #[test] + fn order_decision_builder_emits_canonical_chain_shape() { + let payload = order_decision(); + let built = order_decision_event_build("root-event", "prev-event", &payload).unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderDecision> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_DECISION); + assert_eq!(envelope.message_type, RadrootsOrderEventType::OrderDecision); + assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) + ); + } + + #[test] + fn order_revision_proposal_builder_emits_canonical_chain_shape() { + let payload = order_revision_proposal(); + let built = order_revision_proposal_event_build( + payload.root_event_id.as_str(), + payload.prev_event_id.as_str(), + &payload, + ) + .unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderRevisionProposal> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_REVISION_PROPOSAL); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::OrderRevisionProposed + ); + assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert_eq!(envelope.payload.revision_id, "rev-1"); + assert_eq!(envelope.payload.economics.quote_version, 2); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "decision-event".to_string()]) + ); + } + + #[test] + fn order_revision_decision_builder_emits_canonical_chain_shape() { + let payload = order_revision_decision(RadrootsOrderRevisionOutcome::Accepted); + let built = order_revision_decision_event_build( + payload.root_event_id.as_str(), + payload.prev_event_id.as_str(), + &payload, + ) + .unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderRevisionDecision> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_REVISION_DECISION); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::OrderRevisionDecision + ); + assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert_eq!(envelope.payload.revision_id, "rev-1"); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "revision-event".to_string()]) + ); + } + + #[test] + fn order_fulfillment_update_builder_emits_canonical_chain_shape() { + let payload = order_fulfillment_update(); + let built = + order_fulfillment_update_event_build("root-event", "prev-event", &payload).unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderFulfillmentUpdate> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_FULFILLMENT_UPDATE); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::FulfillmentUpdated + ); + assert_eq!(envelope.payload.status, payload.status); + assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) + ); + } + + #[test] + fn order_cancellation_builder_emits_canonical_buyer_chain_shape() { + let payload = order_cancelled(); + let built = order_cancellation_event_build("root-event", "prev-event", &payload).unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderCancellation> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_CANCELLATION); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::OrderCancelled + ); + assert_eq!(envelope.payload.reason, payload.reason); + assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) + ); + } + + #[test] + fn order_buyer_receipt_builder_emits_canonical_buyer_chain_shape() { + let payload = order_buyer_receipt(false); + let built = order_receipt_event_build("root-event", "prev-event", &payload).unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderReceipt> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_RECEIPT); + assert_eq!(envelope.message_type, RadrootsOrderEventType::BuyerReceipt); + assert_eq!(envelope.payload.received, false); + assert_eq!(envelope.payload.issue.as_deref(), Some("damaged items")); + assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) + ); + } + + #[test] + fn order_payment_recorded_builder_emits_canonical_buyer_chain_shape() { + let payload = order_payment_recorded(); + let built = order_payment_record_event_build( + payload.root_event_id.as_str(), + payload.previous_event_id.as_str(), + &payload, + ) + .unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderPaymentRecord> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_PAYMENT_RECORD); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::PaymentRecorded + ); + assert_eq!(envelope.payload.amount, decimal("15")); + assert_eq!(envelope.payload.method, RadrootsOrderPaymentMethod::Cash); + assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "agreement-event".to_string()]) + ); + } + + #[test] + fn order_settlement_decision_builder_emits_canonical_seller_chain_shape() { + let payload = order_settlement_decision(RadrootsOrderSettlementOutcome::Accepted); + let built = order_settlement_decision_event_build( + payload.root_event_id.as_str(), + payload.previous_event_id.as_str(), + &payload, + ) + .unwrap(); + let envelope: RadrootsOrderEnvelope<RadrootsOrderSettlementDecision> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_ORDER_SETTLEMENT_DECISION); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::SettlementDecision + ); + assert_eq!( + envelope.payload.decision, + RadrootsOrderSettlementOutcome::Accepted + ); + assert_eq!(envelope.payload.reason, None); + assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "payment-event".to_string()]) + ); + } + + #[test] + fn order_request_parse_roundtrips_and_validates_tags() { + let payload = order_request(); + let built = order_request_event_build(&listing_event_ptr(), &payload).unwrap(); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "buyer".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_request_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::OrderRequested + ); + } + + #[test] + fn order_request_parse_rejects_mismatched_economics() { + let mut payload = order_request(); + let built = order_request_event_build(&listing_event_ptr(), &payload).unwrap(); + payload.economics.items[0].bin_id = "other-bin".into(); + let envelope = RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + payload.listing_addr.clone(), + payload.order_id.clone(), + payload, + ); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "buyer".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: serde_json::to_string(&envelope).unwrap(), + sig: "sig".into(), + }; + let err = order_request_from_event(&event).unwrap_err(); + assert_eq!( + err, + RadrootsOrderEnvelopeParseError::InvalidPayload( + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { + field: "items.bin_id" + } + ) + ); + } + + #[test] + fn order_decision_parse_roundtrips_and_validates_chain_tags() { + let payload = order_decision(); + let built = order_decision_event_build("root-event", "prev-event", &payload).unwrap(); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_decision_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!(envelope.message_type, RadrootsOrderEventType::OrderDecision); + } + + #[test] + fn order_fulfillment_update_parse_roundtrips_and_validates_chain_tags() { + let payload = order_fulfillment_update(); + let built = + order_fulfillment_update_event_build("root-event", "prev-event", &payload).unwrap(); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_fulfillment_update_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::FulfillmentUpdated + ); + } + + #[test] + fn order_cancellation_parse_roundtrips_and_validates_buyer_actor() { + let payload = order_cancelled(); + let built = order_cancellation_event_build("root-event", "prev-event", &payload).unwrap(); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "buyer".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_cancellation_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::OrderCancelled + ); + } + + #[test] + fn order_buyer_receipt_parse_roundtrips_and_validates_buyer_actor() { + let payload = order_buyer_receipt(true); + let built = order_receipt_event_build("root-event", "prev-event", &payload).unwrap(); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "buyer".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_receipt_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!(envelope.message_type, RadrootsOrderEventType::BuyerReceipt); + } + + #[test] + fn order_payment_recorded_parse_roundtrips_and_validates_buyer_actor() { + let payload = order_payment_recorded(); + let built = order_payment_record_event_build( + payload.root_event_id.as_str(), + payload.previous_event_id.as_str(), + &payload, + ) + .unwrap(); + let event = RadrootsNostrEvent { + id: "payment-event".into(), + author: "buyer".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_payment_record_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::PaymentRecorded + ); + } + + #[test] + fn order_settlement_decision_parse_roundtrips_and_validates_seller_actor() { + let payload = order_settlement_decision(RadrootsOrderSettlementOutcome::Rejected); + let built = order_settlement_decision_event_build( + payload.root_event_id.as_str(), + payload.previous_event_id.as_str(), + &payload, + ) + .unwrap(); + let event = RadrootsNostrEvent { + id: "settlement-event".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_settlement_decision_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!( + envelope.message_type, + RadrootsOrderEventType::SettlementDecision + ); + } + + #[test] + fn order_revision_proposal_parse_validates_actor_counterparty_and_chain_payload() { + let payload = order_revision_proposal(); + let built = order_revision_proposal_event_build( + payload.root_event_id.as_str(), + payload.prev_event_id.as_str(), + &payload, + ) + .unwrap(); + let mut event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_revision_proposal_from_event(&event).unwrap(); + assert_eq!(envelope.payload, payload); + + event.author = "buyer".into(); + let err = order_revision_proposal_from_event(&event).unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); + } + + #[test] + fn order_revision_decision_parse_validates_actor_counterparty_and_chain_payload() { + let payload = order_revision_decision(RadrootsOrderRevisionOutcome::Declined { + reason: "no change".into(), + }); + let built = order_revision_decision_event_build( + payload.root_event_id.as_str(), + payload.prev_event_id.as_str(), + &payload, + ) + .unwrap(); + let mut event = RadrootsNostrEvent { + id: "event-id".into(), + author: "buyer".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = order_revision_decision_from_event(&event).unwrap(); + assert_eq!(envelope.payload, payload); + + event.author = "seller".into(); + let err = order_revision_decision_from_event(&event).unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); + } + + #[test] + fn order_revision_kinds_parse_with_chain_tags() { + for (kind, message_type) in [ + ( + KIND_ORDER_REVISION_PROPOSAL, + RadrootsOrderEventType::OrderRevisionProposed, + ), + ( + KIND_ORDER_REVISION_DECISION, + RadrootsOrderEventType::OrderRevisionDecision, + ), + ] { + let payload = serde_json::json!({}); + let envelope = RadrootsOrderEnvelope::new( + message_type, + "30402:seller:AAAAAAAAAAAAAAAAAAAAAg", + "order-1", + &payload, + ); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind, + tags: vec![ + vec!["p".into(), "buyer".into()], + vec!["a".into(), "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into()], + vec![TAG_D.into(), "order-1".into()], + vec![TAG_E_ROOT.into(), "root-event".into()], + vec![TAG_E_PREV.into(), "prev-event".into()], + ], + content: serde_json::to_string(&envelope).unwrap(), + sig: "sig".into(), + }; + let parsed = order_envelope_from_event::<serde_json::Value>(&event).unwrap(); + + assert_eq!(parsed.message_type, message_type); + assert_eq!(parsed.order_id, "order-1"); + } + } + + #[test] + fn order_parse_rejects_forbidden_kind() { + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: 3431, + tags: Vec::new(), + content: "{}".into(), + sig: "sig".into(), + }; + let err = order_envelope_from_event::<serde_json::Value>(&event).unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::InvalidKind(3431)); + } + + #[test] + fn order_parse_rejects_missing_required_refs() { + let payload = order_decision(); + let built = order_decision_event_build("root-event", "prev-event", &payload).unwrap(); + let mut event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + event + .tags + .retain(|tag| tag.first().map(String::as_str) != Some(TAG_E_PREV)); + + let err = order_decision_from_event(&event).unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::MissingTag(TAG_E_PREV)); + } + + #[test] + fn order_parse_rejects_author_and_counterparty_mismatch() { + let payload = order_request(); + let built = order_request_event_build(&listing_event_ptr(), &payload).unwrap(); + let mut event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags.clone(), + content: built.content.clone(), + sig: "sig".into(), + }; + let err = order_request_from_event(&event).unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); + + event.author = "buyer".into(); + event.tags[0] = vec!["p".into(), "other-seller".into()]; + let err = order_request_from_event(&event).unwrap_err(); + assert_eq!( + err, + RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch + ); + } + + #[test] + fn order_buyer_lifecycle_parse_rejects_wrong_actor_or_counterparty() { + let cancellation = order_cancelled(); + let cancellation_parts = + order_cancellation_event_build("root-event", "prev-event", &cancellation).unwrap(); + let cancellation_event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: cancellation_parts.kind, + tags: cancellation_parts.tags, + content: cancellation_parts.content, + sig: "sig".into(), + }; + let err = order_cancellation_from_event(&cancellation_event).unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); + + let receipt = order_buyer_receipt(true); + let receipt_parts = + order_receipt_event_build("root-event", "prev-event", &receipt).unwrap(); + let mut receipt_event = RadrootsNostrEvent { + id: "event-id".into(), + author: "buyer".into(), + created_at: 1, + kind: receipt_parts.kind, + tags: receipt_parts.tags, + content: receipt_parts.content, + sig: "sig".into(), + }; + receipt_event.tags[0] = vec!["p".into(), "other-seller".into()]; + let err = order_receipt_from_event(&receipt_event).unwrap_err(); + assert_eq!( + err, + RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch + ); + } +} diff --git a/crates/events_codec/src/order/encode.rs b/crates/events_codec/src/order/encode.rs @@ -0,0 +1,329 @@ +#[cfg(all(not(feature = "std"), feature = "serde_json"))] +use alloc::string::String; + +#[cfg(feature = "serde_json")] +use radroots_events::{ + RadrootsNostrEventPtr, + order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderEnvelope, + RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderFulfillmentUpdate, + RadrootsOrderPayloadError, RadrootsOrderPaymentRecord, RadrootsOrderReceipt, + RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal, + RadrootsOrderSettlementDecision, + }, +}; + +#[cfg(feature = "serde_json")] +use crate::{error::EventEncodeError, order::tags::order_envelope_tags, wire::WireEventParts}; + +#[cfg(feature = "serde_json")] +fn map_order_envelope_error(error: RadrootsOrderEnvelopeError) -> EventEncodeError { + match error { + RadrootsOrderEnvelopeError::MissingOrderId => { + EventEncodeError::EmptyRequiredField("order_id") + } + RadrootsOrderEnvelopeError::MissingListingAddr => { + EventEncodeError::EmptyRequiredField("listing_addr") + } + RadrootsOrderEnvelopeError::InvalidVersion { .. } => { + EventEncodeError::InvalidField("version") + } + } +} + +#[cfg(feature = "serde_json")] +fn map_order_payload_error(error: RadrootsOrderPayloadError) -> EventEncodeError { + match error { + RadrootsOrderPayloadError::EmptyField(field) => EventEncodeError::EmptyRequiredField(field), + RadrootsOrderPayloadError::MissingItems => EventEncodeError::EmptyRequiredField("items"), + RadrootsOrderPayloadError::InvalidItemBinCount { .. } => { + EventEncodeError::InvalidField("items.bin_count") + } + RadrootsOrderPayloadError::MissingEconomicItems => { + EventEncodeError::EmptyRequiredField("economics.items") + } + RadrootsOrderPayloadError::InvalidEconomicItemBinCount { .. } => { + EventEncodeError::InvalidField("economics.items.bin_count") + } + RadrootsOrderPayloadError::InvalidEconomicItemQuantity { .. } => { + EventEncodeError::InvalidField("economics.items.quantity_amount") + } + RadrootsOrderPayloadError::InvalidEconomicItemPrice { .. } => { + EventEncodeError::InvalidField("economics.items.unit_price_amount") + } + RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { .. } => { + EventEncodeError::InvalidField("economics.items.line_subtotal") + } + RadrootsOrderPayloadError::InvalidEconomicLineAmount { field, .. } + | RadrootsOrderPayloadError::InvalidEconomicLineKind { field, .. } + | RadrootsOrderPayloadError::InvalidEconomicLineEffect { field, .. } + | RadrootsOrderPayloadError::InvalidEconomicCurrency { field } + | RadrootsOrderPayloadError::InvalidEconomicOrdering { field } + | RadrootsOrderPayloadError::InvalidEconomicTotal { field } + | RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { field } => { + EventEncodeError::InvalidField(field) + } + RadrootsOrderPayloadError::InvalidQuoteVersion => { + EventEncodeError::InvalidField("economics.quote_version") + } + RadrootsOrderPayloadError::MissingInventoryCommitments => { + EventEncodeError::EmptyRequiredField("inventory_commitments") + } + RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { .. } => { + EventEncodeError::InvalidField("inventory_commitments.bin_count") + } + RadrootsOrderPayloadError::InvalidFulfillmentStatus => { + EventEncodeError::InvalidField("fulfillment.status") + } + RadrootsOrderPayloadError::MissingReceiptIssue => { + EventEncodeError::EmptyRequiredField("receipt.issue") + } + RadrootsOrderPayloadError::UnexpectedReceiptIssue => { + EventEncodeError::InvalidField("receipt.issue") + } + RadrootsOrderPayloadError::InvalidPaymentAmount => { + EventEncodeError::InvalidField("payment.amount") + } + RadrootsOrderPayloadError::MissingSettlementReason => { + EventEncodeError::EmptyRequiredField("settlement.reason") + } + RadrootsOrderPayloadError::UnexpectedSettlementReason => { + EventEncodeError::InvalidField("settlement.reason") + } + } +} + +#[cfg(feature = "serde_json")] +fn order_envelope_event_build<T: serde::Serialize>( + recipient_pubkey: &str, + message_type: RadrootsOrderEventType, + listing_addr: &str, + order_id: &str, + listing_event: Option<&RadrootsNostrEventPtr>, + root_event_id: Option<&str>, + prev_event_id: Option<&str>, + payload: &T, +) -> Result<WireEventParts, EventEncodeError> { + if message_type.requires_listing_snapshot() && listing_event.is_none() { + return Err(EventEncodeError::EmptyRequiredField("listing_event.id")); + } + if message_type.requires_order_chain() { + if root_event_id.is_none() { + return Err(EventEncodeError::EmptyRequiredField("root_event_id")); + } + if prev_event_id.is_none() { + return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); + } + } + + let envelope = RadrootsOrderEnvelope::new(message_type, listing_addr, order_id, payload); + envelope.validate().map_err(map_order_envelope_error)?; + let content = serde_json::to_string(&envelope).map_err(|_| EventEncodeError::Json)?; + let tags = order_envelope_tags( + recipient_pubkey, + listing_addr, + Some(order_id), + listing_event, + root_event_id, + prev_event_id, + )?; + Ok(WireEventParts { + kind: message_type.kind(), + content, + tags, + }) +} + +#[cfg(feature = "serde_json")] +pub fn order_request_event_build( + listing_event: &RadrootsNostrEventPtr, + payload: &RadrootsOrderRequest, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + order_envelope_event_build( + &payload.seller_pubkey, + RadrootsOrderEventType::OrderRequested, + &payload.listing_addr, + &payload.order_id, + Some(listing_event), + None, + None, + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_decision_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderDecision, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + order_envelope_event_build( + &payload.buyer_pubkey, + RadrootsOrderEventType::OrderDecision, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_revision_proposal_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderRevisionProposal, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + if payload.root_event_id != root_event_id { + return Err(EventEncodeError::InvalidField("root_event_id")); + } + if payload.prev_event_id != prev_event_id { + return Err(EventEncodeError::InvalidField("prev_event_id")); + } + order_envelope_event_build( + &payload.buyer_pubkey, + RadrootsOrderEventType::OrderRevisionProposed, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_revision_decision_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderRevisionDecision, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + if payload.root_event_id != root_event_id { + return Err(EventEncodeError::InvalidField("root_event_id")); + } + if payload.prev_event_id != prev_event_id { + return Err(EventEncodeError::InvalidField("prev_event_id")); + } + order_envelope_event_build( + &payload.seller_pubkey, + RadrootsOrderEventType::OrderRevisionDecision, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_fulfillment_update_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderFulfillmentUpdate, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + order_envelope_event_build( + &payload.buyer_pubkey, + RadrootsOrderEventType::FulfillmentUpdated, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_cancellation_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderCancellation, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + order_envelope_event_build( + &payload.seller_pubkey, + RadrootsOrderEventType::OrderCancelled, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_receipt_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderReceipt, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + order_envelope_event_build( + &payload.seller_pubkey, + RadrootsOrderEventType::BuyerReceipt, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_payment_record_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderPaymentRecord, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + if payload.root_event_id != root_event_id { + return Err(EventEncodeError::InvalidField("root_event_id")); + } + if payload.previous_event_id != prev_event_id { + return Err(EventEncodeError::InvalidField("previous_event_id")); + } + order_envelope_event_build( + &payload.seller_pubkey, + RadrootsOrderEventType::PaymentRecorded, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn order_settlement_decision_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderSettlementDecision, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_order_payload_error)?; + if payload.root_event_id != root_event_id { + return Err(EventEncodeError::InvalidField("root_event_id")); + } + if payload.previous_event_id != prev_event_id { + return Err(EventEncodeError::InvalidField("previous_event_id")); + } + order_envelope_event_build( + &payload.buyer_pubkey, + RadrootsOrderEventType::SettlementDecision, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} diff --git a/crates/events_codec/src/order/mod.rs b/crates/events_codec/src/order/mod.rs @@ -0,0 +1,25 @@ +pub mod decode; +pub mod encode; +pub mod tags; + +#[cfg(feature = "serde_json")] +pub use decode::{ + RadrootsOrderEnvelopeParseError, RadrootsOrderEventContext, RadrootsOrderListingAddress, + RadrootsOrderListingAddressError, order_cancellation_from_event, order_decision_from_event, + order_envelope_from_event, order_event_context_from_tags, 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, +}; +#[cfg(feature = "serde_json")] +pub use encode::{ + order_cancellation_event_build, order_decision_event_build, + order_fulfillment_update_event_build, order_payment_record_event_build, + order_receipt_event_build, order_request_event_build, order_revision_decision_event_build, + order_revision_proposal_event_build, order_settlement_decision_event_build, +}; +pub use tags::{ + TAG_LISTING_EVENT, order_envelope_tags, parse_order_counterparty_tag, + parse_order_listing_event_tag, parse_order_prev_tag, parse_order_root_tag, + push_order_chain_tags, validate_order_chain, +}; diff --git a/crates/events_codec/src/order/tags.rs b/crates/events_codec/src/order/tags.rs @@ -0,0 +1,671 @@ +#[cfg(not(feature = "std"))] +use alloc::{borrow::ToOwned, string::String, vec::Vec}; + +use radroots_events::{ + RadrootsNostrEventPtr, + tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, +}; + +use crate::{ + error::{EventEncodeError, EventParseError}, + job::error::JobParseError, +}; + +pub const TAG_LISTING_EVENT: &str = "listing_event"; + +#[inline] +fn push_tag(tags: &mut Vec<Vec<String>>, name: &'static str, value: impl Into<String>) { + let mut tag = Vec::with_capacity(2); + tag.push(name.to_owned()); + tag.push(value.into()); + tags.push(tag); +} + +fn build_event_ptr_tag( + name: &'static str, + ptr: &RadrootsNostrEventPtr, + field_prefix: &'static str, +) -> Result<Vec<String>, EventEncodeError> { + if ptr.id.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField(field_prefix)); + } + let mut tag = Vec::with_capacity(3); + tag.push(name.to_owned()); + tag.push(ptr.id.clone()); + if let Some(relay) = &ptr.relays { + if relay.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("listing_event.relays")); + } + tag.push(relay.clone()); + } + Ok(tag) +} + +fn parse_event_ptr_tag( + tags: &[Vec<String>], + name: &'static str, +) -> Result<Option<RadrootsNostrEventPtr>, EventParseError> { + let Some(tag) = tags + .iter() + .find(|tag| tag.first().map(|value| value.as_str()) == Some(name)) + else { + return Ok(None); + }; + let id = tag.get(1).ok_or(EventParseError::InvalidTag(name))?; + if id.trim().is_empty() { + return Err(EventParseError::InvalidTag(name)); + } + let relay = match tag.get(2) { + Some(value) if value.trim().is_empty() => return Err(EventParseError::InvalidTag(name)), + Some(value) => Some(value.clone()), + None => None, + }; + Ok(Some(RadrootsNostrEventPtr { + id: id.clone(), + relays: relay, + })) +} + +#[inline] +pub fn order_envelope_tags<P, A, D>( + recipient_pubkey: P, + listing_addr: A, + order_id: Option<D>, + listing_event: Option<&RadrootsNostrEventPtr>, + root_event_id: Option<&str>, + prev_event_id: Option<&str>, +) -> Result<Vec<Vec<String>>, EventEncodeError> +where + P: Into<String>, + A: Into<String>, + D: Into<String>, +{ + let recipient_pubkey = recipient_pubkey.into(); + if recipient_pubkey.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("recipient_pubkey")); + } + let listing_addr = listing_addr.into(); + if listing_addr.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("listing_addr")); + } + + let mut capacity = 2 + usize::from(order_id.is_some()) + usize::from(listing_event.is_some()); + capacity += usize::from(root_event_id.is_some()) + usize::from(prev_event_id.is_some()); + let mut tags = Vec::with_capacity(capacity); + push_tag(&mut tags, "p", recipient_pubkey); + push_tag(&mut tags, "a", listing_addr); + if let Some(order_id) = order_id { + let order_id = order_id.into(); + if order_id.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("order_id")); + } + push_tag(&mut tags, TAG_D, order_id); + } + if let Some(listing_event) = listing_event { + tags.push(build_event_ptr_tag( + TAG_LISTING_EVENT, + listing_event, + "listing_event.id", + )?); + } + if let Some(root_event_id) = root_event_id { + if root_event_id.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("root_event_id")); + } + push_tag(&mut tags, TAG_E_ROOT, root_event_id); + } + if let Some(prev_event_id) = prev_event_id { + if prev_event_id.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); + } + push_tag(&mut tags, TAG_E_PREV, prev_event_id); + } + Ok(tags) +} + +#[inline] +pub fn parse_order_counterparty_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> { + let tag = tags + .iter() + .find(|tag| tag.first().map(|value| value.as_str()) == Some("p")) + .ok_or(EventParseError::MissingTag("p"))?; + let value = tag.get(1).ok_or(EventParseError::InvalidTag("p"))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag("p")); + } + Ok(value.clone()) +} + +#[inline] +pub fn parse_order_listing_event_tag( + tags: &[Vec<String>], +) -> Result<Option<RadrootsNostrEventPtr>, EventParseError> { + parse_event_ptr_tag(tags, TAG_LISTING_EVENT) +} + +#[inline] +pub fn parse_order_root_tag(tags: &[Vec<String>]) -> Result<Option<String>, EventParseError> { + let tag = match tags + .iter() + .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_ROOT)) + { + Some(tag) => tag, + None => return Ok(None), + }; + let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_E_ROOT))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag(TAG_E_ROOT)); + } + Ok(Some(value.clone())) +} + +#[inline] +pub fn parse_order_prev_tag(tags: &[Vec<String>]) -> Result<Option<String>, EventParseError> { + let tag = match tags + .iter() + .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_PREV)) + { + Some(tag) => tag, + None => return Ok(None), + }; + let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_E_PREV))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag(TAG_E_PREV)); + } + Ok(Some(value.clone())) +} + +#[inline] +pub fn push_order_chain_tags( + tags: &mut Vec<Vec<String>>, + e_root_id: impl Into<String>, + e_prev_id: Option<impl Into<String>>, + trade_id: Option<impl Into<String>>, +) { + let mut reserve = 1; + if e_prev_id.is_some() { + reserve += 1; + } + if trade_id.is_some() { + reserve += 1; + } + tags.reserve(reserve); + push_tag(tags, TAG_E_ROOT, e_root_id); + if let Some(prev) = e_prev_id { + push_tag(tags, TAG_E_PREV, prev); + } + if let Some(d) = trade_id { + push_tag(tags, TAG_D, d); + } +} + +#[inline] +pub fn validate_order_chain(tags: &[Vec<String>]) -> Result<(), JobParseError> { + let mut has_root = false; + let mut has_d = false; + + for tag in tags { + match tag.as_slice() { + [key, value, ..] if key == TAG_E_ROOT => { + if value.trim().is_empty() { + return Err(JobParseError::InvalidTag(TAG_E_ROOT)); + } + has_root = true; + } + [key] if key == TAG_E_ROOT => return Err(JobParseError::InvalidTag(TAG_E_ROOT)), + [key, value, ..] if key == TAG_D => { + if value.trim().is_empty() { + return Err(JobParseError::InvalidTag(TAG_D)); + } + has_d = true; + } + [key] if key == TAG_D => return Err(JobParseError::InvalidTag(TAG_D)), + _ => {} + } + } + + if !has_root { + Err(JobParseError::MissingChainTag(TAG_E_ROOT)) + } else if !has_d { + Err(JobParseError::MissingChainTag(TAG_D)) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{ + TAG_LISTING_EVENT, order_envelope_tags, parse_order_counterparty_tag, + parse_order_listing_event_tag, parse_order_prev_tag, parse_order_root_tag, + push_order_chain_tags, validate_order_chain, + }; + use crate::{ + error::{EventEncodeError, EventParseError}, + job::error::JobParseError, + }; + use radroots_events::{ + RadrootsNostrEventPtr, + kinds::KIND_LISTING, + tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, + }; + + #[test] + fn order_envelope_tags_build_expected_tags() { + let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); + let tags = order_envelope_tags( + "pubkey", + listing_addr.as_str(), + Some("order-1"), + None, + None, + None, + ) + .expect("trade tags"); + let expected: Vec<Vec<String>> = vec![ + vec![String::from("p"), String::from("pubkey")], + vec![String::from("a"), listing_addr], + vec![String::from(TAG_D), String::from("order-1")], + ]; + assert_eq!(tags, expected); + } + + #[test] + fn order_envelope_tags_include_snapshot_and_chain_refs() { + let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); + let tags = order_envelope_tags( + "buyer", + listing_addr.as_str(), + Some("order-1"), + Some(&RadrootsNostrEventPtr { + id: "listing-snapshot".into(), + relays: Some("wss://relay.example".into()), + }), + Some("root-event"), + Some("prev-event"), + ) + .expect("trade tags"); + assert!(tags.iter().any(|tag| { + tag.as_slice() + == [ + TAG_LISTING_EVENT.to_string(), + "listing-snapshot".to_string(), + "wss://relay.example".to_string(), + ] + })); + assert!( + tags.iter().any(|tag| { + tag.as_slice() == [TAG_E_ROOT.to_string(), "root-event".to_string()] + }) + ); + assert!( + tags.iter().any(|tag| { + tag.as_slice() == [TAG_E_PREV.to_string(), "prev-event".to_string()] + }) + ); + } + + #[test] + fn order_envelope_tags_support_snapshot_without_relay() { + let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); + let tags = order_envelope_tags( + "buyer", + listing_addr.as_str(), + None::<&str>, + Some(&RadrootsNostrEventPtr { + id: "listing-snapshot".into(), + relays: None, + }), + Some("root-event"), + None::<&str>, + ) + .expect("trade tags"); + assert_eq!( + tags, + vec![ + vec![String::from("p"), String::from("buyer")], + vec![String::from("a"), listing_addr], + vec![ + String::from(TAG_LISTING_EVENT), + String::from("listing-snapshot"), + ], + vec![String::from(TAG_E_ROOT), String::from("root-event")], + ] + ); + } + + #[test] + fn order_envelope_tags_accept_str_listing_address() { + let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); + let tags = order_envelope_tags( + "buyer", + listing_addr.as_str(), + Some("order-1"), + None::<&RadrootsNostrEventPtr>, + Some("root-event"), + Some("prev-event"), + ) + .expect("trade tags with str listing address"); + assert_eq!( + tags, + vec![ + vec![String::from("p"), String::from("buyer")], + vec![String::from("a"), listing_addr], + vec![String::from(TAG_D), String::from("order-1")], + vec![String::from(TAG_E_ROOT), String::from("root-event")], + vec![String::from(TAG_E_PREV), String::from("prev-event")], + ] + ); + } + + #[test] + fn order_envelope_tags_accept_str_listing_address_with_snapshot_only() { + let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); + let tags = order_envelope_tags( + "buyer", + listing_addr.as_str(), + None::<&str>, + Some(&RadrootsNostrEventPtr { + id: "listing-snapshot".into(), + relays: None, + }), + None, + None, + ) + .expect("trade tags with str listing address and snapshot only"); + assert_eq!( + tags, + vec![ + vec![String::from("p"), String::from("buyer")], + vec![String::from("a"), listing_addr], + vec![ + String::from(TAG_LISTING_EVENT), + String::from("listing-snapshot"), + ], + ] + ); + } + + #[test] + fn order_envelope_tags_reject_empty_required_fields() { + let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); + + let err = order_envelope_tags(" ", listing_addr.as_str(), None::<&str>, None, None, None) + .expect_err("blank recipient"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("recipient_pubkey") + )); + + let err = order_envelope_tags("buyer", " ", None::<&str>, None, None, None) + .expect_err("blank listing address"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("listing_addr") + )); + + let err = order_envelope_tags("buyer", listing_addr.as_str(), Some(" "), None, None, None) + .expect_err("blank order id"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("order_id") + )); + + let err = order_envelope_tags( + "buyer", + listing_addr.as_str(), + None::<&str>, + Some(&RadrootsNostrEventPtr { + id: " ".into(), + relays: None, + }), + None, + None, + ) + .expect_err("blank listing snapshot id"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("listing_event.id") + )); + + let err = order_envelope_tags( + "buyer", + listing_addr.as_str(), + None::<&str>, + Some(&RadrootsNostrEventPtr { + id: "listing-snapshot".into(), + relays: Some(" ".into()), + }), + None, + None, + ) + .expect_err("blank listing snapshot relay"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("listing_event.relays") + )); + + let err = order_envelope_tags( + "buyer", + listing_addr.as_str(), + None::<&str>, + None, + Some(" "), + None, + ) + .expect_err("blank root event id"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("root_event_id") + )); + + let err = order_envelope_tags( + "buyer", + listing_addr.as_str(), + None::<&str>, + None, + None, + Some(" "), + ) + .expect_err("blank prev event id"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("prev_event_id") + )); + } + + #[test] + fn order_envelope_tag_parsers_cover_public_context() { + let tags = vec![ + vec!["p".into(), "counterparty".into()], + vec![ + TAG_LISTING_EVENT.into(), + "snapshot".into(), + "wss://relay".into(), + ], + vec![TAG_E_ROOT.into(), "root".into()], + vec![TAG_E_PREV.into(), "prev".into()], + ]; + assert_eq!( + parse_order_counterparty_tag(&tags).expect("counterparty"), + "counterparty" + ); + assert_eq!( + parse_order_listing_event_tag(&tags).expect("snapshot"), + Some(RadrootsNostrEventPtr { + id: "snapshot".into(), + relays: Some("wss://relay".into()), + }) + ); + assert_eq!( + parse_order_root_tag(&tags).expect("root"), + Some("root".into()) + ); + assert_eq!( + parse_order_prev_tag(&tags).expect("prev"), + Some("prev".into()) + ); + } + + #[test] + fn order_envelope_tag_parsers_cover_missing_and_invalid_context() { + assert_eq!( + parse_order_listing_event_tag(&[]).expect("no snapshot"), + None + ); + assert_eq!(parse_order_root_tag(&[]).expect("no root"), None); + assert_eq!(parse_order_prev_tag(&[]).expect("no prev"), None); + + assert!(matches!( + parse_order_counterparty_tag(&[]), + Err(EventParseError::MissingTag("p")) + )); + assert!(matches!( + parse_order_counterparty_tag(&[vec![String::from("p")]]), + Err(EventParseError::InvalidTag("p")) + )); + assert!(matches!( + parse_order_counterparty_tag(&[vec![String::from("p"), String::from(" ")]]), + Err(EventParseError::InvalidTag("p")) + )); + + assert!(matches!( + parse_order_listing_event_tag(&[vec![String::from(TAG_LISTING_EVENT)]]), + Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)) + )); + assert!(matches!( + parse_order_listing_event_tag(&[vec![ + String::from(TAG_LISTING_EVENT), + String::from(" "), + ]]), + Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)) + )); + assert!(matches!( + parse_order_listing_event_tag(&[vec![ + String::from(TAG_LISTING_EVENT), + String::from("snapshot"), + String::from(" "), + ]]), + Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)) + )); + assert_eq!( + parse_order_listing_event_tag(&[vec![ + String::from(TAG_LISTING_EVENT), + String::from("snapshot"), + ]]) + .expect("snapshot without relay"), + Some(RadrootsNostrEventPtr { + id: "snapshot".into(), + relays: None, + }) + ); + + assert!(matches!( + parse_order_root_tag(&[vec![String::from(TAG_E_ROOT)]]), + Err(EventParseError::InvalidTag(TAG_E_ROOT)) + )); + assert!(matches!( + parse_order_root_tag(&[vec![String::from(TAG_E_ROOT), String::from(" ")]]), + Err(EventParseError::InvalidTag(TAG_E_ROOT)) + )); + assert!(matches!( + parse_order_prev_tag(&[vec![String::from(TAG_E_PREV)]]), + Err(EventParseError::InvalidTag(TAG_E_PREV)) + )); + assert!(matches!( + parse_order_prev_tag(&[vec![String::from(TAG_E_PREV), String::from(" ")]]), + Err(EventParseError::InvalidTag(TAG_E_PREV)) + )); + } + + #[test] + fn push_order_chain_tags_adds_root_prev_and_trade_id() { + let mut tags = Vec::new(); + push_order_chain_tags(&mut tags, "root", Some("prev"), Some("trade")); + assert_eq!( + tags, + vec![ + vec![String::from(TAG_E_ROOT), String::from("root")], + vec![String::from(TAG_E_PREV), String::from("prev")], + vec![String::from(TAG_D), String::from("trade")], + ] + ); + } + + #[test] + fn push_order_chain_tags_supports_root_only() { + let mut tags = Vec::new(); + push_order_chain_tags(&mut tags, "root", None::<&str>, None::<&str>); + assert_eq!( + tags, + vec![vec![String::from(TAG_E_ROOT), String::from("root")]] + ); + } + + #[test] + fn validate_order_chain_requires_root_and_trade_id() { + let ok = vec![ + vec![String::from(TAG_E_ROOT), String::from("root")], + vec![String::from(TAG_D), String::from("trade")], + ]; + assert!(validate_order_chain(&ok).is_ok()); + let missing = vec![vec![String::from(TAG_D), String::from("trade")]]; + assert!(validate_order_chain(&missing).is_err()); + } + + #[test] + fn validate_order_chain_rejects_invalid_tag_shapes_and_missing_trade_id() { + let root_only = vec![vec![String::from(TAG_E_ROOT), String::from("root")]]; + assert!(matches!( + validate_order_chain(&root_only), + Err(JobParseError::MissingChainTag(TAG_D)) + )); + + let invalid_root_shape = vec![vec![String::from(TAG_E_ROOT)]]; + assert!(matches!( + validate_order_chain(&invalid_root_shape), + Err(JobParseError::InvalidTag(TAG_E_ROOT)) + )); + + let invalid_root_value = vec![ + vec![String::from(TAG_E_ROOT), String::from(" ")], + vec![String::from(TAG_D), String::from("trade")], + ]; + assert!(matches!( + validate_order_chain(&invalid_root_value), + Err(JobParseError::InvalidTag(TAG_E_ROOT)) + )); + + let invalid_trade_shape = vec![ + vec![String::from(TAG_E_ROOT), String::from("root")], + vec![String::from(TAG_D)], + ]; + assert!(matches!( + validate_order_chain(&invalid_trade_shape), + Err(JobParseError::InvalidTag(TAG_D)) + )); + + let invalid_trade_value = vec![ + vec![String::from(TAG_E_ROOT), String::from("root")], + vec![String::from(TAG_D), String::from(" ")], + ]; + assert!(matches!( + validate_order_chain(&invalid_trade_value), + Err(JobParseError::InvalidTag(TAG_D)) + )); + + let with_unrelated_tag = vec![ + vec![String::from("x"), String::from("ignored")], + vec![String::from(TAG_E_ROOT), String::from("root")], + vec![String::from(TAG_D), String::from("trade")], + ]; + assert!(validate_order_chain(&with_unrelated_tag).is_ok()); + + let with_singleton_unrelated_tag = vec![ + vec![String::from("x")], + vec![String::from(TAG_E_ROOT), String::from("root")], + vec![String::from(TAG_D), String::from("trade")], + ]; + assert!(validate_order_chain(&with_singleton_unrelated_tag).is_ok()); + } +} diff --git a/crates/events_codec/src/trade/decode.rs b/crates/events_codec/src/trade/decode.rs @@ -1,1932 +0,0 @@ -#[cfg(all(not(feature = "std"), feature = "serde_json"))] -use alloc::{borrow::ToOwned, format, string::String, vec::Vec}; - -#[cfg(feature = "serde_json")] -use radroots_events::{ - RadrootsNostrEvent, RadrootsNostrEventPtr, - kinds::{KIND_PROFILE, is_active_trade_public_kind, is_trade_kind}, - tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, - trade::{ - RadrootsActiveTradeEnvelope, RadrootsActiveTradeEnvelopeError, - RadrootsActiveTradeMessageType, RadrootsActiveTradePayloadError, RadrootsTradeBuyerReceipt, - RadrootsTradeEnvelope, RadrootsTradeEnvelopeError, RadrootsTradeFulfillmentUpdated, - RadrootsTradeMessageType, RadrootsTradeOrderCancelled, RadrootsTradeOrderDecisionEvent, - RadrootsTradeOrderRequested, RadrootsTradeOrderRevisionDecisionEvent, - RadrootsTradeOrderRevisionProposed, RadrootsTradePaymentRecorded, - RadrootsTradeSettlementDecisionEvent, - }, -}; -#[cfg(feature = "serde_json")] -use serde::de::DeserializeOwned; - -#[cfg(feature = "serde_json")] -use crate::d_tag::is_d_tag_base64url; -#[cfg(feature = "serde_json")] -use crate::trade::tags::{ - TAG_LISTING_EVENT, parse_trade_counterparty_tag, parse_trade_listing_event_tag, - parse_trade_prev_tag, parse_trade_root_tag, -}; - -#[cfg(feature = "serde_json")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeEnvelopeParseError { - InvalidKind(u32), - InvalidJson, - InvalidEnvelope(RadrootsTradeEnvelopeError), - MessageTypeKindMismatch { - event_kind: u32, - message_type: RadrootsTradeMessageType, - }, - MissingTag(&'static str), - InvalidTag(&'static str), - ListingAddrTagMismatch, - OrderIdTagMismatch, - InvalidListingAddr(RadrootsTradeListingAddressError), -} - -#[cfg(feature = "serde_json")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsActiveTradeEnvelopeParseError { - InvalidKind(u32), - InvalidJson, - InvalidEnvelope(RadrootsActiveTradeEnvelopeError), - InvalidPayload(RadrootsActiveTradePayloadError), - MessageTypeKindMismatch { - event_kind: u32, - message_type: RadrootsActiveTradeMessageType, - }, - MissingTag(&'static str), - InvalidTag(&'static str), - ListingAddrTagMismatch, - OrderIdTagMismatch, - PayloadBindingMismatch(&'static str), - AuthorMismatch, - CounterpartyTagMismatch, - InvalidListingAddr(RadrootsTradeListingAddressError), -} - -#[cfg(feature = "serde_json")] -impl core::fmt::Display for RadrootsTradeEnvelopeParseError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidKind(kind) => write!(f, "invalid trade event kind: {kind}"), - Self::InvalidJson => write!(f, "invalid trade envelope json"), - Self::InvalidEnvelope(error) => write!(f, "{error}"), - Self::MessageTypeKindMismatch { - event_kind, - message_type, - } => write!( - f, - "trade envelope type {message_type:?} does not match event kind {event_kind}" - ), - Self::MissingTag(tag) => write!(f, "missing required trade tag: {tag}"), - Self::InvalidTag(tag) => write!(f, "invalid trade tag: {tag}"), - Self::ListingAddrTagMismatch => { - write!(f, "trade listing address tag does not match envelope") - } - Self::OrderIdTagMismatch => write!(f, "trade order id tag does not match envelope"), - Self::InvalidListingAddr(error) => write!(f, "{error}"), - } - } -} - -#[cfg(feature = "serde_json")] -impl core::fmt::Display for RadrootsActiveTradeEnvelopeParseError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidKind(kind) => write!(f, "invalid active trade event kind: {kind}"), - Self::InvalidJson => write!(f, "invalid active trade envelope json"), - Self::InvalidEnvelope(error) => write!(f, "{error}"), - Self::InvalidPayload(error) => write!(f, "{error}"), - Self::MessageTypeKindMismatch { - event_kind, - message_type, - } => write!( - f, - "active trade envelope type {message_type:?} does not match event kind {event_kind}" - ), - Self::MissingTag(tag) => write!(f, "missing required active trade tag: {tag}"), - Self::InvalidTag(tag) => write!(f, "invalid active trade tag: {tag}"), - Self::ListingAddrTagMismatch => { - write!( - f, - "active trade listing address tag does not match envelope" - ) - } - Self::OrderIdTagMismatch => { - write!(f, "active trade order id tag does not match envelope") - } - Self::PayloadBindingMismatch(field) => { - write!(f, "active trade payload {field} does not match envelope") - } - Self::AuthorMismatch => write!(f, "active trade event author does not match payload"), - Self::CounterpartyTagMismatch => { - write!(f, "active trade counterparty tag does not match payload") - } - Self::InvalidListingAddr(error) => write!(f, "{error}"), - } - } -} - -#[cfg(all(feature = "std", feature = "serde_json"))] -impl std::error::Error for RadrootsTradeEnvelopeParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::InvalidEnvelope(error) => Some(error), - Self::InvalidListingAddr(error) => Some(error), - _ => None, - } - } -} - -#[cfg(all(feature = "std", feature = "serde_json"))] -impl std::error::Error for RadrootsActiveTradeEnvelopeParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::InvalidEnvelope(error) => Some(error), - Self::InvalidPayload(error) => Some(error), - Self::InvalidListingAddr(error) => Some(error), - _ => None, - } - } -} - -#[cfg(feature = "serde_json")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeEventContext { - pub counterparty_pubkey: String, - pub listing_event: Option<RadrootsNostrEventPtr>, - pub root_event_id: Option<String>, - pub prev_event_id: Option<String>, -} - -#[cfg(feature = "serde_json")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeListingAddress { - pub kind: u32, - pub seller_pubkey: String, - pub listing_id: String, -} - -#[cfg(feature = "serde_json")] -impl RadrootsTradeListingAddress { - pub fn parse(addr: &str) -> Result<Self, RadrootsTradeListingAddressError> { - let (kind_raw, seller_and_listing) = addr - .split_once(':') - .ok_or(RadrootsTradeListingAddressError::InvalidFormat)?; - let (seller_pubkey_raw, listing_id_raw) = seller_and_listing - .split_once(':') - .ok_or(RadrootsTradeListingAddressError::InvalidFormat)?; - if listing_id_raw.contains(':') { - return Err(RadrootsTradeListingAddressError::InvalidFormat); - } - let kind = kind_raw - .parse::<u32>() - .map_err(|_| RadrootsTradeListingAddressError::InvalidFormat)?; - let seller_pubkey = seller_pubkey_raw.to_owned(); - let listing_id = listing_id_raw.to_owned(); - if kind == KIND_PROFILE - || seller_pubkey.trim().is_empty() - || listing_id.trim().is_empty() - || !is_d_tag_base64url(&listing_id) - { - return Err(RadrootsTradeListingAddressError::InvalidFormat); - } - Ok(Self { - kind, - seller_pubkey, - listing_id, - }) - } - - #[inline] - pub fn as_str(&self) -> String { - format!("{}:{}:{}", self.kind, self.seller_pubkey, self.listing_id) - } -} - -#[cfg(feature = "serde_json")] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsTradeListingAddressError { - InvalidFormat, -} - -#[cfg(feature = "serde_json")] -impl core::fmt::Display for RadrootsTradeListingAddressError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::InvalidFormat => write!(f, "invalid listing address format"), - } - } -} - -#[cfg(all(feature = "std", feature = "serde_json"))] -impl std::error::Error for RadrootsTradeListingAddressError {} - -#[cfg(feature = "serde_json")] -fn required_tag_value<'a>( - tags: &'a [Vec<String>], - key: &'static str, -) -> Result<&'a str, RadrootsTradeEnvelopeParseError> { - let tag = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) - .ok_or(RadrootsTradeEnvelopeParseError::MissingTag(key))?; - let value = tag - .get(1) - .map(|value| value.as_str()) - .ok_or(RadrootsTradeEnvelopeParseError::InvalidTag(key))?; - if value.trim().is_empty() { - return Err(RadrootsTradeEnvelopeParseError::InvalidTag(key)); - } - Ok(value) -} - -#[cfg(feature = "serde_json")] -pub fn trade_envelope_from_event<T: DeserializeOwned>( - event: &RadrootsNostrEvent, -) -> Result<RadrootsTradeEnvelope<T>, RadrootsTradeEnvelopeParseError> { - if !is_trade_kind(event.kind) { - return Err(RadrootsTradeEnvelopeParseError::InvalidKind(event.kind)); - } - let envelope = serde_json::from_str::<RadrootsTradeEnvelope<T>>(&event.content) - .map_err(|_| RadrootsTradeEnvelopeParseError::InvalidJson)?; - envelope - .validate() - .map_err(RadrootsTradeEnvelopeParseError::InvalidEnvelope)?; - if envelope.message_type.kind() != event.kind { - return Err(RadrootsTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }); - } - - let listing_addr = required_tag_value(&event.tags, "a")?; - if envelope.listing_addr != listing_addr { - return Err(RadrootsTradeEnvelopeParseError::ListingAddrTagMismatch); - } - RadrootsTradeListingAddress::parse(&envelope.listing_addr) - .map_err(RadrootsTradeEnvelopeParseError::InvalidListingAddr)?; - - if let Some(order_id) = envelope.order_id.as_deref() { - let tag_order_id = required_tag_value(&event.tags, TAG_D)?; - if tag_order_id != order_id { - return Err(RadrootsTradeEnvelopeParseError::OrderIdTagMismatch); - } - } - - let message_type = envelope.message_type; - trade_event_context_from_tags(message_type, &event.tags)?; - - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_envelope_from_event<T: DeserializeOwned>( - event: &RadrootsNostrEvent, -) -> Result<RadrootsActiveTradeEnvelope<T>, RadrootsActiveTradeEnvelopeParseError> { - if !is_active_trade_public_kind(event.kind) { - return Err(RadrootsActiveTradeEnvelopeParseError::InvalidKind( - event.kind, - )); - } - let envelope = serde_json::from_str::<RadrootsActiveTradeEnvelope<T>>(&event.content) - .map_err(|_| RadrootsActiveTradeEnvelopeParseError::InvalidJson)?; - envelope - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidEnvelope)?; - if envelope.message_type.kind() != event.kind { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - - let listing_addr = required_active_tag_value(&event.tags, "a")?; - if envelope.listing_addr != listing_addr { - return Err(RadrootsActiveTradeEnvelopeParseError::ListingAddrTagMismatch); - } - RadrootsTradeListingAddress::parse(&envelope.listing_addr) - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidListingAddr)?; - - let tag_order_id = required_active_tag_value(&event.tags, TAG_D)?; - if tag_order_id != envelope.order_id { - return Err(RadrootsActiveTradeEnvelopeParseError::OrderIdTagMismatch); - } - - active_trade_event_context_from_tags(envelope.message_type, &event.tags)?; - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_request_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderRequested>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradeOrderRequested>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderRequested { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.buyer_pubkey, - &envelope.payload.seller_pubkey, - )?; - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_decision_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderDecisionEvent>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradeOrderDecisionEvent>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderDecision { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.seller_pubkey, - &envelope.payload.buyer_pubkey, - )?; - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_revision_proposal_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionProposed>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradeOrderRevisionProposed>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderRevisionProposed { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.seller_pubkey, - &envelope.payload.buyer_pubkey, - )?; - let context = active_trade_event_context_from_tags(envelope.message_type, &event.tags)?; - if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("root_event_id")); - } - if context.prev_event_id.as_deref() != Some(envelope.payload.prev_event_id.as_str()) { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("prev_event_id")); - } - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_revision_decision_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionDecisionEvent>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = - active_trade_envelope_from_event::<RadrootsTradeOrderRevisionDecisionEvent>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderRevisionDecision { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.buyer_pubkey, - &envelope.payload.seller_pubkey, - )?; - let context = active_trade_event_context_from_tags(envelope.message_type, &event.tags)?; - if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("root_event_id")); - } - if context.prev_event_id.as_deref() != Some(envelope.payload.prev_event_id.as_str()) { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("prev_event_id")); - } - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_fulfillment_update_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeFulfillmentUpdated>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradeFulfillmentUpdated>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeFulfillmentUpdated { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.seller_pubkey, - &envelope.payload.buyer_pubkey, - )?; - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_cancel_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderCancelled>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradeOrderCancelled>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderCancelled { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.buyer_pubkey, - &envelope.payload.seller_pubkey, - )?; - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_buyer_receipt_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeBuyerReceipt>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradeBuyerReceipt>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeBuyerReceipt { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.buyer_pubkey, - &envelope.payload.seller_pubkey, - )?; - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_payment_recorded_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradePaymentRecorded>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradePaymentRecorded>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradePaymentRecorded { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.buyer_pubkey, - &envelope.payload.seller_pubkey, - )?; - let context = active_trade_event_context_from_tags(envelope.message_type, &event.tags)?; - if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("root_event_id")); - } - if context.prev_event_id.as_deref() != Some(envelope.payload.previous_event_id.as_str()) { - return Err( - RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("previous_event_id"), - ); - } - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_settlement_decision_from_event( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeSettlementDecisionEvent>, - RadrootsActiveTradeEnvelopeParseError, -> { - let envelope = active_trade_envelope_from_event::<RadrootsTradeSettlementDecisionEvent>(event)?; - if envelope.message_type != RadrootsActiveTradeMessageType::TradeSettlementDecision { - return Err( - RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }, - ); - } - envelope - .payload - .validate() - .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; - validate_active_order_binding( - event, - &envelope, - &envelope.payload.order_id, - &envelope.payload.listing_addr, - &envelope.payload.seller_pubkey, - &envelope.payload.buyer_pubkey, - )?; - let context = active_trade_event_context_from_tags(envelope.message_type, &event.tags)?; - if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("root_event_id")); - } - if context.prev_event_id.as_deref() != Some(envelope.payload.previous_event_id.as_str()) { - return Err( - RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("previous_event_id"), - ); - } - Ok(envelope) -} - -#[cfg(feature = "serde_json")] -pub fn trade_event_context_from_tags( - message_type: RadrootsTradeMessageType, - tags: &[Vec<String>], -) -> Result<RadrootsTradeEventContext, RadrootsTradeEnvelopeParseError> { - let counterparty_pubkey = - parse_trade_counterparty_tag(tags).map_err(map_tag_parse_error_for_trade_envelope)?; - let listing_event = - parse_trade_listing_event_tag(tags).map_err(map_tag_parse_error_for_trade_envelope)?; - let root_event_id = - parse_trade_root_tag(tags).map_err(map_tag_parse_error_for_trade_envelope)?; - let prev_event_id = - parse_trade_prev_tag(tags).map_err(map_tag_parse_error_for_trade_envelope)?; - - if message_type.requires_listing_snapshot() && listing_event.is_none() { - return Err(RadrootsTradeEnvelopeParseError::MissingTag( - TAG_LISTING_EVENT, - )); - } - if message_type.requires_trade_chain() { - if root_event_id.is_none() { - return Err(RadrootsTradeEnvelopeParseError::MissingTag(TAG_E_ROOT)); - } - if prev_event_id.is_none() { - return Err(RadrootsTradeEnvelopeParseError::MissingTag(TAG_E_PREV)); - } - } - - Ok(RadrootsTradeEventContext { - counterparty_pubkey, - listing_event, - root_event_id, - prev_event_id, - }) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_event_context_from_tags( - message_type: RadrootsActiveTradeMessageType, - tags: &[Vec<String>], -) -> Result<RadrootsTradeEventContext, RadrootsActiveTradeEnvelopeParseError> { - let counterparty_pubkey = parse_trade_counterparty_tag(tags) - .map_err(map_tag_parse_error_for_active_trade_envelope)?; - let listing_event = parse_trade_listing_event_tag(tags) - .map_err(map_tag_parse_error_for_active_trade_envelope)?; - let root_event_id = - parse_trade_root_tag(tags).map_err(map_tag_parse_error_for_active_trade_envelope)?; - let prev_event_id = - parse_trade_prev_tag(tags).map_err(map_tag_parse_error_for_active_trade_envelope)?; - - if message_type.requires_listing_snapshot() && listing_event.is_none() { - return Err(RadrootsActiveTradeEnvelopeParseError::MissingTag( - TAG_LISTING_EVENT, - )); - } - if message_type.requires_trade_chain() { - if root_event_id.is_none() { - return Err(RadrootsActiveTradeEnvelopeParseError::MissingTag( - TAG_E_ROOT, - )); - } - if prev_event_id.is_none() { - return Err(RadrootsActiveTradeEnvelopeParseError::MissingTag( - TAG_E_PREV, - )); - } - } - - Ok(RadrootsTradeEventContext { - counterparty_pubkey, - listing_event, - root_event_id, - prev_event_id, - }) -} - -#[cfg(feature = "serde_json")] -fn map_tag_parse_error_for_trade_envelope( - error: crate::error::EventParseError, -) -> RadrootsTradeEnvelopeParseError { - match error { - crate::error::EventParseError::MissingTag(tag) => { - RadrootsTradeEnvelopeParseError::MissingTag(tag) - } - crate::error::EventParseError::InvalidTag(tag) => { - RadrootsTradeEnvelopeParseError::InvalidTag(tag) - } - crate::error::EventParseError::InvalidKind { expected: _, got } => { - RadrootsTradeEnvelopeParseError::InvalidKind(got) - } - crate::error::EventParseError::InvalidNumber(tag, _) - | crate::error::EventParseError::InvalidJson(tag) => { - RadrootsTradeEnvelopeParseError::InvalidTag(tag) - } - } -} - -#[cfg(feature = "serde_json")] -fn required_active_tag_value<'a>( - tags: &'a [Vec<String>], - key: &'static str, -) -> Result<&'a str, RadrootsActiveTradeEnvelopeParseError> { - let tag = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) - .ok_or(RadrootsActiveTradeEnvelopeParseError::MissingTag(key))?; - let value = tag - .get(1) - .map(|value| value.as_str()) - .ok_or(RadrootsActiveTradeEnvelopeParseError::InvalidTag(key))?; - if value.trim().is_empty() { - return Err(RadrootsActiveTradeEnvelopeParseError::InvalidTag(key)); - } - Ok(value) -} - -#[cfg(feature = "serde_json")] -fn map_tag_parse_error_for_active_trade_envelope( - error: crate::error::EventParseError, -) -> RadrootsActiveTradeEnvelopeParseError { - match error { - crate::error::EventParseError::MissingTag(tag) => { - RadrootsActiveTradeEnvelopeParseError::MissingTag(tag) - } - crate::error::EventParseError::InvalidTag(tag) => { - RadrootsActiveTradeEnvelopeParseError::InvalidTag(tag) - } - crate::error::EventParseError::InvalidKind { expected: _, got } => { - RadrootsActiveTradeEnvelopeParseError::InvalidKind(got) - } - crate::error::EventParseError::InvalidNumber(tag, _) - | crate::error::EventParseError::InvalidJson(tag) => { - RadrootsActiveTradeEnvelopeParseError::InvalidTag(tag) - } - } -} - -#[cfg(feature = "serde_json")] -fn validate_active_order_binding<T>( - event: &RadrootsNostrEvent, - envelope: &RadrootsActiveTradeEnvelope<T>, - payload_order_id: &str, - payload_listing_addr: &str, - expected_author: &str, - expected_counterparty: &str, -) -> Result<(), RadrootsActiveTradeEnvelopeParseError> { - if envelope.order_id != payload_order_id { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("order_id")); - } - if envelope.listing_addr != payload_listing_addr { - return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("listing_addr")); - } - if event.author != expected_author { - return Err(RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); - } - let counterparty = parse_trade_counterparty_tag(&event.tags) - .map_err(map_tag_parse_error_for_active_trade_envelope)?; - if counterparty != expected_counterparty { - return Err(RadrootsActiveTradeEnvelopeParseError::CounterpartyTagMismatch); - } - Ok(()) -} - -#[cfg(all(test, feature = "serde_json"))] -mod tests { - use super::{ - RadrootsActiveTradeEnvelopeParseError, RadrootsTradeEnvelopeParseError, - RadrootsTradeListingAddress, active_trade_buyer_receipt_from_event, - active_trade_envelope_from_event, active_trade_fulfillment_update_from_event, - active_trade_order_cancel_from_event, active_trade_order_decision_from_event, - active_trade_order_request_from_event, active_trade_order_revision_decision_from_event, - active_trade_order_revision_proposal_from_event, active_trade_payment_recorded_from_event, - active_trade_settlement_decision_from_event, trade_envelope_from_event, - trade_event_context_from_tags, - }; - use crate::trade::encode::{ - active_trade_buyer_receipt_event_build, active_trade_fulfillment_update_event_build, - active_trade_order_cancel_event_build, active_trade_order_decision_event_build, - active_trade_order_request_event_build, active_trade_order_revision_decision_event_build, - active_trade_order_revision_proposal_event_build, - active_trade_payment_recorded_event_build, active_trade_settlement_decision_event_build, - trade_envelope_event_build, - }; - use crate::trade::tags::TAG_LISTING_EVENT; - use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, - }; - use radroots_events::{ - RadrootsNostrEvent, RadrootsNostrEventPtr, - kinds::{ - KIND_TRADE_CANCEL, KIND_TRADE_FULFILLMENT_UPDATE, KIND_TRADE_ORDER_DECISION, - KIND_TRADE_ORDER_REQUEST, KIND_TRADE_ORDER_REVISION, - KIND_TRADE_ORDER_REVISION_RESPONSE, KIND_TRADE_PAYMENT_RECORDED, KIND_TRADE_RECEIPT, - KIND_TRADE_SETTLEMENT_DECISION, - }, - tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, - trade::{ - RadrootsActiveTradeEnvelope, RadrootsActiveTradeFulfillmentState, - RadrootsActiveTradeMessageType, RadrootsActiveTradePayloadError, - RadrootsTradeBuyerReceipt, RadrootsTradeEnvelope, RadrootsTradeFulfillmentUpdated, - RadrootsTradeInventoryCommitment, RadrootsTradeMessagePayload, - RadrootsTradeMessageType, RadrootsTradeOrderCancelled, RadrootsTradeOrderDecision, - RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, - RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, - RadrootsTradeOrderRequested, RadrootsTradeOrderRevisionDecision, - RadrootsTradeOrderRevisionDecisionEvent, RadrootsTradeOrderRevisionProposed, - RadrootsTradePaymentMethod, RadrootsTradePaymentRecorded, RadrootsTradePricingBasis, - RadrootsTradeSettlementDecision, RadrootsTradeSettlementDecisionEvent, - }, - }; - - fn active_order_request() -> RadrootsTradeOrderRequested { - RadrootsTradeOrderRequested { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - items: vec![RadrootsTradeOrderItem { - bin_id: "lb".into(), - bin_count: 3, - }], - economics: request_economics(), - } - } - - fn generic_order_response() -> RadrootsTradeMessagePayload { - RadrootsTradeMessagePayload::OrderResponse( - radroots_events::trade::RadrootsTradeOrderResponse { - accepted: true, - reason: None, - }, - ) - } - - fn decimal(raw: &str) -> RadrootsCoreDecimal { - raw.parse().unwrap() - } - - fn usd(raw: &str) -> RadrootsCoreMoney { - RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) - } - - fn request_economics() -> RadrootsTradeOrderEconomics { - RadrootsTradeOrderEconomics { - quote_id: "quote-1".into(), - quote_version: 1, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsTradeOrderEconomicItem { - bin_id: "lb".into(), - bin_count: 3, - quantity_amount: decimal("1"), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: decimal("5"), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: usd("15"), - }], - discounts: Vec::<RadrootsTradeOrderEconomicLine>::new(), - adjustments: Vec::<RadrootsTradeOrderEconomicLine>::new(), - subtotal: usd("15"), - discount_total: usd("0"), - adjustment_total: usd("0"), - total: usd("15"), - } - } - - fn active_order_decision() -> RadrootsTradeOrderDecisionEvent { - RadrootsTradeOrderDecisionEvent { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - decision: RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { - bin_id: "lb".into(), - bin_count: 3, - }], - }, - } - } - - fn active_order_revision_proposal() -> RadrootsTradeOrderRevisionProposed { - let mut economics = request_economics(); - economics.quote_id = "revision-quote-1".into(); - economics.quote_version = 2; - economics.items[0].bin_count = 4; - economics.items[0].line_subtotal = usd("20"); - economics.subtotal = usd("20"); - economics.total = usd("20"); - economics.canonicalize(); - RadrootsTradeOrderRevisionProposed { - revision_id: "rev-1".into(), - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - root_event_id: "root-event".into(), - prev_event_id: "decision-event".into(), - items: vec![RadrootsTradeOrderItem { - bin_id: "lb".into(), - bin_count: 4, - }], - economics, - reason: "update count".into(), - } - } - - fn active_order_revision_decision( - decision: RadrootsTradeOrderRevisionDecision, - ) -> RadrootsTradeOrderRevisionDecisionEvent { - RadrootsTradeOrderRevisionDecisionEvent { - revision_id: "rev-1".into(), - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - root_event_id: "root-event".into(), - prev_event_id: "revision-event".into(), - decision, - } - } - - fn active_fulfillment_update() -> RadrootsTradeFulfillmentUpdated { - RadrootsTradeFulfillmentUpdated { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - status: RadrootsActiveTradeFulfillmentState::ReadyForPickup, - } - } - - fn active_order_cancelled() -> RadrootsTradeOrderCancelled { - RadrootsTradeOrderCancelled { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - reason: "changed plans".into(), - } - } - - fn active_buyer_receipt(received: bool) -> RadrootsTradeBuyerReceipt { - RadrootsTradeBuyerReceipt { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - received, - issue: (!received).then(|| "damaged items".into()), - received_at: 1_777_665_600, - } - } - - fn active_payment_recorded() -> RadrootsTradePaymentRecorded { - RadrootsTradePaymentRecorded { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer".into(), - seller_pubkey: "seller".into(), - root_event_id: "root-event".into(), - previous_event_id: "agreement-event".into(), - agreement_event_id: "agreement-event".into(), - quote_id: "quote-1".into(), - quote_version: 1, - economics_digest: "digest-1".into(), - amount: decimal("15"), - currency: RadrootsCoreCurrency::USD, - method: RadrootsTradePaymentMethod::Cash, - reference: Some("cash drawer".into()), - paid_at: Some(1_777_665_600), - } - } - - fn active_settlement_decision( - decision: RadrootsTradeSettlementDecision, - ) -> RadrootsTradeSettlementDecisionEvent { - RadrootsTradeSettlementDecisionEvent { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), - seller_pubkey: "seller".into(), - buyer_pubkey: "buyer".into(), - root_event_id: "root-event".into(), - previous_event_id: "payment-event".into(), - agreement_event_id: "agreement-event".into(), - payment_event_id: "payment-event".into(), - quote_id: "quote-1".into(), - quote_version: 1, - economics_digest: "digest-1".into(), - amount: decimal("15"), - currency: RadrootsCoreCurrency::USD, - decision, - reason: (decision == RadrootsTradeSettlementDecision::Rejected) - .then(|| "reference mismatch".into()), - } - } - - fn listing_event_ptr() -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: "listing-snapshot".into(), - relays: Some("wss://relay.example.com".into()), - } - } - - #[test] - fn listing_address_roundtrips() { - let addr = RadrootsTradeListingAddress::parse("30402:seller:AAAAAAAAAAAAAAAAAAAAAg") - .expect("parse listing address"); - assert_eq!(addr.as_str(), "30402:seller:AAAAAAAAAAAAAAAAAAAAAg"); - } - - #[test] - fn parse_generic_order_response_roundtrip() { - let payload = generic_order_response(); - let built = trade_envelope_event_build( - "buyer", - RadrootsTradeMessageType::OrderResponse, - "30402:seller:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1".into()), - None, - Some("root"), - Some("prev"), - &payload, - ) - .expect("build trade envelope"); - let event = RadrootsNostrEvent { - id: "id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope: RadrootsTradeEnvelope<RadrootsTradeMessagePayload> = - trade_envelope_from_event(&event).expect("parse trade envelope"); - assert_eq!( - envelope.message_type, - RadrootsTradeMessageType::OrderResponse - ); - assert_eq!(envelope.order_id.as_deref(), Some("order-1")); - } - - #[test] - fn active_order_request_builder_emits_canonical_shape() { - let payload = active_order_request(); - let built = active_trade_order_request_event_build(&listing_event_ptr(), &payload).unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderRequested> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_ORDER_REQUEST); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderRequested - ); - assert_eq!(envelope.order_id, "order-1"); - assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); - assert_eq!( - built.tags[1], - vec![ - "a".to_string(), - "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".to_string() - ] - ); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert_eq!(envelope.payload.economics.quote_id, "quote-1"); - assert_eq!(envelope.payload.economics.total, usd("15")); - assert!( - built - .tags - .iter() - .any(|tag| tag.first().map(String::as_str) == Some(TAG_LISTING_EVENT)) - ); - assert!( - !built - .tags - .iter() - .any(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) - ); - } - - #[test] - fn active_order_decision_builder_emits_canonical_chain_shape() { - let payload = active_order_decision(); - let built = - active_trade_order_decision_event_build("root-event", "prev-event", &payload).unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderDecisionEvent> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_ORDER_DECISION); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderDecision - ); - assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) - ); - } - - #[test] - fn active_order_revision_proposal_builder_emits_canonical_chain_shape() { - let payload = active_order_revision_proposal(); - let built = active_trade_order_revision_proposal_event_build( - payload.root_event_id.as_str(), - payload.prev_event_id.as_str(), - &payload, - ) - .unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionProposed> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_ORDER_REVISION); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderRevisionProposed - ); - assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert_eq!(envelope.payload.revision_id, "rev-1"); - assert_eq!(envelope.payload.economics.quote_version, 2); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "decision-event".to_string()]) - ); - } - - #[test] - fn active_order_revision_decision_builder_emits_canonical_chain_shape() { - let payload = active_order_revision_decision(RadrootsTradeOrderRevisionDecision::Accepted); - let built = active_trade_order_revision_decision_event_build( - payload.root_event_id.as_str(), - payload.prev_event_id.as_str(), - &payload, - ) - .unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionDecisionEvent> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_ORDER_REVISION_RESPONSE); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderRevisionDecision - ); - assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert_eq!(envelope.payload.revision_id, "rev-1"); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "revision-event".to_string()]) - ); - } - - #[test] - fn active_fulfillment_update_builder_emits_canonical_chain_shape() { - let payload = active_fulfillment_update(); - let built = - active_trade_fulfillment_update_event_build("root-event", "prev-event", &payload) - .unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeFulfillmentUpdated> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_FULFILLMENT_UPDATE); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeFulfillmentUpdated - ); - assert_eq!(envelope.payload.status, payload.status); - assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) - ); - } - - #[test] - fn active_order_cancel_builder_emits_canonical_buyer_chain_shape() { - let payload = active_order_cancelled(); - let built = - active_trade_order_cancel_event_build("root-event", "prev-event", &payload).unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderCancelled> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_CANCEL); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderCancelled - ); - assert_eq!(envelope.payload.reason, payload.reason); - assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) - ); - } - - #[test] - fn active_buyer_receipt_builder_emits_canonical_buyer_chain_shape() { - let payload = active_buyer_receipt(false); - let built = - active_trade_buyer_receipt_event_build("root-event", "prev-event", &payload).unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeBuyerReceipt> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_RECEIPT); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeBuyerReceipt - ); - assert_eq!(envelope.payload.received, false); - assert_eq!(envelope.payload.issue.as_deref(), Some("damaged items")); - assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "prev-event".to_string()]) - ); - } - - #[test] - fn active_payment_recorded_builder_emits_canonical_buyer_chain_shape() { - let payload = active_payment_recorded(); - let built = active_trade_payment_recorded_event_build( - payload.root_event_id.as_str(), - payload.previous_event_id.as_str(), - &payload, - ) - .unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradePaymentRecorded> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_PAYMENT_RECORDED); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradePaymentRecorded - ); - assert_eq!(envelope.payload.amount, decimal("15")); - assert_eq!(envelope.payload.method, RadrootsTradePaymentMethod::Cash); - assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "agreement-event".to_string()]) - ); - } - - #[test] - fn active_settlement_decision_builder_emits_canonical_seller_chain_shape() { - let payload = active_settlement_decision(RadrootsTradeSettlementDecision::Accepted); - let built = active_trade_settlement_decision_event_build( - payload.root_event_id.as_str(), - payload.previous_event_id.as_str(), - &payload, - ) - .unwrap(); - let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeSettlementDecisionEvent> = - serde_json::from_str(&built.content).unwrap(); - - assert_eq!(built.kind, KIND_TRADE_SETTLEMENT_DECISION); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeSettlementDecision - ); - assert_eq!( - envelope.payload.decision, - RadrootsTradeSettlementDecision::Accepted - ); - assert_eq!(envelope.payload.reason, None); - assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); - assert_eq!( - built.tags[2], - vec![TAG_D.to_string(), "order-1".to_string()] - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) - ); - assert!( - built - .tags - .iter() - .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "payment-event".to_string()]) - ); - } - - #[test] - fn active_order_request_parse_roundtrips_and_validates_tags() { - let payload = active_order_request(); - let built = active_trade_order_request_event_build(&listing_event_ptr(), &payload).unwrap(); - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_order_request_from_event(&event).unwrap(); - - assert_eq!(envelope.payload, payload); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderRequested - ); - } - - #[test] - fn active_order_request_parse_rejects_mismatched_economics() { - let mut payload = active_order_request(); - let built = active_trade_order_request_event_build(&listing_event_ptr(), &payload).unwrap(); - payload.economics.items[0].bin_id = "other-bin".into(); - let envelope = RadrootsActiveTradeEnvelope::new( - RadrootsActiveTradeMessageType::TradeOrderRequested, - payload.listing_addr.clone(), - payload.order_id.clone(), - payload, - ); - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: serde_json::to_string(&envelope).unwrap(), - sig: "sig".into(), - }; - let err = active_trade_order_request_from_event(&event).unwrap_err(); - assert_eq!( - err, - RadrootsActiveTradeEnvelopeParseError::InvalidPayload( - RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { - field: "items.bin_id" - } - ) - ); - } - - #[test] - fn active_order_decision_parse_roundtrips_and_validates_chain_tags() { - let payload = active_order_decision(); - let built = - active_trade_order_decision_event_build("root-event", "prev-event", &payload).unwrap(); - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_order_decision_from_event(&event).unwrap(); - - assert_eq!(envelope.payload, payload); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderDecision - ); - } - - #[test] - fn active_fulfillment_update_parse_roundtrips_and_validates_chain_tags() { - let payload = active_fulfillment_update(); - let built = - active_trade_fulfillment_update_event_build("root-event", "prev-event", &payload) - .unwrap(); - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_fulfillment_update_from_event(&event).unwrap(); - - assert_eq!(envelope.payload, payload); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeFulfillmentUpdated - ); - } - - #[test] - fn active_order_cancel_parse_roundtrips_and_validates_buyer_actor() { - let payload = active_order_cancelled(); - let built = - active_trade_order_cancel_event_build("root-event", "prev-event", &payload).unwrap(); - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_order_cancel_from_event(&event).unwrap(); - - assert_eq!(envelope.payload, payload); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeOrderCancelled - ); - } - - #[test] - fn active_buyer_receipt_parse_roundtrips_and_validates_buyer_actor() { - let payload = active_buyer_receipt(true); - let built = - active_trade_buyer_receipt_event_build("root-event", "prev-event", &payload).unwrap(); - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_buyer_receipt_from_event(&event).unwrap(); - - assert_eq!(envelope.payload, payload); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeBuyerReceipt - ); - } - - #[test] - fn active_payment_recorded_parse_roundtrips_and_validates_buyer_actor() { - let payload = active_payment_recorded(); - let built = active_trade_payment_recorded_event_build( - payload.root_event_id.as_str(), - payload.previous_event_id.as_str(), - &payload, - ) - .unwrap(); - let event = RadrootsNostrEvent { - id: "payment-event".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_payment_recorded_from_event(&event).unwrap(); - - assert_eq!(envelope.payload, payload); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradePaymentRecorded - ); - } - - #[test] - fn active_settlement_decision_parse_roundtrips_and_validates_seller_actor() { - let payload = active_settlement_decision(RadrootsTradeSettlementDecision::Rejected); - let built = active_trade_settlement_decision_event_build( - payload.root_event_id.as_str(), - payload.previous_event_id.as_str(), - &payload, - ) - .unwrap(); - let event = RadrootsNostrEvent { - id: "settlement-event".into(), - author: "seller".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_settlement_decision_from_event(&event).unwrap(); - - assert_eq!(envelope.payload, payload); - assert_eq!( - envelope.message_type, - RadrootsActiveTradeMessageType::TradeSettlementDecision - ); - } - - #[test] - fn active_order_revision_proposal_parse_validates_actor_counterparty_and_chain_payload() { - let payload = active_order_revision_proposal(); - let built = active_trade_order_revision_proposal_event_build( - payload.root_event_id.as_str(), - payload.prev_event_id.as_str(), - &payload, - ) - .unwrap(); - let mut event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_order_revision_proposal_from_event(&event).unwrap(); - assert_eq!(envelope.payload, payload); - - event.author = "buyer".into(); - let err = active_trade_order_revision_proposal_from_event(&event).unwrap_err(); - assert_eq!(err, RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); - } - - #[test] - fn active_order_revision_decision_parse_validates_actor_counterparty_and_chain_payload() { - let payload = - active_order_revision_decision(RadrootsTradeOrderRevisionDecision::Declined { - reason: "no change".into(), - }); - let built = active_trade_order_revision_decision_event_build( - payload.root_event_id.as_str(), - payload.prev_event_id.as_str(), - &payload, - ) - .unwrap(); - let mut event = RadrootsNostrEvent { - id: "event-id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - let envelope = active_trade_order_revision_decision_from_event(&event).unwrap(); - assert_eq!(envelope.payload, payload); - - event.author = "seller".into(); - let err = active_trade_order_revision_decision_from_event(&event).unwrap_err(); - assert_eq!(err, RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); - } - - #[test] - fn active_revision_kinds_parse_with_chain_tags() { - for (kind, message_type) in [ - ( - KIND_TRADE_ORDER_REVISION, - RadrootsActiveTradeMessageType::TradeOrderRevisionProposed, - ), - ( - KIND_TRADE_ORDER_REVISION_RESPONSE, - RadrootsActiveTradeMessageType::TradeOrderRevisionDecision, - ), - ] { - let payload = serde_json::json!({}); - let envelope = RadrootsActiveTradeEnvelope::new( - message_type, - "30402:seller:AAAAAAAAAAAAAAAAAAAAAg", - "order-1", - &payload, - ); - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind, - tags: vec![ - vec!["p".into(), "buyer".into()], - vec!["a".into(), "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into()], - vec![TAG_D.into(), "order-1".into()], - vec![TAG_E_ROOT.into(), "root-event".into()], - vec![TAG_E_PREV.into(), "prev-event".into()], - ], - content: serde_json::to_string(&envelope).unwrap(), - sig: "sig".into(), - }; - let parsed = active_trade_envelope_from_event::<serde_json::Value>(&event).unwrap(); - - assert_eq!(parsed.message_type, message_type); - assert_eq!(parsed.order_id, "order-1"); - } - } - - #[test] - fn active_parse_rejects_forbidden_kind() { - let event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind: 3431, - tags: Vec::new(), - content: "{}".into(), - sig: "sig".into(), - }; - let err = active_trade_envelope_from_event::<serde_json::Value>(&event).unwrap_err(); - assert_eq!( - err, - RadrootsActiveTradeEnvelopeParseError::InvalidKind(3431) - ); - } - - #[test] - fn active_parse_rejects_missing_required_refs() { - let payload = active_order_decision(); - let built = - active_trade_order_decision_event_build("root-event", "prev-event", &payload).unwrap(); - let mut event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - event - .tags - .retain(|tag| tag.first().map(String::as_str) != Some(TAG_E_PREV)); - - let err = active_trade_order_decision_from_event(&event).unwrap_err(); - assert_eq!( - err, - RadrootsActiveTradeEnvelopeParseError::MissingTag(TAG_E_PREV) - ); - } - - #[test] - fn active_parse_rejects_author_and_counterparty_mismatch() { - let payload = active_order_request(); - let built = active_trade_order_request_event_build(&listing_event_ptr(), &payload).unwrap(); - let mut event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind: built.kind, - tags: built.tags.clone(), - content: built.content.clone(), - sig: "sig".into(), - }; - let err = active_trade_order_request_from_event(&event).unwrap_err(); - assert_eq!(err, RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); - - event.author = "buyer".into(); - event.tags[0] = vec!["p".into(), "other-seller".into()]; - let err = active_trade_order_request_from_event(&event).unwrap_err(); - assert_eq!( - err, - RadrootsActiveTradeEnvelopeParseError::CounterpartyTagMismatch - ); - } - - #[test] - fn active_buyer_lifecycle_parse_rejects_wrong_actor_or_counterparty() { - let cancellation = active_order_cancelled(); - let cancellation_parts = - active_trade_order_cancel_event_build("root-event", "prev-event", &cancellation) - .unwrap(); - let cancellation_event = RadrootsNostrEvent { - id: "event-id".into(), - author: "seller".into(), - created_at: 1, - kind: cancellation_parts.kind, - tags: cancellation_parts.tags, - content: cancellation_parts.content, - sig: "sig".into(), - }; - let err = active_trade_order_cancel_from_event(&cancellation_event).unwrap_err(); - assert_eq!(err, RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); - - let receipt = active_buyer_receipt(true); - let receipt_parts = - active_trade_buyer_receipt_event_build("root-event", "prev-event", &receipt).unwrap(); - let mut receipt_event = RadrootsNostrEvent { - id: "event-id".into(), - author: "buyer".into(), - created_at: 1, - kind: receipt_parts.kind, - tags: receipt_parts.tags, - content: receipt_parts.content, - sig: "sig".into(), - }; - receipt_event.tags[0] = vec!["p".into(), "other-seller".into()]; - let err = active_trade_buyer_receipt_from_event(&receipt_event).unwrap_err(); - assert_eq!( - err, - RadrootsActiveTradeEnvelopeParseError::CounterpartyTagMismatch - ); - } - - #[test] - fn parse_rejects_listing_addr_mismatch() { - let payload = generic_order_response(); - let built = trade_envelope_event_build( - "buyer", - RadrootsTradeMessageType::OrderResponse, - "30402:seller:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1".into()), - None, - Some("root"), - Some("prev"), - &payload, - ) - .expect("build trade envelope"); - let mut envelope: RadrootsTradeEnvelope<serde_json::Value> = - serde_json::from_str(&built.content).expect("decode json"); - envelope.listing_addr = "30402:seller:BBBBBBBBBBBBBBBBBBBBBg".into(); - let event = RadrootsNostrEvent { - id: "id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: serde_json::to_string(&envelope).expect("encode json"), - sig: "sig".into(), - }; - let err = trade_envelope_from_event::<serde_json::Value>(&event).unwrap_err(); - assert_eq!(err, RadrootsTradeEnvelopeParseError::ListingAddrTagMismatch); - } - - #[test] - fn parse_rejects_missing_public_snapshot_tag() { - let payload = RadrootsTradeMessagePayload::OrderRevision( - radroots_events::trade::RadrootsTradeOrderRevision { - revision_id: "rev-1".into(), - changes: Vec::new(), - }, - ); - let built = trade_envelope_event_build( - "seller", - RadrootsTradeMessageType::OrderRevision, - "30402:seller:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1".into()), - Some(&RadrootsNostrEventPtr { - id: "listing-snapshot".into(), - relays: None, - }), - Some("root"), - Some("prev"), - &payload, - ) - .expect("build trade envelope"); - let mut event = RadrootsNostrEvent { - id: "id".into(), - author: "buyer".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - event - .tags - .retain(|tag| tag.first().map(|value| value.as_str()) != Some(TAG_LISTING_EVENT)); - let err = trade_envelope_from_event::<RadrootsTradeMessagePayload>(&event).unwrap_err(); - assert_eq!( - err, - RadrootsTradeEnvelopeParseError::MissingTag(TAG_LISTING_EVENT) - ); - } - - #[test] - fn parse_rejects_missing_public_chain_tags_after_order_request() { - let payload = RadrootsTradeMessagePayload::OrderResponse( - radroots_events::trade::RadrootsTradeOrderResponse { - accepted: true, - reason: None, - }, - ); - let built = trade_envelope_event_build( - "buyer", - RadrootsTradeMessageType::OrderResponse, - "30402:seller:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1".into()), - None, - Some("root"), - Some("prev"), - &payload, - ) - .expect("build trade envelope"); - let mut event = RadrootsNostrEvent { - id: "id".into(), - author: "seller".into(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - event - .tags - .retain(|tag| tag.first().map(|value| value.as_str()) != Some(TAG_E_PREV)); - let err = trade_envelope_from_event::<RadrootsTradeMessagePayload>(&event).unwrap_err(); - assert_eq!(err, RadrootsTradeEnvelopeParseError::MissingTag(TAG_E_PREV)); - } - - #[test] - fn parse_trade_event_context_extracts_public_refs() { - let context = trade_event_context_from_tags( - RadrootsTradeMessageType::OrderResponse, - &[ - vec!["p".into(), "buyer".into()], - vec!["a".into(), "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into()], - vec![TAG_D.into(), "order-1".into()], - vec![TAG_E_ROOT.into(), "root-id".into()], - vec![TAG_E_PREV.into(), "prev-id".into()], - ], - ) - .expect("event context"); - assert_eq!(context.counterparty_pubkey, "buyer"); - assert_eq!(context.root_event_id.as_deref(), Some("root-id")); - assert_eq!(context.prev_event_id.as_deref(), Some("prev-id")); - assert!(context.listing_event.is_none()); - } -} diff --git a/crates/events_codec/src/trade/encode.rs b/crates/events_codec/src/trade/encode.rs @@ -1,403 +0,0 @@ -#[cfg(all(not(feature = "std"), feature = "serde_json"))] -use alloc::string::String; - -#[cfg(feature = "serde_json")] -use radroots_events::{ - RadrootsNostrEventPtr, - trade::{ - RadrootsActiveTradeEnvelope, RadrootsActiveTradeEnvelopeError, - RadrootsActiveTradeMessageType, RadrootsActiveTradePayloadError, RadrootsTradeBuyerReceipt, - RadrootsTradeEnvelope, RadrootsTradeEnvelopeError, RadrootsTradeFulfillmentUpdated, - RadrootsTradeMessagePayload, RadrootsTradeMessageType, RadrootsTradeOrderCancelled, - RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderRequested, - RadrootsTradeOrderRevisionDecisionEvent, RadrootsTradeOrderRevisionProposed, - RadrootsTradePaymentRecorded, RadrootsTradeSettlementDecisionEvent, - }, -}; - -#[cfg(feature = "serde_json")] -use crate::{error::EventEncodeError, trade::tags::trade_envelope_tags, wire::WireEventParts}; - -#[cfg(feature = "serde_json")] -fn map_envelope_error(error: RadrootsTradeEnvelopeError) -> EventEncodeError { - match error { - RadrootsTradeEnvelopeError::MissingOrderId => { - EventEncodeError::EmptyRequiredField("order_id") - } - RadrootsTradeEnvelopeError::MissingListingAddr => { - EventEncodeError::EmptyRequiredField("listing_addr") - } - RadrootsTradeEnvelopeError::InvalidVersion { .. } => { - EventEncodeError::InvalidField("version") - } - } -} - -#[cfg(feature = "serde_json")] -fn map_active_envelope_error(error: RadrootsActiveTradeEnvelopeError) -> EventEncodeError { - match error { - RadrootsActiveTradeEnvelopeError::MissingOrderId => { - EventEncodeError::EmptyRequiredField("order_id") - } - RadrootsActiveTradeEnvelopeError::MissingListingAddr => { - EventEncodeError::EmptyRequiredField("listing_addr") - } - RadrootsActiveTradeEnvelopeError::InvalidVersion { .. } => { - EventEncodeError::InvalidField("version") - } - } -} - -#[cfg(feature = "serde_json")] -fn map_active_payload_error(error: RadrootsActiveTradePayloadError) -> EventEncodeError { - match error { - RadrootsActiveTradePayloadError::EmptyField(field) => { - EventEncodeError::EmptyRequiredField(field) - } - RadrootsActiveTradePayloadError::MissingItems => { - EventEncodeError::EmptyRequiredField("items") - } - RadrootsActiveTradePayloadError::InvalidItemBinCount { .. } => { - EventEncodeError::InvalidField("items.bin_count") - } - RadrootsActiveTradePayloadError::MissingEconomicItems => { - EventEncodeError::EmptyRequiredField("economics.items") - } - RadrootsActiveTradePayloadError::InvalidEconomicItemBinCount { .. } => { - EventEncodeError::InvalidField("economics.items.bin_count") - } - RadrootsActiveTradePayloadError::InvalidEconomicItemQuantity { .. } => { - EventEncodeError::InvalidField("economics.items.quantity_amount") - } - RadrootsActiveTradePayloadError::InvalidEconomicItemPrice { .. } => { - EventEncodeError::InvalidField("economics.items.unit_price_amount") - } - RadrootsActiveTradePayloadError::InvalidEconomicItemSubtotal { .. } => { - EventEncodeError::InvalidField("economics.items.line_subtotal") - } - RadrootsActiveTradePayloadError::InvalidEconomicLineAmount { field, .. } - | RadrootsActiveTradePayloadError::InvalidEconomicLineKind { field, .. } - | RadrootsActiveTradePayloadError::InvalidEconomicLineEffect { field, .. } - | RadrootsActiveTradePayloadError::InvalidEconomicCurrency { field } - | RadrootsActiveTradePayloadError::InvalidEconomicOrdering { field } - | RadrootsActiveTradePayloadError::InvalidEconomicTotal { field } - | RadrootsActiveTradePayloadError::InvalidOrderEconomicsBinding { field } => { - EventEncodeError::InvalidField(field) - } - RadrootsActiveTradePayloadError::InvalidQuoteVersion => { - EventEncodeError::InvalidField("economics.quote_version") - } - RadrootsActiveTradePayloadError::MissingInventoryCommitments => { - EventEncodeError::EmptyRequiredField("inventory_commitments") - } - RadrootsActiveTradePayloadError::InvalidInventoryCommitmentCount { .. } => { - EventEncodeError::InvalidField("inventory_commitments.bin_count") - } - RadrootsActiveTradePayloadError::InvalidFulfillmentStatus => { - EventEncodeError::InvalidField("fulfillment.status") - } - RadrootsActiveTradePayloadError::MissingReceiptIssue => { - EventEncodeError::EmptyRequiredField("receipt.issue") - } - RadrootsActiveTradePayloadError::UnexpectedReceiptIssue => { - EventEncodeError::InvalidField("receipt.issue") - } - RadrootsActiveTradePayloadError::InvalidPaymentAmount => { - EventEncodeError::InvalidField("payment.amount") - } - RadrootsActiveTradePayloadError::MissingSettlementReason => { - EventEncodeError::EmptyRequiredField("settlement.reason") - } - RadrootsActiveTradePayloadError::UnexpectedSettlementReason => { - EventEncodeError::InvalidField("settlement.reason") - } - } -} - -#[cfg(feature = "serde_json")] -pub fn trade_envelope_event_build( - recipient_pubkey: impl Into<String>, - message_type: RadrootsTradeMessageType, - listing_addr: impl Into<String>, - order_id: Option<String>, - listing_event: Option<&RadrootsNostrEventPtr>, - root_event_id: Option<&str>, - prev_event_id: Option<&str>, - payload: &RadrootsTradeMessagePayload, -) -> Result<WireEventParts, EventEncodeError> { - if message_type == RadrootsTradeMessageType::OrderRequest { - return Err(EventEncodeError::InvalidField("message_type")); - } - if payload.message_type() != message_type { - return Err(EventEncodeError::InvalidField("payload")); - } - if message_type.requires_listing_snapshot() && listing_event.is_none() { - return Err(EventEncodeError::EmptyRequiredField("listing_event.id")); - } - if message_type.requires_trade_chain() { - if root_event_id.is_none() { - return Err(EventEncodeError::EmptyRequiredField("root_event_id")); - } - if prev_event_id.is_none() { - return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); - } - } - - let listing_addr = listing_addr.into(); - let envelope = RadrootsTradeEnvelope::new( - message_type, - listing_addr.clone(), - order_id.clone(), - payload.clone(), - ); - envelope.validate().map_err(map_envelope_error)?; - let content = serde_json::to_string(&envelope).map_err(|_| EventEncodeError::Json)?; - let tags = trade_envelope_tags( - recipient_pubkey, - &listing_addr, - order_id.as_deref(), - listing_event, - root_event_id, - prev_event_id, - )?; - Ok(WireEventParts { - kind: message_type.kind(), - content, - tags, - }) -} - -#[cfg(feature = "serde_json")] -fn active_trade_envelope_event_build<T: serde::Serialize>( - recipient_pubkey: &str, - message_type: RadrootsActiveTradeMessageType, - listing_addr: &str, - order_id: &str, - listing_event: Option<&RadrootsNostrEventPtr>, - root_event_id: Option<&str>, - prev_event_id: Option<&str>, - payload: &T, -) -> Result<WireEventParts, EventEncodeError> { - if message_type.requires_listing_snapshot() && listing_event.is_none() { - return Err(EventEncodeError::EmptyRequiredField("listing_event.id")); - } - if message_type.requires_trade_chain() { - if root_event_id.is_none() { - return Err(EventEncodeError::EmptyRequiredField("root_event_id")); - } - if prev_event_id.is_none() { - return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); - } - } - - let envelope = RadrootsActiveTradeEnvelope::new(message_type, listing_addr, order_id, payload); - envelope.validate().map_err(map_active_envelope_error)?; - let content = serde_json::to_string(&envelope).map_err(|_| EventEncodeError::Json)?; - let tags = trade_envelope_tags( - recipient_pubkey, - listing_addr, - Some(order_id), - listing_event, - root_event_id, - prev_event_id, - )?; - Ok(WireEventParts { - kind: message_type.kind(), - content, - tags, - }) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_request_event_build( - listing_event: &RadrootsNostrEventPtr, - payload: &RadrootsTradeOrderRequested, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - active_trade_envelope_event_build( - &payload.seller_pubkey, - RadrootsActiveTradeMessageType::TradeOrderRequested, - &payload.listing_addr, - &payload.order_id, - Some(listing_event), - None, - None, - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_decision_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderDecisionEvent, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - active_trade_envelope_event_build( - &payload.buyer_pubkey, - RadrootsActiveTradeMessageType::TradeOrderDecision, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_revision_proposal_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderRevisionProposed, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - if payload.root_event_id != root_event_id { - return Err(EventEncodeError::InvalidField("root_event_id")); - } - if payload.prev_event_id != prev_event_id { - return Err(EventEncodeError::InvalidField("prev_event_id")); - } - active_trade_envelope_event_build( - &payload.buyer_pubkey, - RadrootsActiveTradeMessageType::TradeOrderRevisionProposed, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_revision_decision_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderRevisionDecisionEvent, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - if payload.root_event_id != root_event_id { - return Err(EventEncodeError::InvalidField("root_event_id")); - } - if payload.prev_event_id != prev_event_id { - return Err(EventEncodeError::InvalidField("prev_event_id")); - } - active_trade_envelope_event_build( - &payload.seller_pubkey, - RadrootsActiveTradeMessageType::TradeOrderRevisionDecision, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_fulfillment_update_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeFulfillmentUpdated, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - active_trade_envelope_event_build( - &payload.buyer_pubkey, - RadrootsActiveTradeMessageType::TradeFulfillmentUpdated, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_order_cancel_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderCancelled, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - active_trade_envelope_event_build( - &payload.seller_pubkey, - RadrootsActiveTradeMessageType::TradeOrderCancelled, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_buyer_receipt_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeBuyerReceipt, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - active_trade_envelope_event_build( - &payload.seller_pubkey, - RadrootsActiveTradeMessageType::TradeBuyerReceipt, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_payment_recorded_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradePaymentRecorded, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - if payload.root_event_id != root_event_id { - return Err(EventEncodeError::InvalidField("root_event_id")); - } - if payload.previous_event_id != prev_event_id { - return Err(EventEncodeError::InvalidField("previous_event_id")); - } - active_trade_envelope_event_build( - &payload.seller_pubkey, - RadrootsActiveTradeMessageType::TradePaymentRecorded, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn active_trade_settlement_decision_event_build( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeSettlementDecisionEvent, -) -> Result<WireEventParts, EventEncodeError> { - payload.validate().map_err(map_active_payload_error)?; - if payload.root_event_id != root_event_id { - return Err(EventEncodeError::InvalidField("root_event_id")); - } - if payload.previous_event_id != prev_event_id { - return Err(EventEncodeError::InvalidField("previous_event_id")); - } - active_trade_envelope_event_build( - &payload.buyer_pubkey, - RadrootsActiveTradeMessageType::TradeSettlementDecision, - &payload.listing_addr, - &payload.order_id, - None, - Some(root_event_id), - Some(prev_event_id), - payload, - ) -} diff --git a/crates/events_codec/src/trade/mod.rs b/crates/events_codec/src/trade/mod.rs @@ -1,29 +0,0 @@ -pub mod decode; -pub mod encode; -pub mod tags; - -#[cfg(feature = "serde_json")] -pub use decode::{ - RadrootsActiveTradeEnvelopeParseError, RadrootsTradeEnvelopeParseError, - RadrootsTradeEventContext, RadrootsTradeListingAddress, RadrootsTradeListingAddressError, - active_trade_buyer_receipt_from_event, active_trade_envelope_from_event, - active_trade_event_context_from_tags, active_trade_fulfillment_update_from_event, - active_trade_order_cancel_from_event, active_trade_order_decision_from_event, - active_trade_order_request_from_event, active_trade_order_revision_decision_from_event, - active_trade_order_revision_proposal_from_event, active_trade_payment_recorded_from_event, - active_trade_settlement_decision_from_event, trade_envelope_from_event, - trade_event_context_from_tags, -}; -#[cfg(feature = "serde_json")] -pub use encode::{ - active_trade_buyer_receipt_event_build, active_trade_fulfillment_update_event_build, - active_trade_order_cancel_event_build, active_trade_order_decision_event_build, - active_trade_order_request_event_build, active_trade_order_revision_decision_event_build, - active_trade_order_revision_proposal_event_build, active_trade_payment_recorded_event_build, - active_trade_settlement_decision_event_build, trade_envelope_event_build, -}; -pub use tags::{ - TAG_LISTING_EVENT, parse_trade_counterparty_tag, parse_trade_listing_event_tag, - parse_trade_prev_tag, parse_trade_root_tag, push_trade_chain_tags, trade_envelope_tags, - validate_trade_chain, -}; diff --git a/crates/events_codec/src/trade/tags.rs b/crates/events_codec/src/trade/tags.rs @@ -1,671 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{borrow::ToOwned, string::String, vec::Vec}; - -use radroots_events::{ - RadrootsNostrEventPtr, - tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, -}; - -use crate::{ - error::{EventEncodeError, EventParseError}, - job::error::JobParseError, -}; - -pub const TAG_LISTING_EVENT: &str = "listing_event"; - -#[inline] -fn push_tag(tags: &mut Vec<Vec<String>>, name: &'static str, value: impl Into<String>) { - let mut tag = Vec::with_capacity(2); - tag.push(name.to_owned()); - tag.push(value.into()); - tags.push(tag); -} - -fn build_event_ptr_tag( - name: &'static str, - ptr: &RadrootsNostrEventPtr, - field_prefix: &'static str, -) -> Result<Vec<String>, EventEncodeError> { - if ptr.id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField(field_prefix)); - } - let mut tag = Vec::with_capacity(3); - tag.push(name.to_owned()); - tag.push(ptr.id.clone()); - if let Some(relay) = &ptr.relays { - if relay.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("listing_event.relays")); - } - tag.push(relay.clone()); - } - Ok(tag) -} - -fn parse_event_ptr_tag( - tags: &[Vec<String>], - name: &'static str, -) -> Result<Option<RadrootsNostrEventPtr>, EventParseError> { - let Some(tag) = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(name)) - else { - return Ok(None); - }; - let id = tag.get(1).ok_or(EventParseError::InvalidTag(name))?; - if id.trim().is_empty() { - return Err(EventParseError::InvalidTag(name)); - } - let relay = match tag.get(2) { - Some(value) if value.trim().is_empty() => return Err(EventParseError::InvalidTag(name)), - Some(value) => Some(value.clone()), - None => None, - }; - Ok(Some(RadrootsNostrEventPtr { - id: id.clone(), - relays: relay, - })) -} - -#[inline] -pub fn trade_envelope_tags<P, A, D>( - recipient_pubkey: P, - listing_addr: A, - order_id: Option<D>, - listing_event: Option<&RadrootsNostrEventPtr>, - root_event_id: Option<&str>, - prev_event_id: Option<&str>, -) -> Result<Vec<Vec<String>>, EventEncodeError> -where - P: Into<String>, - A: Into<String>, - D: Into<String>, -{ - let recipient_pubkey = recipient_pubkey.into(); - if recipient_pubkey.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("recipient_pubkey")); - } - let listing_addr = listing_addr.into(); - if listing_addr.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("listing_addr")); - } - - let mut capacity = 2 + usize::from(order_id.is_some()) + usize::from(listing_event.is_some()); - capacity += usize::from(root_event_id.is_some()) + usize::from(prev_event_id.is_some()); - let mut tags = Vec::with_capacity(capacity); - push_tag(&mut tags, "p", recipient_pubkey); - push_tag(&mut tags, "a", listing_addr); - if let Some(order_id) = order_id { - let order_id = order_id.into(); - if order_id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("order_id")); - } - push_tag(&mut tags, TAG_D, order_id); - } - if let Some(listing_event) = listing_event { - tags.push(build_event_ptr_tag( - TAG_LISTING_EVENT, - listing_event, - "listing_event.id", - )?); - } - if let Some(root_event_id) = root_event_id { - if root_event_id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("root_event_id")); - } - push_tag(&mut tags, TAG_E_ROOT, root_event_id); - } - if let Some(prev_event_id) = prev_event_id { - if prev_event_id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); - } - push_tag(&mut tags, TAG_E_PREV, prev_event_id); - } - Ok(tags) -} - -#[inline] -pub fn parse_trade_counterparty_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> { - let tag = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some("p")) - .ok_or(EventParseError::MissingTag("p"))?; - let value = tag.get(1).ok_or(EventParseError::InvalidTag("p"))?; - if value.trim().is_empty() { - return Err(EventParseError::InvalidTag("p")); - } - Ok(value.clone()) -} - -#[inline] -pub fn parse_trade_listing_event_tag( - tags: &[Vec<String>], -) -> Result<Option<RadrootsNostrEventPtr>, EventParseError> { - parse_event_ptr_tag(tags, TAG_LISTING_EVENT) -} - -#[inline] -pub fn parse_trade_root_tag(tags: &[Vec<String>]) -> Result<Option<String>, EventParseError> { - let tag = match tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_ROOT)) - { - Some(tag) => tag, - None => return Ok(None), - }; - let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_E_ROOT))?; - if value.trim().is_empty() { - return Err(EventParseError::InvalidTag(TAG_E_ROOT)); - } - Ok(Some(value.clone())) -} - -#[inline] -pub fn parse_trade_prev_tag(tags: &[Vec<String>]) -> Result<Option<String>, EventParseError> { - let tag = match tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_PREV)) - { - Some(tag) => tag, - None => return Ok(None), - }; - let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_E_PREV))?; - if value.trim().is_empty() { - return Err(EventParseError::InvalidTag(TAG_E_PREV)); - } - Ok(Some(value.clone())) -} - -#[inline] -pub fn push_trade_chain_tags( - tags: &mut Vec<Vec<String>>, - e_root_id: impl Into<String>, - e_prev_id: Option<impl Into<String>>, - trade_id: Option<impl Into<String>>, -) { - let mut reserve = 1; - if e_prev_id.is_some() { - reserve += 1; - } - if trade_id.is_some() { - reserve += 1; - } - tags.reserve(reserve); - push_tag(tags, TAG_E_ROOT, e_root_id); - if let Some(prev) = e_prev_id { - push_tag(tags, TAG_E_PREV, prev); - } - if let Some(d) = trade_id { - push_tag(tags, TAG_D, d); - } -} - -#[inline] -pub fn validate_trade_chain(tags: &[Vec<String>]) -> Result<(), JobParseError> { - let mut has_root = false; - let mut has_d = false; - - for tag in tags { - match tag.as_slice() { - [key, value, ..] if key == TAG_E_ROOT => { - if value.trim().is_empty() { - return Err(JobParseError::InvalidTag(TAG_E_ROOT)); - } - has_root = true; - } - [key] if key == TAG_E_ROOT => return Err(JobParseError::InvalidTag(TAG_E_ROOT)), - [key, value, ..] if key == TAG_D => { - if value.trim().is_empty() { - return Err(JobParseError::InvalidTag(TAG_D)); - } - has_d = true; - } - [key] if key == TAG_D => return Err(JobParseError::InvalidTag(TAG_D)), - _ => {} - } - } - - if !has_root { - Err(JobParseError::MissingChainTag(TAG_E_ROOT)) - } else if !has_d { - Err(JobParseError::MissingChainTag(TAG_D)) - } else { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::{ - TAG_LISTING_EVENT, parse_trade_counterparty_tag, parse_trade_listing_event_tag, - parse_trade_prev_tag, parse_trade_root_tag, push_trade_chain_tags, trade_envelope_tags, - validate_trade_chain, - }; - use crate::{ - error::{EventEncodeError, EventParseError}, - job::error::JobParseError, - }; - use radroots_events::{ - RadrootsNostrEventPtr, - kinds::KIND_LISTING, - tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, - }; - - #[test] - fn trade_envelope_tags_build_expected_tags() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let tags = trade_envelope_tags( - "pubkey", - listing_addr.as_str(), - Some("order-1"), - None, - None, - None, - ) - .expect("trade tags"); - let expected: Vec<Vec<String>> = vec![ - vec![String::from("p"), String::from("pubkey")], - vec![String::from("a"), listing_addr], - vec![String::from(TAG_D), String::from("order-1")], - ]; - assert_eq!(tags, expected); - } - - #[test] - fn trade_envelope_tags_include_snapshot_and_chain_refs() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let tags = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - Some("order-1"), - Some(&RadrootsNostrEventPtr { - id: "listing-snapshot".into(), - relays: Some("wss://relay.example".into()), - }), - Some("root-event"), - Some("prev-event"), - ) - .expect("trade tags"); - assert!(tags.iter().any(|tag| { - tag.as_slice() - == [ - TAG_LISTING_EVENT.to_string(), - "listing-snapshot".to_string(), - "wss://relay.example".to_string(), - ] - })); - assert!( - tags.iter().any(|tag| { - tag.as_slice() == [TAG_E_ROOT.to_string(), "root-event".to_string()] - }) - ); - assert!( - tags.iter().any(|tag| { - tag.as_slice() == [TAG_E_PREV.to_string(), "prev-event".to_string()] - }) - ); - } - - #[test] - fn trade_envelope_tags_support_snapshot_without_relay() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let tags = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - None::<&str>, - Some(&RadrootsNostrEventPtr { - id: "listing-snapshot".into(), - relays: None, - }), - Some("root-event"), - None::<&str>, - ) - .expect("trade tags"); - assert_eq!( - tags, - vec![ - vec![String::from("p"), String::from("buyer")], - vec![String::from("a"), listing_addr], - vec![ - String::from(TAG_LISTING_EVENT), - String::from("listing-snapshot"), - ], - vec![String::from(TAG_E_ROOT), String::from("root-event")], - ] - ); - } - - #[test] - fn trade_envelope_tags_accept_str_listing_address() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let tags = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - Some("order-1"), - None::<&RadrootsNostrEventPtr>, - Some("root-event"), - Some("prev-event"), - ) - .expect("trade tags with str listing address"); - assert_eq!( - tags, - vec![ - vec![String::from("p"), String::from("buyer")], - vec![String::from("a"), listing_addr], - vec![String::from(TAG_D), String::from("order-1")], - vec![String::from(TAG_E_ROOT), String::from("root-event")], - vec![String::from(TAG_E_PREV), String::from("prev-event")], - ] - ); - } - - #[test] - fn trade_envelope_tags_accept_str_listing_address_with_snapshot_only() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let tags = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - None::<&str>, - Some(&RadrootsNostrEventPtr { - id: "listing-snapshot".into(), - relays: None, - }), - None, - None, - ) - .expect("trade tags with str listing address and snapshot only"); - assert_eq!( - tags, - vec![ - vec![String::from("p"), String::from("buyer")], - vec![String::from("a"), listing_addr], - vec![ - String::from(TAG_LISTING_EVENT), - String::from("listing-snapshot"), - ], - ] - ); - } - - #[test] - fn trade_envelope_tags_reject_empty_required_fields() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - - let err = trade_envelope_tags(" ", listing_addr.as_str(), None::<&str>, None, None, None) - .expect_err("blank recipient"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("recipient_pubkey") - )); - - let err = trade_envelope_tags("buyer", " ", None::<&str>, None, None, None) - .expect_err("blank listing address"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("listing_addr") - )); - - let err = trade_envelope_tags("buyer", listing_addr.as_str(), Some(" "), None, None, None) - .expect_err("blank order id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("order_id") - )); - - let err = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - None::<&str>, - Some(&RadrootsNostrEventPtr { - id: " ".into(), - relays: None, - }), - None, - None, - ) - .expect_err("blank listing snapshot id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("listing_event.id") - )); - - let err = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - None::<&str>, - Some(&RadrootsNostrEventPtr { - id: "listing-snapshot".into(), - relays: Some(" ".into()), - }), - None, - None, - ) - .expect_err("blank listing snapshot relay"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("listing_event.relays") - )); - - let err = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - None::<&str>, - None, - Some(" "), - None, - ) - .expect_err("blank root event id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("root_event_id") - )); - - let err = trade_envelope_tags( - "buyer", - listing_addr.as_str(), - None::<&str>, - None, - None, - Some(" "), - ) - .expect_err("blank prev event id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("prev_event_id") - )); - } - - #[test] - fn trade_envelope_tag_parsers_cover_public_context() { - let tags = vec![ - vec!["p".into(), "counterparty".into()], - vec![ - TAG_LISTING_EVENT.into(), - "snapshot".into(), - "wss://relay".into(), - ], - vec![TAG_E_ROOT.into(), "root".into()], - vec![TAG_E_PREV.into(), "prev".into()], - ]; - assert_eq!( - parse_trade_counterparty_tag(&tags).expect("counterparty"), - "counterparty" - ); - assert_eq!( - parse_trade_listing_event_tag(&tags).expect("snapshot"), - Some(RadrootsNostrEventPtr { - id: "snapshot".into(), - relays: Some("wss://relay".into()), - }) - ); - assert_eq!( - parse_trade_root_tag(&tags).expect("root"), - Some("root".into()) - ); - assert_eq!( - parse_trade_prev_tag(&tags).expect("prev"), - Some("prev".into()) - ); - } - - #[test] - fn trade_envelope_tag_parsers_cover_missing_and_invalid_context() { - assert_eq!( - parse_trade_listing_event_tag(&[]).expect("no snapshot"), - None - ); - assert_eq!(parse_trade_root_tag(&[]).expect("no root"), None); - assert_eq!(parse_trade_prev_tag(&[]).expect("no prev"), None); - - assert!(matches!( - parse_trade_counterparty_tag(&[]), - Err(EventParseError::MissingTag("p")) - )); - assert!(matches!( - parse_trade_counterparty_tag(&[vec![String::from("p")]]), - Err(EventParseError::InvalidTag("p")) - )); - assert!(matches!( - parse_trade_counterparty_tag(&[vec![String::from("p"), String::from(" ")]]), - Err(EventParseError::InvalidTag("p")) - )); - - assert!(matches!( - parse_trade_listing_event_tag(&[vec![String::from(TAG_LISTING_EVENT)]]), - Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)) - )); - assert!(matches!( - parse_trade_listing_event_tag(&[vec![ - String::from(TAG_LISTING_EVENT), - String::from(" "), - ]]), - Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)) - )); - assert!(matches!( - parse_trade_listing_event_tag(&[vec![ - String::from(TAG_LISTING_EVENT), - String::from("snapshot"), - String::from(" "), - ]]), - Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)) - )); - assert_eq!( - parse_trade_listing_event_tag(&[vec![ - String::from(TAG_LISTING_EVENT), - String::from("snapshot"), - ]]) - .expect("snapshot without relay"), - Some(RadrootsNostrEventPtr { - id: "snapshot".into(), - relays: None, - }) - ); - - assert!(matches!( - parse_trade_root_tag(&[vec![String::from(TAG_E_ROOT)]]), - Err(EventParseError::InvalidTag(TAG_E_ROOT)) - )); - assert!(matches!( - parse_trade_root_tag(&[vec![String::from(TAG_E_ROOT), String::from(" ")]]), - Err(EventParseError::InvalidTag(TAG_E_ROOT)) - )); - assert!(matches!( - parse_trade_prev_tag(&[vec![String::from(TAG_E_PREV)]]), - Err(EventParseError::InvalidTag(TAG_E_PREV)) - )); - assert!(matches!( - parse_trade_prev_tag(&[vec![String::from(TAG_E_PREV), String::from(" ")]]), - Err(EventParseError::InvalidTag(TAG_E_PREV)) - )); - } - - #[test] - fn push_trade_chain_tags_adds_root_prev_and_trade_id() { - let mut tags = Vec::new(); - push_trade_chain_tags(&mut tags, "root", Some("prev"), Some("trade")); - assert_eq!( - tags, - vec![ - vec![String::from(TAG_E_ROOT), String::from("root")], - vec![String::from(TAG_E_PREV), String::from("prev")], - vec![String::from(TAG_D), String::from("trade")], - ] - ); - } - - #[test] - fn push_trade_chain_tags_supports_root_only() { - let mut tags = Vec::new(); - push_trade_chain_tags(&mut tags, "root", None::<&str>, None::<&str>); - assert_eq!( - tags, - vec![vec![String::from(TAG_E_ROOT), String::from("root")]] - ); - } - - #[test] - fn validate_trade_chain_requires_root_and_trade_id() { - let ok = vec![ - vec![String::from(TAG_E_ROOT), String::from("root")], - vec![String::from(TAG_D), String::from("trade")], - ]; - assert!(validate_trade_chain(&ok).is_ok()); - let missing = vec![vec![String::from(TAG_D), String::from("trade")]]; - assert!(validate_trade_chain(&missing).is_err()); - } - - #[test] - fn validate_trade_chain_rejects_invalid_tag_shapes_and_missing_trade_id() { - let root_only = vec![vec![String::from(TAG_E_ROOT), String::from("root")]]; - assert!(matches!( - validate_trade_chain(&root_only), - Err(JobParseError::MissingChainTag(TAG_D)) - )); - - let invalid_root_shape = vec![vec![String::from(TAG_E_ROOT)]]; - assert!(matches!( - validate_trade_chain(&invalid_root_shape), - Err(JobParseError::InvalidTag(TAG_E_ROOT)) - )); - - let invalid_root_value = vec![ - vec![String::from(TAG_E_ROOT), String::from(" ")], - vec![String::from(TAG_D), String::from("trade")], - ]; - assert!(matches!( - validate_trade_chain(&invalid_root_value), - Err(JobParseError::InvalidTag(TAG_E_ROOT)) - )); - - let invalid_trade_shape = vec![ - vec![String::from(TAG_E_ROOT), String::from("root")], - vec![String::from(TAG_D)], - ]; - assert!(matches!( - validate_trade_chain(&invalid_trade_shape), - Err(JobParseError::InvalidTag(TAG_D)) - )); - - let invalid_trade_value = vec![ - vec![String::from(TAG_E_ROOT), String::from("root")], - vec![String::from(TAG_D), String::from(" ")], - ]; - assert!(matches!( - validate_trade_chain(&invalid_trade_value), - Err(JobParseError::InvalidTag(TAG_D)) - )); - - let with_unrelated_tag = vec![ - vec![String::from("x"), String::from("ignored")], - vec![String::from(TAG_E_ROOT), String::from("root")], - vec![String::from(TAG_D), String::from("trade")], - ]; - assert!(validate_trade_chain(&with_unrelated_tag).is_ok()); - - let with_singleton_unrelated_tag = vec![ - vec![String::from("x")], - vec![String::from(TAG_E_ROOT), String::from("root")], - vec![String::from(TAG_D), String::from("trade")], - ]; - assert!(validate_trade_chain(&with_singleton_unrelated_tag).is_ok()); - } -} diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -5,8 +5,8 @@ use crate::config::RadrootsdAuth; use crate::farm::RadrootsFarm; use crate::listing; use crate::listing::RadrootsListing; +use crate::order; use crate::profile::{RadrootsProfile, RadrootsProfileType}; -use crate::trade; use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr}; use radroots_events::kinds::KIND_LISTING; use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}; @@ -187,7 +187,7 @@ impl fmt::Debug for SdkRadrootsdListingPublishRequest { #[derive(Clone, PartialEq, Eq, Serialize)] pub(crate) struct SdkRadrootsdOrderRequestPublishRequest { - pub order: trade::RadrootsTradeOrderRequested, + pub order: order::RadrootsOrderRequest, pub listing_event: RadrootsNostrEventPtr, pub signer_session_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -208,521 +208,15 @@ impl fmt::Debug for SdkRadrootsdOrderRequestPublishRequest { } } -#[derive(Clone, PartialEq, Eq, Serialize)] -pub struct SdkRadrootsdPublicTradePublishRequest { - pub listing_addr: String, - pub order_id: String, - pub counterparty_pubkey: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub listing_event: Option<RadrootsNostrEventPtr>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub root_event_id: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub prev_event_id: Option<String>, - pub payload: trade::RadrootsTradeMessagePayload, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdPublicTradeRoute { - listing_addr: String, - order_id: String, - counterparty_pubkey: String, -} - -impl SdkRadrootsdPublicTradeRoute { - pub fn new( - listing_addr: impl Into<String>, - order_id: impl Into<String>, - counterparty_pubkey: impl Into<String>, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - let listing_addr = normalize_required_string(listing_addr, "listing_addr")?; - let order_id = normalize_required_string(order_id, "order_id")?; - let counterparty_pubkey = - normalize_required_string(counterparty_pubkey, "counterparty_pubkey")?; - Ok(Self { - listing_addr, - order_id, - counterparty_pubkey, - }) - } - - fn listing_addr(&self) -> &str { - self.listing_addr.as_str() - } - - fn order_id(&self) -> &str { - self.order_id.as_str() - } - - fn counterparty_pubkey(&self) -> &str { - self.counterparty_pubkey.as_str() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdTradeChain { - root_event_id: String, - prev_event_id: String, -} - -impl SdkRadrootsdTradeChain { - pub fn new( - root_event_id: impl Into<String>, - prev_event_id: impl Into<String>, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - let root_event_id = normalize_required_string(root_event_id, "root_event_id")?; - let prev_event_id = normalize_required_string(prev_event_id, "prev_event_id")?; - Ok(Self { - root_event_id, - prev_event_id, - }) - } - - fn root_event_id(&self) -> &str { - self.root_event_id.as_str() - } - - fn prev_event_id(&self) -> &str { - self.prev_event_id.as_str() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SdkRadrootsdPublicTradePublishValidationError { - UnsupportedPayload(trade::RadrootsTradeMessageType), - MissingListingSnapshot(trade::RadrootsTradeMessageType), - ListingSnapshotRelaysEmpty, - MissingTradeChain(trade::RadrootsTradeMessageType), - InvalidOrderRevisionAcceptPayload, - InvalidOrderRevisionDeclinePayload, - InvalidDiscountAcceptPayload, - EmptyField(&'static str), -} - -impl fmt::Display for SdkRadrootsdPublicTradePublishValidationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::UnsupportedPayload(message_type) => match message_type { - trade::RadrootsTradeMessageType::OrderRequest => f.write_str( - "trade public publish does not support payload `OrderRequest`; use trade.publish_order_request_via_radrootsd for order requests", - ), - trade::RadrootsTradeMessageType::ListingValidateRequest - | trade::RadrootsTradeMessageType::ListingValidateResult => f.write_str( - "trade public publish does not support listing validation payloads; use trade.validate_listing_event for listing validation", - ), - _ => write!( - f, - "trade public publish does not support payload `{message_type:?}`", - ), - }, - Self::MissingListingSnapshot(message_type) => write!( - f, - "trade public publish requires listing_event for `{message_type:?}`" - ), - Self::ListingSnapshotRelaysEmpty => { - f.write_str("trade public publish listing_event.relays must not be empty") - } - Self::MissingTradeChain(message_type) => write!( - f, - "trade public publish requires root_event_id and prev_event_id for `{message_type:?}`" - ), - Self::InvalidOrderRevisionAcceptPayload => f.write_str( - "trade public publish order revision accept payload must set accepted = true", - ), - Self::InvalidOrderRevisionDeclinePayload => f.write_str( - "trade public publish order revision decline payload must set accepted = false", - ), - Self::InvalidDiscountAcceptPayload => f.write_str( - "trade public publish discount accept payload must be an accept decision", - ), - Self::EmptyField(field) => write!(f, "trade public publish field `{field}` must not be empty"), - } - } -} - -impl SdkRadrootsdPublicTradePublishRequest { - pub fn new( - listing_addr: impl Into<String>, - order_id: impl Into<String>, - counterparty_pubkey: impl Into<String>, - payload: trade::RadrootsTradeMessagePayload, - ) -> Self { - Self { - listing_addr: listing_addr.into(), - order_id: order_id.into(), - counterparty_pubkey: counterparty_pubkey.into(), - listing_event: None, - root_event_id: None, - prev_event_id: None, - payload, - } - } - - pub fn with_listing_event(mut self, listing_event: RadrootsNostrEventPtr) -> Self { - self.listing_event = Some(listing_event); - self - } - - pub fn with_trade_chain( - mut self, - root_event_id: impl Into<String>, - prev_event_id: impl Into<String>, - ) -> Self { - self.root_event_id = Some(root_event_id.into()); - self.prev_event_id = Some(prev_event_id.into()); - self - } - - pub fn message_type(&self) -> Option<trade::RadrootsTradeMessageType> { - match &self.payload { - trade::RadrootsTradeMessagePayload::ListingValidateRequest(_) => None, - trade::RadrootsTradeMessagePayload::ListingValidateResult(_) => None, - trade::RadrootsTradeMessagePayload::TradeOrderRequested(_) => None, - trade::RadrootsTradeMessagePayload::OrderResponse(_) => { - Some(trade::RadrootsTradeMessageType::OrderResponse) - } - trade::RadrootsTradeMessagePayload::OrderRevision(_) => { - Some(trade::RadrootsTradeMessageType::OrderRevision) - } - trade::RadrootsTradeMessagePayload::OrderRevisionAccept(_) => { - Some(trade::RadrootsTradeMessageType::OrderRevisionAccept) - } - trade::RadrootsTradeMessagePayload::OrderRevisionDecline(_) => { - Some(trade::RadrootsTradeMessageType::OrderRevisionDecline) - } - trade::RadrootsTradeMessagePayload::Question(_) => { - Some(trade::RadrootsTradeMessageType::Question) - } - trade::RadrootsTradeMessagePayload::Answer(_) => { - Some(trade::RadrootsTradeMessageType::Answer) - } - trade::RadrootsTradeMessagePayload::DiscountRequest(_) => { - Some(trade::RadrootsTradeMessageType::DiscountRequest) - } - trade::RadrootsTradeMessagePayload::DiscountOffer(_) => { - Some(trade::RadrootsTradeMessageType::DiscountOffer) - } - trade::RadrootsTradeMessagePayload::DiscountAccept(_) => { - Some(trade::RadrootsTradeMessageType::DiscountAccept) - } - trade::RadrootsTradeMessagePayload::DiscountDecline(_) => { - Some(trade::RadrootsTradeMessageType::DiscountDecline) - } - trade::RadrootsTradeMessagePayload::Cancel(_) => { - Some(trade::RadrootsTradeMessageType::Cancel) - } - trade::RadrootsTradeMessagePayload::FulfillmentUpdate(_) => { - Some(trade::RadrootsTradeMessageType::FulfillmentUpdate) - } - trade::RadrootsTradeMessagePayload::Receipt(_) => { - Some(trade::RadrootsTradeMessageType::Receipt) - } - } - } - - pub fn order_response( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderResponse, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::OrderResponse(payload), - ) - } - - pub fn order_revision( - route: &SdkRadrootsdPublicTradeRoute, - listing_event: RadrootsNostrEventPtr, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderRevision, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - Some(listing_event), - trade::RadrootsTradeMessagePayload::OrderRevision(payload), - ) - } - - pub fn order_revision_accept( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderRevisionResponse, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::OrderRevisionAccept(payload), - ) - } - - pub fn order_revision_decline( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderRevisionResponse, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::OrderRevisionDecline(payload), - ) - } - - pub fn question( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeQuestion, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::Question(payload), - ) - } - - pub fn answer( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeAnswer, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::Answer(payload), - ) - } - - pub fn discount_request( - route: &SdkRadrootsdPublicTradeRoute, - listing_event: RadrootsNostrEventPtr, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeDiscountRequest, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - Some(listing_event), - trade::RadrootsTradeMessagePayload::DiscountRequest(payload), - ) - } - - pub fn discount_offer( - route: &SdkRadrootsdPublicTradeRoute, - listing_event: RadrootsNostrEventPtr, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeDiscountOffer, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - Some(listing_event), - trade::RadrootsTradeMessagePayload::DiscountOffer(payload), - ) - } - - pub fn discount_accept( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeDiscountDecision, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::DiscountAccept(payload), - ) - } - - pub fn cancel( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeListingCancel, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::Cancel(payload), - ) - } - - pub fn fulfillment_update( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeFulfillmentUpdate, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::FulfillmentUpdate(payload), - ) - } - - pub fn receipt( - route: &SdkRadrootsdPublicTradeRoute, - chain: &SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeReceipt, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - Self::from_components( - route, - Some(chain), - None, - trade::RadrootsTradeMessagePayload::Receipt(payload), - ) - } - - pub fn validate_for_publish( - &self, - ) -> Result<trade::RadrootsTradeMessageType, SdkRadrootsdPublicTradePublishValidationError> - { - normalize_required_string(self.listing_addr.as_str(), "listing_addr")?; - normalize_required_string(self.order_id.as_str(), "order_id")?; - normalize_required_string(self.counterparty_pubkey.as_str(), "counterparty_pubkey")?; - - let message_type = self.payload.message_type(); - if !message_type.is_public() - || matches!(message_type, trade::RadrootsTradeMessageType::OrderRequest) - { - return Err( - SdkRadrootsdPublicTradePublishValidationError::UnsupportedPayload(message_type), - ); - } - - if message_type.requires_listing_snapshot() { - let Some(listing_event) = self.listing_event.as_ref() else { - return Err( - SdkRadrootsdPublicTradePublishValidationError::MissingListingSnapshot( - message_type, - ), - ); - }; - if listing_event - .relays - .as_ref() - .is_some_and(|relay| relay.trim().is_empty()) - { - return Err( - SdkRadrootsdPublicTradePublishValidationError::ListingSnapshotRelaysEmpty, - ); - } - } - - if message_type.requires_trade_chain() { - let Some(root_event_id) = self.root_event_id.as_deref() else { - return Err( - SdkRadrootsdPublicTradePublishValidationError::MissingTradeChain(message_type), - ); - }; - let Some(prev_event_id) = self.prev_event_id.as_deref() else { - return Err( - SdkRadrootsdPublicTradePublishValidationError::MissingTradeChain(message_type), - ); - }; - normalize_required_string(root_event_id, "root_event_id")?; - normalize_required_string(prev_event_id, "prev_event_id")?; - } - - match &self.payload { - trade::RadrootsTradeMessagePayload::OrderRevisionAccept(response) - if !response.accepted => - { - return Err( - SdkRadrootsdPublicTradePublishValidationError::InvalidOrderRevisionAcceptPayload, - ) - } - trade::RadrootsTradeMessagePayload::OrderRevisionDecline(response) - if response.accepted => - { - return Err( - SdkRadrootsdPublicTradePublishValidationError::InvalidOrderRevisionDeclinePayload, - ) - } - trade::RadrootsTradeMessagePayload::DiscountAccept( - trade::RadrootsTradeDiscountDecision::Decline { .. }, - ) => { - return Err( - SdkRadrootsdPublicTradePublishValidationError::InvalidDiscountAcceptPayload, - ) - } - trade::RadrootsTradeMessagePayload::DiscountDecline(_) => { - return Err( - SdkRadrootsdPublicTradePublishValidationError::UnsupportedPayload( - trade::RadrootsTradeMessageType::DiscountDecline, - ), - ) - } - _ => {} - } - - Ok(message_type) - } - - fn from_components( - route: &SdkRadrootsdPublicTradeRoute, - chain: Option<&SdkRadrootsdTradeChain>, - listing_event: Option<RadrootsNostrEventPtr>, - payload: trade::RadrootsTradeMessagePayload, - ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> { - let request = Self { - listing_addr: route.listing_addr().to_owned(), - order_id: route.order_id().to_owned(), - counterparty_pubkey: route.counterparty_pubkey().to_owned(), - listing_event, - root_event_id: chain.map(|chain| chain.root_event_id().to_owned()), - prev_event_id: chain.map(|chain| chain.prev_event_id().to_owned()), - payload, - }; - request.validate_for_publish()?; - Ok(request) - } -} - -impl fmt::Debug for SdkRadrootsdPublicTradePublishRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdPublicTradePublishRequest"); - debug.field("listing_addr", &self.listing_addr); - debug.field("order_id", &self.order_id); - debug.field("counterparty_pubkey", &self.counterparty_pubkey); - debug.field("listing_event", &self.listing_event); - debug.field("root_event_id", &self.root_event_id); - debug.field("prev_event_id", &self.prev_event_id); - debug.field("payload", &self.payload); - debug.finish() - } -} - -fn normalize_required_string( - value: impl Into<String>, - field: &'static str, -) -> Result<String, SdkRadrootsdPublicTradePublishValidationError> { - let value = value.into().trim().to_owned(); - if value.is_empty() { - return Err(SdkRadrootsdPublicTradePublishValidationError::EmptyField( - field, - )); - } - Ok(value) -} - impl SdkRadrootsdListingPublishRequest { pub fn from_event( event: &RadrootsNostrEvent, signer_session_id: impl Into<String>, signer_authority: Option<SdkRadrootsdSignerAuthority>, idempotency_key: Option<String>, - ) -> Result<Self, listing::RadrootsTradeListingParseError> { + ) -> Result<Self, listing::RadrootsListingParseError> { if event.kind != KIND_LISTING { - return Err(listing::RadrootsTradeListingParseError::InvalidKind( - event.kind, - )); + return Err(listing::RadrootsListingParseError::InvalidKind(event.kind)); } Ok(Self { listing: listing::parse_event(event)?, @@ -1013,23 +507,6 @@ struct SdkRadrootsdBridgeJobParams<'a> { job_id: &'a str, } -#[derive(Clone, Serialize)] -struct SdkRadrootsdPublicTradePublishParams<T> { - listing_addr: String, - order_id: String, - counterparty_pubkey: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - listing_event: Option<RadrootsNostrEventPtr>, - #[serde(default, skip_serializing_if = "Option::is_none")] - root_event_id: Option<String>, - #[serde(default, skip_serializing_if = "Option::is_none")] - prev_event_id: Option<String>, - payload: T, - signer_session_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - idempotency_key: Option<String>, -} - pub async fn publish_listing( endpoint: &str, auth: &RadrootsdAuth, @@ -1098,180 +575,6 @@ pub(crate) async fn publish_order_request( .await } -pub(crate) async fn publish_public_trade( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdPublicTradePublishRequest, - signer_session_id: &str, - idempotency_key: Option<&str>, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - match &request.payload { - trade::RadrootsTradeMessagePayload::OrderResponse(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.response", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::OrderRevision(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.revision", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::OrderRevisionAccept(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.revision.accept", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::OrderRevisionDecline(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.revision.decline", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::Question(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.question", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::Answer(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.answer", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::DiscountRequest(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.discount.request", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::DiscountOffer(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.discount.offer", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::DiscountAccept(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.discount.accept", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::Cancel(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.cancel", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::FulfillmentUpdate(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.fulfillment.update", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::Receipt(payload) => { - public_trade_call( - endpoint, - auth, - "bridge.order.receipt", - request, - payload, - signer_session_id, - idempotency_key, - timeout, - ) - .await - } - trade::RadrootsTradeMessagePayload::ListingValidateRequest(_) - | trade::RadrootsTradeMessagePayload::ListingValidateResult(_) - | trade::RadrootsTradeMessagePayload::TradeOrderRequested(_) - | trade::RadrootsTradeMessagePayload::DiscountDecline(_) => { - unreachable!("unsupported trade payload should be rejected by the curated client") - } - } -} - pub(crate) async fn connect_signer_session( endpoint: &str, auth: &RadrootsdAuth, @@ -1466,41 +769,6 @@ pub fn bridge_listing_publish_request_json( }) } -async fn public_trade_call<T>( - endpoint: &str, - auth: &RadrootsdAuth, - method: &'static str, - request: &SdkRadrootsdPublicTradePublishRequest, - payload: &T, - signer_session_id: &str, - idempotency_key: Option<&str>, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> -where - T: Serialize + Clone, -{ - let params = SdkRadrootsdPublicTradePublishParams { - listing_addr: request.listing_addr.clone(), - order_id: request.order_id.clone(), - counterparty_pubkey: request.counterparty_pubkey.clone(), - listing_event: request.listing_event.clone(), - root_event_id: request.root_event_id.clone(), - prev_event_id: request.prev_event_id.clone(), - payload: payload.clone(), - signer_session_id: signer_session_id.to_owned(), - idempotency_key: idempotency_key.map(str::to_owned), - }; - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-public-trade-publish", - method, - &params, - timeout, - ) - .await -} - async fn jsonrpc_call<P, R>( endpoint: &str, auth: &RadrootsdAuth, diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -22,8 +22,7 @@ use crate::config::{RadrootsSdkConfig, SdkConfigError, SdkTransportMode}; use crate::identity::RadrootsIdentity; use crate::{ NostrTags, RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsProfile, RadrootsProfileType, - RadrootsTradeEnvelope, TradeListingValidateResult, WireEventParts, farm, listing, profile, - trade, + TradeListingValidateResult, WireEventParts, farm, listing, order, profile, }; #[cfg(any( feature = "radrootsd-client", @@ -919,53 +918,6 @@ impl fmt::Debug for SdkRadrootsdOrderRequestPublishOptions { } } -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdPublicTradePublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdPublicTradePublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdPublicTradePublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdPublicTradePublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.finish() - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct RadrootsSdkClient { config: RadrootsSdkConfig, @@ -1022,7 +974,7 @@ impl RadrootsSdkClient { ListingClient { client: self } } - pub fn trade(&self) -> TradeClient<'_> { + pub fn order(&self) -> TradeClient<'_> { TradeClient { client: self } } @@ -1225,12 +1177,12 @@ impl RadrootsSdkClient { if self.transport() != SdkTransportMode::Radrootsd { return Err(SdkPublishError::UnsupportedTransport { transport: self.transport(), - operation: "trade.publish_order_request_via_radrootsd", + operation: "order.publish_order_request_via_radrootsd", }); } self.require_signer_mode( SignerConfig::Nip46, - "trade.publish_order_request_via_radrootsd", + "order.publish_order_request_via_radrootsd", )?; let endpoint = match &self.resolved_transport_target { @@ -1238,7 +1190,7 @@ impl RadrootsSdkClient { SdkResolvedTransportTarget::RelayDirect { .. } => { return Err(SdkPublishError::UnsupportedTransport { transport: self.transport(), - operation: "trade.publish_order_request_via_radrootsd", + operation: "order.publish_order_request_via_radrootsd", }); } }; @@ -1254,46 +1206,6 @@ impl RadrootsSdkClient { } #[cfg(feature = "radrootsd-client")] - async fn publish_public_trade_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdPublicTradePublishRequest, - signer_session: &SdkRadrootsdSignerSessionRef, - idempotency_key: Option<&str>, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "trade.publish_public_message_via_radrootsd", - }); - } - self.require_signer_mode( - SignerConfig::Nip46, - "trade.publish_public_message_via_radrootsd", - )?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "trade.publish_public_message_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_public_trade( - endpoint, - &self.config.radrootsd.auth, - request, - signer_session.session_id(), - idempotency_key, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] async fn connect_radrootsd_signer_session( &self, request: &radrootsd::SdkRadrootsdSignerSessionConnectRequest, @@ -1554,184 +1466,6 @@ impl RadrootsSdkClient { } #[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdPublicTradeMessage { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdPublicTradeMessage { - pub fn order_response( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderResponse, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::order_response( - route, chain, payload, - )?, - }) - } - - pub fn order_revision( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - listing_event: RadrootsNostrEventPtr, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderRevision, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::order_revision( - route, - listing_event, - chain, - payload, - )?, - }) - } - - pub fn order_revision_accept( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderRevisionResponse, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::order_revision_accept( - route, chain, payload, - )?, - }) - } - - pub fn order_revision_decline( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeOrderRevisionResponse, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::order_revision_decline( - route, chain, payload, - )?, - }) - } - - pub fn question( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeQuestion, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::question( - route, chain, payload, - )?, - }) - } - - pub fn answer( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeAnswer, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::answer( - route, chain, payload, - )?, - }) - } - - pub fn discount_request( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - listing_event: RadrootsNostrEventPtr, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeDiscountRequest, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::discount_request( - route, - listing_event, - chain, - payload, - )?, - }) - } - - pub fn discount_offer( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - listing_event: RadrootsNostrEventPtr, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeDiscountOffer, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::discount_offer( - route, - listing_event, - chain, - payload, - )?, - }) - } - - pub fn discount_accept( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeDiscountDecision, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::discount_accept( - route, chain, payload, - )?, - }) - } - - pub fn cancel( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeListingCancel, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::cancel( - route, chain, payload, - )?, - }) - } - - pub fn fulfillment_update( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeFulfillmentUpdate, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::fulfillment_update( - route, chain, payload, - )?, - }) - } - - pub fn receipt( - route: &radrootsd::SdkRadrootsdPublicTradeRoute, - chain: &radrootsd::SdkRadrootsdTradeChain, - payload: trade::RadrootsTradeReceipt, - ) -> Result<Self, radrootsd::SdkRadrootsdPublicTradePublishValidationError> { - Ok(Self { - request: radrootsd::SdkRadrootsdPublicTradePublishRequest::receipt( - route, chain, payload, - )?, - }) - } - - pub(crate) fn as_request(&self) -> &radrootsd::SdkRadrootsdPublicTradePublishRequest { - &self.request - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdPublicTradeMessage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SdkRadrootsdPublicTradeMessage") - .field("request", &self.request) - .finish() - } -} - -#[cfg(feature = "radrootsd-client")] #[derive(Debug, Clone, Copy)] pub struct RadrootsdClient<'a> { client: &'a RadrootsSdkClient, @@ -2122,7 +1856,7 @@ impl<'a> ListingClient<'a> { pub fn parse_event( &self, event: &RadrootsNostrEvent, - ) -> Result<listing::RadrootsListing, listing::RadrootsTradeListingParseError> { + ) -> Result<listing::RadrootsListing, listing::RadrootsListingParseError> { listing::parse_event(event) } @@ -2251,61 +1985,28 @@ impl<'a> TradeClient<'a> { } #[cfg(feature = "serde_json")] - #[allow(clippy::too_many_arguments)] - pub fn build_envelope_draft( - &self, - recipient_pubkey: impl Into<String>, - message_type: trade::RadrootsTradeMessageType, - listing_addr: impl Into<String>, - order_id: Option<String>, - listing_event: Option<&RadrootsNostrEventPtr>, - root_event_id: Option<&str>, - prev_event_id: Option<&str>, - payload: &trade::RadrootsTradeMessagePayload, - ) -> Result<WireEventParts, trade::EventEncodeError> { - trade::build_envelope_draft( - recipient_pubkey, - message_type, - listing_addr, - order_id, - listing_event, - root_event_id, - prev_event_id, - payload, - ) - } - - #[cfg(feature = "serde_json")] - pub fn parse_envelope( - &self, - event: &RadrootsNostrEvent, - ) -> Result<RadrootsTradeEnvelope, trade::RadrootsTradeEnvelopeParseError> { - trade::parse_envelope(event) - } - - #[cfg(feature = "serde_json")] pub fn parse_listing_address( &self, listing_addr: &str, - ) -> Result<trade::RadrootsTradeListingAddress, trade::RadrootsTradeListingAddressError> { - trade::parse_listing_address(listing_addr) + ) -> Result<order::RadrootsOrderListingAddress, order::RadrootsOrderListingAddressError> { + order::parse_listing_address(listing_addr) } #[cfg(feature = "serde_json")] pub fn validate_listing_event( &self, event: &RadrootsNostrEvent, - ) -> Result<TradeListingValidateResult, trade::RadrootsTradeListingValidationError> { - trade::validate_listing_event(event) + ) -> Result<TradeListingValidateResult, order::RadrootsTradeValidationListingError> { + order::validate_listing_event(event) } #[cfg(feature = "serde_json")] pub fn build_order_request_draft( &self, listing_event: &RadrootsNostrEventPtr, - payload: &trade::RadrootsTradeOrderRequested, - ) -> Result<trade::RadrootsTradeOrderRequestDraft, trade::EventEncodeError> { - trade::build_order_request_draft(listing_event, payload) + payload: &order::RadrootsOrderRequest, + ) -> Result<order::RadrootsOrderRequestDraft, order::EventEncodeError> { + order::build_order_request_draft(listing_event, payload) } #[cfg(feature = "serde_json")] @@ -2313,9 +2014,9 @@ impl<'a> TradeClient<'a> { &self, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderDecisionEvent, - ) -> Result<trade::RadrootsTradeOrderDecisionDraft, trade::EventEncodeError> { - trade::build_order_decision_draft(root_event_id, prev_event_id, payload) + payload: &order::RadrootsOrderDecision, + ) -> Result<order::RadrootsOrderDecisionDraft, order::EventEncodeError> { + order::build_order_decision_draft(root_event_id, prev_event_id, payload) } #[cfg(feature = "serde_json")] @@ -2323,9 +2024,9 @@ impl<'a> TradeClient<'a> { &self, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderRevisionProposed, - ) -> Result<trade::RadrootsTradeOrderRevisionProposalDraft, trade::EventEncodeError> { - trade::build_order_revision_proposal_draft(root_event_id, prev_event_id, payload) + payload: &order::RadrootsOrderRevisionProposal, + ) -> Result<order::RadrootsOrderRevisionProposalDraft, order::EventEncodeError> { + order::build_order_revision_proposal_draft(root_event_id, prev_event_id, payload) } #[cfg(feature = "serde_json")] @@ -2333,9 +2034,9 @@ impl<'a> TradeClient<'a> { &self, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderRevisionDecisionEvent, - ) -> Result<trade::RadrootsTradeOrderRevisionDecisionDraft, trade::EventEncodeError> { - trade::build_order_revision_decision_draft(root_event_id, prev_event_id, payload) + payload: &order::RadrootsOrderRevisionDecision, + ) -> Result<order::RadrootsOrderRevisionDecisionDraft, order::EventEncodeError> { + order::build_order_revision_decision_draft(root_event_id, prev_event_id, payload) } #[cfg(feature = "serde_json")] @@ -2343,9 +2044,9 @@ impl<'a> TradeClient<'a> { &self, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeFulfillmentUpdated, - ) -> Result<trade::RadrootsTradeFulfillmentUpdateDraft, trade::EventEncodeError> { - trade::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) + payload: &order::RadrootsOrderFulfillmentUpdate, + ) -> Result<order::RadrootsOrderFulfillmentUpdateDraft, order::EventEncodeError> { + order::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) } #[cfg(feature = "serde_json")] @@ -2353,9 +2054,9 @@ impl<'a> TradeClient<'a> { &self, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderCancelled, - ) -> Result<trade::RadrootsTradeOrderCancellationDraft, trade::EventEncodeError> { - trade::build_order_cancellation_draft(root_event_id, prev_event_id, payload) + payload: &order::RadrootsOrderCancellation, + ) -> Result<order::RadrootsOrderCancellationDraft, order::EventEncodeError> { + order::build_order_cancellation_draft(root_event_id, prev_event_id, payload) } #[cfg(feature = "serde_json")] @@ -2363,9 +2064,9 @@ impl<'a> TradeClient<'a> { &self, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeBuyerReceipt, - ) -> Result<trade::RadrootsTradeBuyerReceiptDraft, trade::EventEncodeError> { - trade::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) + payload: &order::RadrootsOrderReceipt, + ) -> Result<order::RadrootsOrderReceiptDraft, order::EventEncodeError> { + order::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) } #[cfg(feature = "serde_json")] @@ -2373,10 +2074,10 @@ impl<'a> TradeClient<'a> { &self, event: &RadrootsNostrEvent, ) -> Result< - trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderRequested>, - trade::RadrootsActiveTradeEnvelopeParseError, + order::RadrootsOrderEnvelope<order::RadrootsOrderRequest>, + order::RadrootsOrderEnvelopeParseError, > { - trade::parse_order_request(event) + order::parse_order_request(event) } #[cfg(feature = "serde_json")] @@ -2384,10 +2085,10 @@ impl<'a> TradeClient<'a> { &self, event: &RadrootsNostrEvent, ) -> Result< - trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderDecisionEvent>, - trade::RadrootsActiveTradeEnvelopeParseError, + order::RadrootsOrderEnvelope<order::RadrootsOrderDecision>, + order::RadrootsOrderEnvelopeParseError, > { - trade::parse_order_decision(event) + order::parse_order_decision(event) } #[cfg(feature = "serde_json")] @@ -2395,10 +2096,10 @@ impl<'a> TradeClient<'a> { &self, event: &RadrootsNostrEvent, ) -> Result< - trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderRevisionProposed>, - trade::RadrootsActiveTradeEnvelopeParseError, + order::RadrootsOrderEnvelope<order::RadrootsOrderRevisionProposal>, + order::RadrootsOrderEnvelopeParseError, > { - trade::parse_order_revision_proposal(event) + order::parse_order_revision_proposal(event) } #[cfg(feature = "serde_json")] @@ -2406,10 +2107,10 @@ impl<'a> TradeClient<'a> { &self, event: &RadrootsNostrEvent, ) -> Result< - trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderRevisionDecisionEvent>, - trade::RadrootsActiveTradeEnvelopeParseError, + order::RadrootsOrderEnvelope<order::RadrootsOrderRevisionDecision>, + order::RadrootsOrderEnvelopeParseError, > { - trade::parse_order_revision_decision(event) + order::parse_order_revision_decision(event) } #[cfg(feature = "serde_json")] @@ -2417,10 +2118,10 @@ impl<'a> TradeClient<'a> { &self, event: &RadrootsNostrEvent, ) -> Result< - trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeFulfillmentUpdated>, - trade::RadrootsActiveTradeEnvelopeParseError, + order::RadrootsOrderEnvelope<order::RadrootsOrderFulfillmentUpdate>, + order::RadrootsOrderEnvelopeParseError, > { - trade::parse_fulfillment_update(event) + order::parse_fulfillment_update(event) } #[cfg(feature = "serde_json")] @@ -2428,10 +2129,10 @@ impl<'a> TradeClient<'a> { &self, event: &RadrootsNostrEvent, ) -> Result< - trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeOrderCancelled>, - trade::RadrootsActiveTradeEnvelopeParseError, + order::RadrootsOrderEnvelope<order::RadrootsOrderCancellation>, + order::RadrootsOrderEnvelopeParseError, > { - trade::parse_order_cancellation(event) + order::parse_order_cancellation(event) } #[cfg(feature = "serde_json")] @@ -2439,10 +2140,10 @@ impl<'a> TradeClient<'a> { &self, event: &RadrootsNostrEvent, ) -> Result< - trade::RadrootsActiveTradeEnvelope<trade::RadrootsTradeBuyerReceipt>, - trade::RadrootsActiveTradeEnvelopeParseError, + order::RadrootsOrderEnvelope<order::RadrootsOrderReceipt>, + order::RadrootsOrderEnvelopeParseError, > { - trade::parse_buyer_receipt(event) + order::parse_buyer_receipt(event) } #[cfg(all( @@ -2454,15 +2155,15 @@ impl<'a> TradeClient<'a> { &self, identity: &RadrootsIdentity, listing_event: &RadrootsNostrEventPtr, - payload: &trade::RadrootsTradeOrderRequested, + payload: &order::RadrootsOrderRequest, ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = trade::build_order_request_draft(listing_event, payload) + let draft = order::build_order_request_draft(listing_event, payload) .map_err(|err| SdkPublishError::Encode(err.to_string()))?; self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_request_with_identity", + "order.publish_order_request_with_identity", ) .await } @@ -2477,16 +2178,16 @@ impl<'a> TradeClient<'a> { identity: &RadrootsIdentity, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderRevisionProposed, + payload: &order::RadrootsOrderRevisionProposal, ) -> Result<SdkPublishReceipt, SdkPublishError> { let draft = - trade::build_order_revision_proposal_draft(root_event_id, prev_event_id, payload) + order::build_order_revision_proposal_draft(root_event_id, prev_event_id, payload) .map_err(|err| SdkPublishError::Encode(err.to_string()))?; self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_revision_proposal_with_identity", + "order.publish_order_revision_proposal_with_identity", ) .await } @@ -2501,16 +2202,16 @@ impl<'a> TradeClient<'a> { identity: &RadrootsIdentity, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderRevisionDecisionEvent, + payload: &order::RadrootsOrderRevisionDecision, ) -> Result<SdkPublishReceipt, SdkPublishError> { let draft = - trade::build_order_revision_decision_draft(root_event_id, prev_event_id, payload) + order::build_order_revision_decision_draft(root_event_id, prev_event_id, payload) .map_err(|err| SdkPublishError::Encode(err.to_string()))?; self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_revision_decision_with_identity", + "order.publish_order_revision_decision_with_identity", ) .await } @@ -2525,15 +2226,15 @@ impl<'a> TradeClient<'a> { identity: &RadrootsIdentity, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderDecisionEvent, + payload: &order::RadrootsOrderDecision, ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = trade::build_order_decision_draft(root_event_id, prev_event_id, payload) + let draft = order::build_order_decision_draft(root_event_id, prev_event_id, payload) .map_err(|err| SdkPublishError::Encode(err.to_string()))?; self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_decision_with_identity", + "order.publish_order_decision_with_identity", ) .await } @@ -2548,15 +2249,15 @@ impl<'a> TradeClient<'a> { identity: &RadrootsIdentity, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeFulfillmentUpdated, + payload: &order::RadrootsOrderFulfillmentUpdate, ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = trade::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) + let draft = order::build_fulfillment_update_draft(root_event_id, prev_event_id, payload) .map_err(|err| SdkPublishError::Encode(err.to_string()))?; self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_fulfillment_update_with_identity", + "order.publish_fulfillment_update_with_identity", ) .await } @@ -2569,13 +2270,13 @@ impl<'a> TradeClient<'a> { pub async fn publish_order_revision_proposal_draft_with_identity( &self, identity: &RadrootsIdentity, - draft: trade::RadrootsTradeOrderRevisionProposalDraft, + draft: order::RadrootsOrderRevisionProposalDraft, ) -> Result<SdkPublishReceipt, SdkPublishError> { self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_revision_proposal_draft_with_identity", + "order.publish_order_revision_proposal_draft_with_identity", ) .await } @@ -2588,13 +2289,13 @@ impl<'a> TradeClient<'a> { pub async fn publish_order_revision_decision_draft_with_identity( &self, identity: &RadrootsIdentity, - draft: trade::RadrootsTradeOrderRevisionDecisionDraft, + draft: order::RadrootsOrderRevisionDecisionDraft, ) -> Result<SdkPublishReceipt, SdkPublishError> { self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_revision_decision_draft_with_identity", + "order.publish_order_revision_decision_draft_with_identity", ) .await } @@ -2609,15 +2310,15 @@ impl<'a> TradeClient<'a> { identity: &RadrootsIdentity, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeOrderCancelled, + payload: &order::RadrootsOrderCancellation, ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = trade::build_order_cancellation_draft(root_event_id, prev_event_id, payload) + let draft = order::build_order_cancellation_draft(root_event_id, prev_event_id, payload) .map_err(|err| SdkPublishError::Encode(err.to_string()))?; self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_cancellation_with_identity", + "order.publish_order_cancellation_with_identity", ) .await } @@ -2632,15 +2333,15 @@ impl<'a> TradeClient<'a> { identity: &RadrootsIdentity, root_event_id: &str, prev_event_id: &str, - payload: &trade::RadrootsTradeBuyerReceipt, + payload: &order::RadrootsOrderReceipt, ) -> Result<SdkPublishReceipt, SdkPublishError> { - let draft = trade::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) + let draft = order::build_buyer_receipt_draft(root_event_id, prev_event_id, payload) .map_err(|err| SdkPublishError::Encode(err.to_string()))?; self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_buyer_receipt_with_identity", + "order.publish_buyer_receipt_with_identity", ) .await } @@ -2653,13 +2354,13 @@ impl<'a> TradeClient<'a> { pub async fn publish_order_request_draft_with_identity( &self, identity: &RadrootsIdentity, - draft: trade::RadrootsTradeOrderRequestDraft, + draft: order::RadrootsOrderRequestDraft, ) -> Result<SdkPublishReceipt, SdkPublishError> { self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_request_draft_with_identity", + "order.publish_order_request_draft_with_identity", ) .await } @@ -2672,13 +2373,13 @@ impl<'a> TradeClient<'a> { pub async fn publish_order_decision_draft_with_identity( &self, identity: &RadrootsIdentity, - draft: trade::RadrootsTradeOrderDecisionDraft, + draft: order::RadrootsOrderDecisionDraft, ) -> Result<SdkPublishReceipt, SdkPublishError> { self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_decision_draft_with_identity", + "order.publish_order_decision_draft_with_identity", ) .await } @@ -2691,13 +2392,13 @@ impl<'a> TradeClient<'a> { pub async fn publish_fulfillment_update_draft_with_identity( &self, identity: &RadrootsIdentity, - draft: trade::RadrootsTradeFulfillmentUpdateDraft, + draft: order::RadrootsOrderFulfillmentUpdateDraft, ) -> Result<SdkPublishReceipt, SdkPublishError> { self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_fulfillment_update_draft_with_identity", + "order.publish_fulfillment_update_draft_with_identity", ) .await } @@ -2710,13 +2411,13 @@ impl<'a> TradeClient<'a> { pub async fn publish_order_cancellation_draft_with_identity( &self, identity: &RadrootsIdentity, - draft: trade::RadrootsTradeOrderCancellationDraft, + draft: order::RadrootsOrderCancellationDraft, ) -> Result<SdkPublishReceipt, SdkPublishError> { self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_order_cancellation_draft_with_identity", + "order.publish_order_cancellation_draft_with_identity", ) .await } @@ -2729,13 +2430,13 @@ impl<'a> TradeClient<'a> { pub async fn publish_buyer_receipt_draft_with_identity( &self, identity: &RadrootsIdentity, - draft: trade::RadrootsTradeBuyerReceiptDraft, + draft: order::RadrootsOrderReceiptDraft, ) -> Result<SdkPublishReceipt, SdkPublishError> { self.client .publish_parts_via_relay_with_identity( identity, draft.into_wire_parts(), - "trade.publish_buyer_receipt_draft_with_identity", + "order.publish_buyer_receipt_draft_with_identity", ) .await } @@ -2743,7 +2444,7 @@ impl<'a> TradeClient<'a> { #[cfg(feature = "radrootsd-client")] pub async fn publish_order_request_via_radrootsd( &self, - order: &trade::RadrootsTradeOrderRequested, + order: &order::RadrootsOrderRequest, listing_event: &RadrootsNostrEventPtr, session: &SdkRadrootsdSignerSessionHandle, ) -> Result<SdkPublishReceipt, SdkPublishError> { @@ -2758,7 +2459,7 @@ impl<'a> TradeClient<'a> { #[cfg(feature = "radrootsd-client")] pub async fn publish_order_request_via_radrootsd_with_options( &self, - order: &trade::RadrootsTradeOrderRequested, + order: &order::RadrootsOrderRequest, listing_event: &RadrootsNostrEventPtr, options: &SdkRadrootsdOrderRequestPublishOptions, ) -> Result<SdkPublishReceipt, SdkPublishError> { @@ -2773,34 +2474,6 @@ impl<'a> TradeClient<'a> { .publish_order_request_via_radrootsd(&request) .await } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_public_message_via_radrootsd( - &self, - message: &SdkRadrootsdPublicTradeMessage, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_public_message_via_radrootsd_with_options( - message, - &SdkRadrootsdPublicTradePublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_public_message_via_radrootsd_with_options( - &self, - message: &SdkRadrootsdPublicTradeMessage, - options: &SdkRadrootsdPublicTradePublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_public_trade_via_radrootsd( - message.as_request(), - options.session(), - options.idempotency_key(), - ) - .await - } } #[cfg(all( diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -22,16 +22,15 @@ pub mod farm; #[cfg(feature = "identity-models")] pub mod identity; pub mod listing; +pub mod order; pub mod profile; -pub mod trade; #[cfg(feature = "radrootsd-client")] pub use crate::adapters::radrootsd::{ SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeJobStatus, - SdkRadrootsdBridgeRelayPublishResult, SdkRadrootsdPublicTradePublishValidationError, - SdkRadrootsdPublicTradeRoute, SdkRadrootsdSignerAuthority, + SdkRadrootsdBridgeRelayPublishResult, SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, SdkRadrootsdSignerSessionMode, - SdkRadrootsdSignerSessionRole, SdkRadrootsdTradeChain, + SdkRadrootsdSignerSessionRole, }; pub use crate::client::{ FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError, @@ -44,7 +43,6 @@ pub use crate::client::{ SdkRadrootsdBridgeJobRef, SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeStatus, SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdProfilePublishOptions, - SdkRadrootsdPublicTradeMessage, SdkRadrootsdPublicTradePublishOptions, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionPublicKeyResult, SdkRadrootsdSignerSessionRef, @@ -62,15 +60,12 @@ pub use radroots_events::{ farm::RadrootsFarm, listing::RadrootsListing, profile::{RadrootsProfile, RadrootsProfileType}, - trade::{RadrootsTradeMessagePayload, RadrootsTradeMessageType}, }; #[cfg(feature = "serde_json")] -pub use radroots_events_codec::trade::{ - RadrootsTradeEnvelopeParseError, RadrootsTradeListingAddress, RadrootsTradeListingAddressError, +pub use radroots_events_codec::order::{ + RadrootsOrderEnvelopeParseError, RadrootsOrderListingAddress, RadrootsOrderListingAddressError, }; pub use radroots_events_codec::wire::{EventDraft as UnsignedEventDraft, WireEventParts}; pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; pub type NostrTags = Vec<Vec<String>>; -pub type RadrootsTradeEnvelope = - radroots_events::trade::RadrootsTradeEnvelope<RadrootsTradeMessagePayload>; diff --git a/crates/sdk/src/listing.rs b/crates/sdk/src/listing.rs @@ -1,7 +1,6 @@ pub use radroots_events::listing::*; -pub use radroots_events::trade::{ - RadrootsTradeListingParseError, RadrootsTradeListingValidationError, -}; +pub use radroots_events::order::RadrootsListingParseError; +pub use radroots_events::trade_validation::RadrootsTradeValidationListingError; pub use radroots_events_codec::error::EventEncodeError; pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; @@ -36,6 +35,6 @@ pub fn build_draft(listing: &RadrootsListing) -> Result<RadrootsListingDraft, Ev #[cfg(feature = "serde_json")] pub fn parse_event( event: &RadrootsNostrEvent, -) -> Result<RadrootsListing, RadrootsTradeListingParseError> { +) -> Result<RadrootsListing, RadrootsListingParseError> { radroots_trade::listing::parse_listing_event(event) } diff --git a/crates/sdk/src/order.rs b/crates/sdk/src/order.rs @@ -0,0 +1,280 @@ +pub use radroots_events::order::*; +pub use radroots_events::trade_validation::*; +pub use radroots_events_codec::error::EventEncodeError; +#[cfg(feature = "serde_json")] +pub use radroots_events_codec::order::{ + RadrootsOrderEnvelopeParseError, RadrootsOrderEventContext, RadrootsOrderListingAddress, + RadrootsOrderListingAddressError, +}; +pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; + +use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr, WireEventParts}; + +#[derive(Debug, Clone)] +pub struct RadrootsOrderRequestDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsOrderDecisionDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsOrderRevisionProposalDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsOrderRevisionDecisionDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsOrderFulfillmentUpdateDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsOrderCancellationDraft { + parts: WireEventParts, +} + +#[derive(Debug, Clone)] +pub struct RadrootsOrderReceiptDraft { + parts: WireEventParts, +} + +impl RadrootsOrderRequestDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsOrderDecisionDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsOrderRevisionProposalDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsOrderRevisionDecisionDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsOrderFulfillmentUpdateDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsOrderCancellationDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +impl RadrootsOrderReceiptDraft { + pub fn as_wire_parts(&self) -> &WireEventParts { + &self.parts + } + + pub fn into_wire_parts(self) -> WireEventParts { + self.parts + } +} + +#[cfg(feature = "serde_json")] +pub fn build_order_request_draft( + listing_event: &RadrootsNostrEventPtr, + payload: &RadrootsOrderRequest, +) -> Result<RadrootsOrderRequestDraft, EventEncodeError> { + Ok(RadrootsOrderRequestDraft { + parts: radroots_events_codec::order::order_request_event_build(listing_event, payload)?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_order_decision_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderDecision, +) -> Result<RadrootsOrderDecisionDraft, EventEncodeError> { + Ok(RadrootsOrderDecisionDraft { + parts: radroots_events_codec::order::order_decision_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_order_revision_proposal_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderRevisionProposal, +) -> Result<RadrootsOrderRevisionProposalDraft, EventEncodeError> { + Ok(RadrootsOrderRevisionProposalDraft { + parts: radroots_events_codec::order::order_revision_proposal_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_order_revision_decision_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderRevisionDecision, +) -> Result<RadrootsOrderRevisionDecisionDraft, EventEncodeError> { + Ok(RadrootsOrderRevisionDecisionDraft { + parts: radroots_events_codec::order::order_revision_decision_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_fulfillment_update_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderFulfillmentUpdate, +) -> Result<RadrootsOrderFulfillmentUpdateDraft, EventEncodeError> { + Ok(RadrootsOrderFulfillmentUpdateDraft { + parts: radroots_events_codec::order::order_fulfillment_update_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_order_cancellation_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderCancellation, +) -> Result<RadrootsOrderCancellationDraft, EventEncodeError> { + Ok(RadrootsOrderCancellationDraft { + parts: radroots_events_codec::order::order_cancellation_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn build_buyer_receipt_draft( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsOrderReceipt, +) -> Result<RadrootsOrderReceiptDraft, EventEncodeError> { + Ok(RadrootsOrderReceiptDraft { + parts: radroots_events_codec::order::order_receipt_event_build( + root_event_id, + prev_event_id, + payload, + )?, + }) +} + +#[cfg(feature = "serde_json")] +pub fn parse_order_request( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderRequest>, RadrootsOrderEnvelopeParseError> { + radroots_events_codec::order::order_request_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_order_decision( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderDecision>, RadrootsOrderEnvelopeParseError> { + radroots_events_codec::order::order_decision_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_order_revision_proposal( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderRevisionProposal>, RadrootsOrderEnvelopeParseError> { + radroots_events_codec::order::order_revision_proposal_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_order_revision_decision( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderRevisionDecision>, RadrootsOrderEnvelopeParseError> { + radroots_events_codec::order::order_revision_decision_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_fulfillment_update( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderFulfillmentUpdate>, RadrootsOrderEnvelopeParseError> +{ + radroots_events_codec::order::order_fulfillment_update_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_order_cancellation( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderCancellation>, RadrootsOrderEnvelopeParseError> { + radroots_events_codec::order::order_cancellation_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_buyer_receipt( + event: &RadrootsNostrEvent, +) -> Result<RadrootsOrderEnvelope<RadrootsOrderReceipt>, RadrootsOrderEnvelopeParseError> { + radroots_events_codec::order::order_receipt_from_event(event) +} + +#[cfg(feature = "serde_json")] +pub fn parse_listing_address( + listing_addr: &str, +) -> Result<RadrootsOrderListingAddress, RadrootsOrderListingAddressError> { + RadrootsOrderListingAddress::parse(listing_addr) +} + +#[cfg(feature = "serde_json")] +pub fn validate_listing_event( + event: &RadrootsNostrEvent, +) -> Result<TradeListingValidateResult, RadrootsTradeValidationListingError> { + radroots_trade::listing::validation::validate_listing_event(event) +} diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs @@ -1,333 +0,0 @@ -pub use radroots_events::trade::*; -pub use radroots_events_codec::error::EventEncodeError; -#[cfg(feature = "serde_json")] -pub use radroots_events_codec::trade::{ - RadrootsActiveTradeEnvelopeParseError, RadrootsTradeEnvelopeParseError, - RadrootsTradeEventContext, RadrootsTradeListingAddress, RadrootsTradeListingAddressError, -}; -pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; - -use crate::RadrootsTradeEnvelope as SdkTradeEnvelope; -use crate::{RadrootsNostrEvent, RadrootsNostrEventPtr, WireEventParts}; - -#[derive(Debug, Clone)] -pub struct RadrootsTradeOrderRequestDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsTradeOrderDecisionDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsTradeOrderRevisionProposalDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsTradeOrderRevisionDecisionDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsTradeFulfillmentUpdateDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsTradeOrderCancellationDraft { - parts: WireEventParts, -} - -#[derive(Debug, Clone)] -pub struct RadrootsTradeBuyerReceiptDraft { - parts: WireEventParts, -} - -impl RadrootsTradeOrderRequestDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsTradeOrderDecisionDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsTradeOrderRevisionProposalDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsTradeOrderRevisionDecisionDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsTradeFulfillmentUpdateDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsTradeOrderCancellationDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -impl RadrootsTradeBuyerReceiptDraft { - pub fn as_wire_parts(&self) -> &WireEventParts { - &self.parts - } - - pub fn into_wire_parts(self) -> WireEventParts { - self.parts - } -} - -#[cfg(feature = "serde_json")] -pub fn build_envelope_draft( - recipient_pubkey: impl Into<String>, - message_type: RadrootsTradeMessageType, - listing_addr: impl Into<String>, - order_id: Option<String>, - listing_event: Option<&RadrootsNostrEventPtr>, - root_event_id: Option<&str>, - prev_event_id: Option<&str>, - payload: &RadrootsTradeMessagePayload, -) -> Result<WireEventParts, EventEncodeError> { - radroots_events_codec::trade::trade_envelope_event_build( - recipient_pubkey, - message_type, - listing_addr, - order_id, - listing_event, - root_event_id, - prev_event_id, - payload, - ) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_request_draft( - listing_event: &RadrootsNostrEventPtr, - payload: &RadrootsTradeOrderRequested, -) -> Result<RadrootsTradeOrderRequestDraft, EventEncodeError> { - Ok(RadrootsTradeOrderRequestDraft { - parts: radroots_events_codec::trade::active_trade_order_request_event_build( - listing_event, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_decision_draft( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderDecisionEvent, -) -> Result<RadrootsTradeOrderDecisionDraft, EventEncodeError> { - Ok(RadrootsTradeOrderDecisionDraft { - parts: radroots_events_codec::trade::active_trade_order_decision_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_revision_proposal_draft( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderRevisionProposed, -) -> Result<RadrootsTradeOrderRevisionProposalDraft, EventEncodeError> { - Ok(RadrootsTradeOrderRevisionProposalDraft { - parts: radroots_events_codec::trade::active_trade_order_revision_proposal_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_revision_decision_draft( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderRevisionDecisionEvent, -) -> Result<RadrootsTradeOrderRevisionDecisionDraft, EventEncodeError> { - Ok(RadrootsTradeOrderRevisionDecisionDraft { - parts: radroots_events_codec::trade::active_trade_order_revision_decision_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_fulfillment_update_draft( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeFulfillmentUpdated, -) -> Result<RadrootsTradeFulfillmentUpdateDraft, EventEncodeError> { - Ok(RadrootsTradeFulfillmentUpdateDraft { - parts: radroots_events_codec::trade::active_trade_fulfillment_update_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_order_cancellation_draft( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeOrderCancelled, -) -> Result<RadrootsTradeOrderCancellationDraft, EventEncodeError> { - Ok(RadrootsTradeOrderCancellationDraft { - parts: radroots_events_codec::trade::active_trade_order_cancel_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn build_buyer_receipt_draft( - root_event_id: &str, - prev_event_id: &str, - payload: &RadrootsTradeBuyerReceipt, -) -> Result<RadrootsTradeBuyerReceiptDraft, EventEncodeError> { - Ok(RadrootsTradeBuyerReceiptDraft { - parts: radroots_events_codec::trade::active_trade_buyer_receipt_event_build( - root_event_id, - prev_event_id, - payload, - )?, - }) -} - -#[cfg(feature = "serde_json")] -pub fn parse_envelope( - event: &RadrootsNostrEvent, -) -> Result<SdkTradeEnvelope, RadrootsTradeEnvelopeParseError> { - radroots_events_codec::trade::trade_envelope_from_event::<RadrootsTradeMessagePayload>(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_request( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderRequested>, - RadrootsActiveTradeEnvelopeParseError, -> { - radroots_events_codec::trade::active_trade_order_request_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_decision( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderDecisionEvent>, - RadrootsActiveTradeEnvelopeParseError, -> { - radroots_events_codec::trade::active_trade_order_decision_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_revision_proposal( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionProposed>, - RadrootsActiveTradeEnvelopeParseError, -> { - radroots_events_codec::trade::active_trade_order_revision_proposal_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_revision_decision( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionDecisionEvent>, - RadrootsActiveTradeEnvelopeParseError, -> { - radroots_events_codec::trade::active_trade_order_revision_decision_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_fulfillment_update( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeFulfillmentUpdated>, - RadrootsActiveTradeEnvelopeParseError, -> { - radroots_events_codec::trade::active_trade_fulfillment_update_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_order_cancellation( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeOrderCancelled>, - RadrootsActiveTradeEnvelopeParseError, -> { - radroots_events_codec::trade::active_trade_order_cancel_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_buyer_receipt( - event: &RadrootsNostrEvent, -) -> Result< - RadrootsActiveTradeEnvelope<RadrootsTradeBuyerReceipt>, - RadrootsActiveTradeEnvelopeParseError, -> { - radroots_events_codec::trade::active_trade_buyer_receipt_from_event(event) -} - -#[cfg(feature = "serde_json")] -pub fn parse_listing_address( - listing_addr: &str, -) -> Result<RadrootsTradeListingAddress, RadrootsTradeListingAddressError> { - RadrootsTradeListingAddress::parse(listing_addr) -} - -#[cfg(feature = "serde_json")] -pub fn validate_listing_event( - event: &RadrootsNostrEvent, -) -> Result<TradeListingValidateResult, RadrootsTradeListingValidationError> { - radroots_trade::listing::validation::validate_listing_event(event) -} diff --git a/crates/sdk/tests/client.rs b/crates/sdk/tests/client.rs @@ -4,25 +4,23 @@ use radroots_core::{ }; use radroots_events::farm::{RadrootsFarm, RadrootsFarmRef}; use radroots_events::kinds::{ - KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_CANCEL, KIND_TRADE_FULFILLMENT_UPDATE, - KIND_TRADE_LISTING_VALIDATE_REQ, KIND_TRADE_ORDER_REQUEST, KIND_TRADE_ORDER_RESPONSE, - KIND_TRADE_ORDER_REVISION, KIND_TRADE_ORDER_REVISION_RESPONSE, KIND_TRADE_RECEIPT, + KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, + KIND_ORDER_FULFILLMENT_UPDATE, KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST, + KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_PROFILE, }; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }; -use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_events::trade::{ - RadrootsActiveTradeFulfillmentState, RadrootsTradeBuyerReceipt, - RadrootsTradeFulfillmentUpdated, RadrootsTradeInventoryCommitment, - RadrootsTradeListingValidateRequest, RadrootsTradeMessagePayload, RadrootsTradeOrderCancelled, - RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, - RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, - RadrootsTradeOrderRevisionDecision, RadrootsTradeOrderRevisionDecisionEvent, - RadrootsTradeOrderRevisionProposed, RadrootsTradePricingBasis, +use radroots_events::order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderFulfillmentState, + RadrootsOrderFulfillmentUpdate, RadrootsOrderInventoryCommitment, RadrootsOrderItem, + RadrootsOrderPricingBasis, RadrootsOrderReceipt, RadrootsOrderRequest, + RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, }; +use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; use radroots_sdk::{ RADROOTS_SDK_PRODUCTION_RELAY_URL, RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkConfigError, SdkEnvironment, @@ -136,25 +134,22 @@ fn listing_event_ptr() -> RadrootsNostrEventPtr { } } -fn sample_order_request( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsTradeOrderRequested { - RadrootsTradeOrderRequested { +fn sample_order_request(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderRequest { + RadrootsOrderRequest { order_id: "order-1".into(), listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, seller_pubkey, - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: "bin-1".into(), bin_count: 2, }], - economics: RadrootsTradeOrderEconomics { + economics: RadrootsOrderEconomics { quote_id: "quote-1".into(), quote_version: 1, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsTradeOrderEconomicItem { + items: vec![RadrootsOrderEconomicItem { bin_id: "bin-1".into(), bin_count: 2, quantity_amount: decimal("1"), @@ -173,17 +168,14 @@ fn sample_order_request( } } -fn sample_order_decision( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsTradeOrderDecisionEvent { - RadrootsTradeOrderDecisionEvent { +fn sample_order_decision(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderDecision { + RadrootsOrderDecision { order_id: "order-1".into(), listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, seller_pubkey, - decision: RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: "bin-1".into(), bin_count: 2, }], @@ -196,8 +188,8 @@ fn sample_order_revision_proposal( seller_pubkey: String, root_event_id: String, prev_event_id: String, -) -> RadrootsTradeOrderRevisionProposed { - RadrootsTradeOrderRevisionProposed { +) -> RadrootsOrderRevisionProposal { + RadrootsOrderRevisionProposal { revision_id: "revision-1".into(), order_id: "order-1".into(), listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), @@ -205,16 +197,16 @@ fn sample_order_revision_proposal( seller_pubkey, root_event_id, prev_event_id, - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: "bin-1".into(), bin_count: 3, }], - economics: RadrootsTradeOrderEconomics { + economics: RadrootsOrderEconomics { quote_id: "revision-quote-1".into(), quote_version: 2, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsTradeOrderEconomicItem { + items: vec![RadrootsOrderEconomicItem { bin_id: "bin-1".into(), bin_count: 3, quantity_amount: decimal("1"), @@ -235,10 +227,10 @@ fn sample_order_revision_proposal( } fn sample_order_revision_decision( - proposal: &RadrootsTradeOrderRevisionProposed, - decision: RadrootsTradeOrderRevisionDecision, -) -> RadrootsTradeOrderRevisionDecisionEvent { - RadrootsTradeOrderRevisionDecisionEvent { + proposal: &RadrootsOrderRevisionProposal, + decision: RadrootsOrderRevisionOutcome, +) -> RadrootsOrderRevisionDecision { + RadrootsOrderRevisionDecision { revision_id: proposal.revision_id.clone(), order_id: proposal.order_id.clone(), listing_addr: proposal.listing_addr.clone(), @@ -253,21 +245,21 @@ fn sample_order_revision_decision( fn sample_fulfillment_update( buyer_pubkey: String, seller_pubkey: String, -) -> RadrootsTradeFulfillmentUpdated { - RadrootsTradeFulfillmentUpdated { +) -> RadrootsOrderFulfillmentUpdate { + RadrootsOrderFulfillmentUpdate { order_id: "order-1".into(), listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, seller_pubkey, - status: RadrootsActiveTradeFulfillmentState::ReadyForPickup, + status: RadrootsOrderFulfillmentState::ReadyForPickup, } } fn sample_order_cancellation( buyer_pubkey: String, seller_pubkey: String, -) -> RadrootsTradeOrderCancelled { - RadrootsTradeOrderCancelled { +) -> RadrootsOrderCancellation { + RadrootsOrderCancellation { order_id: "order-1".into(), listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, @@ -276,8 +268,8 @@ fn sample_order_cancellation( } } -fn sample_buyer_receipt(buyer_pubkey: String, seller_pubkey: String) -> RadrootsTradeBuyerReceipt { - RadrootsTradeBuyerReceipt { +fn sample_buyer_receipt(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderReceipt { + RadrootsOrderReceipt { order_id: "order-1".into(), listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, @@ -398,14 +390,14 @@ fn namespace_clients_reflect_explicit_transport_mode() { assert_eq!(client.profile().transport(), SdkTransportMode::Radrootsd); assert_eq!(client.farm().transport(), SdkTransportMode::Radrootsd); assert_eq!(client.listing().transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.trade().transport(), SdkTransportMode::Radrootsd); + assert_eq!(client.order().transport(), SdkTransportMode::Radrootsd); #[cfg(feature = "radrootsd-client")] assert_eq!(client.radrootsd().transport(), SdkTransportMode::Radrootsd); assert_eq!(client.signer(), SignerConfig::LocalIdentity); assert_eq!(client.profile().signer(), SignerConfig::LocalIdentity); assert_eq!(client.farm().signer(), SignerConfig::LocalIdentity); assert_eq!(client.listing().signer(), SignerConfig::LocalIdentity); - assert_eq!(client.trade().signer(), SignerConfig::LocalIdentity); + assert_eq!(client.order().signer(), SignerConfig::LocalIdentity); #[cfg(feature = "radrootsd-client")] assert_eq!(client.radrootsd().signer(), SignerConfig::LocalIdentity); } @@ -417,13 +409,13 @@ fn namespace_clients_expose_parent_sdk_and_draft_facades() { let profile = client.profile(); let farm = client.farm(); let listing = client.listing(); - let trade = client.trade(); + let order = client.order(); assert_eq!(client.config().environment, SdkEnvironment::Production); assert!(std::ptr::eq(profile.sdk(), &client)); assert!(std::ptr::eq(farm.sdk(), &client)); assert!(std::ptr::eq(listing.sdk(), &client)); - assert!(std::ptr::eq(trade.sdk(), &client)); + assert!(std::ptr::eq(order.sdk(), &client)); let profile_draft = profile .build_draft(&sample_profile(), Some(RadrootsProfileType::Farm)) @@ -445,7 +437,7 @@ fn namespace_clients_expose_parent_sdk_and_draft_facades() { } #[test] -fn listing_and_trade_clients_wrap_existing_sdk_facades() { +fn listing_and_order_clients_wrap_existing_sdk_facades() { let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::local()).expect("sdk client"); let listing_value = sample_listing(); @@ -477,59 +469,48 @@ fn listing_and_trade_clients_wrap_existing_sdk_facades() { assert_eq!(parsed.d_tag, listing_value.d_tag); let validated = client - .trade() + .order() .validate_listing_event(&event) .expect("validated listing"); assert_eq!(validated.listing_id, listing_value.d_tag); let listing_addr = format!("{KIND_LISTING}:seller:{}", listing_value.d_tag); - let payload = - RadrootsTradeMessagePayload::ListingValidateRequest(RadrootsTradeListingValidateRequest { - listing_event: None, - }); + let payload = sample_order_request("buyer".into(), "seller".into()); let envelope = client - .trade() - .build_envelope_draft( - "buyer", - payload.message_type(), - listing_addr.clone(), - None, - None, - None, - None, - &payload, - ) - .expect("trade draft"); - assert_eq!(envelope.kind, KIND_TRADE_LISTING_VALIDATE_REQ); + .order() + .build_order_request_draft(&listing_event_ptr(), &payload) + .expect("order draft"); + assert_eq!(envelope.as_wire_parts().kind, KIND_ORDER_REQUEST); let envelope_event = RadrootsNostrEvent { - id: "trade-event-1".into(), - author: "seller".into(), + id: "order-event-1".into(), + author: "buyer".into(), created_at: 2, - kind: envelope.kind, - tags: envelope.tags, - content: envelope.content, + kind: envelope.as_wire_parts().kind, + tags: envelope.as_wire_parts().tags.clone(), + content: envelope.as_wire_parts().content.clone(), sig: String::new(), }; assert_eq!( client - .trade() - .parse_envelope(&envelope_event) - .expect("trade envelope") - .message_type, - payload.message_type() + .order() + .parse_order_request(&envelope_event) + .expect("order envelope") + .payload + .order_id, + payload.order_id ); let parsed_addr = client - .trade() + .order() .parse_listing_address(&listing_addr) .expect("listing address"); assert_eq!(parsed_addr.listing_id, listing_value.d_tag); } #[test] -fn active_trade_facades_round_trip_all_draft_types() { +fn order_facades_round_trip_all_draft_types() { let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let trade = client.trade(); + let order_client = client.order(); let buyer_pubkey = "b".repeat(64); let seller_pubkey = "a".repeat(64); let root_event_id = "order-request-event-1"; @@ -537,30 +518,27 @@ fn active_trade_facades_round_trip_all_draft_types() { let proposal_event_id = "order-revision-proposal-event-1"; let fulfillment_event_id = "fulfillment-event-1"; - let order = sample_order_request(buyer_pubkey.clone(), seller_pubkey.clone()); - let order_draft = trade - .build_order_request_draft(&listing_event_ptr(), &order) + let order_request = sample_order_request(buyer_pubkey.clone(), seller_pubkey.clone()); + let order_draft = order_client + .build_order_request_draft(&listing_event_ptr(), &order_request) .expect("order request draft"); - assert_eq!(order_draft.as_wire_parts().kind, KIND_TRADE_ORDER_REQUEST); + assert_eq!(order_draft.as_wire_parts().kind, KIND_ORDER_REQUEST); let order_event = event_from_parts( root_event_id, &buyer_pubkey, 1, order_draft.clone().into_wire_parts(), ); - let order_envelope = trade + let order_envelope = order_client .parse_order_request(&order_event) .expect("order request envelope"); assert_eq!(order_envelope.payload.economics.total, usd("10")); let decision = sample_order_decision(buyer_pubkey.clone(), seller_pubkey.clone()); - let decision_draft = trade + let decision_draft = order_client .build_order_decision_draft(root_event_id, root_event_id, &decision) .expect("order decision draft"); - assert_eq!( - decision_draft.as_wire_parts().kind, - KIND_TRADE_ORDER_RESPONSE - ); + assert_eq!(decision_draft.as_wire_parts().kind, KIND_ORDER_DECISION); let decision_event = event_from_parts( decision_event_id, &seller_pubkey, @@ -568,7 +546,7 @@ fn active_trade_facades_round_trip_all_draft_types() { decision_draft.clone().into_wire_parts(), ); assert_eq!( - trade + order_client .parse_order_decision(&decision_event) .expect("order decision envelope") .payload @@ -582,12 +560,12 @@ fn active_trade_facades_round_trip_all_draft_types() { root_event_id.into(), decision_event_id.into(), ); - let proposal_draft = trade + let proposal_draft = order_client .build_order_revision_proposal_draft(root_event_id, decision_event_id, &proposal) .expect("revision proposal draft"); assert_eq!( proposal_draft.as_wire_parts().kind, - KIND_TRADE_ORDER_REVISION + KIND_ORDER_REVISION_PROPOSAL ); let proposal_event = event_from_parts( proposal_event_id, @@ -596,7 +574,7 @@ fn active_trade_facades_round_trip_all_draft_types() { proposal_draft.clone().into_wire_parts(), ); assert_eq!( - trade + order_client .parse_order_revision_proposal(&proposal_event) .expect("revision proposal envelope") .payload @@ -606,8 +584,8 @@ fn active_trade_facades_round_trip_all_draft_types() { ); let revision_decision = - sample_order_revision_decision(&proposal, RadrootsTradeOrderRevisionDecision::Accepted); - let revision_decision_draft = trade + sample_order_revision_decision(&proposal, RadrootsOrderRevisionOutcome::Accepted); + let revision_decision_draft = order_client .build_order_revision_decision_draft( root_event_id, revision_decision.prev_event_id.as_str(), @@ -616,7 +594,7 @@ fn active_trade_facades_round_trip_all_draft_types() { .expect("revision decision draft"); assert_eq!( revision_decision_draft.as_wire_parts().kind, - KIND_TRADE_ORDER_REVISION_RESPONSE + KIND_ORDER_REVISION_DECISION ); let revision_decision_event = event_from_parts( "order-revision-decision-event-1", @@ -625,7 +603,7 @@ fn active_trade_facades_round_trip_all_draft_types() { revision_decision_draft.clone().into_wire_parts(), ); assert_eq!( - trade + order_client .parse_order_revision_decision(&revision_decision_event) .expect("revision decision envelope") .payload @@ -634,12 +612,12 @@ fn active_trade_facades_round_trip_all_draft_types() { ); let fulfillment = sample_fulfillment_update(buyer_pubkey.clone(), seller_pubkey.clone()); - let fulfillment_draft = trade + let fulfillment_draft = order_client .build_fulfillment_update_draft(root_event_id, decision_event_id, &fulfillment) .expect("fulfillment draft"); assert_eq!( fulfillment_draft.as_wire_parts().kind, - KIND_TRADE_FULFILLMENT_UPDATE + KIND_ORDER_FULFILLMENT_UPDATE ); let fulfillment_event = event_from_parts( fulfillment_event_id, @@ -648,7 +626,7 @@ fn active_trade_facades_round_trip_all_draft_types() { fulfillment_draft.clone().into_wire_parts(), ); assert_eq!( - trade + order_client .parse_fulfillment_update(&fulfillment_event) .expect("fulfillment envelope") .payload @@ -657,10 +635,13 @@ fn active_trade_facades_round_trip_all_draft_types() { ); let cancellation = sample_order_cancellation(buyer_pubkey.clone(), seller_pubkey.clone()); - let cancellation_draft = trade + let cancellation_draft = order_client .build_order_cancellation_draft(root_event_id, decision_event_id, &cancellation) .expect("cancellation draft"); - assert_eq!(cancellation_draft.as_wire_parts().kind, KIND_TRADE_CANCEL); + assert_eq!( + cancellation_draft.as_wire_parts().kind, + KIND_ORDER_CANCELLATION + ); let cancellation_event = event_from_parts( "order-cancellation-event-1", &buyer_pubkey, @@ -668,7 +649,7 @@ fn active_trade_facades_round_trip_all_draft_types() { cancellation_draft.clone().into_wire_parts(), ); assert_eq!( - trade + order_client .parse_order_cancellation(&cancellation_event) .expect("cancellation envelope") .payload @@ -677,10 +658,10 @@ fn active_trade_facades_round_trip_all_draft_types() { ); let receipt = sample_buyer_receipt(buyer_pubkey.clone(), seller_pubkey.clone()); - let receipt_draft = trade + let receipt_draft = order_client .build_buyer_receipt_draft(root_event_id, fulfillment_event_id, &receipt) .expect("receipt draft"); - assert_eq!(receipt_draft.as_wire_parts().kind, KIND_TRADE_RECEIPT); + assert_eq!(receipt_draft.as_wire_parts().kind, KIND_ORDER_RECEIPT); let receipt_event = event_from_parts( "receipt-event-1", &buyer_pubkey, @@ -688,7 +669,7 @@ fn active_trade_facades_round_trip_all_draft_types() { receipt_draft.clone().into_wire_parts(), ); assert!( - trade + order_client .parse_buyer_receipt(&receipt_event) .expect("receipt envelope") .payload @@ -697,10 +678,10 @@ fn active_trade_facades_round_trip_all_draft_types() { } #[test] -fn active_trade_draft_facades_return_encoder_errors() { +fn order_draft_facades_return_encoder_errors() { let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let trade = client.trade(); + let order = client.order(); let buyer_pubkey = "b".repeat(64); let seller_pubkey = "a".repeat(64); let root_event_id = "order-request-event-1"; @@ -709,7 +690,7 @@ fn active_trade_draft_facades_return_encoder_errors() { let mut invalid_order = sample_order_request(buyer_pubkey.clone(), seller_pubkey.clone()); invalid_order.order_id.clear(); assert!( - trade + order .build_order_request_draft(&listing_event_ptr(), &invalid_order) .is_err() ); @@ -717,7 +698,7 @@ fn active_trade_draft_facades_return_encoder_errors() { let mut invalid_decision = sample_order_decision(buyer_pubkey.clone(), seller_pubkey.clone()); invalid_decision.buyer_pubkey.clear(); assert!( - trade + order .build_order_decision_draft(root_event_id, root_event_id, &invalid_decision) .is_err() ); @@ -729,15 +710,15 @@ fn active_trade_draft_facades_return_encoder_errors() { decision_event_id.into(), ); assert!( - trade + order .build_order_revision_proposal_draft("different-root", decision_event_id, &proposal) .is_err() ); let revision_decision = - sample_order_revision_decision(&proposal, RadrootsTradeOrderRevisionDecision::Accepted); + sample_order_revision_decision(&proposal, RadrootsOrderRevisionOutcome::Accepted); assert!( - trade + order .build_order_revision_decision_draft( root_event_id, "different-prev", @@ -747,9 +728,9 @@ fn active_trade_draft_facades_return_encoder_errors() { ); let mut fulfillment = sample_fulfillment_update(buyer_pubkey.clone(), seller_pubkey.clone()); - fulfillment.status = RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled; + fulfillment.status = RadrootsOrderFulfillmentState::AcceptedNotFulfilled; assert!( - trade + order .build_fulfillment_update_draft(root_event_id, decision_event_id, &fulfillment) .is_err() ); @@ -757,7 +738,7 @@ fn active_trade_draft_facades_return_encoder_errors() { let mut cancellation = sample_order_cancellation(buyer_pubkey.clone(), seller_pubkey.clone()); cancellation.reason.clear(); assert!( - trade + order .build_order_cancellation_draft(root_event_id, decision_event_id, &cancellation) .is_err() ); @@ -765,7 +746,7 @@ fn active_trade_draft_facades_return_encoder_errors() { let mut receipt = sample_buyer_receipt(buyer_pubkey, seller_pubkey); receipt.received = false; assert!( - trade + order .build_buyer_receipt_draft(root_event_id, decision_event_id, &receipt) .is_err() ); @@ -800,27 +781,27 @@ fn publish_receipts_and_errors_format_public_details() { SdkPublishError::Encode("encode failed".into()).to_string(), SdkPublishError::UnsupportedTransport { transport: SdkTransportMode::Radrootsd, - operation: "trade.publish", + operation: "order.publish", } .to_string(), SdkPublishError::UnsupportedSignerMode { transport: SdkTransportMode::RelayDirect, signer: SignerConfig::DraftOnly, required: SignerConfig::LocalIdentity, - operation: "trade.publish", + operation: "order.publish", } .to_string(), SdkPublishError::Relay("relay failed".into()).to_string(), SdkPublishError::RelaySetup { transport: SdkTransportMode::RelayDirect, - operation: "trade.publish", + operation: "order.publish", target_relays: Vec::new(), error: "setup failed".into(), } .to_string(), SdkPublishError::RelaySetup { transport: SdkTransportMode::RelayDirect, - operation: "trade.publish", + operation: "order.publish", target_relays: vec!["wss://relay.example".into()], error: "setup failed".into(), } diff --git a/crates/sdk/tests/facade.rs b/crates/sdk/tests/facade.rs @@ -3,17 +3,18 @@ use radroots_core::{ RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::farm::{RadrootsFarm, RadrootsFarmRef}; -use radroots_events::kinds::{ - KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_LISTING_VALIDATE_REQ, -}; +use radroots_events::kinds::{KIND_FARM, KIND_LISTING, KIND_ORDER_REQUEST, KIND_PROFILE}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }; +use radroots_events::order::{ + RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderItem, + RadrootsOrderPricingBasis, RadrootsOrderRequest, +}; use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_events::trade::{RadrootsTradeListingValidateRequest, RadrootsTradeMessagePayload}; -use radroots_sdk::{RadrootsNostrEvent, farm, listing, profile, trade}; +use radroots_sdk::{RadrootsNostrEvent, RadrootsNostrEventPtr, farm, listing, order, profile}; fn sample_profile() -> RadrootsProfile { RadrootsProfile { @@ -119,6 +120,62 @@ fn listing_event(listing_value: &RadrootsListing) -> RadrootsNostrEvent { } } +fn listing_event_ptr() -> RadrootsNostrEventPtr { + RadrootsNostrEventPtr { + id: "listing-event-1".into(), + relays: Some("wss://listing.relay.example".into()), + } +} + +fn sample_order_request() -> RadrootsOrderRequest { + RadrootsOrderRequest { + order_id: "order-1".into(), + listing_addr: format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + items: vec![RadrootsOrderItem { + bin_id: "bin-1".into(), + bin_count: 2, + }], + economics: RadrootsOrderEconomics { + quote_id: "quote-1".into(), + quote_version: 1, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsOrderEconomicItem { + bin_id: "bin-1".into(), + bin_count: 2, + quantity_amount: RadrootsCoreDecimal::from(1u32), + quantity_unit: RadrootsCoreUnit::Each, + 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: 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, + ), + }, + } +} + #[test] fn profile_build_draft_wraps_profile_encoder() { let parts = @@ -154,7 +211,7 @@ fn listing_facade_wraps_build_parse_and_validate() { let parsed = listing::parse_event(&event).expect("parsed listing"); assert_eq!(parsed.d_tag, listing_value.d_tag); - let validated = trade::validate_listing_event(&event).expect("validated listing"); + let validated = order::validate_listing_event(&event).expect("validated listing"); assert_eq!(validated.listing_id, listing_value.d_tag); assert_eq!(event.kind, KIND_LISTING); } @@ -167,46 +224,33 @@ fn listing_parse_rejects_non_listing_kind() { assert_eq!( listing::parse_event(&event).expect_err("listing kind error"), - listing::RadrootsTradeListingParseError::InvalidKind(KIND_PROFILE) + listing::RadrootsListingParseError::InvalidKind(KIND_PROFILE) ); } #[test] -fn trade_facade_wraps_build_parse_and_address_ops() { +fn order_facade_wraps_build_parse_and_address_ops() { let listing_value = sample_listing(); let listing_addr = format!("{KIND_LISTING}:seller:{}", listing_value.d_tag); - let payload = - RadrootsTradeMessagePayload::ListingValidateRequest(RadrootsTradeListingValidateRequest { - listing_event: None, - }); - - let parts = trade::build_envelope_draft( - "buyer", - payload.message_type(), - listing_addr.clone(), - None, - None, - None, - None, - &payload, - ) - .expect("trade envelope draft"); - - assert_eq!(parts.kind, KIND_TRADE_LISTING_VALIDATE_REQ); - - let parsed_addr = trade::parse_listing_address(&listing_addr).expect("listing address"); + let payload = sample_order_request(); + let parts = + order::build_order_request_draft(&listing_event_ptr(), &payload).expect("order draft"); + + assert_eq!(parts.as_wire_parts().kind, KIND_ORDER_REQUEST); + + let parsed_addr = order::parse_listing_address(&listing_addr).expect("listing address"); assert_eq!(parsed_addr.listing_id, listing_value.d_tag); let event = RadrootsNostrEvent { - id: "trade-event".into(), - author: "seller".into(), + id: "order-event".into(), + author: "buyer".into(), created_at: 2, - kind: parts.kind, - tags: parts.tags, - content: parts.content, + kind: parts.as_wire_parts().kind, + tags: parts.as_wire_parts().tags.clone(), + content: parts.as_wire_parts().content.clone(), sig: String::new(), }; - let envelope = trade::parse_envelope(&event).expect("trade envelope"); - assert_eq!(envelope.message_type, payload.message_type()); - assert_eq!(envelope.listing_addr, listing_addr); + let envelope = order::parse_order_request(&event).expect("order envelope"); + assert_eq!(envelope.payload.order_id, payload.order_id); + assert_eq!(envelope.payload.listing_addr, listing_addr); } diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -5,22 +5,22 @@ use radroots_core::{ RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; -use radroots_events::kinds::{KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_PROFILE}; +use radroots_events::kinds::{ + KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_ORDER_REQUEST, KIND_PROFILE, +}; use radroots_sdk::adapters::radrootsd::{ SdkRadrootsdBridgeJob, SdkRadrootsdBridgePublishResponse, SdkRadrootsdListingPublishRequest, - SdkRadrootsdPublicTradePublishRequest, SdkRadrootsdSignerAuthority, - SdkRadrootsdSignerSessionConnectRequest, SdkRadrootsdSignerSessionMode, + SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, + SdkRadrootsdSignerSessionMode, }; use radroots_sdk::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, RadrootsTradeListingParseError, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingParseError, + RadrootsListingProduct, RadrootsListingStatus, }; -use radroots_sdk::trade::{ - RadrootsTradeDiscountDecision, RadrootsTradeMessagePayload, RadrootsTradeMessageType, - RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, - RadrootsTradeOrderItem, RadrootsTradeOrderRequested, RadrootsTradeOrderResponse, - RadrootsTradeOrderRevision, RadrootsTradeOrderRevisionResponse, RadrootsTradePricingBasis, +use radroots_sdk::order::{ + RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, + RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, }; use radroots_sdk::{ RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsProfile, RadrootsProfileType, @@ -28,11 +28,9 @@ use radroots_sdk::{ SdkEnvironment, SdkPublishError, SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeError, SdkRadrootsdBridgeJobStatus, SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdProfilePublishOptions, - SdkRadrootsdPublicTradeMessage, SdkRadrootsdPublicTradePublishOptions, - SdkRadrootsdPublicTradePublishValidationError, SdkRadrootsdPublicTradeRoute, SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle, - SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView, SdkRadrootsdTradeChain, - SdkTransportMode, SdkTransportReceipt, SignerConfig, + SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView, SdkTransportMode, + SdkTransportReceipt, SignerConfig, }; use serde_json::{Value, json}; use std::collections::VecDeque; @@ -416,13 +414,13 @@ fn sample_farm() -> RadrootsFarm { } } -fn sample_trade_order_economics() -> RadrootsTradeOrderEconomics { - RadrootsTradeOrderEconomics { +fn sample_order_request_economics() -> RadrootsOrderEconomics { + RadrootsOrderEconomics { quote_id: "quote-1".to_owned(), 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_owned(), bin_count: 2, quantity_amount: RadrootsCoreDecimal::from(1u32), @@ -434,8 +432,8 @@ fn sample_trade_order_economics() -> RadrootsTradeOrderEconomics { RadrootsCoreCurrency::USD, ), }], - discounts: Vec::<RadrootsTradeOrderEconomicLine>::new(), - adjustments: Vec::<RadrootsTradeOrderEconomicLine>::new(), + discounts: Vec::<RadrootsOrderEconomicLine>::new(), + adjustments: Vec::<RadrootsOrderEconomicLine>::new(), subtotal: RadrootsCoreMoney::new( RadrootsCoreDecimal::from(40u32), RadrootsCoreCurrency::USD, @@ -452,46 +450,20 @@ fn sample_trade_order_economics() -> RadrootsTradeOrderEconomics { } } -fn sample_trade_order() -> RadrootsTradeOrderRequested { - RadrootsTradeOrderRequested { +fn sample_order_request() -> RadrootsOrderRequest { + RadrootsOrderRequest { order_id: "order-1".to_owned(), listing_addr: format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey: "buyer".to_owned(), seller_pubkey: "seller".to_owned(), - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: "bin-1".to_owned(), bin_count: 2, }], - economics: sample_trade_order_economics(), + economics: sample_order_request_economics(), } } -fn sample_public_trade_message() -> SdkRadrootsdPublicTradeMessage { - SdkRadrootsdPublicTradeMessage::order_response( - &sample_public_trade_route(), - &sample_trade_chain(), - RadrootsTradeOrderResponse { - accepted: true, - reason: None, - }, - ) - .expect("sample order response request should be valid") -} - -fn sample_public_trade_route() -> SdkRadrootsdPublicTradeRoute { - SdkRadrootsdPublicTradeRoute::new( - format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), - "order-1", - "buyer", - ) - .expect("sample public trade route should be valid") -} - -fn sample_trade_chain() -> SdkRadrootsdTradeChain { - SdkRadrootsdTradeChain::new("root-event-1", "prev-event-1") - .expect("sample trade chain should be valid") -} - fn listing_event_ptr_with_relays(relays: Option<&str>) -> RadrootsNostrEventPtr { RadrootsNostrEventPtr { id: "listing-event-1".to_owned(), @@ -1596,7 +1568,7 @@ async fn radrootsd_listing_publish_rejects_relay_transport_mode() -> TestResult< } #[tokio::test] -async fn radrootsd_trade_order_request_publish_accepts_session_handle() -> TestResult<()> { +async fn radrootsd_order_request_publish_accepts_session_handle() -> TestResult<()> { let (server, request_rx) = JsonRpcServer::spawn( Some("Bearer sdk-secret"), json!({ @@ -1612,7 +1584,7 @@ async fn radrootsd_trade_order_request_publish_accepts_session_handle() -> TestR "recovered_after_restart": false, "signer_mode": "nip46_session:session-order-1", "signer_session_id": "session-order-1", - "event_kind": RadrootsTradeMessageType::OrderRequest.kind(), + "event_kind": KIND_ORDER_REQUEST, "event_id": "event-order-1", "event_addr": format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), "relay_count": 1, @@ -1634,9 +1606,9 @@ async fn radrootsd_trade_order_request_publish_accepts_session_handle() -> TestR }); let receipt = client - .trade() + .order() .publish_order_request_via_radrootsd_with_options( - &sample_trade_order(), + &sample_order_request(), &listing_event_ptr_with_relays(Some("wss://radroots.org")), &options, ) @@ -1666,200 +1638,14 @@ async fn radrootsd_trade_order_request_publish_accepts_session_handle() -> TestR request_json["params"]["signer_authority"]["provider_signer_session_id"], "provider-session-order-1" ); - assert_eq!( - receipt.event_kind, - Some(RadrootsTradeMessageType::OrderRequest.kind()) - ); + assert_eq!(receipt.event_kind, Some(KIND_ORDER_REQUEST)); assert_eq!(receipt.event_id, Some("event-order-1".to_owned())); Ok(()) } #[tokio::test] -async fn radrootsd_trade_public_message_publish_accepts_typed_request() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-public-trade-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-response-1", - "command": "bridge.order.response", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-response-1", - "signer_session_id": "session-response-1", - "event_kind": RadrootsTradeMessageType::OrderResponse.kind(), - "event_id": "event-response-1", - "event_addr": format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-response-1").await?; - let client = radrootsd_test_client(server.endpoint())?; - let request = sample_public_trade_message(); - let options = SdkRadrootsdPublicTradePublishOptions::from_signer_session(&handle) - .with_idempotency_key("idem-response-1"); - - let receipt = client - .trade() - .publish_public_message_via_radrootsd_with_options(&request, &options) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.order.response"); - assert_eq!( - request_json["params"]["signer_session_id"], - "session-response-1" - ); - assert_eq!(request_json["params"]["idempotency_key"], "idem-response-1"); - assert_eq!( - request_json["params"]["listing_addr"], - format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg") - ); - assert_eq!(request_json["params"]["order_id"], "order-1"); - assert_eq!(request_json["params"]["counterparty_pubkey"], "buyer"); - assert_eq!(request_json["params"]["root_event_id"], "root-event-1"); - assert_eq!(request_json["params"]["prev_event_id"], "prev-event-1"); - assert_eq!(request_json["params"]["payload"]["accepted"], true); - assert_eq!( - receipt.event_kind, - Some(RadrootsTradeMessageType::OrderResponse.kind()) - ); - assert_eq!(receipt.event_id, Some("event-response-1".to_owned())); - - Ok(()) -} - -#[test] -fn public_trade_request_validation_rejects_order_request_payload() { - let request = SdkRadrootsdPublicTradePublishRequest::new( - sample_trade_order().listing_addr.clone(), - "order-1", - "buyer", - RadrootsTradeMessagePayload::TradeOrderRequested(sample_trade_order()), - ); - - let error = request - .validate_for_publish() - .expect_err("order request payload should use the dedicated trade order request path"); - - assert!( - error - .to_string() - .contains("trade.publish_order_request_via_radrootsd"), - "unexpected error: {error}" - ); -} - -#[test] -fn public_trade_request_validation_requires_listing_snapshot_for_order_revision() { - let error = SdkRadrootsdPublicTradePublishRequest::new( - format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), - "order-1", - "buyer", - RadrootsTradeMessagePayload::OrderRevision(RadrootsTradeOrderRevision { - revision_id: "revision-1".to_owned(), - changes: Vec::new(), - }), - ) - .validate_for_publish() - .expect_err("order revision without listing snapshot should be rejected"); - - assert_eq!( - error, - SdkRadrootsdPublicTradePublishValidationError::MissingListingSnapshot( - RadrootsTradeMessageType::OrderRevision, - ) - ); -} - -#[test] -fn public_trade_request_validation_requires_trade_chain_for_order_response() { - let error = SdkRadrootsdPublicTradePublishRequest::new( - format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), - "order-1", - "buyer", - RadrootsTradeMessagePayload::OrderResponse(RadrootsTradeOrderResponse { - accepted: true, - reason: None, - }), - ) - .validate_for_publish() - .expect_err("order response without trade chain should be rejected"); - - assert_eq!( - error, - SdkRadrootsdPublicTradePublishValidationError::MissingTradeChain( - RadrootsTradeMessageType::OrderResponse, - ) - ); -} - -#[test] -fn public_trade_request_validation_rejects_blank_listing_snapshot_relays() { - let error = SdkRadrootsdPublicTradePublishRequest::order_revision( - &sample_public_trade_route(), - listing_event_ptr_with_relays(Some(" ")), - &sample_trade_chain(), - RadrootsTradeOrderRevision { - revision_id: "revision-1".to_owned(), - changes: Vec::new(), - }, - ) - .expect_err("blank listing_event relays should be rejected"); - - assert_eq!( - error, - SdkRadrootsdPublicTradePublishValidationError::ListingSnapshotRelaysEmpty - ); -} - -#[test] -fn public_trade_request_validation_rejects_invalid_order_revision_accept_payload() { - let error = SdkRadrootsdPublicTradePublishRequest::order_revision_accept( - &sample_public_trade_route(), - &sample_trade_chain(), - RadrootsTradeOrderRevisionResponse { - accepted: false, - reason: Some("not accepted".to_owned()), - }, - ) - .expect_err("order revision accept must require accepted = true"); - - assert_eq!( - error, - SdkRadrootsdPublicTradePublishValidationError::InvalidOrderRevisionAcceptPayload - ); -} - -#[test] -fn public_trade_request_validation_rejects_invalid_discount_accept_payload() { - let error = SdkRadrootsdPublicTradePublishRequest::discount_accept( - &sample_public_trade_route(), - &sample_trade_chain(), - RadrootsTradeDiscountDecision::Decline { - reason: Some("declined".to_owned()), - }, - ) - .expect_err("discount accept must use an accept decision"); - - assert_eq!( - error, - SdkRadrootsdPublicTradePublishValidationError::InvalidDiscountAcceptPayload - ); -} - -#[tokio::test] -async fn radrootsd_sdk_workflow_chains_session_listing_trade_and_bridge_job() -> TestResult<()> { +async fn radrootsd_sdk_workflow_chains_session_listing_order_and_bridge_job() -> TestResult<()> { let (server, mut request_rx) = JsonRpcSequenceServer::spawn( Some("Bearer sdk-secret"), vec![ @@ -1908,7 +1694,7 @@ async fn radrootsd_sdk_workflow_chains_session_listing_trade_and_bridge_job() -> "recovered_after_restart": false, "signer_mode": "nip46_session:session-workflow-1", "signer_session_id": "session-workflow-1", - "event_kind": RadrootsTradeMessageType::OrderRequest.kind(), + "event_kind": KIND_ORDER_REQUEST, "event_id": "event-workflow-order", "event_addr": format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"), "relay_count": 1, @@ -1922,7 +1708,7 @@ async fn radrootsd_sdk_workflow_chains_session_listing_trade_and_bridge_job() -> "result": sample_bridge_job_json_for( "job-workflow-order", "bridge.order.request", - RadrootsTradeMessageType::OrderRequest.kind(), + KIND_ORDER_REQUEST, ) }), ], @@ -1953,42 +1739,39 @@ async fn radrootsd_sdk_workflow_chains_session_listing_trade_and_bridge_job() -> "session-workflow-1" ); - let trade_receipt = client - .trade() + let order_receipt = client + .order() .publish_order_request_via_radrootsd( - &sample_trade_order(), + &sample_order_request(), &listing_event_ptr_with_relays(Some("wss://radroots.org")), &handle, ) .await?; - let trade_request = request_rx.recv().await.expect("trade publish request"); - assert_eq!(trade_request["method"], "bridge.order.request"); + let order_request = request_rx.recv().await.expect("order publish request"); + assert_eq!(order_request["method"], "bridge.order.request"); assert_eq!( - trade_request["params"]["signer_session_id"], + order_request["params"]["signer_session_id"], "session-workflow-1" ); - assert_eq!(trade_request["params"]["order"]["order_id"], "order-1"); + assert_eq!(order_request["params"]["order"]["order_id"], "order-1"); assert_eq!( - trade_request["params"]["listing_event"]["id"], + order_request["params"]["listing_event"]["id"], "listing-event-1" ); - let trade_job = match &trade_receipt.transport_receipt { + let order_job = match &order_receipt.transport_receipt { SdkTransportReceipt::Radrootsd(receipt) => receipt.job(), SdkTransportReceipt::RelayDirect(_) => None, } - .expect("trade publish receipt should expose a bridge job ref"); + .expect("order publish receipt should expose a bridge job ref"); - let job_view = client.radrootsd().bridge().job(&trade_job).await?; + let job_view = client.radrootsd().bridge().job(&order_job).await?; let job_request = request_rx.recv().await.expect("bridge job request"); assert_eq!(job_request["method"], "bridge.job.status"); assert_eq!(job_request["params"]["job_id"], "job-workflow-order"); assert_eq!(listing_receipt.event_kind, Some(30402)); - assert_eq!( - trade_receipt.event_kind, - Some(RadrootsTradeMessageType::OrderRequest.kind()) - ); + assert_eq!(order_receipt.event_kind, Some(KIND_ORDER_REQUEST)); assert_eq!(job_view.job().job_id(), "job-workflow-order"); assert_eq!(job_view.command, "bridge.order.request"); assert_eq!(job_view.status, SdkRadrootsdBridgeJobStatus::Published); @@ -2157,9 +1940,7 @@ fn radrootsd_listing_request_from_event_rejects_listing_draft_kind() -> TestResu assert!(matches!( SdkRadrootsdListingPublishRequest::from_event(&event, "session-123", None, None), - Err(RadrootsTradeListingParseError::InvalidKind( - KIND_LISTING_DRAFT - )) + Err(RadrootsListingParseError::InvalidKind(KIND_LISTING_DRAFT)) )); Ok(()) diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs @@ -17,15 +17,14 @@ use radroots_sdk::listing::{ RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }; -use radroots_sdk::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_sdk::trade::{ - RadrootsActiveTradeFulfillmentState, RadrootsTradeBuyerReceipt, - RadrootsTradeFulfillmentUpdated, RadrootsTradeInventoryCommitment, RadrootsTradeOrderCancelled, - RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, - RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, - RadrootsTradeOrderRevisionDecision, RadrootsTradeOrderRevisionDecisionEvent, - RadrootsTradeOrderRevisionProposed, RadrootsTradePricingBasis, +use radroots_sdk::order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderFulfillmentState, + RadrootsOrderFulfillmentUpdate, RadrootsOrderInventoryCommitment, RadrootsOrderItem, + RadrootsOrderPricingBasis, RadrootsOrderReceipt, RadrootsOrderRequest, + RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, }; +use radroots_sdk::profile::{RadrootsProfile, RadrootsProfileType}; use radroots_sdk::{ RadrootsNostrEventPtr, RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, SdkPublishError, SdkTransportMode, SdkTransportReceipt, SignerConfig, @@ -220,25 +219,22 @@ fn listing_event_ptr() -> RadrootsNostrEventPtr { } } -fn sample_order_request( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsTradeOrderRequested { - RadrootsTradeOrderRequested { +fn sample_order_request(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderRequest { + RadrootsOrderRequest { order_id: "order-1".into(), listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, seller_pubkey, - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: "bin-1".into(), bin_count: 2, }], - economics: RadrootsTradeOrderEconomics { + economics: RadrootsOrderEconomics { quote_id: "quote-1".into(), quote_version: 1, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsTradeOrderEconomicItem { + items: vec![RadrootsOrderEconomicItem { bin_id: "bin-1".into(), bin_count: 2, quantity_amount: decimal("1"), @@ -257,17 +253,14 @@ fn sample_order_request( } } -fn sample_order_decision( - buyer_pubkey: String, - seller_pubkey: String, -) -> RadrootsTradeOrderDecisionEvent { - RadrootsTradeOrderDecisionEvent { +fn sample_order_decision(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderDecision { + RadrootsOrderDecision { order_id: "order-1".into(), listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, seller_pubkey, - decision: RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: "bin-1".into(), bin_count: 2, }], @@ -280,8 +273,8 @@ fn sample_order_revision_proposal( seller_pubkey: String, root_event_id: String, prev_event_id: String, -) -> RadrootsTradeOrderRevisionProposed { - RadrootsTradeOrderRevisionProposed { +) -> RadrootsOrderRevisionProposal { + RadrootsOrderRevisionProposal { revision_id: "revision-1".into(), order_id: "order-1".into(), listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), @@ -289,16 +282,16 @@ fn sample_order_revision_proposal( seller_pubkey, root_event_id, prev_event_id, - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: "bin-1".into(), bin_count: 3, }], - economics: RadrootsTradeOrderEconomics { + economics: RadrootsOrderEconomics { quote_id: "revision-quote-1".into(), quote_version: 2, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsTradeOrderEconomicItem { + items: vec![RadrootsOrderEconomicItem { bin_id: "bin-1".into(), bin_count: 3, quantity_amount: decimal("1"), @@ -319,10 +312,10 @@ fn sample_order_revision_proposal( } fn sample_order_revision_decision( - proposal: &RadrootsTradeOrderRevisionProposed, - decision: RadrootsTradeOrderRevisionDecision, -) -> RadrootsTradeOrderRevisionDecisionEvent { - RadrootsTradeOrderRevisionDecisionEvent { + proposal: &RadrootsOrderRevisionProposal, + decision: RadrootsOrderRevisionOutcome, +) -> RadrootsOrderRevisionDecision { + RadrootsOrderRevisionDecision { revision_id: proposal.revision_id.clone(), order_id: proposal.order_id.clone(), listing_addr: proposal.listing_addr.clone(), @@ -337,21 +330,21 @@ fn sample_order_revision_decision( fn sample_fulfillment_update( buyer_pubkey: String, seller_pubkey: String, -) -> RadrootsTradeFulfillmentUpdated { - RadrootsTradeFulfillmentUpdated { +) -> RadrootsOrderFulfillmentUpdate { + RadrootsOrderFulfillmentUpdate { order_id: "order-1".into(), listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, seller_pubkey, - status: RadrootsActiveTradeFulfillmentState::ReadyForPickup, + status: RadrootsOrderFulfillmentState::ReadyForPickup, } } fn sample_order_cancellation( buyer_pubkey: String, seller_pubkey: String, -) -> RadrootsTradeOrderCancelled { - RadrootsTradeOrderCancelled { +) -> RadrootsOrderCancellation { + RadrootsOrderCancellation { order_id: "order-1".into(), listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, @@ -360,8 +353,8 @@ fn sample_order_cancellation( } } -fn sample_buyer_receipt(buyer_pubkey: String, seller_pubkey: String) -> RadrootsTradeBuyerReceipt { - RadrootsTradeBuyerReceipt { +fn sample_buyer_receipt(buyer_pubkey: String, seller_pubkey: String) -> RadrootsOrderReceipt { + RadrootsOrderReceipt { order_id: "order-1".into(), listing_addr: format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"), buyer_pubkey, @@ -440,12 +433,12 @@ async fn relay_direct_order_request_publish_accepts_sdk_built_draft() -> TestRes }; let client = RadrootsSdkClient::from_config(config)?; let draft = client - .trade() + .order() .build_order_request_draft(&listing_event, &payload)?; assert_eq!(draft.as_wire_parts().kind, 3422); let receipt = client - .trade() + .order() .publish_order_request_draft_with_identity(&buyer_identity, draft) .await?; @@ -495,9 +488,9 @@ async fn relay_direct_order_request_publish_accepts_sdk_built_draft() -> TestRes ); assert!(relay_receipt.failed_relays.is_empty()); let envelope = client - .trade() + .order() .parse_order_request(&relay_receipt.event) - .expect("active order request"); + .expect("order request"); assert_eq!(envelope.order_id, payload.order_id); assert_eq!(envelope.listing_addr, payload.listing_addr); assert_eq!(envelope.payload.economics.quote_id, "quote-1"); @@ -528,12 +521,12 @@ async fn relay_direct_order_decision_publish_accepts_sdk_built_draft() -> TestRe let client = RadrootsSdkClient::from_config(config)?; let draft = client - .trade() + .order() .build_order_decision_draft(root_event_id, root_event_id, &payload)?; assert_eq!(draft.as_wire_parts().kind, 3423); let receipt = client - .trade() + .order() .publish_order_decision_draft_with_identity(&seller_identity, draft) .await?; @@ -587,9 +580,9 @@ async fn relay_direct_order_decision_publish_accepts_sdk_built_draft() -> TestRe ); assert!(relay_receipt.failed_relays.is_empty()); let envelope = client - .trade() + .order() .parse_order_decision(&relay_receipt.event) - .expect("active order decision"); + .expect("order decision"); assert_eq!(envelope.order_id, payload.order_id); assert_eq!(envelope.listing_addr, payload.listing_addr); assert_eq!(envelope.payload.decision, payload.decision); @@ -601,7 +594,7 @@ async fn relay_direct_order_decision_publish_accepts_sdk_built_draft() -> TestRe } #[tokio::test] -async fn relay_direct_trade_revision_publish_accepts_sdk_built_payloads() -> TestResult<()> { +async fn relay_direct_order_revision_publish_accepts_sdk_built_payloads() -> TestResult<()> { let relay = AckRelay::spawn().await?; let buyer_identity = RadrootsIdentity::generate(); let seller_identity = RadrootsIdentity::generate(); @@ -616,7 +609,7 @@ async fn relay_direct_trade_revision_publish_accepts_sdk_built_payloads() -> Tes decision_event_id.to_owned(), ); let decision = - sample_order_revision_decision(&proposal, RadrootsTradeOrderRevisionDecision::Accepted); + sample_order_revision_decision(&proposal, RadrootsOrderRevisionOutcome::Accepted); let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); config.transport = SdkTransportMode::RelayDirect; config.signer = SignerConfig::LocalIdentity; @@ -626,7 +619,7 @@ async fn relay_direct_trade_revision_publish_accepts_sdk_built_payloads() -> Tes let client = RadrootsSdkClient::from_config(config)?; let proposal_receipt = client - .trade() + .order() .publish_order_revision_proposal_with_identity( &seller_identity, root_event_id, @@ -635,7 +628,7 @@ async fn relay_direct_trade_revision_publish_accepts_sdk_built_payloads() -> Tes ) .await?; let decision_receipt = client - .trade() + .order() .publish_order_revision_decision_with_identity( &buyer_identity, root_event_id, @@ -670,9 +663,9 @@ async fn relay_direct_trade_revision_publish_accepts_sdk_built_payloads() -> Tes .contains(&vec!["e_prev".to_owned(), decision_event_id.to_owned()]) ); let envelope = client - .trade() + .order() .parse_order_revision_proposal(&relay_receipt.event) - .expect("active order revision proposal"); + .expect("order revision proposal"); assert_eq!(envelope.order_id, proposal.order_id); assert_eq!(envelope.listing_addr, proposal.listing_addr); assert_eq!(envelope.payload.revision_id, "revision-1"); @@ -703,15 +696,15 @@ async fn relay_direct_trade_revision_publish_accepts_sdk_built_payloads() -> Tes "order-revision-proposal-event-1".to_owned() ])); let envelope = client - .trade() + .order() .parse_order_revision_decision(&relay_receipt.event) - .expect("active order revision decision"); + .expect("order revision decision"); assert_eq!(envelope.order_id, decision.order_id); assert_eq!(envelope.listing_addr, decision.listing_addr); assert_eq!(envelope.payload.revision_id, decision.revision_id); assert_eq!( envelope.payload.decision, - RadrootsTradeOrderRevisionDecision::Accepted + RadrootsOrderRevisionOutcome::Accepted ); } SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), @@ -721,7 +714,7 @@ async fn relay_direct_trade_revision_publish_accepts_sdk_built_payloads() -> Tes } #[tokio::test] -async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> TestResult<()> { +async fn relay_direct_order_lifecycle_publish_accepts_sdk_built_payloads() -> TestResult<()> { let relay = AckRelay::spawn().await?; let buyer_identity = RadrootsIdentity::generate(); let seller_identity = RadrootsIdentity::generate(); @@ -742,7 +735,7 @@ async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> Te let client = RadrootsSdkClient::from_config(config)?; let fulfillment_receipt = client - .trade() + .order() .publish_fulfillment_update_with_identity( &seller_identity, root_event_id, @@ -751,7 +744,7 @@ async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> Te ) .await?; let cancellation_receipt = client - .trade() + .order() .publish_order_cancellation_with_identity( &buyer_identity, root_event_id, @@ -760,7 +753,7 @@ async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> Te ) .await?; let buyer_receipt = client - .trade() + .order() .publish_buyer_receipt_with_identity( &buyer_identity, root_event_id, @@ -796,7 +789,7 @@ async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> Te .contains(&vec!["e_prev".to_owned(), decision_event_id.to_owned()]) ); let envelope = client - .trade() + .order() .parse_fulfillment_update(&relay_receipt.event) .expect("active fulfillment update"); assert_eq!(envelope.order_id, fulfillment.order_id); @@ -829,9 +822,9 @@ async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> Te .contains(&vec!["e_prev".to_owned(), root_event_id.to_owned()]) ); let envelope = client - .trade() + .order() .parse_order_cancellation(&relay_receipt.event) - .expect("active order cancellation"); + .expect("order cancellation"); assert_eq!(envelope.order_id, cancellation.order_id); assert_eq!(envelope.listing_addr, cancellation.listing_addr); assert_eq!(envelope.payload.reason, cancellation.reason); @@ -862,7 +855,7 @@ async fn relay_direct_trade_lifecycle_publish_accepts_sdk_built_payloads() -> Te .contains(&vec!["e_prev".to_owned(), fulfillment_event_id.to_owned()]) ); let envelope = client - .trade() + .order() .parse_buyer_receipt(&relay_receipt.event) .expect("active buyer receipt"); assert_eq!(envelope.order_id, receipt.order_id); @@ -893,7 +886,7 @@ async fn relay_direct_order_decision_publish_builds_and_publishes_payload() -> T let client = RadrootsSdkClient::from_config(config)?; let receipt = client - .trade() + .order() .publish_order_decision_with_identity( &seller_identity, "order-request-event-1", @@ -926,7 +919,7 @@ async fn relay_direct_order_request_publish_builds_and_publishes_payload() -> Te let client = RadrootsSdkClient::from_config(config)?; let receipt = client - .trade() + .order() .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) .await?; @@ -950,7 +943,7 @@ async fn relay_direct_order_request_publish_rejects_radrootsd_transport_mode() - let client = RadrootsSdkClient::from_config(config)?; let error = client - .trade() + .order() .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) .await .expect_err("unsupported transport"); @@ -959,7 +952,7 @@ async fn relay_direct_order_request_publish_rejects_radrootsd_transport_mode() - error, SdkPublishError::UnsupportedTransport { transport: SdkTransportMode::Radrootsd, - operation: "trade.publish_order_request_with_identity", + operation: "order.publish_order_request_with_identity", } )); @@ -984,7 +977,7 @@ async fn relay_direct_order_request_publish_rejects_draft_only_signer_mode() -> let client = RadrootsSdkClient::from_config(config)?; let error = client - .trade() + .order() .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) .await .expect_err("unsupported signer mode"); @@ -995,7 +988,7 @@ async fn relay_direct_order_request_publish_rejects_draft_only_signer_mode() -> transport: SdkTransportMode::RelayDirect, signer: SignerConfig::DraftOnly, required: SignerConfig::LocalIdentity, - operation: "trade.publish_order_request_with_identity", + operation: "order.publish_order_request_with_identity", } )); @@ -1020,7 +1013,7 @@ async fn relay_direct_order_request_publish_rejects_invalid_economics() -> TestR let client = RadrootsSdkClient::from_config(config)?; let error = client - .trade() + .order() .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) .await .expect_err("invalid economics"); @@ -1048,7 +1041,7 @@ async fn relay_direct_order_request_publish_reports_setup_error_detail() -> Test let client = RadrootsSdkClient::from_config(config)?; let error = client - .trade() + .order() .publish_order_request_with_identity(&buyer_identity, &listing_event_ptr(), &payload) .await .expect_err("relay setup error"); @@ -1057,7 +1050,7 @@ async fn relay_direct_order_request_publish_reports_setup_error_detail() -> Test error, SdkPublishError::RelaySetup { transport: SdkTransportMode::RelayDirect, - operation: "trade.publish_order_request_with_identity", + operation: "order.publish_order_request_with_identity", target_relays, error: _, } if target_relays == vec!["ws://127.0.0.1:9".to_owned()] diff --git a/crates/trade/src/lib.rs b/crates/trade/src/lib.rs @@ -6,6 +6,5 @@ extern crate alloc; pub mod listing; pub mod order; pub mod prelude; -pub mod public_trade; #[cfg(feature = "serde_json")] pub mod validation_receipt; diff --git a/crates/trade/src/listing/codec.rs b/crates/trade/src/listing/codec.rs @@ -14,10 +14,10 @@ use radroots_events::listing::{ RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingImageSize, RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }; +pub(crate) use radroots_events::order::RadrootsListingParseError as ListingParseError; use radroots_events::plot::RadrootsPlotRef; use radroots_events::resource_area::RadrootsResourceAreaRef; use radroots_events::tags::{TAG_D, TAG_PUBLISHED_AT}; -pub(crate) use radroots_events::trade::RadrootsTradeListingParseError as TradeListingParseError; use radroots_events_codec::d_tag::is_d_tag_base64url; use radroots_events_codec::error::EventEncodeError; use radroots_events_codec::listing::tags::listing_tags_full; @@ -40,43 +40,42 @@ const TAG_EXPIRES_AT: &str = "expires_at"; const TAG_P: &str = "p"; const TAG_A: &str = "a"; -fn parse_decimal(s: &str, field: &str) -> Result<RadrootsCoreDecimal, TradeListingParseError> { +fn parse_decimal(s: &str, field: &str) -> Result<RadrootsCoreDecimal, ListingParseError> { s.parse::<RadrootsCoreDecimal>() - .map_err(|_| TradeListingParseError::InvalidNumber(field.to_string())) + .map_err(|_| ListingParseError::InvalidNumber(field.to_string())) } -fn parse_currency(s: &str) -> Result<RadrootsCoreCurrency, TradeListingParseError> { +fn parse_currency(s: &str) -> Result<RadrootsCoreCurrency, ListingParseError> { let upper = s.trim().to_ascii_uppercase(); - RadrootsCoreCurrency::from_str_upper(&upper) - .map_err(|_| TradeListingParseError::InvalidCurrency) + RadrootsCoreCurrency::from_str_upper(&upper).map_err(|_| ListingParseError::InvalidCurrency) } -fn parse_unit(s: &str) -> Result<RadrootsCoreUnit, TradeListingParseError> { +fn parse_unit(s: &str) -> Result<RadrootsCoreUnit, ListingParseError> { s.parse::<RadrootsCoreUnit>() - .map_err(|_| TradeListingParseError::InvalidUnit) + .map_err(|_| ListingParseError::InvalidUnit) } -fn parse_u64_tag_value(value: Option<&String>, field: &str) -> Result<u64, TradeListingParseError> { +fn parse_u64_tag_value(value: Option<&String>, field: &str) -> Result<u64, ListingParseError> { value - .ok_or_else(|| TradeListingParseError::InvalidTag(field.to_string()))? + .ok_or_else(|| ListingParseError::InvalidTag(field.to_string()))? .parse::<u64>() - .map_err(|_| TradeListingParseError::InvalidNumber(field.to_string())) + .map_err(|_| ListingParseError::InvalidNumber(field.to_string())) } -fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, TradeListingParseError> { +fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, ListingParseError> { let tag = tags .iter() .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D)) - .ok_or_else(|| TradeListingParseError::MissingTag(TAG_D.to_string()))?; + .ok_or_else(|| ListingParseError::MissingTag(TAG_D.to_string()))?; let value = tag .get(1) .map(|s| s.to_string()) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_D.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_D.to_string()))?; if value.trim().is_empty() { - return Err(TradeListingParseError::InvalidTag(TAG_D.to_string())); + return Err(ListingParseError::InvalidTag(TAG_D.to_string())); } if !is_d_tag_base64url(&value) { - return Err(TradeListingParseError::InvalidTag(TAG_D.to_string())); + return Err(ListingParseError::InvalidTag(TAG_D.to_string())); } Ok(value) } @@ -84,7 +83,7 @@ fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, TradeListingParseError> { pub fn listing_from_event_parts( tags: &[Vec<String>], content: &str, -) -> Result<RadrootsListing, TradeListingParseError> { +) -> Result<RadrootsListing, ListingParseError> { let d_tag = parse_d_tag(tags)?; let farm_ref = parse_farm_ref(tags)?; let farm_pubkey = parse_farm_pubkey(tags)?; @@ -98,24 +97,24 @@ pub fn listing_from_event_parts( if listing.d_tag.trim().is_empty() { listing.d_tag = d_tag; } else if listing.d_tag != d_tag { - return Err(TradeListingParseError::InvalidTag(TAG_D.to_string())); + return Err(ListingParseError::InvalidTag(TAG_D.to_string())); } if listing.farm.pubkey.trim().is_empty() || listing.farm.d_tag.trim().is_empty() { listing.farm = farm_ref; } else if listing.farm.pubkey != farm_ref.pubkey || listing.farm.d_tag != farm_ref.d_tag { - return Err(TradeListingParseError::InvalidTag(TAG_A.to_string())); + return Err(ListingParseError::InvalidTag(TAG_A.to_string())); } if listing.farm.pubkey != farm_pubkey { - return Err(TradeListingParseError::InvalidTag(TAG_P.to_string())); + return Err(ListingParseError::InvalidTag(TAG_P.to_string())); } if let Some(tag_area) = resource_area { match listing.resource_area.as_ref() { None => listing.resource_area = Some(tag_area), Some(area) => { if area.pubkey != tag_area.pubkey || area.d_tag != tag_area.d_tag { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_RESOURCE_AREA.to_string(), )); } @@ -129,7 +128,7 @@ pub fn listing_from_event_parts( if existing.pubkey != tag_plot.pubkey || existing.d_tag != tag_plot.d_tag { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PLOT.to_string(), )); } @@ -147,21 +146,19 @@ pub fn listing_from_event_parts( #[allow(dead_code)] pub fn listing_tags_build( listing: &RadrootsListing, -) -> Result<Vec<Vec<String>>, TradeListingParseError> { +) -> Result<Vec<Vec<String>>, ListingParseError> { listing_tags_full(listing).map_err(map_listing_tags_error) } #[allow(dead_code)] -fn map_listing_tags_error(err: EventEncodeError) -> TradeListingParseError { +fn map_listing_tags_error(err: EventEncodeError) -> ListingParseError { match err { EventEncodeError::EmptyRequiredField(field) => { - TradeListingParseError::MissingTag(field.to_string()) + ListingParseError::MissingTag(field.to_string()) } - EventEncodeError::InvalidField(field) => { - TradeListingParseError::InvalidTag(field.to_string()) - } - EventEncodeError::Json => TradeListingParseError::InvalidJson("discount".to_string()), - EventEncodeError::InvalidKind(kind) => TradeListingParseError::InvalidKind(kind), + EventEncodeError::InvalidField(field) => ListingParseError::InvalidTag(field.to_string()), + EventEncodeError::Json => ListingParseError::InvalidJson("discount".to_string()), + EventEncodeError::InvalidKind(kind) => ListingParseError::InvalidKind(kind), } } @@ -172,9 +169,9 @@ fn listing_from_tags( farm_pubkey: String, resource_area: Option<RadrootsResourceAreaRef>, plot: Option<RadrootsPlotRef>, -) -> Result<RadrootsListing, TradeListingParseError> { +) -> Result<RadrootsListing, ListingParseError> { if !is_d_tag_base64url(&d_tag) { - return Err(TradeListingParseError::InvalidTag(TAG_D.to_string())); + return Err(ListingParseError::InvalidTag(TAG_D.to_string())); } let mut product = RadrootsListingProduct { key: String::new(), @@ -230,7 +227,7 @@ fn listing_from_tags( if parse_structured_location { let primary = &tag[1]; if primary.trim().is_empty() { - return Err(TradeListingParseError::InvalidTag(TAG_LOCATION.to_string())); + return Err(ListingParseError::InvalidTag(TAG_LOCATION.to_string())); } let mut loc = RadrootsListingLocation { primary: primary.to_string(), @@ -262,11 +259,11 @@ fn listing_from_tags( } TAG_RADROOTS_PRIMARY_BIN => { let value = tag.get(1).and_then(|v| clean_value(v)).ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN.to_string()) + ListingParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN.to_string()) })?; if let Some(existing) = primary_bin_id.as_ref() { if existing != &value { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRIMARY_BIN.to_string(), )); } @@ -276,30 +273,21 @@ fn listing_from_tags( } TAG_RADROOTS_BIN => { if tag.len() < 4 { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_BIN.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string())); } if tag.len() > 7 { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_BIN.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string())); } - let bin_id = clean_value(&tag[1]).ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string()) - })?; + let bin_id = clean_value(&tag[1]) + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string()))?; let amount = parse_decimal(&tag[2], TAG_RADROOTS_BIN)?; let unit = parse_unit(&tag[3])?; if unit != unit.canonical_unit() { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_BIN.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string())); } let bin = upsert_bin(&mut bin_drafts, &bin_id, &mut bin_order); if bin.quantity.is_some() { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_BIN.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string())); } bin.quantity = Some(RadrootsCoreQuantity::new(amount, unit)); @@ -315,27 +303,24 @@ fn listing_from_tags( } } [_, _, _, _, _] => { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_BIN.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string())); } _ => {} } } TAG_RADROOTS_PRICE => { if tag.len() < 6 { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRICE.to_string(), )); } if tag.len() > 8 { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRICE.to_string(), )); } - let bin_id = clean_value(&tag[1]).ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_RADROOTS_PRICE.to_string()) - })?; + let bin_id = clean_value(&tag[1]) + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_PRICE.to_string()))?; let amount = parse_decimal(&tag[2], TAG_RADROOTS_PRICE)?; let currency = parse_currency(&tag[3])?; let per_amount = parse_decimal(&tag[4], TAG_RADROOTS_PRICE)?; @@ -345,13 +330,13 @@ fn listing_from_tags( RadrootsCoreQuantity::new(per_amount, per_unit), ); if !price_per_canonical_unit.is_price_per_canonical_unit() { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRICE.to_string(), )); } let bin = upsert_bin(&mut bin_drafts, &bin_id, &mut bin_order); if bin.price_per_canonical_unit.is_some() { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRICE.to_string(), )); } @@ -359,7 +344,7 @@ fn listing_from_tags( match tag.as_slice() { [_, _, _, _, _, _, _] => { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRICE.to_string(), )); } @@ -374,7 +359,7 @@ fn listing_from_tags( } TAG_RADROOTS_DISCOUNT => { let payload = tag.get(1).ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_RADROOTS_DISCOUNT.to_string()) + ListingParseError::InvalidTag(TAG_RADROOTS_DISCOUNT.to_string()) })?; let discount = parse_discount(payload)?; discounts.push(discount); @@ -387,26 +372,25 @@ fn listing_from_tags( TAG_INVENTORY => { let value = tag .get(1) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_INVENTORY.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_INVENTORY.to_string()))?; inventory_available = Some(parse_decimal(value, TAG_INVENTORY)?); } TAG_RADROOTS_AVAILABILITY_START => { let value = tag.get(1).ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_RADROOTS_AVAILABILITY_START.to_string()) + ListingParseError::InvalidTag(TAG_RADROOTS_AVAILABILITY_START.to_string()) })?; availability_start = Some(value.parse::<u64>().map_err(|_| { - TradeListingParseError::InvalidNumber( - TAG_RADROOTS_AVAILABILITY_START.to_string(), - ) + ListingParseError::InvalidNumber(TAG_RADROOTS_AVAILABILITY_START.to_string()) })?); } TAG_EXPIRES_AT => { - let value = tag.get(1).ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_EXPIRES_AT.to_string()) - })?; - availability_end = Some(value.parse::<u64>().map_err(|_| { - TradeListingParseError::InvalidNumber(TAG_EXPIRES_AT.to_string()) - })?); + let value = tag + .get(1) + .ok_or_else(|| ListingParseError::InvalidTag(TAG_EXPIRES_AT.to_string()))?; + availability_end = + Some(value.parse::<u64>().map_err(|_| { + ListingParseError::InvalidNumber(TAG_EXPIRES_AT.to_string()) + })?); } TAG_STATUS => { let status = tag.get(1).and_then(|v| clean_value(v)).unwrap_or_default(); @@ -430,7 +414,7 @@ fn listing_from_tags( TAG_IMAGE => { let url = tag .get(1) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_IMAGE.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_IMAGE.to_string()))?; if url.trim().is_empty() { continue; } @@ -458,15 +442,15 @@ fn listing_from_tags( }); if farm_pubkey != farm_ref.pubkey { - return Err(TradeListingParseError::InvalidTag(TAG_P.to_string())); + return Err(ListingParseError::InvalidTag(TAG_P.to_string())); } let primary_bin_id = primary_bin_id .and_then(|v| clean_value(&v)) - .ok_or_else(|| TradeListingParseError::MissingTag(TAG_RADROOTS_PRIMARY_BIN.to_string()))?; + .ok_or_else(|| ListingParseError::MissingTag(TAG_RADROOTS_PRIMARY_BIN.to_string()))?; let bins = build_bins(bin_drafts)?; if !bins.iter().any(|bin| bin.bin_id == primary_bin_id) { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRIMARY_BIN.to_string(), )); } @@ -497,7 +481,7 @@ fn listing_from_tags( }) } -fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsFarmRef, TradeListingParseError> { +fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsFarmRef, ListingParseError> { for tag in tags .iter() .filter(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_A)) @@ -505,95 +489,94 @@ fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsFarmRef, TradeListingP let value = tag .get(1) .map(|s| s.to_string()) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_A.to_string()))?; let mut parts = value.splitn(3, ':'); let kind = parts .next() .and_then(|v| v.parse::<u32>().ok()) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_A.to_string()))?; if kind != KIND_FARM { continue; } let pubkey = parts .next() - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))? + .ok_or_else(|| ListingParseError::InvalidTag(TAG_A.to_string()))? .to_string(); let d_tag = parts .next() - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))? + .ok_or_else(|| ListingParseError::InvalidTag(TAG_A.to_string()))? .to_string(); if pubkey.trim().is_empty() || d_tag.trim().is_empty() { - return Err(TradeListingParseError::InvalidTag(TAG_A.to_string())); + return Err(ListingParseError::InvalidTag(TAG_A.to_string())); } if !is_d_tag_base64url(&d_tag) { - return Err(TradeListingParseError::InvalidTag(TAG_A.to_string())); + return Err(ListingParseError::InvalidTag(TAG_A.to_string())); } return Ok(RadrootsFarmRef { pubkey, d_tag }); } - Err(TradeListingParseError::MissingTag(TAG_A.to_string())) + Err(ListingParseError::MissingTag(TAG_A.to_string())) } -fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, TradeListingParseError> { +fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, ListingParseError> { let tag = tags .iter() .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_P)) - .ok_or_else(|| TradeListingParseError::MissingTag(TAG_P.to_string()))?; + .ok_or_else(|| ListingParseError::MissingTag(TAG_P.to_string()))?; let value = tag .get(1) .map(|s| s.to_string()) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_P.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_P.to_string()))?; if value.trim().is_empty() { - return Err(TradeListingParseError::InvalidTag(TAG_P.to_string())); + return Err(ListingParseError::InvalidTag(TAG_P.to_string())); } Ok(value) } fn parse_resource_area( tags: &[Vec<String>], -) -> Result<Option<RadrootsResourceAreaRef>, TradeListingParseError> { +) -> Result<Option<RadrootsResourceAreaRef>, ListingParseError> { let tag = tags .iter() .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_RADROOTS_RESOURCE_AREA)); let Some(tag) = tag else { return Ok(None); }; - let value = tag.get(1).map(|s| s.to_string()).ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()) - })?; + let value = tag + .get(1) + .map(|s| s.to_string()) + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))?; let mut parts = value.splitn(3, ':'); let kind = parts .next() .and_then(|v| v.parse::<u32>().ok()) - .ok_or_else(|| { - TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()) - })?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))?; if kind != KIND_RESOURCE_AREA { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_RESOURCE_AREA.to_string(), )); } let pubkey = parts .next() - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))? + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))? .to_string(); let d_tag = parts .next() - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))? + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))? .to_string(); if pubkey.trim().is_empty() || d_tag.trim().is_empty() { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_RESOURCE_AREA.to_string(), )); } if !is_d_tag_base64url(&d_tag) { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_RESOURCE_AREA.to_string(), )); } Ok(Some(RadrootsResourceAreaRef { pubkey, d_tag })) } -fn parse_plot_ref(tags: &[Vec<String>]) -> Result<Option<RadrootsPlotRef>, TradeListingParseError> { +fn parse_plot_ref(tags: &[Vec<String>]) -> Result<Option<RadrootsPlotRef>, ListingParseError> { let tag = tags .iter() .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_RADROOTS_PLOT)); @@ -603,34 +586,28 @@ fn parse_plot_ref(tags: &[Vec<String>]) -> Result<Option<RadrootsPlotRef>, Trade let value = tag .get(1) .map(|s| s.to_string()) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?; let mut parts = value.splitn(3, ':'); let kind = parts .next() .and_then(|v| v.parse::<u32>().ok()) - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?; + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?; if kind != KIND_PLOT { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_PLOT.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string())); } let pubkey = parts .next() - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))? + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))? .to_string(); let d_tag = parts .next() - .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))? + .ok_or_else(|| ListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))? .to_string(); if pubkey.trim().is_empty() || d_tag.trim().is_empty() { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_PLOT.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string())); } if !is_d_tag_base64url(&d_tag) { - return Err(TradeListingParseError::InvalidTag( - TAG_RADROOTS_PLOT.to_string(), - )); + return Err(ListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string())); } Ok(Some(RadrootsPlotRef { pubkey, d_tag })) } @@ -709,16 +686,16 @@ mod tests { .expect("listing") } - fn parse_error_tag(error: TradeListingParseError) -> String { + fn parse_error_tag(error: ListingParseError) -> String { match error { - TradeListingParseError::InvalidKind(_) => "kind".to_string(), - TradeListingParseError::MissingTag(tag) => tag, - TradeListingParseError::InvalidTag(tag) => tag, - TradeListingParseError::InvalidNumber(field) => field, - TradeListingParseError::InvalidUnit => "unit".to_string(), - TradeListingParseError::InvalidCurrency => "currency".to_string(), - TradeListingParseError::InvalidJson(field) => field, - TradeListingParseError::InvalidDiscount(kind) => kind, + ListingParseError::InvalidKind(_) => "kind".to_string(), + ListingParseError::MissingTag(tag) => tag, + ListingParseError::InvalidTag(tag) => tag, + ListingParseError::InvalidNumber(field) => field, + ListingParseError::InvalidUnit => "unit".to_string(), + ListingParseError::InvalidCurrency => "currency".to_string(), + ListingParseError::InvalidJson(field) => field, + ListingParseError::InvalidDiscount(kind) => kind, } } @@ -827,13 +804,13 @@ mod tests { #[test] fn parse_error_display_covers_all_variants() { let errors = [ - TradeListingParseError::MissingTag("d".into()), - TradeListingParseError::InvalidTag("a".into()), - TradeListingParseError::InvalidNumber("n".into()), - TradeListingParseError::InvalidUnit, - TradeListingParseError::InvalidCurrency, - TradeListingParseError::InvalidJson("j".into()), - TradeListingParseError::InvalidDiscount("x".into()), + ListingParseError::MissingTag("d".into()), + ListingParseError::InvalidTag("a".into()), + ListingParseError::InvalidNumber("n".into()), + ListingParseError::InvalidUnit, + ListingParseError::InvalidCurrency, + ListingParseError::InvalidJson("j".into()), + ListingParseError::InvalidDiscount("x".into()), ]; for error in errors { assert!(!error.to_string().trim().is_empty()); @@ -2228,7 +2205,7 @@ mod tests { TAG_RADROOTS_DISCOUNT.to_string() ); assert_eq!( - parse_error_tag(TradeListingParseError::InvalidJson("x".into())), + parse_error_tag(ListingParseError::InvalidJson("x".into())), "x".to_string() ); @@ -2385,16 +2362,16 @@ fn parse_image_size(value: &str) -> Option<RadrootsListingImageSize> { Some(RadrootsListingImageSize { w, h }) } -fn parse_discount(payload: &str) -> Result<RadrootsCoreDiscount, TradeListingParseError> { +fn parse_discount(payload: &str) -> Result<RadrootsCoreDiscount, ListingParseError> { #[cfg(feature = "serde_json")] { serde_json::from_str(payload) - .map_err(|_| TradeListingParseError::InvalidDiscount(TAG_RADROOTS_DISCOUNT.to_string())) + .map_err(|_| ListingParseError::InvalidDiscount(TAG_RADROOTS_DISCOUNT.to_string())) } #[cfg(not(feature = "serde_json"))] { let _ = payload; - Err(TradeListingParseError::InvalidJson("discount".to_string())) + Err(ListingParseError::InvalidJson("discount".to_string())) } } @@ -2436,20 +2413,18 @@ fn upsert_bin<'a>( &mut bins[idx] } -fn build_bins( - mut drafts: Vec<BinDraft>, -) -> Result<Vec<RadrootsListingBin>, TradeListingParseError> { +fn build_bins(mut drafts: Vec<BinDraft>) -> Result<Vec<RadrootsListingBin>, ListingParseError> { drafts.sort_by_key(|draft| draft.order_index); let mut bins = Vec::with_capacity(drafts.len()); for draft in drafts { let quantity = draft .quantity - .ok_or_else(|| TradeListingParseError::MissingTag(TAG_RADROOTS_BIN.to_string()))?; + .ok_or_else(|| ListingParseError::MissingTag(TAG_RADROOTS_BIN.to_string()))?; let price = draft .price_per_canonical_unit - .ok_or_else(|| TradeListingParseError::MissingTag(TAG_RADROOTS_PRICE.to_string()))?; + .ok_or_else(|| ListingParseError::MissingTag(TAG_RADROOTS_PRICE.to_string()))?; if quantity.unit != price.quantity.unit { - return Err(TradeListingParseError::InvalidTag( + return Err(ListingParseError::InvalidTag( TAG_RADROOTS_PRICE.to_string(), )); } diff --git a/crates/trade/src/listing/contract.rs b/crates/trade/src/listing/contract.rs @@ -1,70 +0,0 @@ -#![forbid(unsafe_code)] - -#[allow(unused_imports)] -#[cfg(feature = "serde_json")] -use radroots_events::RadrootsNostrEvent; -#[allow(unused_imports)] -pub(crate) 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, TRADE_LISTING_KINDS, is_trade_listing_kind, - }, - trade::{ - RADROOTS_TRADE_ENVELOPE_VERSION as TRADE_LISTING_ENVELOPE_VERSION, - RADROOTS_TRADE_LISTING_DOMAIN as TRADE_LISTING_DOMAIN, RadrootsTradeAnswer as TradeAnswer, - RadrootsTradeDiscountDecision as TradeDiscountDecision, - RadrootsTradeDiscountOffer as TradeDiscountOffer, - RadrootsTradeDiscountRequest as TradeDiscountRequest, - RadrootsTradeEconomicActor as TradeEconomicActor, - RadrootsTradeEconomicEffect as TradeEconomicEffect, - RadrootsTradeEconomicLineKind as TradeEconomicLineKind, - RadrootsTradeEnvelope as TradeListingEnvelope, - RadrootsTradeEnvelopeError as TradeListingEnvelopeError, - RadrootsTradeFulfillmentStatus as TradeFulfillmentStatus, - RadrootsTradeFulfillmentUpdate as TradeFulfillmentUpdate, - RadrootsTradeListingCancel as TradeListingCancel, - RadrootsTradeListingParseError as TradeListingParseError, - RadrootsTradeListingValidateRequest as TradeListingValidateRequest, - RadrootsTradeListingValidateResult as TradeListingValidateResult, - RadrootsTradeListingValidationError as TradeListingValidationError, - RadrootsTradeMessagePayload as TradeListingMessagePayload, - RadrootsTradeMessageType as TradeListingMessageType, - RadrootsTradeOrderChange as TradeOrderChange, - RadrootsTradeOrderEconomicItem as TradeOrderEconomicItem, - RadrootsTradeOrderEconomicLine as TradeOrderEconomicLine, - RadrootsTradeOrderEconomics as TradeOrderEconomics, - RadrootsTradeOrderItem as TradeOrderItem, RadrootsTradeOrderRequested as TradeOrder, - RadrootsTradeOrderResponse as TradeOrderResponse, - RadrootsTradeOrderRevision as TradeOrderRevision, - RadrootsTradeOrderRevisionResponse as TradeOrderRevisionResponse, - RadrootsTradeOrderStatus as TradeOrderStatus, - RadrootsTradePricingBasis as TradePricingBasis, RadrootsTradeQuestion as TradeQuestion, - RadrootsTradeReceipt as TradeReceipt, - }, -}; -#[allow(unused_imports)] -#[cfg(feature = "serde_json")] -pub(crate) use radroots_events_codec::trade::{ - decode::{ - RadrootsTradeEnvelopeParseError as TradeListingEnvelopeParseError, - RadrootsTradeListingAddress as TradeListingAddress, - RadrootsTradeListingAddressError as TradeListingAddressError, trade_envelope_from_event, - }, - encode::trade_envelope_event_build as trade_listing_envelope_event_build, -}; - -#[cfg(feature = "serde_json")] -use serde::de::DeserializeOwned; - -#[cfg(feature = "serde_json")] -pub(crate) fn trade_listing_envelope_from_event<T: DeserializeOwned>( - event: &RadrootsNostrEvent, -) -> Result<TradeListingEnvelope<T>, TradeListingEnvelopeParseError> { - trade_envelope_from_event(event) -} diff --git a/crates/trade/src/listing/dvm.rs b/crates/trade/src/listing/dvm.rs @@ -1,1208 +0,0 @@ -#![forbid(unsafe_code)] - -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; - -#[cfg(feature = "serde_json")] -use radroots_events::{RadrootsNostrEvent, tags::TAG_D}; -use radroots_events::{RadrootsNostrEventPtr, kinds::KIND_PROFILE}; -use radroots_events_codec::d_tag::is_d_tag_base64url; -#[cfg(feature = "serde_json")] -use radroots_events_codec::error::{EventEncodeError, EventParseError}; -#[cfg(feature = "serde_json")] -use serde::de::DeserializeOwned; - -use crate::listing::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, is_trade_listing_kind, -}; -use crate::listing::order::{ - TradeAnswer, TradeDiscountDecision, TradeDiscountOffer, TradeDiscountRequest, - TradeFulfillmentUpdate, TradeOrder, TradeOrderRevision, TradeQuestion, TradeReceipt, -}; -#[cfg(feature = "serde_json")] -use crate::listing::tags::trade_listing_dvm_tags; -#[cfg(feature = "serde_json")] -use crate::listing::tags::{ - TAG_LISTING_EVENT, parse_trade_listing_counterparty_tag, parse_trade_listing_event_tag, - parse_trade_listing_prev_tag, parse_trade_listing_root_tag, -}; -use crate::listing::validation::TradeListingValidationError; - -pub const TRADE_LISTING_DOMAIN: &str = "trade:listing"; -pub const TRADE_LISTING_ENVELOPE_VERSION: u16 = 1; - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TradeListingDomain { - #[cfg_attr(feature = "serde", serde(rename = "trade:listing"))] - TradeListing, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TradeListingMessageType { - ListingValidateRequest, - ListingValidateResult, - OrderRequest, - OrderResponse, - OrderRevision, - OrderRevisionAccept, - OrderRevisionDecline, - Question, - Answer, - DiscountRequest, - DiscountOffer, - DiscountAccept, - DiscountDecline, - Cancel, - FulfillmentUpdate, - Receipt, -} - -impl TradeListingMessageType { - #[inline] - pub const fn from_kind(kind: u16) -> Option<Self> { - match kind { - KIND_TRADE_LISTING_VALIDATE_REQ => { - Some(TradeListingMessageType::ListingValidateRequest) - } - KIND_TRADE_LISTING_VALIDATE_RES => Some(TradeListingMessageType::ListingValidateResult), - KIND_TRADE_LISTING_ORDER_REQ => Some(TradeListingMessageType::OrderRequest), - KIND_TRADE_LISTING_ORDER_RES => Some(TradeListingMessageType::OrderResponse), - KIND_TRADE_LISTING_ORDER_REVISION_REQ => Some(TradeListingMessageType::OrderRevision), - KIND_TRADE_LISTING_ORDER_REVISION_RES => None, - KIND_TRADE_LISTING_QUESTION_REQ => Some(TradeListingMessageType::Question), - KIND_TRADE_LISTING_ANSWER_RES => Some(TradeListingMessageType::Answer), - KIND_TRADE_LISTING_DISCOUNT_REQ => Some(TradeListingMessageType::DiscountRequest), - KIND_TRADE_LISTING_DISCOUNT_OFFER_RES => Some(TradeListingMessageType::DiscountOffer), - KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ => Some(TradeListingMessageType::DiscountAccept), - KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ => { - Some(TradeListingMessageType::DiscountDecline) - } - KIND_TRADE_LISTING_CANCEL_REQ => Some(TradeListingMessageType::Cancel), - KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ => { - Some(TradeListingMessageType::FulfillmentUpdate) - } - KIND_TRADE_LISTING_RECEIPT_REQ => Some(TradeListingMessageType::Receipt), - _ => None, - } - } - - #[inline] - pub const fn kind(self) -> u16 { - match self { - TradeListingMessageType::ListingValidateRequest => KIND_TRADE_LISTING_VALIDATE_REQ, - TradeListingMessageType::ListingValidateResult => KIND_TRADE_LISTING_VALIDATE_RES, - 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, - } - } - - #[inline] - pub const fn requires_order_id(self) -> bool { - !matches!( - self, - TradeListingMessageType::ListingValidateRequest - | TradeListingMessageType::ListingValidateResult - ) - } - - #[inline] - pub const fn requires_listing_snapshot(self) -> bool { - matches!( - self, - TradeListingMessageType::OrderRequest - | TradeListingMessageType::OrderRevision - | TradeListingMessageType::DiscountRequest - | TradeListingMessageType::DiscountOffer - ) - } - - #[inline] - pub const fn requires_trade_chain(self) -> bool { - !matches!( - self, - TradeListingMessageType::ListingValidateRequest - | TradeListingMessageType::ListingValidateResult - | TradeListingMessageType::OrderRequest - ) - } - - #[inline] - pub const fn is_request(self) -> bool { - matches!( - self, - TradeListingMessageType::ListingValidateRequest - | TradeListingMessageType::OrderRequest - | TradeListingMessageType::OrderRevision - | TradeListingMessageType::Question - | TradeListingMessageType::DiscountRequest - | TradeListingMessageType::DiscountAccept - | TradeListingMessageType::DiscountDecline - | TradeListingMessageType::Cancel - | TradeListingMessageType::FulfillmentUpdate - | TradeListingMessageType::Receipt - ) - } - - #[inline] - pub const fn is_result(self) -> bool { - !self.is_request() - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeListingEnvelope<T> { - pub version: u16, - pub domain: TradeListingDomain, - #[cfg_attr(feature = "serde", serde(rename = "type"))] - pub message_type: TradeListingMessageType, - pub order_id: Option<String>, - pub listing_addr: String, - pub payload: T, -} - -impl<T> TradeListingEnvelope<T> { - #[inline] - pub fn new( - message_type: TradeListingMessageType, - listing_addr: impl Into<String>, - order_id: Option<String>, - payload: T, - ) -> Self { - Self { - version: TRADE_LISTING_ENVELOPE_VERSION, - domain: TradeListingDomain::TradeListing, - message_type, - order_id, - listing_addr: listing_addr.into(), - payload, - } - } - - pub fn validate(&self) -> Result<(), TradeListingEnvelopeError> { - if self.version != TRADE_LISTING_ENVELOPE_VERSION { - return Err(TradeListingEnvelopeError::InvalidVersion { - expected: TRADE_LISTING_ENVELOPE_VERSION, - got: self.version, - }); - } - if self.listing_addr.trim().is_empty() { - return Err(TradeListingEnvelopeError::MissingListingAddr); - } - if self.message_type.requires_order_id() { - match self.order_id.as_deref() { - Some(id) if !id.trim().is_empty() => {} - _ => return Err(TradeListingEnvelopeError::MissingOrderId), - } - } - Ok(()) - } -} - -#[cfg(feature = "serde_json")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeListingEnvelopeEvent { - pub kind: u16, - pub content: String, - pub tags: Vec<Vec<String>>, -} - -#[cfg(feature = "serde_json")] -fn map_envelope_error(error: TradeListingEnvelopeError) -> EventEncodeError { - match error { - TradeListingEnvelopeError::MissingOrderId => { - EventEncodeError::EmptyRequiredField("order_id") - } - TradeListingEnvelopeError::MissingListingAddr => { - EventEncodeError::EmptyRequiredField("listing_addr") - } - TradeListingEnvelopeError::InvalidVersion { .. } => { - EventEncodeError::InvalidField("version") - } - } -} - -#[cfg(feature = "serde_json")] -pub fn trade_listing_envelope_event_build( - recipient_pubkey: impl Into<String>, - message_type: TradeListingMessageType, - listing_addr: impl Into<String>, - order_id: Option<String>, - listing_event: Option<&RadrootsNostrEventPtr>, - root_event_id: Option<&str>, - prev_event_id: Option<&str>, - payload: &TradeListingMessagePayload, -) -> Result<TradeListingEnvelopeEvent, EventEncodeError> { - if payload.message_type() != message_type { - return Err(EventEncodeError::InvalidField("payload")); - } - if message_type.requires_listing_snapshot() && listing_event.is_none() { - return Err(EventEncodeError::EmptyRequiredField("listing_event.id")); - } - if message_type.requires_trade_chain() { - if root_event_id.is_none() { - return Err(EventEncodeError::EmptyRequiredField("root_event_id")); - } - if prev_event_id.is_none() { - return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); - } - } - let listing_addr = listing_addr.into(); - let envelope = TradeListingEnvelope::new( - message_type, - listing_addr.clone(), - order_id.clone(), - payload.clone(), - ); - envelope.validate().map_err(map_envelope_error)?; - let content = serde_json::to_string(&envelope).map_err(|_| EventEncodeError::Json)?; - let tags = trade_listing_dvm_tags( - recipient_pubkey, - &listing_addr, - order_id.as_deref(), - listing_event, - root_event_id, - prev_event_id, - )?; - Ok(TradeListingEnvelopeEvent { - kind: message_type.kind(), - content, - tags, - }) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TradeListingEnvelopeError { - InvalidVersion { expected: u16, got: u16 }, - MissingOrderId, - MissingListingAddr, -} - -impl core::fmt::Display for TradeListingEnvelopeError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - TradeListingEnvelopeError::InvalidVersion { expected, got } => { - write!( - f, - "invalid envelope version: expected {expected}, got {got}" - ) - } - TradeListingEnvelopeError::MissingOrderId => { - write!(f, "missing order_id for order-scoped message") - } - TradeListingEnvelopeError::MissingListingAddr => { - write!(f, "missing listing_addr") - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for TradeListingEnvelopeError {} - -#[cfg(feature = "serde_json")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TradeListingEnvelopeParseError { - InvalidKind(u32), - InvalidJson, - InvalidEnvelope(TradeListingEnvelopeError), - MessageTypeKindMismatch { - event_kind: u32, - message_type: TradeListingMessageType, - }, - MissingTag(&'static str), - InvalidTag(&'static str), - ListingAddrTagMismatch, - OrderIdTagMismatch, - InvalidListingAddr(TradeListingAddressError), -} - -#[cfg(feature = "serde_json")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeListingEventContext { - pub counterparty_pubkey: String, - pub listing_event: Option<RadrootsNostrEventPtr>, - pub root_event_id: Option<String>, - pub prev_event_id: Option<String>, -} - -#[cfg(feature = "serde_json")] -impl core::fmt::Display for TradeListingEnvelopeParseError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - TradeListingEnvelopeParseError::InvalidKind(kind) => { - write!(f, "invalid trade listing event kind: {kind}") - } - TradeListingEnvelopeParseError::InvalidJson => { - write!(f, "invalid trade listing envelope json") - } - TradeListingEnvelopeParseError::InvalidEnvelope(error) => write!(f, "{error}"), - TradeListingEnvelopeParseError::MessageTypeKindMismatch { - event_kind, - message_type, - } => write!( - f, - "trade listing envelope type {message_type:?} does not match event kind {event_kind}" - ), - TradeListingEnvelopeParseError::MissingTag(tag) => { - write!(f, "missing required trade listing tag: {tag}") - } - TradeListingEnvelopeParseError::InvalidTag(tag) => { - write!(f, "invalid trade listing tag: {tag}") - } - TradeListingEnvelopeParseError::ListingAddrTagMismatch => { - write!(f, "trade listing address tag does not match envelope") - } - TradeListingEnvelopeParseError::OrderIdTagMismatch => { - write!(f, "trade order id tag does not match envelope") - } - TradeListingEnvelopeParseError::InvalidListingAddr(error) => write!(f, "{error}"), - } - } -} - -#[cfg(feature = "std")] -#[cfg(feature = "serde_json")] -impl std::error::Error for TradeListingEnvelopeParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - TradeListingEnvelopeParseError::InvalidEnvelope(error) => Some(error), - TradeListingEnvelopeParseError::InvalidListingAddr(error) => Some(error), - _ => None, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeListingAddress { - pub kind: u16, - pub seller_pubkey: String, - pub listing_id: String, -} - -impl TradeListingAddress { - pub fn parse(addr: &str) -> Result<Self, TradeListingAddressError> { - let (kind_raw, seller_and_listing) = addr - .split_once(':') - .ok_or(TradeListingAddressError::InvalidFormat)?; - let (seller_pubkey_raw, listing_id_raw) = seller_and_listing - .split_once(':') - .ok_or(TradeListingAddressError::InvalidFormat)?; - if listing_id_raw.contains(':') { - return Err(TradeListingAddressError::InvalidFormat); - } - let kind = kind_raw - .parse::<u16>() - .map_err(|_| TradeListingAddressError::InvalidFormat)?; - let seller_pubkey = seller_pubkey_raw.to_string(); - let listing_id = listing_id_raw.to_string(); - if kind == KIND_PROFILE as u16 - || seller_pubkey.trim().is_empty() - || listing_id.trim().is_empty() - || !is_d_tag_base64url(&listing_id) - { - return Err(TradeListingAddressError::InvalidFormat); - } - Ok(Self { - kind, - seller_pubkey, - listing_id, - }) - } - - #[inline] - pub fn as_str(&self) -> String { - format!("{}:{}:{}", self.kind, self.seller_pubkey, self.listing_id) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TradeListingAddressError { - InvalidFormat, -} - -impl core::fmt::Display for TradeListingAddressError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - TradeListingAddressError::InvalidFormat => { - write!(f, "invalid listing address format") - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for TradeListingAddressError {} - -#[cfg(feature = "serde_json")] -fn required_tag_value<'a>( - tags: &'a [Vec<String>], - key: &'static str, -) -> Result<&'a str, TradeListingEnvelopeParseError> { - let tag = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) - .ok_or(TradeListingEnvelopeParseError::MissingTag(key))?; - let value = tag - .get(1) - .map(|value| value.as_str()) - .ok_or(TradeListingEnvelopeParseError::InvalidTag(key))?; - if value.trim().is_empty() { - return Err(TradeListingEnvelopeParseError::InvalidTag(key)); - } - Ok(value) -} - -#[cfg(feature = "serde_json")] -impl<T> TradeListingEnvelope<T> -where - T: DeserializeOwned, -{ - pub fn from_event(event: &RadrootsNostrEvent) -> Result<Self, TradeListingEnvelopeParseError> { - let event_kind = u16::try_from(event.kind) - .map_err(|_| TradeListingEnvelopeParseError::InvalidKind(event.kind))?; - if !is_trade_listing_kind(event_kind) { - return Err(TradeListingEnvelopeParseError::InvalidKind(event.kind)); - } - let envelope = serde_json::from_str::<Self>(&event.content) - .map_err(|_| TradeListingEnvelopeParseError::InvalidJson)?; - envelope - .validate() - .map_err(TradeListingEnvelopeParseError::InvalidEnvelope)?; - if envelope.message_type.kind() != event_kind { - return Err(TradeListingEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, - message_type: envelope.message_type, - }); - } - - let listing_addr = required_tag_value(&event.tags, "a")?; - if envelope.listing_addr != listing_addr { - return Err(TradeListingEnvelopeParseError::ListingAddrTagMismatch); - } - TradeListingAddress::parse(&envelope.listing_addr) - .map_err(TradeListingEnvelopeParseError::InvalidListingAddr)?; - - if let Some(order_id) = envelope.order_id.as_deref() { - let tag_order_id = required_tag_value(&event.tags, TAG_D)?; - if tag_order_id != order_id { - return Err(TradeListingEnvelopeParseError::OrderIdTagMismatch); - } - } - - trade_listing_event_context_from_tags(envelope.message_type, &event.tags)?; - - Ok(envelope) - } -} - -#[cfg(feature = "serde_json")] -pub fn trade_listing_event_context_from_tags( - message_type: TradeListingMessageType, - tags: &[Vec<String>], -) -> Result<TradeListingEventContext, TradeListingEnvelopeParseError> { - let counterparty_pubkey = - parse_trade_listing_counterparty_tag(tags).map_err(map_event_parse_error)?; - let listing_event = parse_trade_listing_event_tag(tags).map_err(map_event_parse_error)?; - let root_event_id = parse_trade_listing_root_tag(tags).map_err(map_event_parse_error)?; - let prev_event_id = parse_trade_listing_prev_tag(tags).map_err(map_event_parse_error)?; - if message_type.requires_listing_snapshot() && listing_event.is_none() { - return Err(TradeListingEnvelopeParseError::MissingTag( - TAG_LISTING_EVENT, - )); - } - if message_type.requires_trade_chain() { - if root_event_id.is_none() { - return Err(TradeListingEnvelopeParseError::MissingTag( - radroots_events::tags::TAG_E_ROOT, - )); - } - if prev_event_id.is_none() { - return Err(TradeListingEnvelopeParseError::MissingTag( - radroots_events::tags::TAG_E_PREV, - )); - } - } - Ok(TradeListingEventContext { - counterparty_pubkey, - listing_event, - root_event_id, - prev_event_id, - }) -} - -#[cfg(feature = "serde_json")] -fn map_event_parse_error(error: EventParseError) -> TradeListingEnvelopeParseError { - match error { - EventParseError::MissingTag(tag) => TradeListingEnvelopeParseError::MissingTag(tag), - EventParseError::InvalidTag(tag) => TradeListingEnvelopeParseError::InvalidTag(tag), - EventParseError::InvalidKind { expected: _, got } => { - TradeListingEnvelopeParseError::InvalidKind(got) - } - EventParseError::InvalidNumber(tag, _) | EventParseError::InvalidJson(tag) => { - TradeListingEnvelopeParseError::InvalidTag(tag) - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeListingValidateRequest { - pub listing_event: Option<RadrootsNostrEventPtr>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeListingValidateResult { - pub valid: bool, - pub errors: Vec<TradeListingValidationError>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeOrderResponse { - pub accepted: bool, - pub reason: Option<String>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeOrderRevisionResponse { - pub accepted: bool, - pub reason: Option<String>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeListingCancel { - pub reason: Option<String>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TradeListingMessagePayload { - ListingValidateRequest(TradeListingValidateRequest), - ListingValidateResult(TradeListingValidateResult), - TradeOrderRequested(TradeOrder), - OrderResponse(TradeOrderResponse), - OrderRevision(TradeOrderRevision), - OrderRevisionAccept(TradeOrderRevisionResponse), - OrderRevisionDecline(TradeOrderRevisionResponse), - Question(TradeQuestion), - Answer(TradeAnswer), - DiscountRequest(TradeDiscountRequest), - DiscountOffer(TradeDiscountOffer), - DiscountAccept(TradeDiscountDecision), - DiscountDecline(TradeDiscountDecision), - Cancel(TradeListingCancel), - FulfillmentUpdate(TradeFulfillmentUpdate), - Receipt(TradeReceipt), -} - -impl TradeListingMessagePayload { - pub const fn message_type(&self) -> TradeListingMessageType { - match self { - TradeListingMessagePayload::ListingValidateRequest(_) => { - TradeListingMessageType::ListingValidateRequest - } - TradeListingMessagePayload::ListingValidateResult(_) => { - TradeListingMessageType::ListingValidateResult - } - TradeListingMessagePayload::TradeOrderRequested(_) => { - TradeListingMessageType::OrderRequest - } - TradeListingMessagePayload::OrderResponse(_) => TradeListingMessageType::OrderResponse, - TradeListingMessagePayload::OrderRevision(_) => TradeListingMessageType::OrderRevision, - TradeListingMessagePayload::OrderRevisionAccept(_) => { - TradeListingMessageType::OrderRevisionAccept - } - TradeListingMessagePayload::OrderRevisionDecline(_) => { - TradeListingMessageType::OrderRevisionDecline - } - TradeListingMessagePayload::Question(_) => TradeListingMessageType::Question, - TradeListingMessagePayload::Answer(_) => TradeListingMessageType::Answer, - TradeListingMessagePayload::DiscountRequest(_) => { - TradeListingMessageType::DiscountRequest - } - TradeListingMessagePayload::DiscountOffer(_) => TradeListingMessageType::DiscountOffer, - TradeListingMessagePayload::DiscountAccept(_) => { - TradeListingMessageType::DiscountAccept - } - TradeListingMessagePayload::DiscountDecline(_) => { - TradeListingMessageType::DiscountDecline - } - TradeListingMessagePayload::Cancel(_) => TradeListingMessageType::Cancel, - TradeListingMessagePayload::FulfillmentUpdate(_) => { - TradeListingMessageType::FulfillmentUpdate - } - TradeListingMessagePayload::Receipt(_) => TradeListingMessageType::Receipt, - } - } -} - -#[cfg(test)] -mod tests { - use super::{ - TradeListingAddress, TradeListingAddressError, TradeListingEnvelope, - TradeListingEnvelopeError, TradeListingEnvelopeParseError, TradeListingMessagePayload, - TradeListingMessageType, TradeListingValidateRequest, TradeOrderResponse, - trade_listing_envelope_event_build, - }; - use radroots_events::kinds::KIND_LISTING; - #[cfg(feature = "serde_json")] - use radroots_events::{RadrootsNostrEvent, RadrootsNostrEventPtr}; - #[cfg(feature = "serde_json")] - use radroots_events_codec::error::EventEncodeError; - - #[cfg(feature = "serde_json")] - use crate::listing::order::{ - TradeOrder, TradeOrderEconomicItem, TradeOrderEconomicLine, TradeOrderEconomics, - TradeOrderItem, TradePricingBasis, - }; - #[cfg(feature = "serde_json")] - use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit}; - - #[test] - fn envelope_requires_listing_addr() { - let env = TradeListingEnvelope::new( - TradeListingMessageType::ListingValidateRequest, - "", - None, - TradeListingValidateRequest { - listing_event: None, - }, - ); - assert_eq!( - env.validate().unwrap_err(), - TradeListingEnvelopeError::MissingListingAddr - ); - } - - #[test] - fn envelope_requires_order_id_for_order_scoped() { - let env = TradeListingEnvelope::new( - TradeListingMessageType::OrderRequest, - format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - None, - TradeListingValidateRequest { - listing_event: None, - }, - ); - assert_eq!( - env.validate().unwrap_err(), - TradeListingEnvelopeError::MissingOrderId - ); - } - - #[test] - fn envelope_accepts_non_empty_order_id_for_order_scoped() { - let env = TradeListingEnvelope::new( - TradeListingMessageType::OrderRequest, - format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - Some("order-1".to_string()), - TradeListingValidateRequest { - listing_event: None, - }, - ); - assert!(env.validate().is_ok()); - } - - #[test] - fn envelope_rejects_blank_order_id_for_order_scoped() { - let env = TradeListingEnvelope::new( - TradeListingMessageType::OrderRequest, - format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - Some(" ".to_string()), - TradeListingValidateRequest { - listing_event: None, - }, - ); - assert_eq!( - env.validate().unwrap_err(), - TradeListingEnvelopeError::MissingOrderId - ); - } - - #[test] - fn envelope_accepts_non_order_message_without_order_id() { - let env = TradeListingEnvelope::new( - TradeListingMessageType::ListingValidateResult, - format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - None, - TradeListingValidateRequest { - listing_event: None, - }, - ); - assert!(env.validate().is_ok()); - } - - #[test] - fn message_type_kind_and_request_flags_cover_all_variants() { - let expected_kinds = crate::listing::kinds::TRADE_LISTING_KINDS; - let assert_case = - |message_type: TradeListingMessageType, is_request: bool, is_result: bool| { - assert_eq!(message_type.is_request(), is_request); - assert_eq!(message_type.is_result(), is_result); - assert!(expected_kinds.contains(&message_type.kind())); - }; - - assert_case(TradeListingMessageType::ListingValidateRequest, true, false); - assert_case(TradeListingMessageType::ListingValidateResult, false, true); - assert_case(TradeListingMessageType::OrderRequest, true, false); - assert_case(TradeListingMessageType::OrderResponse, false, true); - assert_case(TradeListingMessageType::OrderRevision, true, false); - assert_case(TradeListingMessageType::OrderRevisionAccept, false, true); - assert_case(TradeListingMessageType::OrderRevisionDecline, false, true); - assert_case(TradeListingMessageType::Question, true, false); - assert_case(TradeListingMessageType::Answer, false, true); - assert_case(TradeListingMessageType::DiscountRequest, true, false); - assert_case(TradeListingMessageType::DiscountOffer, false, true); - assert_case(TradeListingMessageType::DiscountAccept, true, false); - assert_case(TradeListingMessageType::DiscountDecline, true, false); - assert_case(TradeListingMessageType::Cancel, true, false); - assert_case(TradeListingMessageType::FulfillmentUpdate, true, false); - assert_case(TradeListingMessageType::Receipt, true, false); - } - - #[test] - fn message_type_from_kind_roundtrips_supported_variants() { - for message_type in [ - TradeListingMessageType::ListingValidateRequest, - TradeListingMessageType::ListingValidateResult, - TradeListingMessageType::OrderRequest, - TradeListingMessageType::OrderResponse, - TradeListingMessageType::OrderRevision, - TradeListingMessageType::Question, - TradeListingMessageType::Answer, - TradeListingMessageType::DiscountRequest, - TradeListingMessageType::DiscountOffer, - TradeListingMessageType::DiscountAccept, - TradeListingMessageType::DiscountDecline, - TradeListingMessageType::Cancel, - TradeListingMessageType::FulfillmentUpdate, - TradeListingMessageType::Receipt, - ] { - assert_eq!( - TradeListingMessageType::from_kind(message_type.kind()), - Some(message_type) - ); - } - assert_eq!( - TradeListingMessageType::from_kind(super::KIND_TRADE_LISTING_ORDER_REVISION_RES), - None - ); - assert_eq!(TradeListingMessageType::from_kind(5000), None); - } - - #[test] - fn envelope_validate_rejects_invalid_version() { - let mut env = TradeListingEnvelope::new( - TradeListingMessageType::ListingValidateRequest, - format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - None, - TradeListingValidateRequest { - listing_event: None, - }, - ); - env.version = 9; - assert_eq!( - env.validate().unwrap_err(), - TradeListingEnvelopeError::InvalidVersion { - expected: super::TRADE_LISTING_ENVELOPE_VERSION, - got: 9 - } - ); - } - - #[test] - fn envelope_error_display_messages_are_stable() { - assert_eq!( - TradeListingEnvelopeError::MissingOrderId.to_string(), - "missing order_id for order-scoped message" - ); - assert_eq!( - TradeListingEnvelopeError::MissingListingAddr.to_string(), - "missing listing_addr" - ); - assert!( - TradeListingEnvelopeError::InvalidVersion { - expected: 1, - got: 2 - } - .to_string() - .contains("expected 1, got 2") - ); - } - - #[test] - fn trade_listing_address_parse_and_render_roundtrip() { - let addr_raw = format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"); - let parsed = TradeListingAddress::parse(&addr_raw).expect("valid address"); - assert_eq!(parsed.kind, KIND_LISTING as u16); - assert_eq!(parsed.seller_pubkey, "seller"); - assert_eq!(parsed.listing_id, "AAAAAAAAAAAAAAAAAAAAAg"); - assert_eq!(parsed.as_str(), addr_raw); - } - - #[test] - fn trade_listing_address_parse_rejects_invalid_shapes() { - assert_eq!( - TradeListingAddress::parse("not-a-kind:seller:AAAAAAAAAAAAAAAAAAAAAg").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - assert_eq!( - TradeListingAddress::parse("30340").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - assert_eq!( - TradeListingAddress::parse("30340:seller").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - assert_eq!( - TradeListingAddress::parse("30340:seller:AAAAAAAAAAAAAAAAAAAAAg:extra").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - assert_eq!( - TradeListingAddress::parse("0:seller:AAAAAAAAAAAAAAAAAAAAAg").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - assert_eq!( - TradeListingAddress::parse("30340: :AAAAAAAAAAAAAAAAAAAAAg").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - assert_eq!( - TradeListingAddress::parse("30340:seller: ").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - assert_eq!( - TradeListingAddress::parse("30340:seller:not-base64").unwrap_err(), - TradeListingAddressError::InvalidFormat - ); - } - - #[test] - fn trade_listing_address_error_display_message_is_stable() { - assert_eq!( - TradeListingAddressError::InvalidFormat.to_string(), - "invalid listing address format" - ); - } - - #[cfg(feature = "serde_json")] - fn order_economics(items: &[TradeOrderItem]) -> TradeOrderEconomics { - let economic_items = items - .iter() - .map(|item| { - let line_subtotal = - RadrootsCoreDecimal::from(item.bin_count) * RadrootsCoreDecimal::from(5u32); - TradeOrderEconomicItem { - bin_id: item.bin_id.clone(), - bin_count: item.bin_count, - quantity_amount: RadrootsCoreDecimal::from(1u32), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: RadrootsCoreDecimal::from(5u32), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: RadrootsCoreMoney::new(line_subtotal, RadrootsCoreCurrency::USD), - } - }) - .collect::<Vec<_>>(); - let subtotal = items - .iter() - .fold(RadrootsCoreDecimal::from(0u32), |total, item| { - total - + (RadrootsCoreDecimal::from(item.bin_count) - * RadrootsCoreDecimal::from(5u32)) - }); - - TradeOrderEconomics { - quote_id: "quote-1".into(), - quote_version: 1, - pricing_basis: TradePricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: economic_items, - discounts: Vec::<TradeOrderEconomicLine>::new(), - adjustments: Vec::<TradeOrderEconomicLine>::new(), - subtotal: RadrootsCoreMoney::new(subtotal, RadrootsCoreCurrency::USD), - discount_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - adjustment_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - total: RadrootsCoreMoney::new(subtotal, RadrootsCoreCurrency::USD), - } - } - - #[cfg(feature = "serde_json")] - fn base_order() -> TradeOrder { - let items = vec![TradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 2, - }]; - TradeOrder { - order_id: "order-1".into(), - listing_addr: format!("{KIND_LISTING}:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - buyer_pubkey: "buyer-pubkey".into(), - seller_pubkey: "seller-pubkey".into(), - economics: order_economics(&items), - items, - } - } - - #[cfg(feature = "serde_json")] - fn listing_snapshot() -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: "listing-event-id".into(), - relays: None, - } - } - - #[cfg(feature = "serde_json")] - fn base_event( - actor_pubkey: &str, - recipient_pubkey: &str, - message_type: TradeListingMessageType, - listing_addr: &str, - order_id: Option<&str>, - payload: &TradeListingMessagePayload, - ) -> RadrootsNostrEvent { - let message_type = payload.message_type(); - let listing_event = message_type - .requires_listing_snapshot() - .then(listing_snapshot); - let built = trade_listing_envelope_event_build( - recipient_pubkey, - message_type, - listing_addr.to_string(), - order_id.map(str::to_string), - listing_event.as_ref(), - None, - None, - payload, - ) - .expect("canonical envelope event"); - RadrootsNostrEvent { - id: "event-id".into(), - author: actor_pubkey.into(), - created_at: 1_700_000_000, - kind: u32::from(built.kind), - tags: built.tags, - content: built.content, - sig: "sig".into(), - } - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_event_build_includes_order_and_snapshot_tags() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let payload = TradeListingMessagePayload::TradeOrderRequested(base_order()); - let built = super::trade_listing_envelope_event_build( - "pubkey", - TradeListingMessageType::OrderRequest, - listing_addr.clone(), - Some(String::from("order-1")), - Some(&listing_snapshot()), - None, - None, - &payload, - ) - .unwrap(); - - assert_eq!(built.kind, TradeListingMessageType::OrderRequest.kind()); - - let envelope: TradeListingEnvelope<serde_json::Value> = - serde_json::from_str(&built.content).unwrap(); - assert_eq!(envelope.listing_addr, listing_addr.clone()); - assert_eq!(envelope.order_id.as_deref(), Some("order-1")); - assert_eq!(built.tags.len(), 4); - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_event_build_omits_order_tag_when_missing() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let payload = - TradeListingMessagePayload::ListingValidateRequest(TradeListingValidateRequest { - listing_event: None, - }); - let built = super::trade_listing_envelope_event_build( - "pubkey", - TradeListingMessageType::ListingValidateRequest, - listing_addr.clone(), - None, - None, - None, - None, - &payload, - ) - .unwrap(); - - assert_eq!( - built.kind, - TradeListingMessageType::ListingValidateRequest.kind() - ); - - let envelope: TradeListingEnvelope<serde_json::Value> = - serde_json::from_str(&built.content).unwrap(); - assert_eq!(envelope.listing_addr, listing_addr); - assert!(envelope.order_id.is_none()); - assert_eq!(built.tags.len(), 2); - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_event_build_requires_snapshot_for_order_request() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let payload = TradeListingMessagePayload::TradeOrderRequested(base_order()); - let err = super::trade_listing_envelope_event_build( - "pubkey", - TradeListingMessageType::OrderRequest, - listing_addr, - Some(String::from("order-1")), - None, - None, - None, - &payload, - ) - .unwrap_err(); - assert_eq!( - err, - EventEncodeError::EmptyRequiredField("listing_event.id") - ); - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_event_build_requires_chain_tags_for_order_response() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let payload = TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }); - let err = super::trade_listing_envelope_event_build( - "buyer-pubkey", - TradeListingMessageType::OrderResponse, - listing_addr, - Some(String::from("order-1")), - None, - None, - None, - &payload, - ) - .unwrap_err(); - assert_eq!(err, EventEncodeError::EmptyRequiredField("root_event_id")); - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_from_event_parses_canonical_order_request() { - let payload = TradeListingMessagePayload::TradeOrderRequested(base_order()); - let event = base_event( - "buyer-pubkey", - "seller-pubkey", - TradeListingMessageType::OrderRequest, - &format!("{KIND_LISTING}:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - Some("order-1"), - &payload, - ); - - let envelope = - TradeListingEnvelope::<TradeListingMessagePayload>::from_event(&event).unwrap(); - assert_eq!(envelope.message_type, TradeListingMessageType::OrderRequest); - assert_eq!(envelope.order_id.as_deref(), Some("order-1")); - assert_eq!(envelope.payload, payload); - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_from_event_rejects_kind_mismatch() { - let payload = TradeListingMessagePayload::TradeOrderRequested(base_order()); - let mut event = base_event( - "buyer-pubkey", - "seller-pubkey", - TradeListingMessageType::OrderRequest, - &format!("{KIND_LISTING}:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - Some("order-1"), - &payload, - ); - event.kind = u32::from(TradeListingMessageType::OrderResponse.kind()); - - let err = TradeListingEnvelope::<TradeListingMessagePayload>::from_event(&event) - .expect_err("kind mismatch should fail"); - assert_eq!( - err, - TradeListingEnvelopeParseError::MessageTypeKindMismatch { - event_kind: u32::from(TradeListingMessageType::OrderResponse.kind()), - message_type: TradeListingMessageType::OrderRequest, - } - ); - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_from_event_rejects_listing_addr_tag_mismatch() { - let payload = TradeListingMessagePayload::TradeOrderRequested(base_order()); - let mut event = base_event( - "buyer-pubkey", - "seller-pubkey", - TradeListingMessageType::OrderRequest, - &format!("{KIND_LISTING}:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - Some("order-1"), - &payload, - ); - event.tags[1][1] = format!("{KIND_LISTING}:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw"); - - let err = TradeListingEnvelope::<TradeListingMessagePayload>::from_event(&event) - .expect_err("listing addr mismatch should fail"); - assert_eq!(err, TradeListingEnvelopeParseError::ListingAddrTagMismatch); - } - - #[cfg(feature = "serde_json")] - #[test] - fn envelope_from_event_rejects_order_id_tag_mismatch() { - let payload = TradeListingMessagePayload::TradeOrderRequested(base_order()); - let mut event = base_event( - "buyer-pubkey", - "seller-pubkey", - TradeListingMessageType::OrderRequest, - &format!("{KIND_LISTING}:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"), - Some("order-1"), - &payload, - ); - event.tags[2][1] = "order-2".into(); - - let err = TradeListingEnvelope::<TradeListingMessagePayload>::from_event(&event) - .expect_err("order id mismatch should fail"); - assert_eq!(err, TradeListingEnvelopeParseError::OrderIdTagMismatch); - } -} diff --git a/crates/trade/src/listing/kinds.rs b/crates/trade/src/listing/kinds.rs @@ -1,210 +0,0 @@ -#![forbid(unsafe_code)] - - -pub const KIND_TRADE_LISTING_VALIDATE_REQ: u16 = 5321; -pub const KIND_TRADE_LISTING_VALIDATE_RES: u16 = 6321; - -pub const KIND_TRADE_LISTING_ORDER_REQ: u16 = 5322; -pub const KIND_TRADE_LISTING_ORDER_RES: u16 = 6322; - -pub const KIND_TRADE_LISTING_ORDER_REVISION_REQ: u16 = 5323; -pub const KIND_TRADE_LISTING_ORDER_REVISION_RES: u16 = 6323; - -pub const KIND_TRADE_LISTING_QUESTION_REQ: u16 = 5324; -pub const KIND_TRADE_LISTING_ANSWER_RES: u16 = 6324; - -pub const KIND_TRADE_LISTING_DISCOUNT_REQ: u16 = 5325; -pub const KIND_TRADE_LISTING_DISCOUNT_OFFER_RES: u16 = 6325; - -pub const KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ: u16 = 5326; -pub const KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ: u16 = 5327; - -pub const KIND_TRADE_LISTING_CANCEL_REQ: u16 = 5328; -pub const KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ: u16 = 5329; -pub const KIND_TRADE_LISTING_RECEIPT_REQ: u16 = 5330; - -pub const TRADE_LISTING_KINDS: [u16; 15] = [ - KIND_TRADE_LISTING_VALIDATE_REQ, - KIND_TRADE_LISTING_VALIDATE_RES, - 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_ANSWER_RES, - KIND_TRADE_LISTING_DISCOUNT_REQ, - KIND_TRADE_LISTING_DISCOUNT_OFFER_RES, - KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ, - KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ, - KIND_TRADE_LISTING_CANCEL_REQ, - KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ, - KIND_TRADE_LISTING_RECEIPT_REQ, -]; - -#[repr(u16)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum TradeListingKind { - KindTradeListingValidateReq = KIND_TRADE_LISTING_VALIDATE_REQ, - KindTradeListingValidateRes = KIND_TRADE_LISTING_VALIDATE_RES, - KindTradeListingOrderReq = KIND_TRADE_LISTING_ORDER_REQ, - KindTradeListingOrderRes = KIND_TRADE_LISTING_ORDER_RES, - KindTradeListingOrderRevisionReq = KIND_TRADE_LISTING_ORDER_REVISION_REQ, - KindTradeListingOrderRevisionRes = KIND_TRADE_LISTING_ORDER_REVISION_RES, - KindTradeListingQuestionReq = KIND_TRADE_LISTING_QUESTION_REQ, - KindTradeListingAnswerRes = KIND_TRADE_LISTING_ANSWER_RES, - KindTradeListingDiscountReq = KIND_TRADE_LISTING_DISCOUNT_REQ, - KindTradeListingDiscountOfferRes = KIND_TRADE_LISTING_DISCOUNT_OFFER_RES, - KindTradeListingDiscountAcceptReq = KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ, - KindTradeListingDiscountDeclineReq = KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ, - KindTradeListingCancelReq = KIND_TRADE_LISTING_CANCEL_REQ, - KindTradeListingFulfillmentUpdateReq = KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ, - KindTradeListingReceiptReq = KIND_TRADE_LISTING_RECEIPT_REQ, -} - -#[inline] -pub const fn is_trade_listing_request_kind(kind: u16) -> bool { - matches!( - kind, - KIND_TRADE_LISTING_VALIDATE_REQ - | KIND_TRADE_LISTING_ORDER_REQ - | KIND_TRADE_LISTING_ORDER_REVISION_REQ - | KIND_TRADE_LISTING_QUESTION_REQ - | KIND_TRADE_LISTING_DISCOUNT_REQ - | KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ - | KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ - | KIND_TRADE_LISTING_CANCEL_REQ - | KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ - | KIND_TRADE_LISTING_RECEIPT_REQ - ) -} - -#[inline] -pub const fn is_trade_listing_result_kind(kind: u16) -> bool { - matches!( - kind, - KIND_TRADE_LISTING_VALIDATE_RES - | KIND_TRADE_LISTING_ORDER_RES - | KIND_TRADE_LISTING_ORDER_REVISION_RES - | KIND_TRADE_LISTING_ANSWER_RES - | KIND_TRADE_LISTING_DISCOUNT_OFFER_RES - ) -} - -#[inline] -pub const fn is_trade_listing_kind(kind: u16) -> bool { - is_trade_listing_request_kind(kind) || is_trade_listing_result_kind(kind) -} - -#[inline] -pub const fn trade_listing_result_kind_for_request(kind: u16) -> Option<u16> { - match kind { - KIND_TRADE_LISTING_VALIDATE_REQ => Some(KIND_TRADE_LISTING_VALIDATE_RES), - KIND_TRADE_LISTING_ORDER_REQ => Some(KIND_TRADE_LISTING_ORDER_RES), - KIND_TRADE_LISTING_ORDER_REVISION_REQ => Some(KIND_TRADE_LISTING_ORDER_REVISION_RES), - KIND_TRADE_LISTING_QUESTION_REQ => Some(KIND_TRADE_LISTING_ANSWER_RES), - KIND_TRADE_LISTING_DISCOUNT_REQ => Some(KIND_TRADE_LISTING_DISCOUNT_OFFER_RES), - _ => None, - } -} - -#[inline] -pub const fn trade_listing_request_kind_for_result(kind: u16) -> Option<u16> { - match kind { - KIND_TRADE_LISTING_VALIDATE_RES => Some(KIND_TRADE_LISTING_VALIDATE_REQ), - KIND_TRADE_LISTING_ORDER_RES => Some(KIND_TRADE_LISTING_ORDER_REQ), - KIND_TRADE_LISTING_ORDER_REVISION_RES => Some(KIND_TRADE_LISTING_ORDER_REVISION_REQ), - KIND_TRADE_LISTING_ANSWER_RES => Some(KIND_TRADE_LISTING_QUESTION_REQ), - KIND_TRADE_LISTING_DISCOUNT_OFFER_RES => Some(KIND_TRADE_LISTING_DISCOUNT_REQ), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn classifies_request_and_result_kinds() { - for kind in [ - KIND_TRADE_LISTING_VALIDATE_REQ, - KIND_TRADE_LISTING_ORDER_REQ, - KIND_TRADE_LISTING_ORDER_REVISION_REQ, - KIND_TRADE_LISTING_QUESTION_REQ, - KIND_TRADE_LISTING_DISCOUNT_REQ, - KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ, - KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ, - KIND_TRADE_LISTING_CANCEL_REQ, - KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ, - KIND_TRADE_LISTING_RECEIPT_REQ, - ] { - assert!(is_trade_listing_request_kind(kind)); - assert!(is_trade_listing_kind(kind)); - assert!(!is_trade_listing_result_kind(kind)); - } - - for kind in [ - KIND_TRADE_LISTING_VALIDATE_RES, - KIND_TRADE_LISTING_ORDER_RES, - KIND_TRADE_LISTING_ORDER_REVISION_RES, - KIND_TRADE_LISTING_ANSWER_RES, - KIND_TRADE_LISTING_DISCOUNT_OFFER_RES, - ] { - assert!(is_trade_listing_result_kind(kind)); - assert!(is_trade_listing_kind(kind)); - assert!(!is_trade_listing_request_kind(kind)); - } - } - - #[test] - fn request_to_result_roundtrip_is_defined_for_request_response_pairs() { - let pairs = [ - ( - KIND_TRADE_LISTING_VALIDATE_REQ, - KIND_TRADE_LISTING_VALIDATE_RES, - ), - (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_ANSWER_RES, - ), - ( - KIND_TRADE_LISTING_DISCOUNT_REQ, - KIND_TRADE_LISTING_DISCOUNT_OFFER_RES, - ), - ]; - - for (req, res) in pairs { - assert_eq!(trade_listing_result_kind_for_request(req), Some(res)); - assert_eq!(trade_listing_request_kind_for_result(res), Some(req)); - } - } - - #[test] - fn request_to_result_rejects_non_roundtrip_kinds() { - for kind in [ - KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ, - KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ, - KIND_TRADE_LISTING_CANCEL_REQ, - KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ, - KIND_TRADE_LISTING_RECEIPT_REQ, - ] { - assert_eq!(trade_listing_result_kind_for_request(kind), None); - } - assert_eq!(trade_listing_request_kind_for_result(5000), None); - assert!(!is_trade_listing_kind(5000)); - } - - #[test] - fn kind_array_contains_expected_kinds() { - assert_eq!(TRADE_LISTING_KINDS.len(), 15); - assert!(TRADE_LISTING_KINDS.contains(&KIND_TRADE_LISTING_VALIDATE_REQ)); - assert!(TRADE_LISTING_KINDS.contains(&KIND_TRADE_LISTING_VALIDATE_RES)); - assert!(TRADE_LISTING_KINDS.contains(&KIND_TRADE_LISTING_ORDER_REQ)); - assert!(TRADE_LISTING_KINDS.contains(&KIND_TRADE_LISTING_ORDER_RES)); - assert!(TRADE_LISTING_KINDS.contains(&KIND_TRADE_LISTING_RECEIPT_REQ)); - } -} diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs @@ -1,25 +1,18 @@ mod codec; -pub(crate) mod contract; pub mod model; -pub mod overlay; pub mod price_ext; -pub mod projection; pub mod publish; pub mod validation; use radroots_events::{RadrootsNostrEvent, kinds::is_listing_kind, listing::RadrootsListing}; -pub(crate) use self::contract as dvm; -#[allow(unused_imports)] -pub(crate) use self::contract as kinds; -pub(crate) use self::contract as order; -pub use radroots_events::trade::RadrootsTradeListingParseError as TradeListingParseError; +pub use radroots_events::order::RadrootsListingParseError as ListingParseError; pub fn parse_listing_event( event: &RadrootsNostrEvent, -) -> Result<RadrootsListing, TradeListingParseError> { +) -> Result<RadrootsListing, ListingParseError> { if !is_listing_kind(event.kind) { - return Err(TradeListingParseError::InvalidKind(event.kind)); + return Err(ListingParseError::InvalidKind(event.kind)); } self::codec::listing_from_event_parts(&event.tags, &event.content) } @@ -28,7 +21,7 @@ pub fn parse_listing_event( mod tests { use super::parse_listing_event; use radroots_events::{ - RadrootsNostrEvent, kinds::KIND_PROFILE, trade::RadrootsTradeListingParseError, + RadrootsNostrEvent, kinds::KIND_PROFILE, order::RadrootsListingParseError, }; #[test] @@ -45,7 +38,7 @@ mod tests { assert!(matches!( parse_listing_event(&event), - Err(RadrootsTradeListingParseError::InvalidKind(KIND_PROFILE)) + Err(RadrootsListingParseError::InvalidKind(KIND_PROFILE)) )); } } diff --git a/crates/trade/src/listing/order.rs b/crates/trade/src/listing/order.rs @@ -1,127 +0,0 @@ -#![forbid(unsafe_code)] - -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; - -use radroots_core::RadrootsCoreDiscountValue; - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeOrderItem { - pub bin_id: String, - pub bin_count: u32, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TradeOrderChange { - BinCount { item_index: u32, bin_count: u32 }, - ItemAdd { item: TradeOrderItem }, - ItemRemove { item_index: u32 }, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeOrderRevision { - pub revision_id: String, - pub changes: Vec<TradeOrderChange>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeOrder { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub items: Vec<TradeOrderItem>, - pub discounts: Option<Vec<RadrootsCoreDiscountValue>>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TradeOrderStatus { - Draft, - Validated, - Requested, - Questioned, - Revised, - Accepted, - Declined, - Cancelled, - Fulfilled, - Completed, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeQuestion { - pub question_id: String, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeAnswer { - pub question_id: String, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeDiscountRequest { - pub discount_id: String, - pub value: RadrootsCoreDiscountValue, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeDiscountOffer { - pub discount_id: String, - pub value: RadrootsCoreDiscountValue, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TradeDiscountDecision { - Accept { - value: RadrootsCoreDiscountValue, - }, - Decline { - reason: Option<String>, - }, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "kind", content = "amount") -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TradeFulfillmentStatus { - Preparing, - Shipped, - ReadyForPickup, - Delivered, - Cancelled, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeFulfillmentUpdate { - pub status: TradeFulfillmentStatus, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TradeReceipt { - pub acknowledged: bool, - pub at: u64, -} diff --git a/crates/trade/src/listing/overlay.rs b/crates/trade/src/listing/overlay.rs @@ -1,1194 +0,0 @@ -#![forbid(unsafe_code)] - -#[cfg(not(feature = "std"))] -use alloc::{collections::BTreeMap, string::String, vec::Vec}; -#[cfg(feature = "std")] -use std::collections::BTreeMap; - -use crate::listing::projection::{ - RadrootsTradeListingProjection, RadrootsTradeListingQuery, RadrootsTradeListingSort, - RadrootsTradeMarketplaceListingSummary, RadrootsTradeMarketplaceOrderSummary, - RadrootsTradeOrderQuery, RadrootsTradeOrderSort, RadrootsTradeOrderWorkflowProjection, - RadrootsTradeReadIndex, -}; - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeReviewPriority { - Low, - Normal, - High, - Critical, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeReviewStatus { - Queued, - InProgress, - Blocked, - Resolved, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeReviewQueueEntry { - pub queue: String, - pub priority: RadrootsTradeReviewPriority, - pub status: RadrootsTradeReviewStatus, - pub assigned_operator: Option<String>, - pub reason: Option<String>, -} - -impl RadrootsTradeReviewQueueEntry { - pub fn requires_review(&self) -> bool { - !matches!(self.status, RadrootsTradeReviewStatus::Resolved) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeModerationSeverity { - Notice, - Warning, - Block, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeModerationStatus { - Open, - Snoozed, - Resolved, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeModerationFlag { - pub code: String, - pub severity: RadrootsTradeModerationSeverity, - pub status: RadrootsTradeModerationStatus, - pub source: Option<String>, - pub reason: Option<String>, -} - -impl RadrootsTradeModerationFlag { - pub fn is_open(&self) -> bool { - !matches!(self.status, RadrootsTradeModerationStatus::Resolved) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeFulfillmentExceptionSeverity { - Notice, - Warning, - Blocking, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeFulfillmentExceptionStatus { - Open, - Monitoring, - Resolved, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeFulfillmentException { - pub code: String, - pub severity: RadrootsTradeFulfillmentExceptionSeverity, - pub status: RadrootsTradeFulfillmentExceptionStatus, - pub source: Option<String>, - pub notes: Option<String>, -} - -impl RadrootsTradeFulfillmentException { - pub fn is_open(&self) -> bool { - !matches!( - self.status, - RadrootsTradeFulfillmentExceptionStatus::Resolved - ) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeListingBackofficeOverlay { - pub listing_addr: String, - pub review_queue: Option<RadrootsTradeReviewQueueEntry>, - pub moderation_flags: Vec<RadrootsTradeModerationFlag>, -} - -impl RadrootsTradeListingBackofficeOverlay { - pub fn requires_review(&self) -> bool { - self.review_queue - .as_ref() - .is_some_and(RadrootsTradeReviewQueueEntry::requires_review) - } - - pub fn open_moderation_flag_count(&self) -> u32 { - self.moderation_flags.iter().fold(0u32, |count, flag| { - if flag.is_open() { - count.saturating_add(1) - } else { - count - } - }) - } - - pub fn has_open_moderation_flags(&self) -> bool { - self.open_moderation_flag_count() > 0 - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderBackofficeOverlay { - pub order_id: String, - pub review_queue: Option<RadrootsTradeReviewQueueEntry>, - pub moderation_flags: Vec<RadrootsTradeModerationFlag>, - pub fulfillment_exceptions: Vec<RadrootsTradeFulfillmentException>, -} - -impl RadrootsTradeOrderBackofficeOverlay { - pub fn requires_review(&self) -> bool { - self.review_queue - .as_ref() - .is_some_and(RadrootsTradeReviewQueueEntry::requires_review) - } - - pub fn open_moderation_flag_count(&self) -> u32 { - self.moderation_flags.iter().fold(0u32, |count, flag| { - if flag.is_open() { - count.saturating_add(1) - } else { - count - } - }) - } - - pub fn has_open_moderation_flags(&self) -> bool { - self.open_moderation_flag_count() > 0 - } - - pub fn open_fulfillment_exception_count(&self) -> u32 { - self.fulfillment_exceptions - .iter() - .fold(0u32, |count, exception| { - if exception.is_open() { - count.saturating_add(1) - } else { - count - } - }) - } - - pub fn has_open_fulfillment_exceptions(&self) -> bool { - self.open_fulfillment_exception_count() > 0 - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct RadrootsTradeListingBackofficeQuery { - pub listing: RadrootsTradeListingQuery, - pub requires_review: Option<bool>, - pub has_open_moderation_flags: Option<bool>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct RadrootsTradeOrderBackofficeQuery { - pub order: RadrootsTradeOrderQuery, - pub requires_review: Option<bool>, - pub has_open_moderation_flags: Option<bool>, - pub has_open_fulfillment_exceptions: Option<bool>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] -pub struct RadrootsTradeListingBackofficeView { - pub listing: RadrootsTradeListingProjection, - pub marketplace: Option<RadrootsTradeMarketplaceListingSummary>, - pub overlay: Option<RadrootsTradeListingBackofficeOverlay>, - pub requires_review: bool, - pub open_moderation_flag_count: u32, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] -pub struct RadrootsTradeOrderBackofficeView { - pub order: RadrootsTradeOrderWorkflowProjection, - pub marketplace: RadrootsTradeMarketplaceOrderSummary, - pub overlay: Option<RadrootsTradeOrderBackofficeOverlay>, - pub requires_review: bool, - pub open_moderation_flag_count: u32, - pub open_fulfillment_exception_count: u32, -} - -#[derive(Clone, Debug, Default)] -pub struct RadrootsTradeBackofficeOverlayStore { - listing_overlays: BTreeMap<String, RadrootsTradeListingBackofficeOverlay>, - order_overlays: BTreeMap<String, RadrootsTradeOrderBackofficeOverlay>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeBackofficeOverlayError { - MissingListingAddr, - MissingOrderId, -} - -impl core::fmt::Display for RadrootsTradeBackofficeOverlayError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::MissingListingAddr => write!(f, "missing listing address"), - Self::MissingOrderId => write!(f, "missing order id"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsTradeBackofficeOverlayError {} - -impl RadrootsTradeBackofficeOverlayStore { - pub fn new() -> Self { - Self::default() - } - - pub fn listing_overlays(&self) -> &BTreeMap<String, RadrootsTradeListingBackofficeOverlay> { - &self.listing_overlays - } - - pub fn order_overlays(&self) -> &BTreeMap<String, RadrootsTradeOrderBackofficeOverlay> { - &self.order_overlays - } - - pub fn listing_overlay( - &self, - listing_addr: &str, - ) -> Option<&RadrootsTradeListingBackofficeOverlay> { - self.listing_overlays.get(listing_addr) - } - - pub fn order_overlay(&self, order_id: &str) -> Option<&RadrootsTradeOrderBackofficeOverlay> { - self.order_overlays.get(order_id) - } - - pub fn upsert_listing_overlay( - &mut self, - overlay: RadrootsTradeListingBackofficeOverlay, - ) -> Result<&RadrootsTradeListingBackofficeOverlay, RadrootsTradeBackofficeOverlayError> { - if overlay.listing_addr.is_empty() { - return Err(RadrootsTradeBackofficeOverlayError::MissingListingAddr); - } - let listing_addr = overlay.listing_addr.clone(); - self.listing_overlays.insert(listing_addr.clone(), overlay); - Ok(self - .listing_overlays - .get(&listing_addr) - .expect("listing overlay should exist after upsert")) - } - - pub fn upsert_order_overlay( - &mut self, - overlay: RadrootsTradeOrderBackofficeOverlay, - ) -> Result<&RadrootsTradeOrderBackofficeOverlay, RadrootsTradeBackofficeOverlayError> { - if overlay.order_id.is_empty() { - return Err(RadrootsTradeBackofficeOverlayError::MissingOrderId); - } - let order_id = overlay.order_id.clone(); - self.order_overlays.insert(order_id.clone(), overlay); - Ok(self - .order_overlays - .get(&order_id) - .expect("order overlay should exist after upsert")) - } - - pub fn merge_listing_projection( - &self, - listing: &RadrootsTradeListingProjection, - ) -> RadrootsTradeListingBackofficeView { - let overlay = self.listing_overlay(&listing.listing_addr).cloned(); - let requires_review = overlay - .as_ref() - .is_some_and(RadrootsTradeListingBackofficeOverlay::requires_review); - let open_moderation_flag_count = overlay.as_ref().map_or( - 0, - RadrootsTradeListingBackofficeOverlay::open_moderation_flag_count, - ); - - RadrootsTradeListingBackofficeView { - listing: listing.clone(), - marketplace: listing.marketplace_summary(), - overlay, - requires_review, - open_moderation_flag_count, - } - } - - pub fn merge_order_projection( - &self, - order: &RadrootsTradeOrderWorkflowProjection, - ) -> RadrootsTradeOrderBackofficeView { - let overlay = self.order_overlay(&order.order_id).cloned(); - let requires_review = overlay - .as_ref() - .is_some_and(RadrootsTradeOrderBackofficeOverlay::requires_review); - let open_moderation_flag_count = overlay.as_ref().map_or( - 0, - RadrootsTradeOrderBackofficeOverlay::open_moderation_flag_count, - ); - let open_fulfillment_exception_count = overlay.as_ref().map_or( - 0, - RadrootsTradeOrderBackofficeOverlay::open_fulfillment_exception_count, - ); - - RadrootsTradeOrderBackofficeView { - order: order.clone(), - marketplace: order.marketplace_summary(), - overlay, - requires_review, - open_moderation_flag_count, - open_fulfillment_exception_count, - } - } - - pub fn listing_backoffice_views( - &self, - read_index: &RadrootsTradeReadIndex, - query: &RadrootsTradeListingBackofficeQuery, - sort: RadrootsTradeListingSort, - ) -> Vec<RadrootsTradeListingBackofficeView> { - read_index - .query_listings(&query.listing, sort) - .into_iter() - .map(|listing| self.merge_listing_projection(listing)) - .filter(|view| listing_backoffice_matches_query(view, query)) - .collect() - } - - pub fn order_backoffice_views( - &self, - read_index: &RadrootsTradeReadIndex, - query: &RadrootsTradeOrderBackofficeQuery, - sort: RadrootsTradeOrderSort, - ) -> Vec<RadrootsTradeOrderBackofficeView> { - read_index - .query_orders(&query.order, sort) - .into_iter() - .map(|order| self.merge_order_projection(order)) - .filter(|view| order_backoffice_matches_query(view, query)) - .collect() - } -} - -fn bool_filter_matches(value: bool, filter: Option<bool>) -> bool { - match filter { - Some(expected) => expected == value, - None => true, - } -} - -fn listing_backoffice_matches_query( - view: &RadrootsTradeListingBackofficeView, - query: &RadrootsTradeListingBackofficeQuery, -) -> bool { - bool_filter_matches(view.requires_review, query.requires_review) - && bool_filter_matches( - view.open_moderation_flag_count > 0, - query.has_open_moderation_flags, - ) -} - -fn order_backoffice_matches_query( - view: &RadrootsTradeOrderBackofficeView, - query: &RadrootsTradeOrderBackofficeQuery, -) -> bool { - bool_filter_matches(view.requires_review, query.requires_review) - && bool_filter_matches( - view.open_moderation_flag_count > 0, - query.has_open_moderation_flags, - ) - && bool_filter_matches( - view.open_fulfillment_exception_count > 0, - query.has_open_fulfillment_exceptions, - ) -} - -#[cfg(test)] -mod tests { - use std::cell::RefCell; - - use super::{ - RadrootsTradeBackofficeOverlayError, RadrootsTradeBackofficeOverlayStore, - RadrootsTradeFulfillmentException, RadrootsTradeFulfillmentExceptionSeverity, - RadrootsTradeFulfillmentExceptionStatus, RadrootsTradeListingBackofficeOverlay, - RadrootsTradeListingBackofficeQuery, RadrootsTradeModerationFlag, - RadrootsTradeModerationSeverity, RadrootsTradeModerationStatus, - RadrootsTradeOrderBackofficeOverlay, RadrootsTradeOrderBackofficeQuery, - RadrootsTradeReviewPriority, RadrootsTradeReviewQueueEntry, RadrootsTradeReviewStatus, - }; - use crate::listing::{ - dvm::{TradeListingCancel, TradeListingMessagePayload, TradeOrderResponse}, - projection::RadrootsTradeOrderWorkflowMessage, - }; - use crate::listing::{ - order::{ - TradeEconomicActor, TradeEconomicEffect, TradeEconomicLineKind, TradeFulfillmentStatus, - TradeFulfillmentUpdate, TradeOrder, TradeOrderEconomicItem, TradeOrderEconomicLine, - TradeOrderEconomics, TradeOrderItem, TradeOrderStatus, TradePricingBasis, TradeReceipt, - }, - projection::{ - RadrootsTradeListingSort, RadrootsTradeListingSortField, RadrootsTradeOrderQuery, - RadrootsTradeOrderSort, RadrootsTradeOrderSortField, RadrootsTradeReadIndex, - RadrootsTradeSortDirection, - }, - }; - use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, - }; - use radroots_events::RadrootsNostrEventPtr; - use radroots_events::farm::RadrootsFarmRef; - use radroots_events::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, - }; - - #[derive(Clone, Debug)] - struct TestWorkflowChain { - buyer_pubkey: String, - seller_pubkey: String, - root_event_id: String, - last_event_id: String, - next_sequence: u32, - } - - thread_local! { - static TEST_WORKFLOW_CHAINS: RefCell<std::collections::BTreeMap<String, TestWorkflowChain>> = - RefCell::new(std::collections::BTreeMap::new()); - } - - fn listing_snapshot(listing_addr: &str) -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: format!("snapshot:{listing_addr}"), - relays: None, - } - } - - fn seller_pubkey_from_listing_addr(listing_addr: &str) -> String { - listing_addr - .split(':') - .nth(1) - .unwrap_or_default() - .to_string() - } - - fn workflow_refs( - actor_pubkey: &str, - listing_addr: &str, - order_id: Option<&str>, - payload: &TradeListingMessagePayload, - ) -> ( - String, - String, - Option<RadrootsNostrEventPtr>, - Option<String>, - Option<String>, - ) { - let message_type = payload.message_type(); - let listing_event = message_type - .requires_listing_snapshot() - .then(|| listing_snapshot(listing_addr)); - let default_seller = seller_pubkey_from_listing_addr(listing_addr); - - match (payload, order_id) { - (_, None) => ( - format!("event:no-order:{}:{actor_pubkey}", message_type.kind()), - default_seller, - listing_event, - None, - None, - ), - (TradeListingMessagePayload::TradeOrderRequested(order), Some(order_id)) => { - let event_id = format!("{order_id}:request"); - TEST_WORKFLOW_CHAINS.with(|chains| { - chains.borrow_mut().insert( - order_id.to_string(), - TestWorkflowChain { - buyer_pubkey: order.buyer_pubkey.clone(), - seller_pubkey: order.seller_pubkey.clone(), - root_event_id: event_id.clone(), - last_event_id: event_id.clone(), - next_sequence: 1, - }, - ); - }); - ( - event_id, - order.seller_pubkey.clone(), - listing_event, - None, - None, - ) - } - (_, Some(order_id)) => TEST_WORKFLOW_CHAINS.with(|chains| { - let mut chains = chains.borrow_mut(); - let chain = - chains - .entry(order_id.to_string()) - .or_insert_with(|| TestWorkflowChain { - buyer_pubkey: String::from("buyer-pubkey"), - seller_pubkey: default_seller.clone(), - root_event_id: format!("{order_id}:root"), - last_event_id: format!("{order_id}:root"), - next_sequence: 1, - }); - let event_id = - format!("{order_id}:{}:{}", message_type.kind(), chain.next_sequence); - chain.next_sequence += 1; - let counterparty_pubkey = if actor_pubkey == chain.seller_pubkey { - chain.buyer_pubkey.clone() - } else { - chain.seller_pubkey.clone() - }; - let prev_event_id = chain.last_event_id.clone(); - let root_event_id = chain.root_event_id.clone(); - chain.last_event_id = event_id.clone(); - ( - event_id, - counterparty_pubkey, - listing_event, - Some(root_event_id), - Some(prev_event_id), - ) - }), - } - } - - fn base_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "farm-pubkey".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("single origin".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".into(), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".into(), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(2u32), - RadrootsCoreCurrency::USD, - ), - RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - ), - display_amount: None, - display_unit: None, - display_label: Some("1kg bag".into()), - display_price: Some(RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(2000u32), - RadrootsCoreCurrency::USD, - )), - display_price_unit: Some(RadrootsCoreUnit::Each), - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(10u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "farm".into(), - city: Some("Nashville".into()), - region: Some("TN".into()), - country: Some("US".into()), - lat: None, - lng: None, - geohash: None, - }), - images: None, - } - } - - fn alternate_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".into(), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "farm-pubkey-2".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAABA".into(), - }, - product: RadrootsListingProduct { - key: "greens".into(), - title: "Greens".into(), - category: "vegetables".into(), - summary: Some("washed bunches".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".into(), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".into(), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(500u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(4u32), - RadrootsCoreCurrency::USD, - ), - RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - ), - display_amount: None, - display_unit: None, - display_label: Some("500g bunch".into()), - display_price: Some(RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(2000u32), - RadrootsCoreCurrency::USD, - )), - display_price_unit: Some(RadrootsCoreUnit::Each), - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(4u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Sold, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Shipping), - location: Some(RadrootsListingLocation { - primary: "warehouse".into(), - city: Some("Louisville".into()), - region: Some("KY".into()), - country: Some("US".into()), - lat: None, - lng: None, - geohash: None, - }), - images: None, - } - } - - fn order_economics(items: &[TradeOrderItem], include_discount: bool) -> TradeOrderEconomics { - let mut subtotal = RadrootsCoreDecimal::from(0u32); - let economic_items = items - .iter() - .map(|item| { - let line_subtotal = - RadrootsCoreDecimal::from(item.bin_count) * RadrootsCoreDecimal::from(5u32); - subtotal = subtotal + line_subtotal; - TradeOrderEconomicItem { - bin_id: item.bin_id.clone(), - bin_count: item.bin_count, - quantity_amount: RadrootsCoreDecimal::from(1u32), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: RadrootsCoreDecimal::from(5u32), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: RadrootsCoreMoney::new(line_subtotal, RadrootsCoreCurrency::USD), - } - }) - .collect::<Vec<_>>(); - let discounts = include_discount - .then(|| { - vec![TradeOrderEconomicLine { - id: "discount-1".into(), - kind: TradeEconomicLineKind::ListingDiscount, - actor: TradeEconomicActor::Seller, - effect: TradeEconomicEffect::Decrease, - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreCurrency::USD, - ), - reason: "listing discount".into(), - }] - }) - .unwrap_or_default(); - let discount_total = if include_discount { - RadrootsCoreDecimal::from(1u32) - } else { - RadrootsCoreDecimal::from(0u32) - }; - TradeOrderEconomics { - quote_id: "quote-1".into(), - quote_version: 1, - pricing_basis: TradePricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: economic_items, - discounts, - adjustments: Vec::new(), - subtotal: RadrootsCoreMoney::new(subtotal, RadrootsCoreCurrency::USD), - discount_total: RadrootsCoreMoney::new(discount_total, RadrootsCoreCurrency::USD), - adjustment_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - total: RadrootsCoreMoney::new(subtotal - discount_total, RadrootsCoreCurrency::USD), - } - } - - fn base_order() -> TradeOrder { - let items = vec![TradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 2, - }]; - TradeOrder { - order_id: "order-1".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer-pubkey".into(), - seller_pubkey: "seller-pubkey".into(), - economics: order_economics(&items, true), - items, - } - } - - fn alternate_order() -> TradeOrder { - let items = vec![TradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 3, - }]; - TradeOrder { - order_id: "order-2".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw".into(), - buyer_pubkey: "buyer-pubkey-2".into(), - seller_pubkey: "seller-pubkey".into(), - economics: order_economics(&items, false), - items, - } - } - - fn message( - actor_pubkey: &str, - listing_addr: &str, - order_id: Option<&str>, - payload: TradeListingMessagePayload, - ) -> RadrootsTradeOrderWorkflowMessage { - let (event_id, counterparty_pubkey, listing_event, root_event_id, prev_event_id) = - workflow_refs(actor_pubkey, listing_addr, order_id, &payload); - RadrootsTradeOrderWorkflowMessage { - event_id, - actor_pubkey: actor_pubkey.into(), - counterparty_pubkey, - listing_addr: listing_addr.into(), - order_id: order_id.map(str::to_string), - listing_event, - root_event_id, - prev_event_id, - payload, - } - } - - #[test] - fn overlay_helpers_and_store_accessors_cover_flags_and_errors() { - let review_entry = RadrootsTradeReviewQueueEntry { - queue: "queue".into(), - priority: RadrootsTradeReviewPriority::Normal, - status: RadrootsTradeReviewStatus::Resolved, - assigned_operator: None, - reason: None, - }; - assert!(!review_entry.requires_review()); - - let listing_overlay = RadrootsTradeListingBackofficeOverlay { - listing_addr: "listing-1".into(), - review_queue: Some(review_entry), - moderation_flags: vec![ - RadrootsTradeModerationFlag { - code: "resolved".into(), - severity: RadrootsTradeModerationSeverity::Notice, - status: RadrootsTradeModerationStatus::Resolved, - source: None, - reason: None, - }, - RadrootsTradeModerationFlag { - code: "open".into(), - severity: RadrootsTradeModerationSeverity::Warning, - status: RadrootsTradeModerationStatus::Open, - source: None, - reason: None, - }, - ], - }; - assert!(!listing_overlay.requires_review()); - assert_eq!(listing_overlay.open_moderation_flag_count(), 1); - assert!(listing_overlay.has_open_moderation_flags()); - - let order_overlay = RadrootsTradeOrderBackofficeOverlay { - order_id: "order-1".into(), - review_queue: Some(RadrootsTradeReviewQueueEntry { - queue: "queue".into(), - priority: RadrootsTradeReviewPriority::Low, - status: RadrootsTradeReviewStatus::Resolved, - assigned_operator: None, - reason: None, - }), - moderation_flags: vec![RadrootsTradeModerationFlag { - code: "resolved".into(), - severity: RadrootsTradeModerationSeverity::Notice, - status: RadrootsTradeModerationStatus::Resolved, - source: None, - reason: None, - }], - fulfillment_exceptions: vec![ - RadrootsTradeFulfillmentException { - code: "resolved".into(), - severity: RadrootsTradeFulfillmentExceptionSeverity::Notice, - status: RadrootsTradeFulfillmentExceptionStatus::Resolved, - source: None, - notes: None, - }, - RadrootsTradeFulfillmentException { - code: "open".into(), - severity: RadrootsTradeFulfillmentExceptionSeverity::Blocking, - status: RadrootsTradeFulfillmentExceptionStatus::Open, - source: None, - notes: None, - }, - ], - }; - assert!(!order_overlay.requires_review()); - assert_eq!(order_overlay.open_moderation_flag_count(), 0); - assert!(!order_overlay.has_open_moderation_flags()); - assert_eq!(order_overlay.open_fulfillment_exception_count(), 1); - assert!(order_overlay.has_open_fulfillment_exceptions()); - - let mut store = RadrootsTradeBackofficeOverlayStore::new(); - assert!(store.listing_overlays().is_empty()); - assert!(store.order_overlays().is_empty()); - store - .upsert_listing_overlay(listing_overlay) - .expect("listing overlay"); - store - .upsert_order_overlay(order_overlay) - .expect("order overlay"); - assert_eq!(store.listing_overlays().len(), 1); - assert_eq!(store.order_overlays().len(), 1); - assert!(store.listing_overlay("listing-1").is_some()); - assert!(store.order_overlay("order-1").is_some()); - - let missing_listing = RadrootsTradeBackofficeOverlayError::MissingListingAddr; - let missing_order = RadrootsTradeBackofficeOverlayError::MissingOrderId; - assert_eq!(missing_listing.to_string(), "missing listing address"); - assert_eq!(missing_order.to_string(), "missing order id"); - assert!(std::error::Error::source(&missing_listing).is_none()); - } - - #[test] - fn message_helper_bootstraps_missing_chain_for_non_request_payload() { - let orphan_message = message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("orphan-order"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("operator-cancelled".into()), - }), - ); - - assert_eq!(orphan_message.order_id.as_deref(), Some("orphan-order")); - assert_eq!(orphan_message.counterparty_pubkey, "buyer-pubkey"); - assert_eq!( - orphan_message.root_event_id.as_deref(), - Some("orphan-order:root") - ); - assert_eq!( - orphan_message.prev_event_id.as_deref(), - Some("orphan-order:root") - ); - - let no_order_message = message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - None, - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("operator-cancelled".into()), - }), - ); - assert!(no_order_message.order_id.is_none()); - assert!(no_order_message.root_event_id.is_none()); - assert!(no_order_message.prev_event_id.is_none()); - } - - #[test] - fn listing_backoffice_views_merge_overlay_without_mutating_canonical_projection() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("base listing"); - index - .upsert_listing("seller-pubkey", &alternate_listing()) - .expect("alternate listing"); - - let mut overlays = RadrootsTradeBackofficeOverlayStore::new(); - overlays - .upsert_listing_overlay(RadrootsTradeListingBackofficeOverlay { - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(), - review_queue: Some(RadrootsTradeReviewQueueEntry { - queue: "listing-review".into(), - priority: RadrootsTradeReviewPriority::High, - status: RadrootsTradeReviewStatus::Queued, - assigned_operator: Some("ops-1".into()), - reason: Some("verify organic claim".into()), - }), - moderation_flags: vec![RadrootsTradeModerationFlag { - code: "needs-copy-review".into(), - severity: RadrootsTradeModerationSeverity::Warning, - status: RadrootsTradeModerationStatus::Open, - source: Some("policy".into()), - reason: Some("contains superlative marketing copy".into()), - }], - }) - .expect("listing overlay"); - - let views = overlays.listing_backoffice_views( - &index, - &RadrootsTradeListingBackofficeQuery { - has_open_moderation_flags: Some(true), - ..Default::default() - }, - RadrootsTradeListingSort { - field: RadrootsTradeListingSortField::ListingAddr, - direction: RadrootsTradeSortDirection::Asc, - }, - ); - - assert_eq!(views.len(), 1); - assert_eq!(views[0].listing.listing_addr, base_order().listing_addr); - assert!(views[0].requires_review); - assert_eq!(views[0].open_moderation_flag_count, 1); - assert!(!super::listing_backoffice_matches_query( - &views[0], - &RadrootsTradeListingBackofficeQuery { - requires_review: Some(false), - has_open_moderation_flags: Some(false), - ..Default::default() - } - )); - assert_eq!( - views[0] - .overlay - .as_ref() - .and_then(|overlay| overlay.review_queue.as_ref()) - .and_then(|entry| entry.assigned_operator.as_deref()), - Some("ops-1") - ); - - let listing = index - .listing("30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg") - .expect("canonical listing"); - assert_eq!(listing.order_count, 0); - assert_eq!(listing.open_order_count, 0); - assert_eq!(listing.terminal_order_count, 0); - } - - #[test] - fn order_backoffice_views_filter_review_and_exception_overlays() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("base listing"); - index - .upsert_listing("seller-pubkey", &alternate_listing()) - .expect("alternate listing"); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("first order"); - index - .apply_workflow_message(&message( - "buyer-pubkey-2", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::TradeOrderRequested(alternate_order()), - )) - .expect("second order"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: Some("approved".into()), - }), - )) - .expect("accepted"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Delivered, - }), - )) - .expect("fulfilled"); - index - .apply_workflow_message(&message( - "buyer-pubkey-2", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: true, - at: 1_700_000_020, - }), - )) - .expect("completed"); - - let mut overlays = RadrootsTradeBackofficeOverlayStore::new(); - overlays - .upsert_order_overlay(RadrootsTradeOrderBackofficeOverlay { - order_id: "order-1".into(), - review_queue: Some(RadrootsTradeReviewQueueEntry { - queue: "order-review".into(), - priority: RadrootsTradeReviewPriority::Critical, - status: RadrootsTradeReviewStatus::InProgress, - assigned_operator: Some("ops-2".into()), - reason: Some("buyer requested rush handling".into()), - }), - moderation_flags: vec![RadrootsTradeModerationFlag { - code: "buyer-note-review".into(), - severity: RadrootsTradeModerationSeverity::Notice, - status: RadrootsTradeModerationStatus::Snoozed, - source: Some("operator".into()), - reason: Some("monitor communication tone".into()), - }], - fulfillment_exceptions: vec![RadrootsTradeFulfillmentException { - code: "dock-delay".into(), - severity: RadrootsTradeFulfillmentExceptionSeverity::Blocking, - status: RadrootsTradeFulfillmentExceptionStatus::Open, - source: Some("fulfillment".into()), - notes: Some("carrier missed pickup window".into()), - }], - }) - .expect("order overlay"); - overlays - .upsert_order_overlay(RadrootsTradeOrderBackofficeOverlay { - order_id: "order-2".into(), - review_queue: Some(RadrootsTradeReviewQueueEntry { - queue: "order-review".into(), - priority: RadrootsTradeReviewPriority::Low, - status: RadrootsTradeReviewStatus::Resolved, - assigned_operator: None, - reason: Some("completed successfully".into()), - }), - moderation_flags: Vec::new(), - fulfillment_exceptions: vec![RadrootsTradeFulfillmentException { - code: "tracking-delay".into(), - severity: RadrootsTradeFulfillmentExceptionSeverity::Notice, - status: RadrootsTradeFulfillmentExceptionStatus::Resolved, - source: Some("fulfillment".into()), - notes: Some("carrier synced late".into()), - }], - }) - .expect("resolved order overlay"); - - let views = overlays.order_backoffice_views( - &index, - &RadrootsTradeOrderBackofficeQuery { - order: RadrootsTradeOrderQuery { - seller_pubkey: Some("seller-pubkey".into()), - ..Default::default() - }, - requires_review: Some(true), - has_open_fulfillment_exceptions: Some(true), - ..Default::default() - }, - RadrootsTradeOrderSort { - field: RadrootsTradeOrderSortField::OrderId, - direction: RadrootsTradeSortDirection::Asc, - }, - ); - - assert_eq!(views.len(), 1); - assert_eq!(views[0].order.order_id, "order-1"); - assert_eq!(views[0].open_moderation_flag_count, 1); - assert_eq!(views[0].open_fulfillment_exception_count, 1); - assert!(views[0].requires_review); - assert_eq!(views[0].marketplace.status, TradeOrderStatus::Requested); - assert!(!super::order_backoffice_matches_query( - &views[0], - &RadrootsTradeOrderBackofficeQuery { - requires_review: Some(true), - has_open_moderation_flags: Some(false), - has_open_fulfillment_exceptions: Some(true), - ..Default::default() - } - )); - - let completed_order = index.order("order-2").expect("canonical completed order"); - assert_eq!(completed_order.status, TradeOrderStatus::Completed); - assert_eq!(completed_order.receipt_count, 1); - } - - #[test] - fn overlay_store_rejects_missing_identity_keys() { - let mut overlays = RadrootsTradeBackofficeOverlayStore::new(); - - let listing_err = overlays - .upsert_listing_overlay(RadrootsTradeListingBackofficeOverlay { - listing_addr: String::new(), - review_queue: None, - moderation_flags: Vec::new(), - }) - .expect_err("missing listing addr should fail"); - assert_eq!( - listing_err, - RadrootsTradeBackofficeOverlayError::MissingListingAddr - ); - - let order_err = overlays - .upsert_order_overlay(RadrootsTradeOrderBackofficeOverlay { - order_id: String::new(), - review_queue: None, - moderation_flags: Vec::new(), - fulfillment_exceptions: Vec::new(), - }) - .expect_err("missing order id should fail"); - assert_eq!( - order_err, - RadrootsTradeBackofficeOverlayError::MissingOrderId - ); - } -} diff --git a/crates/trade/src/listing/projection.rs b/crates/trade/src/listing/projection.rs @@ -1,5766 +0,0 @@ -#![forbid(unsafe_code)] - -use core::cmp::Ordering; - -#[cfg(not(feature = "std"))] -use alloc::{collections::BTreeMap, format, string::String, vec::Vec}; -#[cfg(feature = "std")] -use std::collections::BTreeMap; - -use radroots_core::{RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountValue}; -use radroots_events::{ - RadrootsNostrEvent, RadrootsNostrEventPtr, - farm::RadrootsFarmRef, - kinds::{KIND_LISTING, KIND_TRADE_ORDER_REQUEST, is_listing_kind}, - listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingLocation, - RadrootsListingProduct, - }, - plot::RadrootsPlotRef, - resource_area::RadrootsResourceAreaRef, -}; - -use crate::listing::{ - codec::{TradeListingParseError, listing_from_event_parts}, - dvm::{ - TradeListingEnvelopeParseError, TradeListingMessagePayload, TradeListingMessageType, - trade_listing_envelope_from_event, - }, - model::RadrootsTradeListingTotal, - order::{ - TradeFulfillmentStatus, TradeOrder, TradeOrderChange, TradeOrderEconomicLine, - TradeOrderItem, TradeOrderStatus, - }, - price_ext::BinPricingExt, -}; -#[cfg(feature = "serde_json")] -use radroots_events_codec::trade::{ - RadrootsActiveTradeEnvelopeParseError, active_trade_order_request_from_event, - trade_event_context_from_tags, -}; - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] -pub struct RadrootsTradeListingBinProjection { - pub bin: RadrootsListingBin, - pub one_bin_total: RadrootsTradeListingTotal, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] -pub struct RadrootsTradeListingProjection { - pub listing_addr: String, - pub seller_pubkey: String, - pub listing_id: String, - pub farm: RadrootsFarmRef, - pub product: RadrootsListingProduct, - pub primary_bin_id: String, - pub bins: Vec<RadrootsTradeListingBinProjection>, - pub resource_area: Option<RadrootsResourceAreaRef>, - pub plot: Option<RadrootsPlotRef>, - pub discounts: Option<Vec<RadrootsCoreDiscount>>, - pub inventory_available: Option<RadrootsCoreDecimal>, - pub availability: Option<RadrootsListingAvailability>, - pub delivery_method: Option<RadrootsListingDeliveryMethod>, - pub location: Option<RadrootsListingLocation>, - pub images: Option<Vec<RadrootsListingImage>>, - pub order_count: u32, - pub open_order_count: u32, - pub terminal_order_count: u32, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderWorkflowProjection { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub items: Vec<TradeOrderItem>, - pub requested_discounts: Option<Vec<TradeOrderEconomicLine>>, - pub status: TradeOrderStatus, - pub listing_snapshot: Option<RadrootsNostrEventPtr>, - pub root_event_id: String, - pub last_event_id: String, - pub last_discount_request: Option<RadrootsCoreDiscountValue>, - pub last_discount_offer: Option<RadrootsCoreDiscountValue>, - pub accepted_discount: Option<RadrootsCoreDiscountValue>, - pub last_fulfillment_status: Option<TradeFulfillmentStatus>, - pub receipt_acknowledged: Option<bool>, - pub receipt_at: Option<u64>, - pub last_reason: Option<String>, - pub last_discount_decline_reason: Option<String>, - pub question_count: u32, - pub answer_count: u32, - pub revision_count: u32, - pub discount_request_count: u32, - pub discount_offer_count: u32, - pub discount_accept_count: u32, - pub discount_decline_count: u32, - pub cancellation_count: u32, - pub fulfillment_update_count: u32, - pub receipt_count: u32, - pub last_message_type: TradeListingMessageType, - pub last_actor_pubkey: String, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderWorkflowMessage { - pub event_id: String, - pub actor_pubkey: String, - pub counterparty_pubkey: String, - pub listing_addr: String, - pub order_id: Option<String>, - pub listing_event: Option<RadrootsNostrEventPtr>, - pub root_event_id: Option<String>, - pub prev_event_id: Option<String>, - pub payload: TradeListingMessagePayload, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeSortDirection { - Asc, - Desc, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeListingMarketStatus { - Unknown, - Window, - Active, - Sold, - Other { value: String }, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct RadrootsTradeListingQuery { - pub seller_pubkey: Option<String>, - pub farm_pubkey: Option<String>, - pub farm_id: Option<String>, - pub product_key: Option<String>, - pub product_category: Option<String>, - pub listing_status: Option<RadrootsTradeListingMarketStatus>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeListingSortField { - ListingAddr, - ProductTitle, - ProductCategory, - SellerPubkey, - InventoryAvailable, - OpenOrderCount, - TotalOrderCount, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct RadrootsTradeListingSort { - pub field: RadrootsTradeListingSortField, - pub direction: RadrootsTradeSortDirection, -} - -impl Default for RadrootsTradeListingSort { - fn default() -> Self { - Self { - field: RadrootsTradeListingSortField::ListingAddr, - direction: RadrootsTradeSortDirection::Asc, - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct RadrootsTradeOrderQuery { - pub listing_addr: Option<String>, - pub buyer_pubkey: Option<String>, - pub seller_pubkey: Option<String>, - pub status: Option<TradeOrderStatus>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTradeOrderSortField { - OrderId, - ListingAddr, - BuyerPubkey, - SellerPubkey, - Status, - LastMessageType, - TotalBinCount, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderSort { - pub field: RadrootsTradeOrderSortField, - pub direction: RadrootsTradeSortDirection, -} - -impl Default for RadrootsTradeOrderSort { - fn default() -> Self { - Self { - field: RadrootsTradeOrderSortField::OrderId, - direction: RadrootsTradeSortDirection::Asc, - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeFacetCount { - pub key: String, - pub count: u32, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeListingFacets { - pub seller_pubkeys: Vec<RadrootsTradeFacetCount>, - pub farm_pubkeys: Vec<RadrootsTradeFacetCount>, - pub farm_ids: Vec<RadrootsTradeFacetCount>, - pub product_keys: Vec<RadrootsTradeFacetCount>, - pub product_categories: Vec<RadrootsTradeFacetCount>, - pub listing_statuses: Vec<RadrootsTradeFacetCount>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeOrderFacets { - pub buyer_pubkeys: Vec<RadrootsTradeFacetCount>, - pub seller_pubkeys: Vec<RadrootsTradeFacetCount>, - pub listing_addrs: Vec<RadrootsTradeFacetCount>, - pub statuses: Vec<RadrootsTradeFacetCount>, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeMarketplaceListingSummary { - pub listing_addr: String, - pub seller_pubkey: String, - pub farm_pubkey: String, - pub farm_id: String, - pub product_key: String, - pub product_title: String, - pub product_category: String, - pub product_summary: Option<String>, - pub listing_status: RadrootsTradeListingMarketStatus, - pub location_primary: Option<String>, - pub inventory_available: Option<RadrootsCoreDecimal>, - pub primary_bin_id: String, - pub primary_bin_label: Option<String>, - pub primary_bin_total: RadrootsTradeListingTotal, - pub order_count: u32, - pub open_order_count: u32, - pub terminal_order_count: u32, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsTradeMarketplaceOrderSummary { - pub order_id: String, - pub listing_addr: String, - pub buyer_pubkey: String, - pub seller_pubkey: String, - pub status: TradeOrderStatus, - pub last_message_type: TradeListingMessageType, - pub item_count: u32, - pub total_bin_count: u32, - pub has_requested_discounts: bool, - pub last_reason: Option<String>, -} - -#[derive(Clone, Debug, Default)] -pub struct RadrootsTradeReadIndex { - listings: BTreeMap<String, RadrootsTradeListingProjection>, - orders: BTreeMap<String, RadrootsTradeOrderWorkflowProjection>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsTradeProjectionError { - InvalidListingKind { - kind: u32, - }, - InvalidListingContract { - error: TradeListingParseError, - }, - MissingPrimaryBin(String), - MissingOrderId, - OrderIdMismatch, - ListingAddrMismatch, - MissingOrder(String), - InvalidTransition { - from: TradeOrderStatus, - to: TradeOrderStatus, - }, - InvalidItemIndex(u32), - InvalidDiscountDecision, - InvalidRevisionResponse, - NonOrderWorkflowMessage(TradeListingMessageType), - UnauthorizedActor, - CounterpartyMismatch, - MissingListingSnapshot, - MissingTradeRootEventId, - MissingTradePrevEventId, - TradeThreadRootMismatch, - TradeThreadPrevMismatch, - #[cfg(feature = "serde_json")] - InvalidWorkflowEvent { - error: TradeListingEnvelopeParseError, - }, -} - -impl core::fmt::Display for RadrootsTradeProjectionError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - RadrootsTradeProjectionError::InvalidListingKind { kind } => { - write!(f, "invalid listing event kind: {kind}") - } - RadrootsTradeProjectionError::InvalidListingContract { error } => { - write!(f, "invalid listing contract event: {error}") - } - RadrootsTradeProjectionError::MissingPrimaryBin(bin_id) => { - write!(f, "missing primary bin: {bin_id}") - } - RadrootsTradeProjectionError::MissingOrderId => write!(f, "missing order id"), - RadrootsTradeProjectionError::OrderIdMismatch => write!(f, "order id mismatch"), - RadrootsTradeProjectionError::ListingAddrMismatch => { - write!(f, "listing address mismatch") - } - RadrootsTradeProjectionError::MissingOrder(order_id) => { - write!(f, "missing order projection: {order_id}") - } - RadrootsTradeProjectionError::InvalidTransition { from, to } => { - write!(f, "invalid order transition: {from:?} -> {to:?}") - } - RadrootsTradeProjectionError::InvalidItemIndex(index) => { - write!(f, "invalid order item index: {index}") - } - RadrootsTradeProjectionError::InvalidDiscountDecision => { - write!(f, "invalid discount decision payload") - } - RadrootsTradeProjectionError::InvalidRevisionResponse => { - write!(f, "invalid order revision response payload") - } - RadrootsTradeProjectionError::NonOrderWorkflowMessage(message_type) => { - write!(f, "non-order workflow message: {message_type:?}") - } - RadrootsTradeProjectionError::UnauthorizedActor => write!(f, "unauthorized actor"), - RadrootsTradeProjectionError::CounterpartyMismatch => { - write!(f, "counterparty pubkey mismatch") - } - RadrootsTradeProjectionError::MissingListingSnapshot => { - write!(f, "missing listing snapshot") - } - RadrootsTradeProjectionError::MissingTradeRootEventId => { - write!(f, "missing trade root event id") - } - RadrootsTradeProjectionError::MissingTradePrevEventId => { - write!(f, "missing trade previous event id") - } - RadrootsTradeProjectionError::TradeThreadRootMismatch => { - write!(f, "trade thread root mismatch") - } - RadrootsTradeProjectionError::TradeThreadPrevMismatch => { - write!(f, "trade thread previous event mismatch") - } - #[cfg(feature = "serde_json")] - RadrootsTradeProjectionError::InvalidWorkflowEvent { error } => write!(f, "{error}"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsTradeProjectionError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - RadrootsTradeProjectionError::InvalidListingContract { error } => Some(error), - #[cfg(feature = "serde_json")] - RadrootsTradeProjectionError::InvalidWorkflowEvent { error } => Some(error), - _ => None, - } - } -} - -impl RadrootsTradeListingProjection { - pub fn market_status(&self) -> RadrootsTradeListingMarketStatus { - match &self.availability { - Some(RadrootsListingAvailability::Status { status }) => match status { - radroots_events::listing::RadrootsListingStatus::Active => { - RadrootsTradeListingMarketStatus::Active - } - radroots_events::listing::RadrootsListingStatus::Sold => { - RadrootsTradeListingMarketStatus::Sold - } - radroots_events::listing::RadrootsListingStatus::Other { value } => { - RadrootsTradeListingMarketStatus::Other { - value: value.clone(), - } - } - }, - Some(RadrootsListingAvailability::Window { .. }) => { - RadrootsTradeListingMarketStatus::Window - } - None => RadrootsTradeListingMarketStatus::Unknown, - } - } - - pub fn primary_bin(&self) -> Option<&RadrootsTradeListingBinProjection> { - self.bins - .iter() - .find(|bin| bin.bin.bin_id == self.primary_bin_id) - } - - pub fn marketplace_summary(&self) -> Option<RadrootsTradeMarketplaceListingSummary> { - let primary_bin = self.primary_bin()?; - Some(RadrootsTradeMarketplaceListingSummary { - listing_addr: self.listing_addr.clone(), - seller_pubkey: self.seller_pubkey.clone(), - farm_pubkey: self.farm.pubkey.clone(), - farm_id: self.farm.d_tag.clone(), - product_key: self.product.key.clone(), - product_title: self.product.title.clone(), - product_category: self.product.category.clone(), - product_summary: self.product.summary.clone(), - listing_status: self.market_status(), - location_primary: self - .location - .as_ref() - .map(|location| location.primary.clone()), - inventory_available: self.inventory_available.clone(), - primary_bin_id: self.primary_bin_id.clone(), - primary_bin_label: primary_bin.bin.display_label.clone(), - primary_bin_total: primary_bin.one_bin_total.clone(), - order_count: self.order_count, - open_order_count: self.open_order_count, - terminal_order_count: self.terminal_order_count, - }) - } - - pub fn from_listing_event( - event: &RadrootsNostrEvent, - ) -> Result<Self, RadrootsTradeProjectionError> { - if !is_listing_kind(event.kind) { - return Err(RadrootsTradeProjectionError::InvalidListingKind { kind: event.kind }); - } - let listing = listing_from_event_parts(&event.tags, &event.content) - .map_err(|error| RadrootsTradeProjectionError::InvalidListingContract { error })?; - let mut projection = Self::from_listing_contract(event.author.clone(), &listing)?; - projection.listing_addr = format!("{}:{}:{}", event.kind, event.author, listing.d_tag); - Ok(projection) - } - - pub fn from_listing_contract( - seller_pubkey: impl Into<String>, - listing: &RadrootsListing, - ) -> Result<Self, RadrootsTradeProjectionError> { - let seller_pubkey = seller_pubkey.into(); - if !listing - .bins - .iter() - .any(|bin| bin.bin_id == listing.primary_bin_id) - { - return Err(RadrootsTradeProjectionError::MissingPrimaryBin( - listing.primary_bin_id.clone(), - )); - } - - let bins = listing - .bins - .iter() - .cloned() - .map(|bin| RadrootsTradeListingBinProjection { - one_bin_total: bin.total_for_count(1), - bin, - }) - .collect(); - - Ok(Self { - listing_addr: format!("{KIND_LISTING}:{}:{}", seller_pubkey, listing.d_tag), - seller_pubkey, - listing_id: listing.d_tag.clone(), - farm: listing.farm.clone(), - product: listing.product.clone(), - primary_bin_id: listing.primary_bin_id.clone(), - bins, - resource_area: listing.resource_area.clone(), - plot: listing.plot.clone(), - discounts: listing.discounts.clone(), - inventory_available: listing.inventory_available.clone(), - availability: listing.availability.clone(), - delivery_method: listing.delivery_method.clone(), - location: listing.location.clone(), - images: listing.images.clone(), - order_count: 0, - open_order_count: 0, - terminal_order_count: 0, - }) - } -} - -impl RadrootsTradeOrderWorkflowProjection { - pub fn is_terminal(&self) -> bool { - radroots_trade_order_status_is_terminal(&self.status) - } - - pub fn item_count(&self) -> u32 { - u32::try_from(self.items.len()).unwrap_or(u32::MAX) - } - - pub fn total_bin_count(&self) -> u32 { - self.items - .iter() - .fold(0u32, |total, item| total.saturating_add(item.bin_count)) - } - - pub fn marketplace_summary(&self) -> RadrootsTradeMarketplaceOrderSummary { - RadrootsTradeMarketplaceOrderSummary { - order_id: self.order_id.clone(), - listing_addr: self.listing_addr.clone(), - buyer_pubkey: self.buyer_pubkey.clone(), - seller_pubkey: self.seller_pubkey.clone(), - status: self.status.clone(), - last_message_type: self.last_message_type, - item_count: self.item_count(), - total_bin_count: self.total_bin_count(), - has_requested_discounts: self - .requested_discounts - .as_ref() - .is_some_and(|discounts| !discounts.is_empty()), - last_reason: self.last_reason.clone(), - } - } - - fn from_order_request( - message: &RadrootsTradeOrderWorkflowMessage, - order: &TradeOrder, - ) -> Result<Self, RadrootsTradeProjectionError> { - let listing_snapshot = require_listing_snapshot(message)?; - Ok(Self { - order_id: order.order_id.clone(), - listing_addr: order.listing_addr.clone(), - buyer_pubkey: order.buyer_pubkey.clone(), - seller_pubkey: order.seller_pubkey.clone(), - items: order.items.clone(), - requested_discounts: (!order.economics.discounts.is_empty()) - .then(|| order.economics.discounts.clone()), - status: TradeOrderStatus::Requested, - listing_snapshot: Some(listing_snapshot), - root_event_id: message.event_id.clone(), - last_event_id: message.event_id.clone(), - last_discount_request: None, - last_discount_offer: None, - accepted_discount: None, - last_fulfillment_status: None, - receipt_acknowledged: None, - receipt_at: None, - last_reason: None, - last_discount_decline_reason: None, - question_count: 0, - answer_count: 0, - revision_count: 0, - discount_request_count: 0, - discount_offer_count: 0, - discount_accept_count: 0, - discount_decline_count: 0, - cancellation_count: 0, - fulfillment_update_count: 0, - receipt_count: 0, - last_message_type: TradeListingMessageType::OrderRequest, - last_actor_pubkey: order.buyer_pubkey.clone(), - }) - } -} - -impl RadrootsTradeOrderWorkflowMessage { - #[cfg(feature = "serde_json")] - pub fn from_event(event: &RadrootsNostrEvent) -> Result<Self, TradeListingEnvelopeParseError> { - if event.kind == KIND_TRADE_ORDER_REQUEST { - let envelope = active_trade_order_request_from_event(event) - .map_err(map_active_order_request_parse_error)?; - let context = - trade_event_context_from_tags(TradeListingMessageType::OrderRequest, &event.tags)?; - return Ok(Self { - event_id: event.id.clone(), - actor_pubkey: event.author.clone(), - counterparty_pubkey: context.counterparty_pubkey, - listing_addr: envelope.listing_addr, - order_id: Some(envelope.order_id), - listing_event: context.listing_event, - root_event_id: context.root_event_id, - prev_event_id: context.prev_event_id, - payload: TradeListingMessagePayload::TradeOrderRequested(envelope.payload), - }); - } - - let envelope = trade_listing_envelope_from_event::<TradeListingMessagePayload>(event)?; - trade_event_context_from_tags(envelope.message_type, &event.tags).map(|context| Self { - event_id: event.id.clone(), - actor_pubkey: event.author.clone(), - counterparty_pubkey: context.counterparty_pubkey, - listing_addr: envelope.listing_addr, - order_id: envelope.order_id, - listing_event: context.listing_event, - root_event_id: context.root_event_id, - prev_event_id: context.prev_event_id, - payload: envelope.payload, - }) - } - - pub fn message_type(&self) -> TradeListingMessageType { - match &self.payload { - TradeListingMessagePayload::ListingValidateRequest(_) => { - TradeListingMessageType::ListingValidateRequest - } - TradeListingMessagePayload::ListingValidateResult(_) => { - TradeListingMessageType::ListingValidateResult - } - TradeListingMessagePayload::TradeOrderRequested(_) => { - TradeListingMessageType::OrderRequest - } - TradeListingMessagePayload::OrderResponse(_) => TradeListingMessageType::OrderResponse, - TradeListingMessagePayload::OrderRevision(_) => TradeListingMessageType::OrderRevision, - TradeListingMessagePayload::OrderRevisionAccept(_) => { - TradeListingMessageType::OrderRevisionAccept - } - TradeListingMessagePayload::OrderRevisionDecline(_) => { - TradeListingMessageType::OrderRevisionDecline - } - TradeListingMessagePayload::Question(_) => TradeListingMessageType::Question, - TradeListingMessagePayload::Answer(_) => TradeListingMessageType::Answer, - TradeListingMessagePayload::DiscountRequest(_) => { - TradeListingMessageType::DiscountRequest - } - TradeListingMessagePayload::DiscountOffer(_) => TradeListingMessageType::DiscountOffer, - TradeListingMessagePayload::DiscountAccept(_) => { - TradeListingMessageType::DiscountAccept - } - TradeListingMessagePayload::DiscountDecline(_) => { - TradeListingMessageType::DiscountDecline - } - TradeListingMessagePayload::Cancel(_) => TradeListingMessageType::Cancel, - TradeListingMessagePayload::FulfillmentUpdate(_) => { - TradeListingMessageType::FulfillmentUpdate - } - TradeListingMessagePayload::Receipt(_) => TradeListingMessageType::Receipt, - } - } -} - -#[cfg(feature = "serde_json")] -fn map_active_order_request_parse_error( - error: RadrootsActiveTradeEnvelopeParseError, -) -> TradeListingEnvelopeParseError { - match error { - RadrootsActiveTradeEnvelopeParseError::InvalidKind(kind) => { - TradeListingEnvelopeParseError::InvalidKind(kind) - } - RadrootsActiveTradeEnvelopeParseError::MissingTag(tag) => { - TradeListingEnvelopeParseError::MissingTag(tag) - } - RadrootsActiveTradeEnvelopeParseError::InvalidTag(tag) => { - TradeListingEnvelopeParseError::InvalidTag(tag) - } - RadrootsActiveTradeEnvelopeParseError::ListingAddrTagMismatch => { - TradeListingEnvelopeParseError::ListingAddrTagMismatch - } - RadrootsActiveTradeEnvelopeParseError::OrderIdTagMismatch => { - TradeListingEnvelopeParseError::OrderIdTagMismatch - } - RadrootsActiveTradeEnvelopeParseError::InvalidListingAddr(error) => { - TradeListingEnvelopeParseError::InvalidListingAddr(error) - } - RadrootsActiveTradeEnvelopeParseError::InvalidJson - | RadrootsActiveTradeEnvelopeParseError::InvalidEnvelope(_) - | RadrootsActiveTradeEnvelopeParseError::InvalidPayload(_) - | RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { .. } - | RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch(_) - | RadrootsActiveTradeEnvelopeParseError::AuthorMismatch - | RadrootsActiveTradeEnvelopeParseError::CounterpartyTagMismatch => { - TradeListingEnvelopeParseError::InvalidJson - } - } -} - -impl RadrootsTradeReadIndex { - pub fn new() -> Self { - Self::default() - } - - pub fn listings(&self) -> &BTreeMap<String, RadrootsTradeListingProjection> { - &self.listings - } - - pub fn orders(&self) -> &BTreeMap<String, RadrootsTradeOrderWorkflowProjection> { - &self.orders - } - - pub fn listing(&self, listing_addr: &str) -> Option<&RadrootsTradeListingProjection> { - self.listings.get(listing_addr) - } - - pub fn order(&self, order_id: &str) -> Option<&RadrootsTradeOrderWorkflowProjection> { - self.orders.get(order_id) - } - - pub fn query_listings<'a>( - &'a self, - query: &RadrootsTradeListingQuery, - sort: RadrootsTradeListingSort, - ) -> Vec<&'a RadrootsTradeListingProjection> { - let mut listings = self - .listings - .values() - .filter(|listing| listing_matches_query(listing, query)) - .collect::<Vec<_>>(); - listings.sort_by(|left, right| compare_listings(left, right, sort)); - listings - } - - pub fn query_orders<'a>( - &'a self, - query: &RadrootsTradeOrderQuery, - sort: RadrootsTradeOrderSort, - ) -> Vec<&'a RadrootsTradeOrderWorkflowProjection> { - let mut orders = self - .orders - .values() - .filter(|order| order_matches_query(order, query)) - .collect::<Vec<_>>(); - orders.sort_by(|left, right| compare_orders(left, right, sort)); - orders - } - - pub fn listing_facets(&self, query: &RadrootsTradeListingQuery) -> RadrootsTradeListingFacets { - let mut seller_pubkeys = BTreeMap::<String, u32>::new(); - let mut farm_pubkeys = BTreeMap::<String, u32>::new(); - let mut farm_ids = BTreeMap::<String, u32>::new(); - let mut product_keys = BTreeMap::<String, u32>::new(); - let mut product_categories = BTreeMap::<String, u32>::new(); - let mut listing_statuses = BTreeMap::<String, u32>::new(); - - for listing in self - .listings - .values() - .filter(|listing| listing_matches_query(listing, query)) - { - increment_count(&mut seller_pubkeys, listing.seller_pubkey.clone()); - increment_count(&mut farm_pubkeys, listing.farm.pubkey.clone()); - increment_count(&mut farm_ids, listing.farm.d_tag.clone()); - increment_count(&mut product_keys, listing.product.key.clone()); - increment_count(&mut product_categories, listing.product.category.clone()); - increment_count(&mut listing_statuses, listing.market_status().facet_key()); - } - - RadrootsTradeListingFacets { - seller_pubkeys: facet_counts_from_map(seller_pubkeys), - farm_pubkeys: facet_counts_from_map(farm_pubkeys), - farm_ids: facet_counts_from_map(farm_ids), - product_keys: facet_counts_from_map(product_keys), - product_categories: facet_counts_from_map(product_categories), - listing_statuses: facet_counts_from_map(listing_statuses), - } - } - - pub fn order_facets(&self, query: &RadrootsTradeOrderQuery) -> RadrootsTradeOrderFacets { - let mut buyer_pubkeys = BTreeMap::<String, u32>::new(); - let mut seller_pubkeys = BTreeMap::<String, u32>::new(); - let mut listing_addrs = BTreeMap::<String, u32>::new(); - let mut statuses = BTreeMap::<String, u32>::new(); - - for order in self - .orders - .values() - .filter(|order| order_matches_query(order, query)) - { - increment_count(&mut buyer_pubkeys, order.buyer_pubkey.clone()); - increment_count(&mut seller_pubkeys, order.seller_pubkey.clone()); - increment_count(&mut listing_addrs, order.listing_addr.clone()); - increment_count(&mut statuses, order_status_key(&order.status)); - } - - RadrootsTradeOrderFacets { - buyer_pubkeys: facet_counts_from_map(buyer_pubkeys), - seller_pubkeys: facet_counts_from_map(seller_pubkeys), - listing_addrs: facet_counts_from_map(listing_addrs), - statuses: facet_counts_from_map(statuses), - } - } - - pub fn marketplace_listing_summaries( - &self, - query: &RadrootsTradeListingQuery, - sort: RadrootsTradeListingSort, - ) -> Vec<RadrootsTradeMarketplaceListingSummary> { - self.query_listings(query, sort) - .into_iter() - .filter_map(|listing| listing.marketplace_summary()) - .collect() - } - - pub fn marketplace_order_summaries( - &self, - query: &RadrootsTradeOrderQuery, - sort: RadrootsTradeOrderSort, - ) -> Vec<RadrootsTradeMarketplaceOrderSummary> { - self.query_orders(query, sort) - .into_iter() - .map(RadrootsTradeOrderWorkflowProjection::marketplace_summary) - .collect() - } - - pub fn upsert_listing( - &mut self, - seller_pubkey: impl Into<String>, - listing: &RadrootsListing, - ) -> Result<&RadrootsTradeListingProjection, RadrootsTradeProjectionError> { - let projection = - RadrootsTradeListingProjection::from_listing_contract(seller_pubkey, listing)?; - let listing_addr = projection.listing_addr.clone(); - self.listings.insert(listing_addr.clone(), projection); - self.refresh_listing_counts(&listing_addr); - Ok(self - .listings - .get(&listing_addr) - .expect("listing projection should exist after upsert")) - } - - pub fn upsert_listing_event( - &mut self, - event: &RadrootsNostrEvent, - ) -> Result<&RadrootsTradeListingProjection, RadrootsTradeProjectionError> { - let projection = RadrootsTradeListingProjection::from_listing_event(event)?; - let listing_addr = projection.listing_addr.clone(); - self.listings.insert(listing_addr.clone(), projection); - self.refresh_listing_counts(&listing_addr); - Ok(self - .listings - .get(&listing_addr) - .expect("listing projection should exist after upsert")) - } - - pub fn apply_workflow_message( - &mut self, - message: &RadrootsTradeOrderWorkflowMessage, - ) -> Result<&RadrootsTradeOrderWorkflowProjection, RadrootsTradeProjectionError> { - let order_id = self.apply_workflow_message_inner(message)?; - let listing_addr = self - .orders - .get(&order_id) - .expect("order projection should exist after workflow apply") - .listing_addr - .clone(); - self.refresh_listing_counts(&listing_addr); - Ok(self - .orders - .get(&order_id) - .expect("order projection should exist after workflow apply")) - } - - #[cfg(feature = "serde_json")] - pub fn apply_workflow_event( - &mut self, - event: &RadrootsNostrEvent, - ) -> Result<&RadrootsTradeOrderWorkflowProjection, RadrootsTradeProjectionError> { - let message = RadrootsTradeOrderWorkflowMessage::from_event(event) - .map_err(|error| RadrootsTradeProjectionError::InvalidWorkflowEvent { error })?; - self.apply_workflow_message(&message) - } - - fn apply_workflow_message_inner( - &mut self, - message: &RadrootsTradeOrderWorkflowMessage, - ) -> Result<String, RadrootsTradeProjectionError> { - match &message.payload { - TradeListingMessagePayload::ListingValidateRequest(_) - | TradeListingMessagePayload::ListingValidateResult(_) => Err( - RadrootsTradeProjectionError::NonOrderWorkflowMessage(message.message_type()), - ), - TradeListingMessagePayload::TradeOrderRequested(order) => { - self.apply_order_request(message, order) - } - TradeListingMessagePayload::OrderResponse(response) => { - let (order_id, order) = self.order_mut_for_seller_action(message)?; - let next_status = if response.accepted { - TradeOrderStatus::Accepted - } else { - TradeOrderStatus::Declined - }; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.last_message_type = TradeListingMessageType::OrderResponse; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = response.reason.clone(); - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::OrderRevision(revision) => { - let (order_id, order) = self.order_mut_for_seller_action(message)?; - let next_status = TradeOrderStatus::Revised; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - for change in &revision.changes { - apply_order_change(&mut order.items, change)?; - } - order.listing_snapshot = Some(require_listing_snapshot(message)?); - order.status = next_status; - order.revision_count = order.revision_count.saturating_add(1); - order.last_message_type = TradeListingMessageType::OrderRevision; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = None; - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::OrderRevisionAccept(response) => { - if !response.accepted { - return Err(RadrootsTradeProjectionError::InvalidRevisionResponse); - } - let (order_id, order) = self.order_mut_for_buyer_action(message)?; - let next_status = TradeOrderStatus::Accepted; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.last_message_type = TradeListingMessageType::OrderRevisionAccept; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = response.reason.clone(); - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::OrderRevisionDecline(response) => { - if response.accepted { - return Err(RadrootsTradeProjectionError::InvalidRevisionResponse); - } - let (order_id, order) = self.order_mut_for_buyer_action(message)?; - let next_status = TradeOrderStatus::Declined; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.last_message_type = TradeListingMessageType::OrderRevisionDecline; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = response.reason.clone(); - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::Question(question) => { - let (order_id, order) = self.order_mut_for_buyer_action(message)?; - let next_status = TradeOrderStatus::Questioned; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.question_count = order.question_count.saturating_add(1); - order.last_message_type = TradeListingMessageType::Question; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = Some(question.question_id.clone()); - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::Answer(answer) => { - let (order_id, order) = self.order_mut_for_seller_action(message)?; - let next_status = TradeOrderStatus::Requested; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.answer_count = order.answer_count.saturating_add(1); - order.last_message_type = TradeListingMessageType::Answer; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = Some(answer.question_id.clone()); - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::DiscountRequest(request) => { - let (order_id, order) = self.order_mut_for_buyer_action(message)?; - order.discount_request_count = order.discount_request_count.saturating_add(1); - order.last_discount_request = Some(request.value.clone()); - order.listing_snapshot = Some(require_listing_snapshot(message)?); - order.last_message_type = TradeListingMessageType::DiscountRequest; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = None; - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::DiscountOffer(offer) => { - let (order_id, order) = self.order_mut_for_seller_action(message)?; - let next_status = TradeOrderStatus::Revised; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.discount_offer_count = order.discount_offer_count.saturating_add(1); - order.last_discount_offer = Some(offer.value.clone()); - order.listing_snapshot = Some(require_listing_snapshot(message)?); - order.last_message_type = TradeListingMessageType::DiscountOffer; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = None; - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::DiscountAccept(decision) => { - let (order_id, order) = self.order_mut_for_buyer_action(message)?; - let TradeDiscountDecisionValue::Accepted(value) = - trade_discount_decision_value(decision) - else { - return Err(RadrootsTradeProjectionError::InvalidDiscountDecision); - }; - let next_status = TradeOrderStatus::Accepted; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.discount_accept_count = order.discount_accept_count.saturating_add(1); - order.accepted_discount = Some(value); - order.last_discount_decline_reason = None; - order.last_message_type = TradeListingMessageType::DiscountAccept; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = None; - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::DiscountDecline(decision) => { - let (order_id, order) = self.order_mut_for_buyer_action(message)?; - let TradeDiscountDecisionValue::Declined(reason) = - trade_discount_decision_value(decision) - else { - return Err(RadrootsTradeProjectionError::InvalidDiscountDecision); - }; - let next_status = TradeOrderStatus::Requested; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.discount_decline_count = order.discount_decline_count.saturating_add(1); - order.last_discount_decline_reason = reason.clone(); - order.last_message_type = TradeListingMessageType::DiscountDecline; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = reason; - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::Cancel(cancel) => { - let (order_id, order) = self.order_mut_for_participant_action(message)?; - let next_status = TradeOrderStatus::Cancelled; - let from_status = order.status.clone(); - radroots_trade_order_status_ensure_transition(from_status, next_status.clone())?; - order.status = next_status; - order.cancellation_count = order.cancellation_count.saturating_add(1); - order.last_message_type = TradeListingMessageType::Cancel; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = cancel.reason.clone(); - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::FulfillmentUpdate(update) => { - let (order_id, order) = self.order_mut_for_seller_action(message)?; - if let Some(next_status) = - trade_order_status_for_fulfillment_update(&order.status, &update.status)? - { - order.status = next_status; - } - order.fulfillment_update_count = order.fulfillment_update_count.saturating_add(1); - order.last_fulfillment_status = Some(update.status.clone()); - order.last_message_type = TradeListingMessageType::FulfillmentUpdate; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = None; - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - TradeListingMessagePayload::Receipt(receipt) => { - let (order_id, order) = self.order_mut_for_buyer_action(message)?; - if let Some(next_status) = - trade_order_status_for_receipt(&order.status, receipt.acknowledged)? - { - order.status = next_status; - } - order.receipt_count = order.receipt_count.saturating_add(1); - order.receipt_acknowledged = Some(receipt.acknowledged); - order.receipt_at = Some(receipt.at); - order.last_message_type = TradeListingMessageType::Receipt; - order.last_actor_pubkey = message.actor_pubkey.clone(); - order.last_reason = None; - order.last_event_id = message.event_id.clone(); - Ok(order_id) - } - } - } - - fn apply_order_request( - &mut self, - message: &RadrootsTradeOrderWorkflowMessage, - order: &TradeOrder, - ) -> Result<String, RadrootsTradeProjectionError> { - if message - .order_id - .as_deref() - .is_some_and(|value| value != order.order_id) - { - return Err(RadrootsTradeProjectionError::OrderIdMismatch); - } - if message.listing_addr != order.listing_addr { - return Err(RadrootsTradeProjectionError::ListingAddrMismatch); - } - ensure_actor(&order.buyer_pubkey, &message.actor_pubkey)?; - ensure_counterparty(&order.seller_pubkey, &message.counterparty_pubkey)?; - if let Some(existing) = self.orders.get(&order.order_id) { - if existing.listing_addr != order.listing_addr - || existing.buyer_pubkey != order.buyer_pubkey - || existing.seller_pubkey != order.seller_pubkey - { - return Err(RadrootsTradeProjectionError::ListingAddrMismatch); - } - return Ok(order.order_id.clone()); - } - - self.orders.insert( - order.order_id.clone(), - RadrootsTradeOrderWorkflowProjection::from_order_request(message, order)?, - ); - Ok(order.order_id.clone()) - } - - fn order_mut_checked( - &mut self, - order_id: &str, - listing_addr: &str, - ) -> Result<&mut RadrootsTradeOrderWorkflowProjection, RadrootsTradeProjectionError> { - let order = self - .orders - .get_mut(order_id) - .ok_or_else(|| RadrootsTradeProjectionError::MissingOrder(order_id.to_string()))?; - if order.listing_addr != listing_addr { - return Err(RadrootsTradeProjectionError::ListingAddrMismatch); - } - Ok(order) - } - - fn order_mut_for_buyer_action( - &mut self, - message: &RadrootsTradeOrderWorkflowMessage, - ) -> Result<(String, &mut RadrootsTradeOrderWorkflowProjection), RadrootsTradeProjectionError> - { - let order_id = required_order_id(message)?.to_string(); - let order = self.order_mut_checked(&order_id, &message.listing_addr)?; - ensure_actor(&order.buyer_pubkey, &message.actor_pubkey)?; - ensure_counterparty(&order.seller_pubkey, &message.counterparty_pubkey)?; - ensure_trade_chain(order, message)?; - Ok((order_id, order)) - } - - fn order_mut_for_seller_action( - &mut self, - message: &RadrootsTradeOrderWorkflowMessage, - ) -> Result<(String, &mut RadrootsTradeOrderWorkflowProjection), RadrootsTradeProjectionError> - { - let order_id = required_order_id(message)?.to_string(); - let order = self.order_mut_checked(&order_id, &message.listing_addr)?; - ensure_actor(&order.seller_pubkey, &message.actor_pubkey)?; - ensure_counterparty(&order.buyer_pubkey, &message.counterparty_pubkey)?; - ensure_trade_chain(order, message)?; - Ok((order_id, order)) - } - - fn order_mut_for_participant_action( - &mut self, - message: &RadrootsTradeOrderWorkflowMessage, - ) -> Result<(String, &mut RadrootsTradeOrderWorkflowProjection), RadrootsTradeProjectionError> - { - let order_id = required_order_id(message)?.to_string(); - let order = self.order_mut_checked(&order_id, &message.listing_addr)?; - if order.buyer_pubkey != message.actor_pubkey && order.seller_pubkey != message.actor_pubkey - { - return Err(RadrootsTradeProjectionError::UnauthorizedActor); - } - let expected_counterparty = if order.buyer_pubkey == message.actor_pubkey { - &order.seller_pubkey - } else { - &order.buyer_pubkey - }; - ensure_counterparty(expected_counterparty, &message.counterparty_pubkey)?; - ensure_trade_chain(order, message)?; - Ok((order_id, order)) - } - - fn refresh_listing_counts(&mut self, listing_addr: &str) { - let Some(listing) = self.listings.get_mut(listing_addr) else { - return; - }; - - let mut order_count = 0u32; - let mut open_order_count = 0u32; - let mut terminal_order_count = 0u32; - - for order in self.orders.values() { - if order.listing_addr != listing_addr { - continue; - } - order_count = order_count.saturating_add(1); - if order.is_terminal() { - terminal_order_count = terminal_order_count.saturating_add(1); - } else { - open_order_count = open_order_count.saturating_add(1); - } - } - - listing.order_count = order_count; - listing.open_order_count = open_order_count; - listing.terminal_order_count = terminal_order_count; - } -} - -pub fn radroots_trade_order_status_can_transition( - from: &TradeOrderStatus, - to: &TradeOrderStatus, -) -> bool { - if from == to { - return true; - } - - match from { - TradeOrderStatus::Draft => matches!(to, TradeOrderStatus::Requested), - TradeOrderStatus::Validated => matches!(to, TradeOrderStatus::Requested), - TradeOrderStatus::Requested => match to { - TradeOrderStatus::Accepted - | TradeOrderStatus::Declined - | TradeOrderStatus::Questioned - | TradeOrderStatus::Revised - | TradeOrderStatus::Cancelled - | TradeOrderStatus::Requested => true, - _ => false, - }, - TradeOrderStatus::Questioned => match to { - TradeOrderStatus::Requested - | TradeOrderStatus::Revised - | TradeOrderStatus::Cancelled => true, - _ => false, - }, - TradeOrderStatus::Revised => match to { - TradeOrderStatus::Accepted - | TradeOrderStatus::Declined - | TradeOrderStatus::Cancelled - | TradeOrderStatus::Requested => true, - _ => false, - }, - TradeOrderStatus::Accepted => { - matches!( - to, - TradeOrderStatus::Fulfilled | TradeOrderStatus::Cancelled - ) - } - TradeOrderStatus::Declined => false, - TradeOrderStatus::Cancelled => false, - TradeOrderStatus::Fulfilled => match to { - TradeOrderStatus::Completed - | TradeOrderStatus::Fulfilled - | TradeOrderStatus::Cancelled => true, - _ => false, - }, - TradeOrderStatus::Completed => false, - } -} - -pub fn radroots_trade_order_status_is_terminal(status: &TradeOrderStatus) -> bool { - matches!( - status, - TradeOrderStatus::Declined | TradeOrderStatus::Cancelled | TradeOrderStatus::Completed - ) -} - -fn trade_order_status_for_fulfillment_update( - current: &TradeOrderStatus, - fulfillment_status: &TradeFulfillmentStatus, -) -> Result<Option<TradeOrderStatus>, RadrootsTradeProjectionError> { - match fulfillment_status { - TradeFulfillmentStatus::Preparing - | TradeFulfillmentStatus::Shipped - | TradeFulfillmentStatus::ReadyForPickup => { - if matches!(current, TradeOrderStatus::Accepted) { - Ok(None) - } else { - Err(RadrootsTradeProjectionError::InvalidTransition { - from: current.clone(), - to: TradeOrderStatus::Accepted, - }) - } - } - TradeFulfillmentStatus::Delivered => { - let next_status = TradeOrderStatus::Fulfilled; - radroots_trade_order_status_ensure_transition(current.clone(), next_status.clone())?; - Ok(Some(next_status)) - } - TradeFulfillmentStatus::Cancelled => { - let next_status = TradeOrderStatus::Cancelled; - radroots_trade_order_status_ensure_transition(current.clone(), next_status.clone())?; - Ok(Some(next_status)) - } - } -} - -fn trade_order_status_for_receipt( - current: &TradeOrderStatus, - acknowledged: bool, -) -> Result<Option<TradeOrderStatus>, RadrootsTradeProjectionError> { - if acknowledged { - let next_status = TradeOrderStatus::Completed; - radroots_trade_order_status_ensure_transition(current.clone(), next_status.clone())?; - Ok(Some(next_status)) - } else if matches!(current, TradeOrderStatus::Fulfilled) { - Ok(None) - } else { - Err(RadrootsTradeProjectionError::InvalidTransition { - from: current.clone(), - to: TradeOrderStatus::Fulfilled, - }) - } -} - -pub fn radroots_trade_order_status_ensure_transition( - from: TradeOrderStatus, - to: TradeOrderStatus, -) -> Result<(), RadrootsTradeProjectionError> { - if radroots_trade_order_status_can_transition(&from, &to) { - Ok(()) - } else { - Err(RadrootsTradeProjectionError::InvalidTransition { from, to }) - } -} - -fn required_order_id( - message: &RadrootsTradeOrderWorkflowMessage, -) -> Result<&str, RadrootsTradeProjectionError> { - message - .order_id - .as_deref() - .ok_or(RadrootsTradeProjectionError::MissingOrderId) -} - -fn require_listing_snapshot( - message: &RadrootsTradeOrderWorkflowMessage, -) -> Result<RadrootsNostrEventPtr, RadrootsTradeProjectionError> { - message - .listing_event - .clone() - .ok_or(RadrootsTradeProjectionError::MissingListingSnapshot) -} - -fn ensure_actor(expected: &str, actual: &str) -> Result<(), RadrootsTradeProjectionError> { - if expected == actual { - Ok(()) - } else { - Err(RadrootsTradeProjectionError::UnauthorizedActor) - } -} - -fn ensure_counterparty(expected: &str, actual: &str) -> Result<(), RadrootsTradeProjectionError> { - if expected == actual { - Ok(()) - } else { - Err(RadrootsTradeProjectionError::CounterpartyMismatch) - } -} - -fn ensure_trade_chain( - order: &RadrootsTradeOrderWorkflowProjection, - message: &RadrootsTradeOrderWorkflowMessage, -) -> Result<(), RadrootsTradeProjectionError> { - let root_event_id = message - .root_event_id - .as_deref() - .ok_or(RadrootsTradeProjectionError::MissingTradeRootEventId)?; - if root_event_id != order.root_event_id { - return Err(RadrootsTradeProjectionError::TradeThreadRootMismatch); - } - let prev_event_id = message - .prev_event_id - .as_deref() - .ok_or(RadrootsTradeProjectionError::MissingTradePrevEventId)?; - if prev_event_id != order.last_event_id { - return Err(RadrootsTradeProjectionError::TradeThreadPrevMismatch); - } - Ok(()) -} - -fn apply_order_change( - items: &mut Vec<TradeOrderItem>, - change: &TradeOrderChange, -) -> Result<(), RadrootsTradeProjectionError> { - match change { - TradeOrderChange::BinCount { - item_index, - bin_count, - } => { - let index = *item_index as usize; - let item = items - .get_mut(index) - .ok_or(RadrootsTradeProjectionError::InvalidItemIndex(*item_index))?; - item.bin_count = *bin_count; - } - TradeOrderChange::ItemAdd { item } => items.push(item.clone()), - TradeOrderChange::ItemRemove { item_index } => { - let index = *item_index as usize; - if index >= items.len() { - return Err(RadrootsTradeProjectionError::InvalidItemIndex(*item_index)); - } - items.remove(index); - } - } - Ok(()) -} - -enum TradeDiscountDecisionValue { - Accepted(RadrootsCoreDiscountValue), - Declined(Option<String>), -} - -fn trade_discount_decision_value( - decision: &crate::listing::order::TradeDiscountDecision, -) -> TradeDiscountDecisionValue { - match decision { - crate::listing::order::TradeDiscountDecision::Accept { value } => { - TradeDiscountDecisionValue::Accepted(value.clone()) - } - crate::listing::order::TradeDiscountDecision::Decline { reason } => { - TradeDiscountDecisionValue::Declined(reason.clone()) - } - } -} - -impl RadrootsTradeListingMarketStatus { - fn facet_key(&self) -> String { - match self { - Self::Unknown => "unknown".into(), - Self::Window => "window".into(), - Self::Active => "active".into(), - Self::Sold => "sold".into(), - Self::Other { value } => value.clone(), - } - } -} - -fn order_status_key(status: &TradeOrderStatus) -> String { - match status { - TradeOrderStatus::Draft => "draft".into(), - TradeOrderStatus::Validated => "validated".into(), - TradeOrderStatus::Requested => "requested".into(), - TradeOrderStatus::Questioned => "questioned".into(), - TradeOrderStatus::Revised => "revised".into(), - TradeOrderStatus::Accepted => "accepted".into(), - TradeOrderStatus::Declined => "declined".into(), - TradeOrderStatus::Cancelled => "cancelled".into(), - TradeOrderStatus::Fulfilled => "fulfilled".into(), - TradeOrderStatus::Completed => "completed".into(), - } -} - -fn message_type_key(message_type: TradeListingMessageType) -> &'static str { - match message_type { - TradeListingMessageType::ListingValidateRequest => "listing_validate_request", - TradeListingMessageType::ListingValidateResult => "listing_validate_result", - TradeListingMessageType::OrderRequest => "order_request", - TradeListingMessageType::OrderResponse => "order_response", - TradeListingMessageType::OrderRevision => "order_revision", - TradeListingMessageType::OrderRevisionAccept => "order_revision_accept", - TradeListingMessageType::OrderRevisionDecline => "order_revision_decline", - TradeListingMessageType::Question => "question", - TradeListingMessageType::Answer => "answer", - TradeListingMessageType::DiscountRequest => "discount_request", - TradeListingMessageType::DiscountOffer => "discount_offer", - TradeListingMessageType::DiscountAccept => "discount_accept", - TradeListingMessageType::DiscountDecline => "discount_decline", - TradeListingMessageType::Cancel => "cancel", - TradeListingMessageType::FulfillmentUpdate => "fulfillment_update", - TradeListingMessageType::Receipt => "receipt", - } -} - -fn compare_direction(ordering: Ordering, direction: RadrootsTradeSortDirection) -> Ordering { - match direction { - RadrootsTradeSortDirection::Asc => ordering, - RadrootsTradeSortDirection::Desc => ordering.reverse(), - } -} - -fn compare_option_decimal( - left: &Option<RadrootsCoreDecimal>, - right: &Option<RadrootsCoreDecimal>, -) -> Ordering { - match (left, right) { - (Some(left), Some(right)) => left.partial_cmp(right).unwrap_or(Ordering::Equal), - (Some(_), None) => Ordering::Less, - (None, Some(_)) => Ordering::Greater, - (None, None) => Ordering::Equal, - } -} - -fn compare_listings( - left: &RadrootsTradeListingProjection, - right: &RadrootsTradeListingProjection, - sort: RadrootsTradeListingSort, -) -> Ordering { - let ordering = match sort.field { - RadrootsTradeListingSortField::ListingAddr => left.listing_addr.cmp(&right.listing_addr), - RadrootsTradeListingSortField::ProductTitle => left - .product - .title - .cmp(&right.product.title) - .then_with(|| left.listing_addr.cmp(&right.listing_addr)), - RadrootsTradeListingSortField::ProductCategory => left - .product - .category - .cmp(&right.product.category) - .then_with(|| left.listing_addr.cmp(&right.listing_addr)), - RadrootsTradeListingSortField::SellerPubkey => left - .seller_pubkey - .cmp(&right.seller_pubkey) - .then_with(|| left.listing_addr.cmp(&right.listing_addr)), - RadrootsTradeListingSortField::InventoryAvailable => { - compare_option_decimal(&left.inventory_available, &right.inventory_available) - .then_with(|| left.listing_addr.cmp(&right.listing_addr)) - } - RadrootsTradeListingSortField::OpenOrderCount => left - .open_order_count - .cmp(&right.open_order_count) - .then_with(|| left.listing_addr.cmp(&right.listing_addr)), - RadrootsTradeListingSortField::TotalOrderCount => left - .order_count - .cmp(&right.order_count) - .then_with(|| left.listing_addr.cmp(&right.listing_addr)), - }; - compare_direction(ordering, sort.direction) -} - -fn compare_orders( - left: &RadrootsTradeOrderWorkflowProjection, - right: &RadrootsTradeOrderWorkflowProjection, - sort: RadrootsTradeOrderSort, -) -> Ordering { - let ordering = match sort.field { - RadrootsTradeOrderSortField::OrderId => left.order_id.cmp(&right.order_id), - RadrootsTradeOrderSortField::ListingAddr => left - .listing_addr - .cmp(&right.listing_addr) - .then_with(|| left.order_id.cmp(&right.order_id)), - RadrootsTradeOrderSortField::BuyerPubkey => left - .buyer_pubkey - .cmp(&right.buyer_pubkey) - .then_with(|| left.order_id.cmp(&right.order_id)), - RadrootsTradeOrderSortField::SellerPubkey => left - .seller_pubkey - .cmp(&right.seller_pubkey) - .then_with(|| left.order_id.cmp(&right.order_id)), - RadrootsTradeOrderSortField::Status => order_status_key(&left.status) - .cmp(&order_status_key(&right.status)) - .then_with(|| left.order_id.cmp(&right.order_id)), - RadrootsTradeOrderSortField::LastMessageType => message_type_key(left.last_message_type) - .cmp(message_type_key(right.last_message_type)) - .then_with(|| left.order_id.cmp(&right.order_id)), - RadrootsTradeOrderSortField::TotalBinCount => left - .total_bin_count() - .cmp(&right.total_bin_count()) - .then_with(|| left.order_id.cmp(&right.order_id)), - }; - compare_direction(ordering, sort.direction) -} - -fn listing_matches_query( - listing: &RadrootsTradeListingProjection, - query: &RadrootsTradeListingQuery, -) -> bool { - if query - .seller_pubkey - .as_deref() - .is_some_and(|value| value != listing.seller_pubkey) - { - return false; - } - if query - .farm_pubkey - .as_deref() - .is_some_and(|value| value != listing.farm.pubkey) - { - return false; - } - if query - .farm_id - .as_deref() - .is_some_and(|value| value != listing.farm.d_tag) - { - return false; - } - if query - .product_key - .as_deref() - .is_some_and(|value| value != listing.product.key) - { - return false; - } - if query - .product_category - .as_deref() - .is_some_and(|value| value != listing.product.category) - { - return false; - } - if query - .listing_status - .as_ref() - .is_some_and(|value| value != &listing.market_status()) - { - return false; - } - true -} - -fn order_matches_query( - order: &RadrootsTradeOrderWorkflowProjection, - query: &RadrootsTradeOrderQuery, -) -> bool { - if query - .listing_addr - .as_deref() - .is_some_and(|value| value != order.listing_addr) - { - return false; - } - if query - .buyer_pubkey - .as_deref() - .is_some_and(|value| value != order.buyer_pubkey) - { - return false; - } - if query - .seller_pubkey - .as_deref() - .is_some_and(|value| value != order.seller_pubkey) - { - return false; - } - if query - .status - .as_ref() - .is_some_and(|value| value != &order.status) - { - return false; - } - true -} - -fn increment_count(counts: &mut BTreeMap<String, u32>, key: String) { - let count = counts.entry(key).or_insert(0); - *count = count.saturating_add(1); -} - -fn facet_counts_from_map(counts: BTreeMap<String, u32>) -> Vec<RadrootsTradeFacetCount> { - let mut values = counts - .into_iter() - .map(|(key, count)| RadrootsTradeFacetCount { key, count }) - .collect::<Vec<_>>(); - values.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.key.cmp(&right.key)) - }); - values -} - -#[cfg(test)] -mod tests { - use std::cell::RefCell; - - use super::{ - RadrootsTradeListingMarketStatus, RadrootsTradeListingProjection, - RadrootsTradeListingQuery, RadrootsTradeListingSort, RadrootsTradeListingSortField, - RadrootsTradeOrderQuery, RadrootsTradeOrderSort, RadrootsTradeOrderSortField, - RadrootsTradeOrderWorkflowMessage, RadrootsTradeOrderWorkflowProjection, - RadrootsTradeProjectionError, RadrootsTradeReadIndex, RadrootsTradeSortDirection, - radroots_trade_order_status_can_transition, radroots_trade_order_status_is_terminal, - }; - use crate::listing::{ - codec::{TradeListingParseError, listing_tags_build}, - dvm::{ - TradeListingAddressError, TradeListingCancel, TradeListingEnvelopeParseError, - TradeListingMessagePayload, TradeListingMessageType, TradeListingValidateRequest, - TradeListingValidateResult, TradeOrderResponse, TradeOrderRevisionResponse, - trade_listing_envelope_event_build, - }, - order::{ - TradeAnswer, TradeDiscountDecision, TradeDiscountOffer, TradeDiscountRequest, - TradeEconomicActor, TradeEconomicEffect, TradeEconomicLineKind, TradeFulfillmentStatus, - TradeFulfillmentUpdate, TradeOrder, TradeOrderChange, TradeOrderEconomicItem, - TradeOrderEconomicLine, TradeOrderEconomics, TradeOrderItem, TradeOrderRevision, - TradeOrderStatus, TradePricingBasis, TradeQuestion, TradeReceipt, - }, - }; - use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCorePercent, - RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, - }; - use radroots_events::farm::RadrootsFarmRef; - use radroots_events::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, - }; - use radroots_events::{RadrootsNostrEvent, RadrootsNostrEventPtr, kinds::KIND_LISTING}; - use radroots_events_codec::trade::active_trade_order_request_event_build; - - #[derive(Clone, Debug)] - struct TestWorkflowChain { - buyer_pubkey: String, - seller_pubkey: String, - root_event_id: String, - last_event_id: String, - next_sequence: u32, - } - - thread_local! { - static TEST_WORKFLOW_CHAINS: RefCell<std::collections::BTreeMap<String, TestWorkflowChain>> = - RefCell::new(std::collections::BTreeMap::new()); - } - - fn listing_snapshot(listing_addr: &str) -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: format!("snapshot:{listing_addr}"), - relays: None, - } - } - - fn seller_pubkey_from_listing_addr(listing_addr: &str) -> String { - listing_addr - .split(':') - .nth(1) - .unwrap_or_default() - .to_string() - } - - fn workflow_refs( - actor_pubkey: &str, - listing_addr: &str, - order_id: Option<&str>, - payload: &TradeListingMessagePayload, - ) -> ( - String, - String, - Option<RadrootsNostrEventPtr>, - Option<String>, - Option<String>, - ) { - let message_type = payload.message_type(); - let listing_event = message_type - .requires_listing_snapshot() - .then(|| listing_snapshot(listing_addr)); - let default_seller = seller_pubkey_from_listing_addr(listing_addr); - - match (payload, order_id) { - (_, None) => ( - format!("event:no-order:{}:{actor_pubkey}", message_type.kind()), - default_seller, - listing_event, - None, - None, - ), - (TradeListingMessagePayload::TradeOrderRequested(order), Some(order_id)) => { - let event_id = format!("{order_id}:request"); - TEST_WORKFLOW_CHAINS.with(|chains| { - chains.borrow_mut().insert( - order_id.to_string(), - TestWorkflowChain { - buyer_pubkey: order.buyer_pubkey.clone(), - seller_pubkey: order.seller_pubkey.clone(), - root_event_id: event_id.clone(), - last_event_id: event_id.clone(), - next_sequence: 1, - }, - ); - }); - ( - event_id, - order.seller_pubkey.clone(), - listing_event, - None, - None, - ) - } - (_, Some(order_id)) => TEST_WORKFLOW_CHAINS.with(|chains| { - let mut chains = chains.borrow_mut(); - let chain = - chains - .entry(order_id.to_string()) - .or_insert_with(|| TestWorkflowChain { - buyer_pubkey: String::from("buyer-pubkey"), - seller_pubkey: default_seller.clone(), - root_event_id: format!("{order_id}:root"), - last_event_id: format!("{order_id}:root"), - next_sequence: 1, - }); - let event_id = - format!("{order_id}:{}:{}", message_type.kind(), chain.next_sequence); - chain.next_sequence += 1; - let counterparty_pubkey = if actor_pubkey == chain.seller_pubkey { - chain.buyer_pubkey.clone() - } else { - chain.seller_pubkey.clone() - }; - let prev_event_id = chain.last_event_id.clone(); - let root_event_id = chain.root_event_id.clone(); - chain.last_event_id = event_id.clone(); - ( - event_id, - counterparty_pubkey, - listing_event, - Some(root_event_id), - Some(prev_event_id), - ) - }), - } - } - - fn base_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "farm-pubkey".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("single origin".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".into(), - bins: vec![ - RadrootsListingBin { - bin_id: "bin-1".into(), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(2u32), - RadrootsCoreCurrency::USD, - ), - RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - ), - display_amount: None, - display_unit: None, - display_label: Some("1kg bag".into()), - display_price: Some(RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(2000u32), - RadrootsCoreCurrency::USD, - )), - display_price_unit: Some(RadrootsCoreUnit::Each), - }, - RadrootsListingBin { - bin_id: "bin-2".into(), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(500u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(3u32), - RadrootsCoreCurrency::USD, - ), - RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - ), - display_amount: None, - display_unit: None, - display_label: Some("500g bag".into()), - display_price: None, - display_price_unit: None, - }, - ], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(10u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "farm".into(), - city: Some("Nashville".into()), - region: Some("TN".into()), - country: Some("US".into()), - lat: None, - lng: None, - geohash: None, - }), - images: None, - } - } - - fn order_economics(items: &[TradeOrderItem], include_discount: bool) -> TradeOrderEconomics { - let mut subtotal = RadrootsCoreDecimal::from(0u32); - let economic_items = items - .iter() - .map(|item| { - let line_subtotal = - RadrootsCoreDecimal::from(item.bin_count) * RadrootsCoreDecimal::from(5u32); - subtotal = subtotal + line_subtotal; - TradeOrderEconomicItem { - bin_id: item.bin_id.clone(), - bin_count: item.bin_count, - quantity_amount: RadrootsCoreDecimal::from(1u32), - quantity_unit: RadrootsCoreUnit::Each, - unit_price_amount: RadrootsCoreDecimal::from(5u32), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: RadrootsCoreMoney::new(line_subtotal, RadrootsCoreCurrency::USD), - } - }) - .collect::<Vec<_>>(); - let discounts = include_discount - .then(|| { - vec![TradeOrderEconomicLine { - id: "discount-1".into(), - kind: TradeEconomicLineKind::ListingDiscount, - actor: TradeEconomicActor::Seller, - effect: TradeEconomicEffect::Decrease, - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreCurrency::USD, - ), - reason: "listing discount".into(), - }] - }) - .unwrap_or_default(); - let discount_total = if include_discount { - RadrootsCoreDecimal::from(1u32) - } else { - RadrootsCoreDecimal::from(0u32) - }; - TradeOrderEconomics { - quote_id: "quote-1".into(), - quote_version: 1, - pricing_basis: TradePricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: economic_items, - discounts, - adjustments: Vec::new(), - subtotal: RadrootsCoreMoney::new(subtotal, RadrootsCoreCurrency::USD), - discount_total: RadrootsCoreMoney::new(discount_total, RadrootsCoreCurrency::USD), - adjustment_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - total: RadrootsCoreMoney::new(subtotal - discount_total, RadrootsCoreCurrency::USD), - } - } - - fn base_order() -> TradeOrder { - let items = vec![TradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 2, - }]; - TradeOrder { - order_id: "order-1".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer-pubkey".into(), - seller_pubkey: "seller-pubkey".into(), - economics: order_economics(&items, true), - items, - } - } - - fn alternate_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".into(), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "farm-pubkey-2".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAABA".into(), - }, - product: RadrootsListingProduct { - key: "greens".into(), - title: "Greens".into(), - category: "vegetables".into(), - summary: Some("washed bunches".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".into(), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".into(), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(500u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(4u32), - RadrootsCoreCurrency::USD, - ), - RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - ), - display_amount: None, - display_unit: None, - display_label: Some("500g bunch".into()), - display_price: Some(RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(2000u32), - RadrootsCoreCurrency::USD, - )), - display_price_unit: Some(RadrootsCoreUnit::Each), - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(4u32)), - availability: Some(RadrootsListingAvailability::Window { - start: Some(1_700_000_000), - end: Some(1_800_000_000), - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Shipping), - location: Some(RadrootsListingLocation { - primary: "warehouse".into(), - city: Some("Louisville".into()), - region: Some("KY".into()), - country: Some("US".into()), - lat: None, - lng: None, - geohash: None, - }), - images: None, - } - } - - fn alternate_order() -> TradeOrder { - let items = vec![ - TradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 3, - }, - TradeOrderItem { - bin_id: "bin-2".into(), - bin_count: 1, - }, - ]; - TradeOrder { - order_id: "order-2".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw".into(), - buyer_pubkey: "buyer-pubkey-2".into(), - seller_pubkey: "seller-pubkey".into(), - economics: order_economics(&items, false), - items, - } - } - - fn message( - actor_pubkey: &str, - listing_addr: &str, - order_id: Option<&str>, - payload: TradeListingMessagePayload, - ) -> RadrootsTradeOrderWorkflowMessage { - let (event_id, counterparty_pubkey, listing_event, root_event_id, prev_event_id) = - workflow_refs(actor_pubkey, listing_addr, order_id, &payload); - RadrootsTradeOrderWorkflowMessage { - event_id, - actor_pubkey: actor_pubkey.into(), - counterparty_pubkey, - listing_addr: listing_addr.into(), - order_id: order_id.map(str::to_string), - listing_event, - root_event_id, - prev_event_id, - payload, - } - } - - fn listing_event(seller_pubkey: &str, listing: &RadrootsListing) -> RadrootsNostrEvent { - RadrootsNostrEvent { - id: "listing-event-id".into(), - author: seller_pubkey.into(), - created_at: 1_700_000_000, - kind: KIND_LISTING, - tags: listing_tags_build(listing).expect("listing tags"), - content: serde_json::to_string(listing).expect("listing json"), - sig: "sig".into(), - } - } - - fn workflow_event( - actor_pubkey: &str, - recipient_pubkey: &str, - message_type: crate::listing::dvm::TradeListingMessageType, - listing_addr: &str, - order_id: Option<&str>, - payload: &TradeListingMessagePayload, - ) -> RadrootsNostrEvent { - let (_, _, listing_event, root_event_id, prev_event_id) = - workflow_refs(actor_pubkey, listing_addr, order_id, payload); - let built = if message_type == crate::listing::dvm::TradeListingMessageType::OrderRequest { - let TradeListingMessagePayload::TradeOrderRequested(order) = payload else { - panic!("order-request workflow event requires active order payload") - }; - active_trade_order_request_event_build( - listing_event - .as_ref() - .expect("order-request workflow event requires listing snapshot"), - order, - ) - .expect("trade workflow event") - } else { - trade_listing_envelope_event_build( - recipient_pubkey, - message_type, - listing_addr.to_string(), - order_id.map(str::to_string), - listing_event.as_ref(), - root_event_id.as_deref(), - prev_event_id.as_deref(), - payload, - ) - .expect("trade workflow event") - }; - RadrootsNostrEvent { - id: "workflow-event-id".into(), - author: actor_pubkey.into(), - created_at: 1_700_000_000, - kind: u32::from(built.kind), - tags: built.tags, - content: built.content, - sig: "sig".into(), - } - } - - #[test] - fn projection_defaults_and_helper_errors_cover_paths() { - let listing_sort = RadrootsTradeListingSort::default(); - assert_eq!( - listing_sort.field, - RadrootsTradeListingSortField::ListingAddr - ); - assert_eq!(listing_sort.direction, RadrootsTradeSortDirection::Asc); - - let order_sort = RadrootsTradeOrderSort::default(); - assert_eq!(order_sort.field, RadrootsTradeOrderSortField::OrderId); - assert_eq!(order_sort.direction, RadrootsTradeSortDirection::Asc); - - let mut listing = base_listing(); - listing.availability = Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Other { - value: "archived".into(), - }, - }); - let projection = - RadrootsTradeListingProjection::from_listing_contract("seller-pubkey", &listing) - .expect("listing projection"); - assert_eq!( - projection.market_status(), - RadrootsTradeListingMarketStatus::Other { - value: "archived".into(), - } - ); - listing.availability = None; - let unknown_projection = - RadrootsTradeListingProjection::from_listing_contract("seller-pubkey", &listing) - .expect("unknown listing projection"); - assert_eq!( - unknown_projection.market_status(), - RadrootsTradeListingMarketStatus::Unknown - ); - let mut missing_primary_bin_projection = unknown_projection.clone(); - missing_primary_bin_projection.primary_bin_id = "missing-bin".into(); - assert!( - missing_primary_bin_projection - .marketplace_summary() - .is_none() - ); - - let mut index = RadrootsTradeReadIndex::new(); - assert!(index.listings().is_empty()); - assert!(index.orders().is_empty()); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("listing"); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - assert_eq!(index.listings().len(), 1); - assert_eq!(index.orders().len(), 1); - - let cases = [ - ( - RadrootsTradeProjectionError::InvalidListingKind { kind: 7 }, - "invalid listing event kind: 7", - false, - ), - ( - RadrootsTradeProjectionError::InvalidListingContract { - error: TradeListingParseError::InvalidTag("d".into()), - }, - "invalid listing contract event: invalid tag: d", - true, - ), - ( - RadrootsTradeProjectionError::MissingPrimaryBin("bin-9".into()), - "missing primary bin: bin-9", - false, - ), - ( - RadrootsTradeProjectionError::MissingOrderId, - "missing order id", - false, - ), - ( - RadrootsTradeProjectionError::OrderIdMismatch, - "order id mismatch", - false, - ), - ( - RadrootsTradeProjectionError::ListingAddrMismatch, - "listing address mismatch", - false, - ), - ( - RadrootsTradeProjectionError::MissingOrder("order-9".into()), - "missing order projection: order-9", - false, - ), - ( - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Draft, - to: TradeOrderStatus::Accepted, - }, - "invalid order transition: Draft -> Accepted", - false, - ), - ( - RadrootsTradeProjectionError::InvalidItemIndex(3), - "invalid order item index: 3", - false, - ), - ( - RadrootsTradeProjectionError::InvalidDiscountDecision, - "invalid discount decision payload", - false, - ), - ( - RadrootsTradeProjectionError::InvalidRevisionResponse, - "invalid order revision response payload", - false, - ), - ( - RadrootsTradeProjectionError::NonOrderWorkflowMessage( - TradeListingMessageType::ListingValidateRequest, - ), - "non-order workflow message: ListingValidateRequest", - false, - ), - ( - RadrootsTradeProjectionError::UnauthorizedActor, - "unauthorized actor", - false, - ), - ( - RadrootsTradeProjectionError::CounterpartyMismatch, - "counterparty pubkey mismatch", - false, - ), - ( - RadrootsTradeProjectionError::MissingListingSnapshot, - "missing listing snapshot", - false, - ), - ( - RadrootsTradeProjectionError::MissingTradeRootEventId, - "missing trade root event id", - false, - ), - ( - RadrootsTradeProjectionError::MissingTradePrevEventId, - "missing trade previous event id", - false, - ), - ( - RadrootsTradeProjectionError::TradeThreadRootMismatch, - "trade thread root mismatch", - false, - ), - ( - RadrootsTradeProjectionError::TradeThreadPrevMismatch, - "trade thread previous event mismatch", - false, - ), - ( - RadrootsTradeProjectionError::InvalidWorkflowEvent { - error: TradeListingEnvelopeParseError::InvalidListingAddr( - TradeListingAddressError::InvalidFormat, - ), - }, - "invalid listing address format", - true, - ), - ]; - for (error, expected, has_source) in cases { - assert_eq!(error.to_string(), expected); - assert_eq!(std::error::Error::source(&error).is_some(), has_source); - } - } - - #[test] - fn listing_projection_from_event_rejects_invalid_kind_and_invalid_contract() { - let mut invalid_kind = listing_event("seller-pubkey", &base_listing()); - invalid_kind.kind = 7; - assert_eq!( - RadrootsTradeListingProjection::from_listing_event(&invalid_kind) - .expect_err("invalid kind"), - RadrootsTradeProjectionError::InvalidListingKind { kind: 7 } - ); - - let invalid_contract = RadrootsNostrEvent { - id: "bad-listing".into(), - author: "seller-pubkey".into(), - created_at: 1_700_000_000, - kind: KIND_LISTING, - tags: vec![], - content: "{}".into(), - sig: "sig".into(), - }; - let invalid_contract_error = - RadrootsTradeListingProjection::from_listing_event(&invalid_contract) - .expect_err("invalid contract"); - let invalid_contract_source = - std::error::Error::source(&invalid_contract_error).expect("invalid contract source"); - assert_eq!( - invalid_contract_error.to_string(), - format!("invalid listing contract event: {invalid_contract_source}") - ); - - let mut missing_primary_bin = base_listing(); - missing_primary_bin.primary_bin_id = "missing-bin".into(); - let missing_primary_bin_event = listing_event("seller-pubkey", &missing_primary_bin); - assert_eq!( - RadrootsTradeListingProjection::from_listing_event(&missing_primary_bin_event) - .expect_err("missing primary bin"), - RadrootsTradeProjectionError::MissingPrimaryBin("missing-bin".into()) - ); - - let mut index = RadrootsTradeReadIndex::new(); - assert_eq!( - index - .upsert_listing_event(&missing_primary_bin_event) - .expect_err("index missing primary bin"), - RadrootsTradeProjectionError::MissingPrimaryBin("missing-bin".into()) - ); - } - - #[test] - fn message_helper_bootstraps_missing_chain_for_non_request_payload() { - let message = message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("orphan-order"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("cancelled".into()), - }), - ); - - assert_eq!(message.order_id.as_deref(), Some("orphan-order")); - assert_eq!(message.counterparty_pubkey, "buyer-pubkey"); - assert_eq!(message.root_event_id.as_deref(), Some("orphan-order:root")); - assert_eq!(message.prev_event_id.as_deref(), Some("orphan-order:root")); - } - - #[test] - fn workflow_message_from_event_rejects_missing_trade_context_tags() { - let valid_event = workflow_event( - "seller-pubkey", - "buyer-pubkey", - crate::listing::dvm::TradeListingMessageType::OrderResponse, - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-valid-tags"), - &TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - let valid_message = - RadrootsTradeOrderWorkflowMessage::from_event(&valid_event).expect("valid workflow"); - assert_eq!(valid_message.order_id.as_deref(), Some("order-valid-tags")); - - let mut event = workflow_event( - "seller-pubkey", - "buyer-pubkey", - crate::listing::dvm::TradeListingMessageType::OrderResponse, - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-missing-tags"), - &TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - event.tags.retain(|tag| { - !matches!( - tag.first().map(String::as_str), - Some("e_root") | Some("e_prev") - ) - }); - - assert_eq!( - RadrootsTradeOrderWorkflowMessage::from_event(&event), - Err(TradeListingEnvelopeParseError::MissingTag("e_root")) - ); - } - - #[test] - fn listing_projection_builds_query_friendly_view() { - let mut index = RadrootsTradeReadIndex::new(); - let listing = base_listing(); - let projection = index - .upsert_listing("seller-pubkey", &listing) - .expect("listing projection"); - - assert_eq!( - projection.listing_addr, - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg" - ); - assert_eq!(projection.primary_bin_id, "bin-1"); - assert_eq!(projection.bins.len(), 2); - assert_eq!( - projection.bins[0].one_bin_total.price_amount.amount, - RadrootsCoreDecimal::from(2000u32) - ); - assert_eq!(projection.order_count, 0); - assert_eq!(projection.open_order_count, 0); - assert_eq!(projection.terminal_order_count, 0); - } - - #[test] - fn listing_projection_can_ingest_canonical_nostr_event() { - let mut index = RadrootsTradeReadIndex::new(); - let event = listing_event("seller-pubkey", &base_listing()); - - let projection = index - .upsert_listing_event(&event) - .expect("listing event projection"); - - assert_eq!( - projection.listing_addr, - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg" - ); - assert_eq!(projection.bins.len(), 2); - } - - #[test] - fn workflow_projection_can_ingest_canonical_trade_event() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing_event(&listing_event("seller-pubkey", &base_listing())) - .expect("listing projection"); - let event = workflow_event( - "buyer-pubkey", - "seller-pubkey", - crate::listing::dvm::TradeListingMessageType::OrderRequest, - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - &TradeListingMessagePayload::TradeOrderRequested(base_order()), - ); - - let order = index - .apply_workflow_event(&event) - .expect("workflow event projection"); - - assert_eq!(order.order_id, "order-1"); - assert_eq!(order.status, TradeOrderStatus::Requested); - assert_eq!(order.last_actor_pubkey, "buyer-pubkey"); - } - - #[test] - fn workflow_projection_updates_order_and_listing_views() { - let mut index = RadrootsTradeReadIndex::new(); - let listing = base_listing(); - index - .upsert_listing("seller-pubkey", &listing) - .expect("listing projection"); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - let listing_after_request = index - .listing("30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg") - .expect("listing after request"); - assert_eq!(listing_after_request.order_count, 1); - assert_eq!(listing_after_request.open_order_count, 1); - assert_eq!(listing_after_request.terminal_order_count, 0); - - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect("order response"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Delivered, - }), - )) - .expect("fulfillment"); - let order_after_fulfillment = index.order("order-1").expect("order after fulfillment"); - assert_eq!(order_after_fulfillment.status, TradeOrderStatus::Fulfilled); - assert_eq!( - order_after_fulfillment.last_fulfillment_status, - Some(TradeFulfillmentStatus::Delivered) - ); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: true, - at: 1_700_000_000, - }), - )) - .expect("receipt"); - let order = index.order("order-1").expect("order"); - assert_eq!(order.status, TradeOrderStatus::Completed); - assert_eq!(order.receipt_count, 1); - let listing_after_receipt = index - .listing("30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg") - .expect("listing after receipt"); - assert_eq!(listing_after_receipt.open_order_count, 0); - assert_eq!(listing_after_receipt.terminal_order_count, 1); - } - - #[test] - fn workflow_projection_keeps_in_progress_fulfillment_as_accepted() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("listing projection"); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect("order response"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Shipped, - }), - )) - .expect("fulfillment update"); - - let order = index.order("order-1").expect("order"); - assert_eq!(order.status, TradeOrderStatus::Accepted); - assert_eq!( - order.last_fulfillment_status, - Some(TradeFulfillmentStatus::Shipped) - ); - let listing = index - .listing("30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg") - .expect("listing"); - assert_eq!(listing.open_order_count, 1); - assert_eq!(listing.terminal_order_count, 0); - } - - #[test] - fn workflow_projection_requires_acknowledged_receipt_for_completion() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("listing projection"); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect("order response"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Delivered, - }), - )) - .expect("fulfilled"); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: false, - at: 1_700_000_000, - }), - )) - .expect("receipt"); - - let order = index.order("order-1").expect("order"); - assert_eq!(order.status, TradeOrderStatus::Fulfilled); - assert_eq!(order.receipt_acknowledged, Some(false)); - let listing = index - .listing("30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg") - .expect("listing"); - assert_eq!(listing.open_order_count, 1); - assert_eq!(listing.terminal_order_count, 0); - } - - #[test] - fn workflow_projection_applies_revision_question_and_answer() { - let mut index = RadrootsTradeReadIndex::new(); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::Question(TradeQuestion { - question_id: "q-1".into(), - }), - )) - .expect("question"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::Answer(TradeAnswer { - question_id: "q-1".into(), - }), - )) - .expect("answer"); - let order_after_answer = index.order("order-1").expect("order after answer"); - assert_eq!(order_after_answer.status, TradeOrderStatus::Requested); - assert_eq!(order_after_answer.question_count, 1); - assert_eq!(order_after_answer.answer_count, 1); - - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "rev-1".into(), - changes: vec![ - TradeOrderChange::BinCount { - item_index: 0, - bin_count: 3, - }, - TradeOrderChange::ItemAdd { - item: TradeOrderItem { - bin_id: "bin-2".into(), - bin_count: 1, - }, - }, - ], - }), - )) - .expect("order revision"); - let order = index.order("order-1").expect("order"); - assert_eq!(order.status, TradeOrderStatus::Revised); - assert_eq!(order.revision_count, 1); - assert_eq!(order.items[0].bin_count, 3); - assert_eq!(order.items.len(), 2); - } - - #[test] - fn workflow_projection_covers_discount_and_cancel_paths() { - let mut index = RadrootsTradeReadIndex::new(); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::DiscountRequest(TradeDiscountRequest { - discount_id: "disc-1".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(15u32)), - ), - }), - )) - .expect("discount request"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer { - discount_id: "disc-1".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(12u32)), - ), - }), - )) - .expect("discount offer"); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline { - reason: Some("need full price".into()), - }), - )) - .expect("discount decline"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("inventory issue".into()), - }), - )) - .expect("cancel"); - let order = index.order("order-1").expect("order"); - assert_eq!(order.status, TradeOrderStatus::Cancelled); - assert_eq!(order.discount_request_count, 1); - assert_eq!(order.discount_offer_count, 1); - assert_eq!(order.discount_decline_count, 1); - assert_eq!( - order.last_discount_decline_reason.as_deref(), - Some("need full price") - ); - assert_eq!(order.cancellation_count, 1); - } - - #[test] - fn workflow_projection_backfills_listing_counts_when_listing_arrives_late() { - let mut index = RadrootsTradeReadIndex::new(); - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("listing projection"); - let listing = index - .listing("30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg") - .expect("listing"); - assert_eq!(listing.order_count, 1); - assert_eq!(listing.open_order_count, 1); - } - - #[test] - fn workflow_projection_rejects_invalid_inputs() { - let mut index = RadrootsTradeReadIndex::new(); - let err = index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - None, - TradeListingMessagePayload::Question(TradeQuestion { - question_id: "q-1".into(), - }), - )) - .expect_err("missing order id should fail"); - assert_eq!(err, RadrootsTradeProjectionError::MissingOrderId); - - let err = index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - None, - TradeListingMessagePayload::ListingValidateRequest( - crate::listing::dvm::TradeListingValidateRequest { - listing_event: None, - }, - ), - )) - .expect_err("non-order message should fail"); - assert_eq!( - err, - RadrootsTradeProjectionError::NonOrderWorkflowMessage( - crate::listing::dvm::TradeListingMessageType::ListingValidateRequest - ) - ); - - let listing = RadrootsListing { - primary_bin_id: "missing".into(), - ..base_listing() - }; - let err = index - .upsert_listing("seller-pubkey", &listing) - .expect_err("missing primary bin should fail"); - assert_eq!( - err, - RadrootsTradeProjectionError::MissingPrimaryBin("missing".into()) - ); - - let order = index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - assert_eq!( - order.status, - TradeOrderStatus::Requested, - "canonical helper should still create a requested order" - ); - - let missing_snapshot_items = vec![TradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 1, - }]; - let err = index - .apply_workflow_message(&RadrootsTradeOrderWorkflowMessage { - event_id: "missing-snapshot".into(), - actor_pubkey: "buyer-pubkey".into(), - counterparty_pubkey: "seller-pubkey".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(), - order_id: Some("order-2".into()), - listing_event: None, - root_event_id: None, - prev_event_id: None, - payload: TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-2".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(), - buyer_pubkey: "buyer-pubkey".into(), - seller_pubkey: "seller-pubkey".into(), - economics: order_economics(&missing_snapshot_items, false), - items: missing_snapshot_items, - }), - }) - .expect_err("order request without snapshot should fail"); - assert_eq!(err, RadrootsTradeProjectionError::MissingListingSnapshot); - } - - #[test] - fn workflow_projection_rejects_invalid_canonical_trade_event() { - let mut index = RadrootsTradeReadIndex::new(); - let mut event = workflow_event( - "buyer-pubkey", - "seller-pubkey", - crate::listing::dvm::TradeListingMessageType::OrderRequest, - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - &TradeListingMessagePayload::TradeOrderRequested(base_order()), - ); - event.tags[1][1] = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw".into(); - - let err = index - .apply_workflow_event(&event) - .expect_err("invalid workflow event should fail"); - assert_eq!( - err, - RadrootsTradeProjectionError::InvalidWorkflowEvent { - error: TradeListingEnvelopeParseError::ListingAddrTagMismatch, - } - ); - } - - #[test] - fn listing_query_helpers_filter_sort_and_facet_marketplace_views() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("base listing"); - index - .upsert_listing("seller-pubkey", &alternate_listing()) - .expect("alternate listing"); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("open order"); - index - .apply_workflow_message(&message( - "buyer-pubkey-2", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::TradeOrderRequested(alternate_order()), - )) - .expect("second order request"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect("order accepted"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Delivered, - }), - )) - .expect("order fulfilled"); - index - .apply_workflow_message(&message( - "buyer-pubkey-2", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: true, - at: 1_700_000_010, - }), - )) - .expect("order receipt"); - - let coffee_query = RadrootsTradeListingQuery { - product_category: Some("coffee".into()), - ..Default::default() - }; - let coffee_results = index.query_listings( - &coffee_query, - RadrootsTradeListingSort { - field: RadrootsTradeListingSortField::ProductTitle, - direction: RadrootsTradeSortDirection::Asc, - }, - ); - assert_eq!(coffee_results.len(), 1); - assert_eq!(coffee_results[0].listing_id, "AAAAAAAAAAAAAAAAAAAAAg"); - - let listing_summaries = index.marketplace_listing_summaries( - &RadrootsTradeListingQuery::default(), - RadrootsTradeListingSort { - field: RadrootsTradeListingSortField::OpenOrderCount, - direction: RadrootsTradeSortDirection::Desc, - }, - ); - assert_eq!(listing_summaries.len(), 2); - assert_eq!(listing_summaries[0].listing_addr, base_order().listing_addr); - assert_eq!(listing_summaries[0].open_order_count, 1); - assert_eq!( - listing_summaries[0].primary_bin_label.as_deref(), - Some("1kg bag") - ); - assert_eq!( - listing_summaries[1].listing_status, - RadrootsTradeListingMarketStatus::Window - ); - assert_eq!( - listing_summaries[1].location_primary.as_deref(), - Some("warehouse") - ); - - let facets = index.listing_facets(&RadrootsTradeListingQuery::default()); - assert_eq!(facets.farm_pubkeys.len(), 2); - assert_eq!(facets.product_categories.len(), 2); - assert_eq!( - facets - .listing_statuses - .iter() - .map(|facet| (facet.key.as_str(), facet.count)) - .collect::<Vec<_>>(), - vec![("active", 1), ("window", 1)] - ); - } - - #[test] - fn projection_helper_comparators_and_queries_cover_remaining_paths() { - let listing_a = - RadrootsTradeListingProjection::from_listing_contract("seller-pubkey", &base_listing()) - .expect("listing a"); - let mut listing_b = listing_a.clone(); - listing_b.listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAz".into(); - listing_b.listing_id = "AAAAAAAAAAAAAAAAAAAAAz".into(); - - assert_eq!( - super::compare_option_decimal( - &Some(RadrootsCoreDecimal::from(10u32)), - &Some(RadrootsCoreDecimal::from(10u32)), - ), - core::cmp::Ordering::Equal - ); - assert_eq!( - super::compare_option_decimal(&Some(RadrootsCoreDecimal::from(10u32)), &None), - core::cmp::Ordering::Less - ); - assert_eq!( - super::compare_option_decimal(&None, &Some(RadrootsCoreDecimal::from(10u32))), - core::cmp::Ordering::Greater - ); - assert_eq!( - super::compare_option_decimal(&None, &None), - core::cmp::Ordering::Equal - ); - - for field in [ - RadrootsTradeListingSortField::ProductTitle, - RadrootsTradeListingSortField::ProductCategory, - RadrootsTradeListingSortField::SellerPubkey, - RadrootsTradeListingSortField::InventoryAvailable, - RadrootsTradeListingSortField::OpenOrderCount, - RadrootsTradeListingSortField::TotalOrderCount, - ] { - assert_eq!( - super::compare_listings( - &listing_a, - &listing_b, - RadrootsTradeListingSort { - field, - direction: RadrootsTradeSortDirection::Asc, - }, - ), - core::cmp::Ordering::Less - ); - } - - assert!(super::listing_matches_query( - &listing_a, - &RadrootsTradeListingQuery::default() - )); - assert!(!super::listing_matches_query( - &listing_a, - &RadrootsTradeListingQuery { - seller_pubkey: Some("other-seller".into()), - ..Default::default() - } - )); - assert!(!super::listing_matches_query( - &listing_a, - &RadrootsTradeListingQuery { - farm_pubkey: Some("other-farm".into()), - ..Default::default() - } - )); - assert!(!super::listing_matches_query( - &listing_a, - &RadrootsTradeListingQuery { - farm_id: Some("other-farm-id".into()), - ..Default::default() - } - )); - assert!(!super::listing_matches_query( - &listing_a, - &RadrootsTradeListingQuery { - product_key: Some("other-key".into()), - ..Default::default() - } - )); - assert!(!super::listing_matches_query( - &listing_a, - &RadrootsTradeListingQuery { - product_category: Some("other-category".into()), - ..Default::default() - } - )); - assert!(!super::listing_matches_query( - &listing_a, - &RadrootsTradeListingQuery { - listing_status: Some(RadrootsTradeListingMarketStatus::Sold), - ..Default::default() - } - )); - - let request_message = message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - ); - let order_a = RadrootsTradeOrderWorkflowProjection::from_order_request( - &request_message, - &base_order(), - ) - .expect("order a"); - let mut order_b = order_a.clone(); - order_b.order_id = "order-2".into(); - - for field in [ - RadrootsTradeOrderSortField::ListingAddr, - RadrootsTradeOrderSortField::BuyerPubkey, - RadrootsTradeOrderSortField::SellerPubkey, - RadrootsTradeOrderSortField::Status, - RadrootsTradeOrderSortField::LastMessageType, - RadrootsTradeOrderSortField::TotalBinCount, - ] { - assert_eq!( - super::compare_orders( - &order_a, - &order_b, - RadrootsTradeOrderSort { - field, - direction: RadrootsTradeSortDirection::Asc, - }, - ), - core::cmp::Ordering::Less - ); - } - - let message_type_expectations = [ - ( - TradeListingMessageType::ListingValidateRequest, - "listing_validate_request", - ), - ( - TradeListingMessageType::ListingValidateResult, - "listing_validate_result", - ), - (TradeListingMessageType::OrderRequest, "order_request"), - (TradeListingMessageType::OrderResponse, "order_response"), - (TradeListingMessageType::OrderRevision, "order_revision"), - ( - TradeListingMessageType::OrderRevisionAccept, - "order_revision_accept", - ), - ( - TradeListingMessageType::OrderRevisionDecline, - "order_revision_decline", - ), - (TradeListingMessageType::Question, "question"), - (TradeListingMessageType::Answer, "answer"), - (TradeListingMessageType::DiscountRequest, "discount_request"), - (TradeListingMessageType::DiscountOffer, "discount_offer"), - (TradeListingMessageType::DiscountAccept, "discount_accept"), - (TradeListingMessageType::DiscountDecline, "discount_decline"), - (TradeListingMessageType::Cancel, "cancel"), - ( - TradeListingMessageType::FulfillmentUpdate, - "fulfillment_update", - ), - (TradeListingMessageType::Receipt, "receipt"), - ]; - for (message_type, expected) in message_type_expectations { - assert_eq!(super::message_type_key(message_type), expected); - } - - let message_type_cases = [ - ( - message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - None, - TradeListingMessagePayload::ListingValidateRequest( - TradeListingValidateRequest { - listing_event: Some(listing_snapshot( - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - )), - }, - ), - ), - TradeListingMessageType::ListingValidateRequest, - ), - ( - message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - None, - TradeListingMessagePayload::ListingValidateResult(TradeListingValidateResult { - valid: true, - errors: vec![], - }), - ), - TradeListingMessageType::ListingValidateResult, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-order-request"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "message-type-order-request".into(), - ..base_order() - }), - ), - TradeListingMessageType::OrderRequest, - ), - ( - message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-order-response"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ), - TradeListingMessageType::OrderResponse, - ), - ( - message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-order-revision"), - TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "revision-1".into(), - changes: vec![TradeOrderChange::BinCount { - item_index: 0, - bin_count: 3, - }], - }), - ), - TradeListingMessageType::OrderRevision, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-order-revision-accept"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - }), - ), - TradeListingMessageType::OrderRevisionAccept, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-order-revision-decline"), - TradeListingMessagePayload::OrderRevisionDecline(TradeOrderRevisionResponse { - accepted: false, - reason: Some("no".into()), - }), - ), - TradeListingMessageType::OrderRevisionDecline, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-question"), - TradeListingMessagePayload::Question(TradeQuestion { - question_id: "question-1".into(), - }), - ), - TradeListingMessageType::Question, - ), - ( - message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-answer"), - TradeListingMessagePayload::Answer(TradeAnswer { - question_id: "question-1".into(), - }), - ), - TradeListingMessageType::Answer, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-discount-request"), - TradeListingMessagePayload::DiscountRequest(TradeDiscountRequest { - discount_id: "discount-1".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(10u32)), - ), - }), - ), - TradeListingMessageType::DiscountRequest, - ), - ( - message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-discount-offer"), - TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer { - discount_id: "discount-1".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - ), - TradeListingMessageType::DiscountOffer, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-discount-accept"), - TradeListingMessagePayload::DiscountAccept(TradeDiscountDecision::Accept { - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - ), - TradeListingMessageType::DiscountAccept, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-discount-decline"), - TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline { - reason: Some("no".into()), - }), - ), - TradeListingMessageType::DiscountDecline, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-cancel"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("cancel".into()), - }), - ), - TradeListingMessageType::Cancel, - ), - ( - message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-fulfillment"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Preparing, - }), - ), - TradeListingMessageType::FulfillmentUpdate, - ), - ( - message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("message-type-receipt"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: true, - at: 1_700_000_000, - }), - ), - TradeListingMessageType::Receipt, - ), - ]; - for (message, expected) in message_type_cases { - assert_eq!(message.message_type(), expected); - } - - assert!(super::order_matches_query( - &order_a, - &RadrootsTradeOrderQuery::default() - )); - assert!(!super::order_matches_query( - &order_a, - &RadrootsTradeOrderQuery { - listing_addr: Some("other-listing".into()), - ..Default::default() - } - )); - assert!(!super::order_matches_query( - &order_a, - &RadrootsTradeOrderQuery { - buyer_pubkey: Some("other-buyer".into()), - ..Default::default() - } - )); - assert!(!super::order_matches_query( - &order_a, - &RadrootsTradeOrderQuery { - seller_pubkey: Some("other-seller".into()), - ..Default::default() - } - )); - assert!(!super::order_matches_query( - &order_a, - &RadrootsTradeOrderQuery { - status: Some(TradeOrderStatus::Completed), - ..Default::default() - } - )); - } - - #[test] - fn order_query_helpers_filter_sort_and_facet_marketplace_views() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("base listing"); - index - .upsert_listing("seller-pubkey", &alternate_listing()) - .expect("alternate listing"); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("first order"); - index - .apply_workflow_message(&message( - "buyer-pubkey-2", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::TradeOrderRequested(alternate_order()), - )) - .expect("second order"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: Some("approved".into()), - }), - )) - .expect("accepted"); - index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Delivered, - }), - )) - .expect("fulfilled"); - index - .apply_workflow_message(&message( - "buyer-pubkey-2", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw", - Some("order-2"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: true, - at: 1_700_000_020, - }), - )) - .expect("completed"); - - let completed_query = RadrootsTradeOrderQuery { - seller_pubkey: Some("seller-pubkey".into()), - status: Some(TradeOrderStatus::Completed), - ..Default::default() - }; - let completed_orders = index.query_orders( - &completed_query, - RadrootsTradeOrderSort { - field: RadrootsTradeOrderSortField::TotalBinCount, - direction: RadrootsTradeSortDirection::Desc, - }, - ); - assert_eq!(completed_orders.len(), 1); - assert_eq!(completed_orders[0].order_id, "order-2"); - assert_eq!(completed_orders[0].total_bin_count(), 4); - - let summaries = index.marketplace_order_summaries( - &RadrootsTradeOrderQuery { - seller_pubkey: Some("seller-pubkey".into()), - ..Default::default() - }, - RadrootsTradeOrderSort { - field: RadrootsTradeOrderSortField::TotalBinCount, - direction: RadrootsTradeSortDirection::Desc, - }, - ); - assert_eq!(summaries.len(), 2); - assert_eq!(summaries[0].order_id, "order-2"); - assert_eq!(summaries[0].item_count, 2); - assert_eq!(summaries[0].total_bin_count, 4); - assert!(!summaries[0].has_requested_discounts); - assert_eq!(summaries[0].last_reason, None); - assert_eq!(summaries[1].order_id, "order-1"); - assert!(summaries[1].has_requested_discounts); - - let facets = index.order_facets(&RadrootsTradeOrderQuery::default()); - assert_eq!( - facets - .statuses - .iter() - .map(|facet| (facet.key.as_str(), facet.count)) - .collect::<Vec<_>>(), - vec![("completed", 1), ("requested", 1)] - ); - assert_eq!( - facets - .buyer_pubkeys - .iter() - .map(|facet| (facet.key.as_str(), facet.count)) - .collect::<Vec<_>>(), - vec![("buyer-pubkey", 1), ("buyer-pubkey-2", 1)] - ); - } - - #[test] - fn workflow_projection_covers_remaining_error_branches() { - let mut index = RadrootsTradeReadIndex::new(); - index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("listing"); - - assert_eq!( - index - .order_mut_checked("missing", "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg",) - .expect_err("missing order"), - RadrootsTradeProjectionError::MissingOrder("missing".into()) - ); - - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("order request"); - - assert_eq!( - index - .order_mut_checked("order-1", "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw",) - .expect_err("listing mismatch"), - RadrootsTradeProjectionError::ListingAddrMismatch - ); - assert_eq!( - super::ensure_actor("seller-pubkey", "buyer-pubkey"), - Err(RadrootsTradeProjectionError::UnauthorizedActor) - ); - assert_eq!( - super::ensure_counterparty("seller-pubkey", "buyer-pubkey"), - Err(RadrootsTradeProjectionError::CounterpartyMismatch) - ); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Requested, - &TradeOrderStatus::Requested - )); - assert_eq!( - super::radroots_trade_order_status_ensure_transition( - TradeOrderStatus::Accepted, - TradeOrderStatus::Requested, - ), - Err(RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Accepted, - to: TradeOrderStatus::Requested, - }) - ); - - let existing_order = index.order("order-1").expect("existing order").clone(); - let mut bad_root = message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - bad_root.root_event_id = Some("wrong-root".into()); - assert_eq!( - super::ensure_trade_chain(&existing_order, &bad_root), - Err(RadrootsTradeProjectionError::TradeThreadRootMismatch) - ); - let mut bad_prev = bad_root.clone(); - bad_prev.root_event_id = Some(existing_order.root_event_id.clone()); - bad_prev.prev_event_id = Some("wrong-prev".into()); - assert_eq!( - super::ensure_trade_chain(&existing_order, &bad_prev), - Err(RadrootsTradeProjectionError::TradeThreadPrevMismatch) - ); - - let mut items = vec![TradeOrderItem { - bin_id: "bin-1".into(), - bin_count: 1, - }]; - assert_eq!( - super::apply_order_change( - &mut items, - &TradeOrderChange::BinCount { - item_index: 7, - bin_count: 2, - }, - ), - Err(RadrootsTradeProjectionError::InvalidItemIndex(7)) - ); - assert_eq!( - super::apply_order_change(&mut items, &TradeOrderChange::ItemRemove { item_index: 7 },), - Err(RadrootsTradeProjectionError::InvalidItemIndex(7)) - ); - - let mut decline_index = RadrootsTradeReadIndex::new(); - decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("decline order request"); - let declined = decline_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: false, - reason: Some("declined".into()), - }), - )) - .expect("declined order"); - assert_eq!(declined.order_id, "order-1"); - assert_eq!( - decline_index - .order("order-1") - .expect("declined order") - .status, - TradeOrderStatus::Declined - ); - - let mut invalid_accept_index = RadrootsTradeReadIndex::new(); - invalid_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-2".into(), - ..base_order() - }), - )) - .expect("second order"); - assert_eq!( - invalid_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: false, - reason: None, - }), - )) - .expect_err("invalid revision accept"), - RadrootsTradeProjectionError::InvalidRevisionResponse - ); - - let mut invalid_decline_index = RadrootsTradeReadIndex::new(); - invalid_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-2".into(), - ..base_order() - }), - )) - .expect("third order"); - assert_eq!( - invalid_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::OrderRevisionDecline(TradeOrderRevisionResponse { - accepted: true, - reason: None, - }), - )) - .expect_err("invalid revision decline"), - RadrootsTradeProjectionError::InvalidRevisionResponse - ); - - let mut invalid_discount_accept_index = RadrootsTradeReadIndex::new(); - invalid_discount_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-2".into(), - ..base_order() - }), - )) - .expect("fourth order"); - assert_eq!( - invalid_discount_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::DiscountAccept(TradeDiscountDecision::Decline { - reason: Some("wrong-shape".into()), - }), - )) - .expect_err("invalid discount accept"), - RadrootsTradeProjectionError::InvalidDiscountDecision - ); - - let mut invalid_discount_decline_index = RadrootsTradeReadIndex::new(); - invalid_discount_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-2".into(), - ..base_order() - }), - )) - .expect("fifth order"); - assert_eq!( - invalid_discount_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-2"), - TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Accept { - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(10u32)), - ), - }), - )) - .expect_err("invalid discount decline"), - RadrootsTradeProjectionError::InvalidDiscountDecision - ); - - let mut mismatched_order = base_order(); - mismatched_order.order_id = "order-3".into(); - assert_eq!( - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("wrong-order-id"), - TradeListingMessagePayload::TradeOrderRequested(mismatched_order.clone()), - )) - .expect_err("order id mismatch"), - RadrootsTradeProjectionError::OrderIdMismatch - ); - mismatched_order.listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw".into(); - assert_eq!( - index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-3"), - TradeListingMessagePayload::TradeOrderRequested(mismatched_order), - )) - .expect_err("listing addr mismatch"), - RadrootsTradeProjectionError::ListingAddrMismatch - ); - - let mut duplicate_order = TradeOrder { - order_id: "order-1".into(), - ..base_order() - }; - duplicate_order.buyer_pubkey = "buyer-pubkey-2".into(); - assert_eq!( - index - .apply_workflow_message(&message( - "buyer-pubkey-2", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(duplicate_order), - )) - .expect_err("duplicate order identity mismatch"), - RadrootsTradeProjectionError::ListingAddrMismatch - ); - - let duplicate_listing_mismatch_order = TradeOrder { - order_id: "order-1".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAw".into(), - ..base_order() - }; - let duplicate_listing_mismatch_message = RadrootsTradeOrderWorkflowMessage { - event_id: "order-1:duplicate-listing".into(), - actor_pubkey: "buyer-pubkey".into(), - counterparty_pubkey: "seller-pubkey".into(), - listing_addr: duplicate_listing_mismatch_order.listing_addr.clone(), - order_id: Some("order-1".into()), - listing_event: Some(listing_snapshot( - &duplicate_listing_mismatch_order.listing_addr, - )), - root_event_id: None, - prev_event_id: None, - payload: TradeListingMessagePayload::TradeOrderRequested( - duplicate_listing_mismatch_order, - ), - }; - assert_eq!( - index - .apply_workflow_message(&duplicate_listing_mismatch_message) - .expect_err("duplicate listing mismatch"), - RadrootsTradeProjectionError::ListingAddrMismatch - ); - - let duplicate_same = index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-1"), - TradeListingMessagePayload::TradeOrderRequested(base_order()), - )) - .expect("duplicate same order"); - assert_eq!(duplicate_same.order_id, "order-1"); - - let duplicate_seller_mismatch_message = RadrootsTradeOrderWorkflowMessage { - event_id: "order-1:duplicate-seller".into(), - actor_pubkey: "buyer-pubkey".into(), - counterparty_pubkey: "other-seller".into(), - listing_addr: "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg".into(), - order_id: Some("order-1".into()), - listing_event: Some(listing_snapshot( - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - )), - root_event_id: None, - prev_event_id: None, - payload: TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-1".into(), - seller_pubkey: "other-seller".into(), - ..base_order() - }), - }; - assert_eq!( - index - .apply_workflow_message(&duplicate_seller_mismatch_message) - .expect_err("duplicate seller mismatch"), - RadrootsTradeProjectionError::ListingAddrMismatch - ); - - let mut cancel_index = RadrootsTradeReadIndex::new(); - cancel_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-4"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-4".into(), - ..base_order() - }), - )) - .expect("cancel order request"); - assert_eq!( - cancel_index - .apply_workflow_message(&message( - "intruder", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-4"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("bad-actor".into()), - }), - )) - .expect_err("unauthorized cancel"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut buyer_cancel_index = RadrootsTradeReadIndex::new(); - buyer_cancel_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-4"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-4".into(), - ..base_order() - }), - )) - .expect("buyer cancel order request"); - buyer_cancel_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-4"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("buyer-cancel".into()), - }), - )) - .expect("buyer cancel"); - assert_eq!( - buyer_cancel_index - .order("order-4") - .expect("cancelled order") - .status, - TradeOrderStatus::Cancelled - ); - } - - #[test] - fn workflow_projection_rejects_order_request_identity_mismatches() { - let listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"; - let mut unauthorized_index = RadrootsTradeReadIndex::new(); - assert_eq!( - unauthorized_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-request-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-request-actor".into(), - ..base_order() - }), - )) - .expect_err("unauthorized order request"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut wrong_counterparty = message( - "buyer-pubkey", - listing_addr, - Some("order-request-counterparty"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-request-counterparty".into(), - ..base_order() - }), - ); - wrong_counterparty.counterparty_pubkey = "wrong-seller".into(); - let mut counterparty_index = RadrootsTradeReadIndex::new(); - assert_eq!( - counterparty_index - .apply_workflow_message(&wrong_counterparty) - .expect_err("counterparty mismatch"), - RadrootsTradeProjectionError::CounterpartyMismatch - ); - } - - #[test] - fn workflow_action_helpers_cover_remaining_error_paths() { - let listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"; - let mut index = RadrootsTradeReadIndex::new(); - index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-helper".into(), - ..base_order() - }), - )) - .expect("seed order"); - - let missing_buyer_order = message( - "buyer-pubkey", - listing_addr, - Some("missing-order"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - }), - ); - assert_eq!( - index - .order_mut_for_buyer_action(&missing_buyer_order) - .expect_err("missing buyer order"), - RadrootsTradeProjectionError::MissingOrder("missing-order".into()) - ); - - let wrong_buyer_actor = message( - "intruder", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - }), - ); - assert_eq!( - index - .order_mut_for_buyer_action(&wrong_buyer_actor) - .expect_err("wrong buyer actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut wrong_buyer_counterparty = message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - }), - ); - wrong_buyer_counterparty.counterparty_pubkey = "wrong-seller".into(); - assert_eq!( - index - .order_mut_for_buyer_action(&wrong_buyer_counterparty) - .expect_err("wrong buyer counterparty"), - RadrootsTradeProjectionError::CounterpartyMismatch - ); - - let mut missing_buyer_root = message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - }), - ); - missing_buyer_root.root_event_id = None; - assert_eq!( - index - .order_mut_for_buyer_action(&missing_buyer_root) - .expect_err("missing buyer root"), - RadrootsTradeProjectionError::MissingTradeRootEventId - ); - - let mut missing_buyer_prev = message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - }), - ); - missing_buyer_prev.prev_event_id = None; - assert_eq!( - index - .order_mut_for_buyer_action(&missing_buyer_prev) - .expect_err("missing buyer prev"), - RadrootsTradeProjectionError::MissingTradePrevEventId - ); - - let missing_seller_order = message( - "seller-pubkey", - listing_addr, - Some("missing-order"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - assert_eq!( - index - .order_mut_for_seller_action(&missing_seller_order) - .expect_err("missing seller order"), - RadrootsTradeProjectionError::MissingOrder("missing-order".into()) - ); - - let missing_seller_order_id = RadrootsTradeOrderWorkflowMessage { - order_id: None, - ..message( - "seller-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ) - }; - assert_eq!( - index - .order_mut_for_seller_action(&missing_seller_order_id) - .expect_err("missing seller order id"), - RadrootsTradeProjectionError::MissingOrderId - ); - - let wrong_seller_actor = message( - "intruder", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - assert_eq!( - index - .order_mut_for_seller_action(&wrong_seller_actor) - .expect_err("wrong seller actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut wrong_seller_counterparty = message( - "seller-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - wrong_seller_counterparty.counterparty_pubkey = "wrong-buyer".into(); - assert_eq!( - index - .order_mut_for_seller_action(&wrong_seller_counterparty) - .expect_err("wrong seller counterparty"), - RadrootsTradeProjectionError::CounterpartyMismatch - ); - - let mut missing_seller_root = message( - "seller-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - missing_seller_root.root_event_id = None; - assert_eq!( - index - .order_mut_for_seller_action(&missing_seller_root) - .expect_err("missing seller root"), - RadrootsTradeProjectionError::MissingTradeRootEventId - ); - - let mut missing_seller_prev = message( - "seller-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - missing_seller_prev.prev_event_id = None; - assert_eq!( - index - .order_mut_for_seller_action(&missing_seller_prev) - .expect_err("missing seller prev"), - RadrootsTradeProjectionError::MissingTradePrevEventId - ); - - let missing_participant_order = message( - "buyer-pubkey", - listing_addr, - Some("missing-order"), - TradeListingMessagePayload::Cancel(TradeListingCancel { reason: None }), - ); - assert_eq!( - index - .order_mut_for_participant_action(&missing_participant_order) - .expect_err("missing participant order"), - RadrootsTradeProjectionError::MissingOrder("missing-order".into()) - ); - - let missing_participant_order_id = RadrootsTradeOrderWorkflowMessage { - order_id: None, - ..message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::Cancel(TradeListingCancel { reason: None }), - ) - }; - assert_eq!( - index - .order_mut_for_participant_action(&missing_participant_order_id) - .expect_err("missing participant order id"), - RadrootsTradeProjectionError::MissingOrderId - ); - - let mut wrong_participant_counterparty = message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::Cancel(TradeListingCancel { reason: None }), - ); - wrong_participant_counterparty.counterparty_pubkey = "wrong-seller".into(); - assert_eq!( - index - .order_mut_for_participant_action(&wrong_participant_counterparty) - .expect_err("wrong participant counterparty"), - RadrootsTradeProjectionError::CounterpartyMismatch - ); - - let mut missing_participant_root = message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::Cancel(TradeListingCancel { reason: None }), - ); - missing_participant_root.root_event_id = None; - assert_eq!( - index - .order_mut_for_participant_action(&missing_participant_root) - .expect_err("missing participant root"), - RadrootsTradeProjectionError::MissingTradeRootEventId - ); - - let mut missing_participant_prev = message( - "buyer-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::Cancel(TradeListingCancel { reason: None }), - ); - missing_participant_prev.prev_event_id = None; - assert_eq!( - index - .order_mut_for_participant_action(&missing_participant_prev) - .expect_err("missing participant prev"), - RadrootsTradeProjectionError::MissingTradePrevEventId - ); - - let existing_order = index.order("order-helper").expect("helper order").clone(); - let mut missing_root = message( - "seller-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - missing_root.root_event_id = None; - assert_eq!( - super::ensure_trade_chain(&existing_order, &missing_root), - Err(RadrootsTradeProjectionError::MissingTradeRootEventId) - ); - - let mut missing_prev = message( - "seller-pubkey", - listing_addr, - Some("order-helper"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - ); - missing_prev.prev_event_id = None; - assert_eq!( - super::ensure_trade_chain(&existing_order, &missing_prev), - Err(RadrootsTradeProjectionError::MissingTradePrevEventId) - ); - } - - #[test] - fn workflow_helpers_cover_transition_and_terminal_tables() { - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Draft, - &TradeOrderStatus::Requested - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Draft, - &TradeOrderStatus::Accepted - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Validated, - &TradeOrderStatus::Requested - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Validated, - &TradeOrderStatus::Accepted - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Requested, - &TradeOrderStatus::Accepted - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Requested, - &TradeOrderStatus::Declined - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Requested, - &TradeOrderStatus::Questioned - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Requested, - &TradeOrderStatus::Revised - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Requested, - &TradeOrderStatus::Cancelled - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Questioned, - &TradeOrderStatus::Requested - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Questioned, - &TradeOrderStatus::Revised - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Questioned, - &TradeOrderStatus::Cancelled - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Revised, - &TradeOrderStatus::Accepted - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Revised, - &TradeOrderStatus::Declined - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Revised, - &TradeOrderStatus::Cancelled - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Revised, - &TradeOrderStatus::Requested - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Accepted, - &TradeOrderStatus::Fulfilled - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Accepted, - &TradeOrderStatus::Cancelled - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Fulfilled, - &TradeOrderStatus::Completed - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Fulfilled, - &TradeOrderStatus::Fulfilled - )); - assert!(radroots_trade_order_status_can_transition( - &TradeOrderStatus::Fulfilled, - &TradeOrderStatus::Cancelled - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Accepted, - &TradeOrderStatus::Requested - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Requested, - &TradeOrderStatus::Fulfilled - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Questioned, - &TradeOrderStatus::Accepted - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Revised, - &TradeOrderStatus::Completed - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Declined, - &TradeOrderStatus::Accepted - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Cancelled, - &TradeOrderStatus::Accepted - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Completed, - &TradeOrderStatus::Accepted - )); - assert!(!radroots_trade_order_status_can_transition( - &TradeOrderStatus::Fulfilled, - &TradeOrderStatus::Accepted - )); - assert!(radroots_trade_order_status_is_terminal( - &TradeOrderStatus::Completed - )); - assert!(radroots_trade_order_status_is_terminal( - &TradeOrderStatus::Declined - )); - assert!(radroots_trade_order_status_is_terminal( - &TradeOrderStatus::Cancelled - )); - assert!(!radroots_trade_order_status_is_terminal( - &TradeOrderStatus::Fulfilled - )); - - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Accepted, - &TradeFulfillmentStatus::Preparing, - ), - Ok(None) - ); - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Accepted, - &TradeFulfillmentStatus::Shipped, - ), - Ok(None) - ); - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Accepted, - &TradeFulfillmentStatus::ReadyForPickup, - ), - Ok(None) - ); - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Requested, - &TradeFulfillmentStatus::Preparing, - ), - Err(RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Requested, - to: TradeOrderStatus::Accepted, - }) - ); - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Accepted, - &TradeFulfillmentStatus::Delivered, - ), - Ok(Some(TradeOrderStatus::Fulfilled)) - ); - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Requested, - &TradeFulfillmentStatus::Delivered, - ), - Err(RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Requested, - to: TradeOrderStatus::Fulfilled, - }) - ); - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Accepted, - &TradeFulfillmentStatus::Cancelled, - ), - Ok(Some(TradeOrderStatus::Cancelled)) - ); - assert_eq!( - super::trade_order_status_for_fulfillment_update( - &TradeOrderStatus::Completed, - &TradeFulfillmentStatus::Cancelled, - ), - Err(RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Cancelled, - }) - ); - - assert_eq!( - super::trade_order_status_for_receipt(&TradeOrderStatus::Fulfilled, true), - Ok(Some(TradeOrderStatus::Completed)) - ); - assert_eq!( - super::trade_order_status_for_receipt(&TradeOrderStatus::Accepted, true), - Err(RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Accepted, - to: TradeOrderStatus::Completed, - }) - ); - assert_eq!( - super::trade_order_status_for_receipt(&TradeOrderStatus::Fulfilled, false), - Ok(None) - ); - assert_eq!( - super::trade_order_status_for_receipt(&TradeOrderStatus::Accepted, false), - Err(RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Accepted, - to: TradeOrderStatus::Fulfilled, - }) - ); - - let facet_expectations = [ - (RadrootsTradeListingMarketStatus::Unknown, "unknown"), - (RadrootsTradeListingMarketStatus::Window, "window"), - (RadrootsTradeListingMarketStatus::Active, "active"), - (RadrootsTradeListingMarketStatus::Sold, "sold"), - ( - RadrootsTradeListingMarketStatus::Other { - value: "archived".into(), - }, - "archived", - ), - ]; - for (status, expected) in facet_expectations { - assert_eq!(status.facet_key(), expected); - } - - let order_status_expectations = [ - (TradeOrderStatus::Draft, "draft"), - (TradeOrderStatus::Validated, "validated"), - (TradeOrderStatus::Requested, "requested"), - (TradeOrderStatus::Questioned, "questioned"), - (TradeOrderStatus::Revised, "revised"), - (TradeOrderStatus::Accepted, "accepted"), - (TradeOrderStatus::Declined, "declined"), - (TradeOrderStatus::Cancelled, "cancelled"), - (TradeOrderStatus::Fulfilled, "fulfilled"), - (TradeOrderStatus::Completed, "completed"), - ]; - for (status, expected) in order_status_expectations { - assert_eq!(super::order_status_key(&status), expected); - } - } - - #[test] - fn workflow_projection_rejects_follow_up_messages_with_wrong_actor_or_missing_snapshot() { - let listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"; - - let mut response_index = RadrootsTradeReadIndex::new(); - response_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-response-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-response-actor".into(), - ..base_order() - }), - )) - .expect("seed response order"); - assert_eq!( - response_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-response-actor"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect_err("response wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut revision_index = RadrootsTradeReadIndex::new(); - revision_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-actor".into(), - ..base_order() - }), - )) - .expect("seed revision order"); - assert_eq!( - revision_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-revision-actor"), - TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "rev-invalid-actor".into(), - changes: vec![TradeOrderChange::BinCount { - item_index: 0, - bin_count: 3, - }], - }), - )) - .expect_err("revision wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - let seeded_revision_order = revision_index - .order("order-revision-actor") - .expect("seeded revision order") - .clone(); - let empty_revision = RadrootsTradeOrderWorkflowMessage { - event_id: "order-revision-actor:empty-revision".into(), - actor_pubkey: "seller-pubkey".into(), - counterparty_pubkey: "buyer-pubkey".into(), - listing_addr: listing_addr.into(), - order_id: Some("order-revision-actor".into()), - listing_event: Some(listing_snapshot(listing_addr)), - root_event_id: Some(seeded_revision_order.root_event_id.clone()), - prev_event_id: Some(seeded_revision_order.last_event_id.clone()), - payload: TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "rev-empty".into(), - changes: vec![], - }), - }; - revision_index - .apply_workflow_message(&empty_revision) - .expect("empty revision"); - let revised_order = revision_index - .order("order-revision-actor") - .expect("revised order") - .clone(); - let mut missing_revision_snapshot = empty_revision.clone(); - missing_revision_snapshot.event_id = "order-revision-actor:missing-snapshot".into(); - missing_revision_snapshot.listing_event = None; - missing_revision_snapshot.prev_event_id = Some(revised_order.last_event_id.clone()); - assert_eq!( - revision_index - .apply_workflow_message(&missing_revision_snapshot) - .expect_err("revision missing snapshot"), - RadrootsTradeProjectionError::MissingListingSnapshot - ); - - let mut revision_accept_index = RadrootsTradeReadIndex::new(); - revision_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-accept-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-accept-actor".into(), - ..base_order() - }), - )) - .expect("seed revision accept order"); - assert_eq!( - revision_accept_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-revision-accept-actor"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - },), - )) - .expect_err("revision accept wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut revision_decline_index = RadrootsTradeReadIndex::new(); - revision_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-decline-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-decline-actor".into(), - ..base_order() - }), - )) - .expect("seed revision decline order"); - assert_eq!( - revision_decline_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-revision-decline-actor"), - TradeListingMessagePayload::OrderRevisionDecline(TradeOrderRevisionResponse { - accepted: false, - reason: None, - },), - )) - .expect_err("revision decline wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut answer_index = RadrootsTradeReadIndex::new(); - answer_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-answer-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-answer-actor".into(), - ..base_order() - }), - )) - .expect("seed answer order"); - answer_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-answer-actor"), - TradeListingMessagePayload::Question(TradeQuestion { - question_id: "question-1".into(), - }), - )) - .expect("seed answer question"); - assert_eq!( - answer_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-answer-actor"), - TradeListingMessagePayload::Answer(TradeAnswer { - question_id: "question-1".into(), - }), - )) - .expect_err("answer wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut discount_request_index = RadrootsTradeReadIndex::new(); - discount_request_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-request-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-request-actor".into(), - ..base_order() - }), - )) - .expect("seed discount request order"); - assert_eq!( - discount_request_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-discount-request-actor"), - TradeListingMessagePayload::DiscountRequest(TradeDiscountRequest { - discount_id: "discount-request-invalid-actor".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - )) - .expect_err("discount request wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - let discount_request_order = discount_request_index - .order("order-discount-request-actor") - .expect("discount request order") - .clone(); - let missing_discount_request_snapshot = RadrootsTradeOrderWorkflowMessage { - event_id: "order-discount-request-actor:missing-snapshot".into(), - actor_pubkey: "buyer-pubkey".into(), - counterparty_pubkey: "seller-pubkey".into(), - listing_addr: listing_addr.into(), - order_id: Some("order-discount-request-actor".into()), - listing_event: None, - root_event_id: Some(discount_request_order.root_event_id.clone()), - prev_event_id: Some(discount_request_order.last_event_id.clone()), - payload: TradeListingMessagePayload::DiscountRequest(TradeDiscountRequest { - discount_id: "discount-request-missing-snapshot".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent(RadrootsCorePercent::new( - RadrootsCoreDecimal::from(5u32), - )), - }), - }; - assert_eq!( - discount_request_index - .apply_workflow_message(&missing_discount_request_snapshot) - .expect_err("discount request missing snapshot"), - RadrootsTradeProjectionError::MissingListingSnapshot - ); - - let mut discount_offer_index = RadrootsTradeReadIndex::new(); - discount_offer_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-offer-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-offer-actor".into(), - ..base_order() - }), - )) - .expect("seed discount offer order"); - assert_eq!( - discount_offer_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-discount-offer-actor"), - TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer { - discount_id: "discount-offer-invalid-actor".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - )) - .expect_err("discount offer wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - let discount_offer_order = discount_offer_index - .order("order-discount-offer-actor") - .expect("discount offer order") - .clone(); - let missing_discount_offer_snapshot = RadrootsTradeOrderWorkflowMessage { - event_id: "order-discount-offer-actor:missing-snapshot".into(), - actor_pubkey: "seller-pubkey".into(), - counterparty_pubkey: "buyer-pubkey".into(), - listing_addr: listing_addr.into(), - order_id: Some("order-discount-offer-actor".into()), - listing_event: None, - root_event_id: Some(discount_offer_order.root_event_id.clone()), - prev_event_id: Some(discount_offer_order.last_event_id.clone()), - payload: TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer { - discount_id: "discount-offer-missing-snapshot".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent(RadrootsCorePercent::new( - RadrootsCoreDecimal::from(5u32), - )), - }), - }; - assert_eq!( - discount_offer_index - .apply_workflow_message(&missing_discount_offer_snapshot) - .expect_err("discount offer missing snapshot"), - RadrootsTradeProjectionError::MissingListingSnapshot - ); - - let mut discount_accept_index = RadrootsTradeReadIndex::new(); - discount_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-accept-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-accept-actor".into(), - ..base_order() - }), - )) - .expect("seed discount accept order"); - assert_eq!( - discount_accept_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-discount-accept-actor"), - TradeListingMessagePayload::DiscountAccept(TradeDiscountDecision::Accept { - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - )) - .expect_err("discount accept wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut discount_decline_index = RadrootsTradeReadIndex::new(); - discount_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-decline-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-decline-actor".into(), - ..base_order() - }), - )) - .expect("seed discount decline order"); - assert_eq!( - discount_decline_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-discount-decline-actor"), - TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline { - reason: Some("no".into()), - },), - )) - .expect_err("discount decline wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut fulfillment_index = RadrootsTradeReadIndex::new(); - fulfillment_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-fulfillment-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-fulfillment-actor".into(), - ..base_order() - }), - )) - .expect("seed fulfillment order"); - assert_eq!( - fulfillment_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-fulfillment-actor"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Preparing, - }), - )) - .expect_err("fulfillment wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - - let mut receipt_index = RadrootsTradeReadIndex::new(); - receipt_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-receipt-actor"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-receipt-actor".into(), - ..base_order() - }), - )) - .expect("seed receipt order"); - assert_eq!( - receipt_index - .apply_workflow_message(&message( - "intruder", - listing_addr, - Some("order-receipt-actor"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: false, - at: 1_700_000_123, - }), - )) - .expect_err("receipt wrong actor"), - RadrootsTradeProjectionError::UnauthorizedActor - ); - } - - #[test] - fn workflow_projection_rejects_follow_up_messages_with_invalid_transitions() { - let listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"; - - let mut response_index = RadrootsTradeReadIndex::new(); - response_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-response-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-response-transition".into(), - ..base_order() - }), - )) - .expect("seed response transition"); - response_index - .orders - .get_mut("order-response-transition") - .expect("response order") - .status = TradeOrderStatus::Completed; - assert_eq!( - response_index - .apply_workflow_message(&message( - "seller-pubkey", - listing_addr, - Some("order-response-transition"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect_err("response invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Accepted, - } - ); - - let mut revision_index = RadrootsTradeReadIndex::new(); - revision_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-transition".into(), - ..base_order() - }), - )) - .expect("seed revision transition"); - revision_index - .orders - .get_mut("order-revision-transition") - .expect("revision order") - .status = TradeOrderStatus::Completed; - assert_eq!( - revision_index - .apply_workflow_message(&message( - "seller-pubkey", - listing_addr, - Some("order-revision-transition"), - TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "rev-invalid-transition".into(), - changes: vec![TradeOrderChange::BinCount { - item_index: 0, - bin_count: 3, - }], - }), - )) - .expect_err("revision invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Revised, - } - ); - - let mut revision_accept_index = RadrootsTradeReadIndex::new(); - revision_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-accept-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-accept-transition".into(), - ..base_order() - }), - )) - .expect("seed revision accept transition"); - revision_accept_index - .orders - .get_mut("order-revision-accept-transition") - .expect("revision accept order") - .status = TradeOrderStatus::Completed; - assert_eq!( - revision_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-accept-transition"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: None, - },), - )) - .expect_err("revision accept invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Accepted, - } - ); - - let mut revision_decline_index = RadrootsTradeReadIndex::new(); - revision_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-decline-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-decline-transition".into(), - ..base_order() - }), - )) - .expect("seed revision decline transition"); - revision_decline_index - .orders - .get_mut("order-revision-decline-transition") - .expect("revision decline order") - .status = TradeOrderStatus::Completed; - assert_eq!( - revision_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-decline-transition"), - TradeListingMessagePayload::OrderRevisionDecline(TradeOrderRevisionResponse { - accepted: false, - reason: None, - },), - )) - .expect_err("revision decline invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Declined, - } - ); - - let mut question_index = RadrootsTradeReadIndex::new(); - question_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-question-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-question-transition".into(), - ..base_order() - }), - )) - .expect("seed question transition"); - question_index - .orders - .get_mut("order-question-transition") - .expect("question order") - .status = TradeOrderStatus::Completed; - assert_eq!( - question_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-question-transition"), - TradeListingMessagePayload::Question(TradeQuestion { - question_id: "question-1".into(), - }), - )) - .expect_err("question invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Questioned, - } - ); - - let mut answer_index = RadrootsTradeReadIndex::new(); - answer_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-answer-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-answer-transition".into(), - ..base_order() - }), - )) - .expect("seed answer transition"); - answer_index - .orders - .get_mut("order-answer-transition") - .expect("answer order") - .status = TradeOrderStatus::Accepted; - assert_eq!( - answer_index - .apply_workflow_message(&message( - "seller-pubkey", - listing_addr, - Some("order-answer-transition"), - TradeListingMessagePayload::Answer(TradeAnswer { - question_id: "question-1".into(), - }), - )) - .expect_err("answer invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Accepted, - to: TradeOrderStatus::Requested, - } - ); - - let mut discount_offer_index = RadrootsTradeReadIndex::new(); - discount_offer_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-offer-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-offer-transition".into(), - ..base_order() - }), - )) - .expect("seed discount offer transition"); - discount_offer_index - .orders - .get_mut("order-discount-offer-transition") - .expect("discount offer order") - .status = TradeOrderStatus::Completed; - assert_eq!( - discount_offer_index - .apply_workflow_message(&message( - "seller-pubkey", - listing_addr, - Some("order-discount-offer-transition"), - TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer { - discount_id: "discount-offer-invalid-transition".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - )) - .expect_err("discount offer invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Revised, - } - ); - - let mut discount_accept_index = RadrootsTradeReadIndex::new(); - discount_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-accept-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-accept-transition".into(), - ..base_order() - }), - )) - .expect("seed discount accept transition"); - discount_accept_index - .orders - .get_mut("order-discount-accept-transition") - .expect("discount accept order") - .status = TradeOrderStatus::Completed; - assert_eq!( - discount_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-accept-transition"), - TradeListingMessagePayload::DiscountAccept(TradeDiscountDecision::Accept { - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - )) - .expect_err("discount accept invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Accepted, - } - ); - - let mut discount_decline_index = RadrootsTradeReadIndex::new(); - discount_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-decline-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-decline-transition".into(), - ..base_order() - }), - )) - .expect("seed discount decline transition"); - discount_decline_index - .orders - .get_mut("order-discount-decline-transition") - .expect("discount decline order") - .status = TradeOrderStatus::Completed; - assert_eq!( - discount_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-discount-decline-transition"), - TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline { - reason: Some("no".into()), - },), - )) - .expect_err("discount decline invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Requested, - } - ); - - let mut cancel_index = RadrootsTradeReadIndex::new(); - cancel_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-cancel-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-cancel-transition".into(), - ..base_order() - }), - )) - .expect("seed cancel transition"); - cancel_index - .orders - .get_mut("order-cancel-transition") - .expect("cancel order") - .status = TradeOrderStatus::Completed; - assert_eq!( - cancel_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-cancel-transition"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("late cancel".into()), - }), - )) - .expect_err("cancel invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Completed, - to: TradeOrderStatus::Cancelled, - } - ); - - let mut fulfillment_index = RadrootsTradeReadIndex::new(); - fulfillment_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-fulfillment-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-fulfillment-transition".into(), - ..base_order() - }), - )) - .expect("seed fulfillment transition"); - assert_eq!( - fulfillment_index - .apply_workflow_message(&message( - "seller-pubkey", - listing_addr, - Some("order-fulfillment-transition"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Delivered, - }), - )) - .expect_err("fulfillment invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Requested, - to: TradeOrderStatus::Fulfilled, - } - ); - - let mut receipt_index = RadrootsTradeReadIndex::new(); - receipt_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-receipt-transition"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-receipt-transition".into(), - ..base_order() - }), - )) - .expect("seed receipt transition"); - receipt_index - .orders - .get_mut("order-receipt-transition") - .expect("receipt order") - .status = TradeOrderStatus::Accepted; - assert_eq!( - receipt_index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-receipt-transition"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: true, - at: 1_700_000_123, - }), - )) - .expect_err("receipt invalid transition"), - RadrootsTradeProjectionError::InvalidTransition { - from: TradeOrderStatus::Accepted, - to: TradeOrderStatus::Completed, - } - ); - } - - #[test] - fn workflow_projection_rejects_invalid_revision_change_indices() { - let listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"; - let mut index = RadrootsTradeReadIndex::new(); - index - .apply_workflow_message(&message( - "buyer-pubkey", - listing_addr, - Some("order-revision-invalid-change"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-invalid-change".into(), - ..base_order() - }), - )) - .expect("seed invalid revision order"); - - assert_eq!( - index - .apply_workflow_message(&message( - "seller-pubkey", - listing_addr, - Some("order-revision-invalid-change"), - TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "rev-invalid-index".into(), - changes: vec![TradeOrderChange::BinCount { - item_index: 7, - bin_count: 3, - }], - }), - )) - .expect_err("invalid revision change"), - RadrootsTradeProjectionError::InvalidItemIndex(7) - ); - } - - #[test] - fn workflow_projection_covers_successful_follow_up_paths() { - let mut question_index = RadrootsTradeReadIndex::new(); - question_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("question listing"); - question_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-question"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-question".into(), - ..base_order() - }), - )) - .expect("question order"); - question_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-question"), - TradeListingMessagePayload::Question(TradeQuestion { - question_id: "question-1".into(), - }), - )) - .expect("question"); - let answered = question_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-question"), - TradeListingMessagePayload::Answer(TradeAnswer { - question_id: "question-1".into(), - }), - )) - .expect("answer"); - assert_eq!(answered.status, TradeOrderStatus::Requested); - assert_eq!(answered.answer_count, 1); - - let mut revision_accept_index = RadrootsTradeReadIndex::new(); - revision_accept_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("revision accept listing"); - revision_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-revision-accept"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-accept".into(), - ..base_order() - }), - )) - .expect("revision accept request"); - revision_accept_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-revision-accept"), - TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "revision-accept".into(), - changes: vec![TradeOrderChange::ItemAdd { - item: TradeOrderItem { - bin_id: "bin-2".into(), - bin_count: 1, - }, - }], - }), - )) - .expect("revision"); - let accepted_revision = revision_accept_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-revision-accept"), - TradeListingMessagePayload::OrderRevisionAccept(TradeOrderRevisionResponse { - accepted: true, - reason: Some("works".into()), - }), - )) - .expect("revision accept"); - assert_eq!(accepted_revision.status, TradeOrderStatus::Accepted); - assert_eq!( - accepted_revision.last_message_type, - TradeListingMessageType::OrderRevisionAccept - ); - - let mut revision_decline_index = RadrootsTradeReadIndex::new(); - revision_decline_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("revision decline listing"); - revision_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-revision-decline"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-revision-decline".into(), - ..base_order() - }), - )) - .expect("revision decline request"); - revision_decline_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-revision-decline"), - TradeListingMessagePayload::OrderRevision(TradeOrderRevision { - revision_id: "revision-decline".into(), - changes: vec![TradeOrderChange::ItemRemove { item_index: 0 }], - }), - )) - .expect("revision decline"); - let declined_revision = revision_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-revision-decline"), - TradeListingMessagePayload::OrderRevisionDecline(TradeOrderRevisionResponse { - accepted: false, - reason: Some("no thanks".into()), - }), - )) - .expect("revision decline"); - assert_eq!(declined_revision.status, TradeOrderStatus::Declined); - assert_eq!( - declined_revision.last_message_type, - TradeListingMessageType::OrderRevisionDecline - ); - - let mut discount_index = RadrootsTradeReadIndex::new(); - discount_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("discount listing"); - discount_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-discount"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount".into(), - ..base_order() - }), - )) - .expect("discount request order"); - discount_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-discount"), - TradeListingMessagePayload::DiscountRequest(TradeDiscountRequest { - discount_id: "discount-request".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(10u32)), - ), - }), - )) - .expect("discount request"); - discount_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-discount"), - TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer { - discount_id: "discount-request".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - )) - .expect("discount offer"); - let accepted_discount = discount_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-discount"), - TradeListingMessagePayload::DiscountAccept(TradeDiscountDecision::Accept { - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(5u32)), - ), - }), - )) - .expect("discount accept"); - assert_eq!(accepted_discount.status, TradeOrderStatus::Accepted); - assert_eq!(accepted_discount.discount_accept_count, 1); - - let mut discount_decline_index = RadrootsTradeReadIndex::new(); - discount_decline_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("discount decline listing"); - discount_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-discount-decline"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-discount-decline".into(), - ..base_order() - }), - )) - .expect("discount decline request order"); - discount_decline_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-discount-decline"), - TradeListingMessagePayload::DiscountOffer(TradeDiscountOffer { - discount_id: "discount-decline".into(), - value: radroots_core::RadrootsCoreDiscountValue::Percent( - RadrootsCorePercent::new(RadrootsCoreDecimal::from(7u32)), - ), - }), - )) - .expect("discount decline offer"); - let declined_discount = discount_decline_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-discount-decline"), - TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline { - reason: Some("still too high".into()), - }), - )) - .expect("discount decline"); - assert_eq!(declined_discount.status, TradeOrderStatus::Requested); - assert_eq!(declined_discount.discount_decline_count, 1); - - let mut seller_cancel_index = RadrootsTradeReadIndex::new(); - seller_cancel_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("seller cancel listing"); - seller_cancel_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-seller-cancel"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-seller-cancel".into(), - ..base_order() - }), - )) - .expect("seller cancel order"); - let seller_cancelled = seller_cancel_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-seller-cancel"), - TradeListingMessagePayload::Cancel(TradeListingCancel { - reason: Some("seller-cancel".into()), - }), - )) - .expect("seller cancel"); - assert_eq!(seller_cancelled.status, TradeOrderStatus::Cancelled); - - let mut preparing_index = RadrootsTradeReadIndex::new(); - preparing_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("preparing listing"); - preparing_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-preparing"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-preparing".into(), - ..base_order() - }), - )) - .expect("preparing request"); - preparing_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-preparing"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect("preparing accepted"); - let preparing = preparing_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-preparing"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Preparing, - }), - )) - .expect("preparing update"); - assert_eq!(preparing.status, TradeOrderStatus::Accepted); - - let mut receipt_index = RadrootsTradeReadIndex::new(); - receipt_index - .upsert_listing("seller-pubkey", &base_listing()) - .expect("receipt listing"); - receipt_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-receipt"), - TradeListingMessagePayload::TradeOrderRequested(TradeOrder { - order_id: "order-receipt".into(), - ..base_order() - }), - )) - .expect("receipt request"); - receipt_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-receipt"), - TradeListingMessagePayload::OrderResponse(TradeOrderResponse { - accepted: true, - reason: None, - }), - )) - .expect("receipt accepted"); - receipt_index - .apply_workflow_message(&message( - "seller-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-receipt"), - TradeListingMessagePayload::FulfillmentUpdate(TradeFulfillmentUpdate { - status: TradeFulfillmentStatus::Delivered, - }), - )) - .expect("receipt fulfilled"); - let receipt = receipt_index - .apply_workflow_message(&message( - "buyer-pubkey", - "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg", - Some("order-receipt"), - TradeListingMessagePayload::Receipt(TradeReceipt { - acknowledged: false, - at: 1_700_000_040, - }), - )) - .expect("receipt pending"); - assert_eq!(receipt.status, TradeOrderStatus::Fulfilled); - } -} diff --git a/crates/trade/src/listing/tags.rs b/crates/trade/src/listing/tags.rs @@ -1,330 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; - -use radroots_events::{ - RadrootsNostrEventPtr, - tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, -}; -use radroots_events_codec::{ - error::{EventEncodeError, EventParseError}, - job::error::JobParseError, -}; - -pub const TAG_LISTING_EVENT: &str = "listing_event"; - -#[inline] -fn push_tag(tags: &mut Vec<Vec<String>>, name: &'static str, value: impl Into<String>) { - let mut tag = Vec::with_capacity(2); - tag.push(name.to_string()); - tag.push(value.into()); - tags.push(tag); -} - -#[inline] -pub fn trade_listing_dvm_tags<P, A, D>( - recipient_pubkey: P, - listing_addr: A, - order_id: Option<D>, - listing_event: Option<&RadrootsNostrEventPtr>, - root_event_id: Option<&str>, - prev_event_id: Option<&str>, -) -> Result<Vec<Vec<String>>, EventEncodeError> -where - P: Into<String>, - A: Into<String>, - D: Into<String>, -{ - let recipient_pubkey = recipient_pubkey.into(); - if recipient_pubkey.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("recipient_pubkey")); - } - let listing_addr = listing_addr.into(); - if listing_addr.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("listing_addr")); - } - let mut tags = Vec::with_capacity( - 2 + usize::from(order_id.is_some()) - + usize::from(listing_event.is_some()) - + usize::from(root_event_id.is_some()) - + usize::from(prev_event_id.is_some()), - ); - push_tag(&mut tags, "p", recipient_pubkey); - push_tag(&mut tags, "a", listing_addr); - if let Some(order_id) = order_id { - let order_id = order_id.into(); - if order_id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("order_id")); - } - push_tag(&mut tags, TAG_D, order_id); - } - if let Some(listing_event) = listing_event { - if listing_event.id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("listing_event.id")); - } - let mut tag = vec![TAG_LISTING_EVENT.to_string(), listing_event.id.clone()]; - if let Some(relay) = &listing_event.relays { - if relay.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("listing_event.relays")); - } - tag.push(relay.clone()); - } - tags.push(tag); - } - if let Some(root_event_id) = root_event_id { - if root_event_id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("root_event_id")); - } - push_tag(&mut tags, TAG_E_ROOT, root_event_id); - } - if let Some(prev_event_id) = prev_event_id { - if prev_event_id.trim().is_empty() { - return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); - } - push_tag(&mut tags, TAG_E_PREV, prev_event_id); - } - Ok(tags) -} - -#[inline] -pub fn parse_trade_listing_counterparty_tag( - tags: &[Vec<String>], -) -> Result<String, EventParseError> { - let tag = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some("p")) - .ok_or(EventParseError::MissingTag("p"))?; - let value = tag.get(1).ok_or(EventParseError::InvalidTag("p"))?; - if value.trim().is_empty() { - return Err(EventParseError::InvalidTag("p")); - } - Ok(value.clone()) -} - -#[inline] -pub fn parse_trade_listing_event_tag( - tags: &[Vec<String>], -) -> Result<Option<RadrootsNostrEventPtr>, EventParseError> { - let Some(tag) = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_LISTING_EVENT)) - else { - return Ok(None); - }; - let id = tag - .get(1) - .ok_or(EventParseError::InvalidTag(TAG_LISTING_EVENT))?; - if id.trim().is_empty() { - return Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)); - } - let relays = match tag.get(2) { - Some(value) if value.trim().is_empty() => { - return Err(EventParseError::InvalidTag(TAG_LISTING_EVENT)); - } - Some(value) => Some(value.clone()), - None => None, - }; - Ok(Some(RadrootsNostrEventPtr { - id: id.clone(), - relays, - })) -} - -#[inline] -pub fn parse_trade_listing_root_tag( - tags: &[Vec<String>], -) -> Result<Option<String>, EventParseError> { - let Some(tag) = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_ROOT)) - else { - return Ok(None); - }; - let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_E_ROOT))?; - if value.trim().is_empty() { - return Err(EventParseError::InvalidTag(TAG_E_ROOT)); - } - Ok(Some(value.clone())) -} - -#[inline] -pub fn parse_trade_listing_prev_tag( - tags: &[Vec<String>], -) -> Result<Option<String>, EventParseError> { - let Some(tag) = tags - .iter() - .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_PREV)) - else { - return Ok(None); - }; - let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_E_PREV))?; - if value.trim().is_empty() { - return Err(EventParseError::InvalidTag(TAG_E_PREV)); - } - Ok(Some(value.clone())) -} - -#[inline] -pub fn push_trade_listing_chain_tags( - tags: &mut Vec<Vec<String>>, - e_root_id: impl Into<String>, - e_prev_id: Option<impl Into<String>>, - trade_id: Option<impl Into<String>>, -) { - let mut reserve = 1; - if e_prev_id.is_some() { - reserve += 1; - } - if trade_id.is_some() { - reserve += 1; - } - tags.reserve(reserve); - - push_tag(tags, TAG_E_ROOT, e_root_id); - if let Some(prev) = e_prev_id { - push_tag(tags, TAG_E_PREV, prev); - } - if let Some(d) = trade_id { - push_tag(tags, TAG_D, d); - } -} - -#[inline] -pub fn validate_trade_listing_chain(tags: &[Vec<String>]) -> Result<(), JobParseError> { - let mut has_root = false; - let mut has_d = false; - - for tag in tags { - match tag.as_slice() { - [key, value, ..] if key == TAG_E_ROOT => { - if value.trim().is_empty() { - return Err(JobParseError::InvalidTag(TAG_E_ROOT)); - } - has_root = true; - } - [key] if key == TAG_E_ROOT => { - return Err(JobParseError::InvalidTag(TAG_E_ROOT)); - } - [key, value, ..] if key == TAG_D => { - if value.trim().is_empty() { - return Err(JobParseError::InvalidTag(TAG_D)); - } - has_d = true; - } - [key] if key == TAG_D => { - return Err(JobParseError::InvalidTag(TAG_D)); - } - _ => {} - } - } - - if !has_root { - Err(JobParseError::MissingChainTag(TAG_E_ROOT)) - } else if !has_d { - Err(JobParseError::MissingChainTag(TAG_D)) - } else { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::{ - TAG_LISTING_EVENT, parse_trade_listing_counterparty_tag, parse_trade_listing_event_tag, - parse_trade_listing_prev_tag, parse_trade_listing_root_tag, push_trade_listing_chain_tags, - trade_listing_dvm_tags, validate_trade_listing_chain, - }; - use radroots_events::{ - RadrootsNostrEventPtr, - kinds::KIND_LISTING, - tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, - }; - - #[test] - fn validate_trade_listing_chain_ok() { - let tags = vec![ - vec![TAG_E_ROOT.into(), "root".into()], - vec![TAG_D.into(), "trade".into()], - ]; - assert!(validate_trade_listing_chain(&tags).is_ok()); - } - - #[test] - fn validate_trade_listing_chain_rejects_missing_root() { - let tags = vec![vec![TAG_D.into(), "trade".into()]]; - let err = validate_trade_listing_chain(&tags).unwrap_err(); - assert_eq!( - err.to_string(), - format!("missing required chain tag: {TAG_E_ROOT}") - ); - } - - #[test] - fn trade_listing_dvm_tags_builds_expected_tags() { - let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg"); - let tags = trade_listing_dvm_tags( - "pubkey", - &listing_addr, - Some("order-1"), - Some(&RadrootsNostrEventPtr { - id: "listing-snapshot".into(), - relays: None, - }), - Some("root"), - Some("prev"), - ) - .expect("trade listing tags"); - assert_eq!(tags[0], vec!["p".to_string(), "pubkey".to_string()]); - assert_eq!(tags[1], vec!["a".to_string(), listing_addr]); - assert!(tags.iter().any(|tag| tag[0] == TAG_LISTING_EVENT)); - } - - #[test] - fn trade_listing_tag_parsers_extract_context() { - let tags = vec![ - vec!["p".into(), "counterparty".into()], - vec![TAG_LISTING_EVENT.into(), "snapshot".into()], - vec![TAG_E_ROOT.into(), "root".into()], - vec![TAG_E_PREV.into(), "prev".into()], - ]; - assert_eq!( - parse_trade_listing_counterparty_tag(&tags).expect("counterparty"), - "counterparty" - ); - assert_eq!( - parse_trade_listing_event_tag(&tags).expect("snapshot"), - Some(RadrootsNostrEventPtr { - id: "snapshot".into(), - relays: None, - }) - ); - assert_eq!( - parse_trade_listing_root_tag(&tags).expect("root"), - Some("root".into()) - ); - assert_eq!( - parse_trade_listing_prev_tag(&tags).expect("prev"), - Some("prev".into()) - ); - } - - #[test] - fn push_trade_listing_chain_tags_appends_optional_fields() { - let mut tags = vec![vec![String::from("x"), String::from("seed")]]; - push_trade_listing_chain_tags( - &mut tags, - "root-id", - Some("prev-id".to_string()), - Some("trade-id".to_string()), - ); - - assert_eq!( - tags, - vec![ - vec![String::from("x"), String::from("seed")], - vec![String::from(TAG_E_ROOT), String::from("root-id")], - vec![String::from(TAG_E_PREV), String::from("prev-id")], - vec![String::from(TAG_D), String::from("trade-id")], - ] - ); - } -} diff --git a/crates/trade/src/listing/validation.rs b/crates/trade/src/listing/validation.rs @@ -13,11 +13,11 @@ use radroots_events::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingDeliveryMethod, RadrootsListingLocation, }, - trade::RadrootsTradeListingValidationError as TradeListingValidationError, + trade_validation::RadrootsTradeValidationListingError as TradeListingValidationError, }; +use radroots_events_codec::order::RadrootsOrderListingAddress as OrderListingAddress; use crate::listing::codec::listing_from_event_parts; -use crate::listing::dvm::TradeListingAddress; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] @@ -54,7 +54,7 @@ pub fn validate_listing_event( if listing.farm.pubkey != seller_pubkey { return Err(TradeListingValidationError::InvalidSeller); } - let listing_addr = TradeListingAddress { + let listing_addr = OrderListingAddress { kind: event.kind as _, seller_pubkey: seller_pubkey.clone(), listing_id: listing_id.clone(), @@ -295,7 +295,7 @@ mod tests { assert_eq!( err, TradeListingValidationError::ParseError { - error: crate::listing::codec::TradeListingParseError::MissingTag("d".to_string()) + error: crate::listing::codec::ListingParseError::MissingTag("d".to_string()) } ); } @@ -484,7 +484,7 @@ mod tests { listing_addr: "addr".into(), }, TradeListingValidationError::ParseError { - error: crate::listing::codec::TradeListingParseError::InvalidTag("d".into()), + error: crate::listing::codec::ListingParseError::InvalidTag("d".into()), }, TradeListingValidationError::InvalidSeller, TradeListingValidationError::MissingFarmProfile, diff --git a/crates/trade/src/order.rs b/crates/trade/src/order.rs @@ -8,22 +8,21 @@ use alloc::{ use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDecimal}; use radroots_events::kinds::KIND_LISTING; -use radroots_events::trade::{ - RadrootsActiveTradeFulfillmentState, RadrootsTradeBuyerReceipt, - RadrootsTradeFulfillmentUpdated, RadrootsTradeInventoryCommitment, RadrootsTradeOrderCancelled, - RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomics, - RadrootsTradeOrderItem, RadrootsTradeOrderRequested, RadrootsTradeOrderRevisionDecision, - RadrootsTradeOrderRevisionDecisionEvent, RadrootsTradeOrderRevisionProposed, - RadrootsTradePaymentMethod, RadrootsTradePaymentRecorded, RadrootsTradeSettlementDecision, - RadrootsTradeSettlementDecisionEvent, +use radroots_events::order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomics, RadrootsOrderFulfillmentState, RadrootsOrderFulfillmentUpdate, + RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPaymentMethod, + RadrootsOrderPaymentRecord as RadrootsOrderPaymentPayload, RadrootsOrderReceipt, + RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, + RadrootsOrderRevisionProposal, RadrootsOrderSettlementDecision, RadrootsOrderSettlementOutcome, }; -use radroots_events_codec::trade::RadrootsTradeListingAddress as TradeListingAddress; +use radroots_events_codec::order::RadrootsOrderListingAddress as OrderListingAddress; #[cfg(feature = "serde_json")] use sha2::{Digest, Sha256}; use thiserror::Error; #[derive(Debug, Error)] -pub enum RadrootsTradeOrderCanonicalizationError { +pub enum RadrootsOrderCanonicalizationError { #[error("{0} cannot be empty")] EmptyField(&'static str), #[error("invalid listing_addr: {0}")] @@ -45,94 +44,94 @@ pub enum RadrootsTradeOrderCanonicalizationError { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderRequestRecord { +pub struct RadrootsOrderRequestRecord { pub event_id: String, pub author_pubkey: String, - pub payload: RadrootsTradeOrderRequested, + pub payload: RadrootsOrderRequest, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderDecisionRecord { +pub struct RadrootsOrderDecisionRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradeOrderDecisionEvent, + pub payload: RadrootsOrderDecision, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderRevisionProposalRecord { +pub struct RadrootsOrderRevisionProposalRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradeOrderRevisionProposed, + pub payload: RadrootsOrderRevisionProposal, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderRevisionDecisionRecord { +pub struct RadrootsOrderRevisionDecisionRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradeOrderRevisionDecisionEvent, + pub payload: RadrootsOrderRevisionDecision, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderFulfillmentRecord { +pub struct RadrootsOrderFulfillmentRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradeFulfillmentUpdated, + pub payload: RadrootsOrderFulfillmentUpdate, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderCancellationRecord { +pub struct RadrootsOrderCancellationRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradeOrderCancelled, + pub payload: RadrootsOrderCancellation, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderReceiptRecord { +pub struct RadrootsOrderReceiptRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradeBuyerReceipt, + pub payload: RadrootsOrderReceipt, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderPaymentRecord { +pub struct RadrootsOrderPaymentEventRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradePaymentRecorded, + pub payload: RadrootsOrderPaymentPayload, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderSettlementRecord { +pub struct RadrootsOrderSettlementRecord { pub event_id: String, pub author_pubkey: String, pub counterparty_pubkey: String, pub root_event_id: String, pub prev_event_id: String, - pub payload: RadrootsTradeSettlementDecisionEvent, + pub payload: RadrootsOrderSettlementDecision, } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsActiveOrderStatus { +pub enum RadrootsOrderStatus { Missing, Requested, Accepted, @@ -144,7 +143,7 @@ pub enum RadrootsActiveOrderStatus { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsActiveOrderPaymentState { +pub enum RadrootsOrderPaymentState { NotRecorded, Recorded, Settled, @@ -153,7 +152,7 @@ pub enum RadrootsActiveOrderPaymentState { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsActiveOrderSettlementState { +pub enum RadrootsOrderSettlementState { NotRequired, Pending, Accepted, @@ -162,7 +161,7 @@ pub enum RadrootsActiveOrderSettlementState { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsActiveOrderReducerIssue { +pub enum RadrootsOrderIssue { MissingRequest, MultipleRequests { event_ids: Vec<String> }, RequestPayloadInvalid { event_id: String }, @@ -287,9 +286,9 @@ pub enum RadrootsActiveOrderReducerIssue { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderPaymentProjection { - pub state: RadrootsActiveOrderPaymentState, - pub settlement_state: RadrootsActiveOrderSettlementState, +pub struct RadrootsOrderPaymentProjection { + pub state: RadrootsOrderPaymentState, + pub settlement_state: RadrootsOrderSettlementState, pub payment_event_id: Option<String>, pub settlement_event_id: Option<String>, pub agreement_event_id: Option<String>, @@ -298,17 +297,17 @@ pub struct RadrootsActiveOrderPaymentProjection { pub economics_digest: Option<String>, pub amount: Option<RadrootsCoreDecimal>, pub currency: Option<RadrootsCoreCurrency>, - pub method: Option<RadrootsTradePaymentMethod>, + pub method: Option<RadrootsOrderPaymentMethod>, pub reference: Option<String>, pub paid_at: Option<u64>, pub reason: Option<String>, } -impl RadrootsActiveOrderPaymentProjection { +impl RadrootsOrderPaymentProjection { pub fn not_recorded() -> Self { Self { - state: RadrootsActiveOrderPaymentState::NotRecorded, - settlement_state: RadrootsActiveOrderSettlementState::NotRequired, + state: RadrootsOrderPaymentState::NotRecorded, + settlement_state: RadrootsOrderSettlementState::NotRequired, payment_event_id: None, settlement_event_id: None, agreement_event_id: None, @@ -326,39 +325,39 @@ impl RadrootsActiveOrderPaymentProjection { pub fn invalid() -> Self { let mut projection = Self::not_recorded(); - projection.state = RadrootsActiveOrderPaymentState::Invalid; - projection.settlement_state = RadrootsActiveOrderSettlementState::Invalid; + projection.state = RadrootsOrderPaymentState::Invalid; + projection.settlement_state = RadrootsOrderSettlementState::Invalid; projection } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsActiveOrderProjection { +pub struct RadrootsOrderProjection { pub order_id: String, - pub status: RadrootsActiveOrderStatus, + pub status: RadrootsOrderStatus, pub request_event_id: Option<String>, pub decision_event_id: Option<String>, pub fulfillment_event_id: Option<String>, - pub fulfillment_status: Option<RadrootsActiveTradeFulfillmentState>, + pub fulfillment_status: Option<RadrootsOrderFulfillmentState>, pub cancellation_event_id: Option<String>, pub receipt_event_id: Option<String>, pub receipt_received: Option<bool>, pub receipt_issue: Option<String>, pub receipt_received_at: Option<u64>, pub lifecycle_terminal: bool, - pub payment: RadrootsActiveOrderPaymentProjection, - pub economics: Option<RadrootsTradeOrderEconomics>, + pub payment: RadrootsOrderPaymentProjection, + pub economics: Option<RadrootsOrderEconomics>, pub agreement_event_id: Option<String>, pub listing_addr: Option<String>, pub buyer_pubkey: Option<String>, pub seller_pubkey: Option<String>, pub last_event_id: Option<String>, - pub issues: Vec<RadrootsActiveOrderReducerIssue>, + pub issues: Vec<RadrootsOrderIssue>, } #[cfg(feature = "serde_json")] #[derive(Debug, Error)] -pub enum RadrootsTradeOrderEconomicsDigestError { +pub enum RadrootsOrderEconomicsDigestError { #[error("failed to serialize order economics for digest: {0}")] Serialize(#[from] serde_json::Error), } @@ -388,7 +387,7 @@ pub struct RadrootsListingInventoryBinAccounting { #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsListingInventoryAccountingIssue { - InvalidActiveOrder { + InvalidOrder { order_id: String, event_ids: Vec<String>, }, @@ -420,7 +419,7 @@ pub struct RadrootsListingInventoryAccountingProjection { } #[cfg_attr(coverage_nightly, coverage(off))] -pub fn reduce_active_order_events<I, J, K, L, M, N, O, P, Q>( +pub fn reduce_order_events<I, J, K, L, M, N, O, P, Q>( order_id: &str, requests: I, decisions: J, @@ -431,19 +430,19 @@ pub fn reduce_active_order_events<I, J, K, L, M, N, O, P, Q>( receipts: O, payments: P, settlements: Q, -) -> RadrootsActiveOrderProjection +) -> RadrootsOrderProjection where - I: IntoIterator<Item = RadrootsActiveOrderRequestRecord>, - J: IntoIterator<Item = RadrootsActiveOrderDecisionRecord>, - K: IntoIterator<Item = RadrootsActiveOrderRevisionProposalRecord>, - L: IntoIterator<Item = RadrootsActiveOrderRevisionDecisionRecord>, - M: IntoIterator<Item = RadrootsActiveOrderFulfillmentRecord>, - N: IntoIterator<Item = RadrootsActiveOrderCancellationRecord>, - O: IntoIterator<Item = RadrootsActiveOrderReceiptRecord>, - P: IntoIterator<Item = RadrootsActiveOrderPaymentRecord>, - Q: IntoIterator<Item = RadrootsActiveOrderSettlementRecord>, + I: IntoIterator<Item = RadrootsOrderRequestRecord>, + J: IntoIterator<Item = RadrootsOrderDecisionRecord>, + K: IntoIterator<Item = RadrootsOrderRevisionProposalRecord>, + L: IntoIterator<Item = RadrootsOrderRevisionDecisionRecord>, + M: IntoIterator<Item = RadrootsOrderFulfillmentRecord>, + N: IntoIterator<Item = RadrootsOrderCancellationRecord>, + O: IntoIterator<Item = RadrootsOrderReceiptRecord>, + P: IntoIterator<Item = RadrootsOrderPaymentEventRecord>, + Q: IntoIterator<Item = RadrootsOrderSettlementRecord>, { - reduce_active_order_event_records( + reduce_order_event_records( order_id, requests.into_iter().collect(), decisions.into_iter().collect(), @@ -457,18 +456,18 @@ where ) } -fn reduce_active_order_event_records( +fn reduce_order_event_records( order_id: &str, - requests: Vec<RadrootsActiveOrderRequestRecord>, - decisions: Vec<RadrootsActiveOrderDecisionRecord>, - revision_proposals: Vec<RadrootsActiveOrderRevisionProposalRecord>, - revision_decisions: Vec<RadrootsActiveOrderRevisionDecisionRecord>, - fulfillments: Vec<RadrootsActiveOrderFulfillmentRecord>, - cancellations: Vec<RadrootsActiveOrderCancellationRecord>, - receipts: Vec<RadrootsActiveOrderReceiptRecord>, - payments: Vec<RadrootsActiveOrderPaymentRecord>, - settlements: Vec<RadrootsActiveOrderSettlementRecord>, -) -> RadrootsActiveOrderProjection { + requests: Vec<RadrootsOrderRequestRecord>, + decisions: Vec<RadrootsOrderDecisionRecord>, + revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>, + revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>, + fulfillments: Vec<RadrootsOrderFulfillmentRecord>, + cancellations: Vec<RadrootsOrderCancellationRecord>, + receipts: Vec<RadrootsOrderReceiptRecord>, + payments: Vec<RadrootsOrderPaymentEventRecord>, + settlements: Vec<RadrootsOrderSettlementRecord>, +) -> RadrootsOrderProjection { let requests = unique_request_records(requests); let decisions = unique_decision_records(decisions); let revision_proposals = unique_revision_proposal_records(revision_proposals); @@ -488,9 +487,9 @@ fn reduce_active_order_event_records( && payments.is_empty() && settlements.is_empty() { - return RadrootsActiveOrderProjection { + return RadrootsOrderProjection { order_id: order_id.to_string(), - status: RadrootsActiveOrderStatus::Missing, + status: RadrootsOrderStatus::Missing, request_event_id: None, decision_event_id: None, fulfillment_event_id: None, @@ -501,7 +500,7 @@ fn reduce_active_order_event_records( receipt_issue: None, receipt_received_at: None, lifecycle_terminal: false, - payment: RadrootsActiveOrderPaymentProjection::not_recorded(), + payment: RadrootsOrderPaymentProjection::not_recorded(), economics: None, agreement_event_id: None, listing_addr: None, @@ -515,7 +514,7 @@ fn reduce_active_order_event_records( let mut issues = Vec::new(); let mut valid_requests = Vec::new(); for request in requests { - if validate_active_request_record(order_id, &request, &mut issues) { + if validate_order_request_record(order_id, &request, &mut issues) { valid_requests.push(request); } } @@ -526,7 +525,7 @@ fn reduce_active_order_event_records( .map(|request| request.event_id.clone()) .collect::<Vec<_>>(); event_ids.sort(); - issues.push(RadrootsActiveOrderReducerIssue::MultipleRequests { event_ids }); + issues.push(RadrootsOrderIssue::MultipleRequests { event_ids }); } let Some(request) = valid_requests.first() else { @@ -541,7 +540,7 @@ fn reduce_active_order_event_records( { return invalid_projection(order_id, None, issues); } - issues.push(RadrootsActiveOrderReducerIssue::MissingRequest); + issues.push(RadrootsOrderIssue::MissingRequest); return invalid_projection(order_id, None, issues); }; @@ -551,21 +550,21 @@ fn reduce_active_order_event_records( let mut valid_decisions = Vec::new(); for decision in decisions { - if validate_active_decision_record(request, &decision, &mut issues) { + if validate_order_decision_record(request, &decision, &mut issues) { valid_decisions.push(decision); } } let mut valid_revision_proposals = Vec::new(); for proposal in revision_proposals { - if validate_active_revision_proposal_record(request, &proposal, &mut issues) { + if validate_order_revision_proposal_record(request, &proposal, &mut issues) { valid_revision_proposals.push(proposal); } } let mut valid_revision_decisions = Vec::new(); for decision in revision_decisions { - if validate_active_revision_decision_record(request, &decision, &mut issues) { + if validate_order_revision_decision_record(request, &decision, &mut issues) { valid_revision_decisions.push(decision); } } @@ -576,13 +575,13 @@ fn reduce_active_order_event_records( let mut valid_cancellations = Vec::new(); for cancellation in cancellations { - if validate_active_cancellation_record(request, &cancellation, &mut issues) { + if validate_order_cancellation_record(request, &cancellation, &mut issues) { valid_cancellations.push(cancellation); } } let mut valid_receipts = Vec::new(); for receipt in receipts { - if validate_active_receipt_record(request, &receipt, &mut issues) { + if validate_order_receipt_record(request, &receipt, &mut issues) { valid_receipts.push(receipt); } } @@ -608,7 +607,7 @@ fn reduce_active_order_event_records( return invalid_projection( order_id, Some(request), - vec![RadrootsActiveOrderReducerIssue::ForkedLifecycle { event_ids }], + vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }], ); } @@ -632,7 +631,7 @@ fn reduce_active_order_event_records( order_id, Some(request), issues, - RadrootsActiveOrderPaymentProjection::invalid(), + RadrootsOrderPaymentProjection::invalid(), ) } else if valid_cancellations.is_empty() { requested_projection(order_id, request) @@ -661,7 +660,7 @@ fn reduce_active_order_event_records( invalid_projection( order_id, Some(request), - vec![RadrootsActiveOrderReducerIssue::ConflictingDecisions { event_ids }], + vec![RadrootsOrderIssue::ConflictingDecisions { event_ids }], ) } } @@ -682,13 +681,13 @@ pub fn reduce_listing_inventory_accounting<I, J, K, L, M, N, O, P>( ) -> RadrootsListingInventoryAccountingProjection where I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>, - J: IntoIterator<Item = RadrootsActiveOrderRequestRecord>, - K: IntoIterator<Item = RadrootsActiveOrderDecisionRecord>, - L: IntoIterator<Item = RadrootsActiveOrderRevisionProposalRecord>, - M: IntoIterator<Item = RadrootsActiveOrderRevisionDecisionRecord>, - N: IntoIterator<Item = RadrootsActiveOrderFulfillmentRecord>, - O: IntoIterator<Item = RadrootsActiveOrderCancellationRecord>, - P: IntoIterator<Item = RadrootsActiveOrderReceiptRecord>, + J: IntoIterator<Item = RadrootsOrderRequestRecord>, + K: IntoIterator<Item = RadrootsOrderDecisionRecord>, + L: IntoIterator<Item = RadrootsOrderRevisionProposalRecord>, + M: IntoIterator<Item = RadrootsOrderRevisionDecisionRecord>, + N: IntoIterator<Item = RadrootsOrderFulfillmentRecord>, + O: IntoIterator<Item = RadrootsOrderCancellationRecord>, + P: IntoIterator<Item = RadrootsOrderReceiptRecord>, { reduce_listing_inventory_accounting_records( listing_addr, @@ -708,13 +707,13 @@ fn reduce_listing_inventory_accounting_records( listing_addr: &str, listing_event_id: &str, bins: Vec<RadrootsListingInventoryBinAvailability>, - requests: Vec<RadrootsActiveOrderRequestRecord>, - decisions: Vec<RadrootsActiveOrderDecisionRecord>, - revision_proposals: Vec<RadrootsActiveOrderRevisionProposalRecord>, - revision_decisions: Vec<RadrootsActiveOrderRevisionDecisionRecord>, - fulfillments: Vec<RadrootsActiveOrderFulfillmentRecord>, - cancellations: Vec<RadrootsActiveOrderCancellationRecord>, - receipts: Vec<RadrootsActiveOrderReceiptRecord>, + requests: Vec<RadrootsOrderRequestRecord>, + decisions: Vec<RadrootsOrderDecisionRecord>, + revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>, + revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>, + fulfillments: Vec<RadrootsOrderFulfillmentRecord>, + cancellations: Vec<RadrootsOrderCancellationRecord>, + receipts: Vec<RadrootsOrderReceiptRecord>, ) -> RadrootsListingInventoryAccountingProjection { let (mut bins, mut issues) = normalized_listing_inventory_bins(bins); let requests = unique_request_records(requests) @@ -794,7 +793,7 @@ fn reduce_listing_inventory_accounting_records( .filter(|receipt| receipt.payload.order_id == order_id) .cloned() .collect::<Vec<_>>(); - let projection = reduce_active_order_events( + let projection = reduce_order_events( &order_id, order_requests.clone(), order_decisions.clone(), @@ -803,15 +802,15 @@ fn reduce_listing_inventory_accounting_records( order_fulfillments.clone(), order_cancellations.clone(), order_receipts.clone(), - Vec::<RadrootsActiveOrderPaymentRecord>::new(), - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderPaymentEventRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); match projection.status { - RadrootsActiveOrderStatus::Accepted - | RadrootsActiveOrderStatus::Completed - | RadrootsActiveOrderStatus::Disputed => { + RadrootsOrderStatus::Accepted + | RadrootsOrderStatus::Completed + | RadrootsOrderStatus::Disputed => { if projection.fulfillment_status - == Some(RadrootsActiveTradeFulfillmentState::SellerCancelled) + == Some(RadrootsOrderFulfillmentState::SellerCancelled) { continue; } @@ -827,9 +826,9 @@ fn reduce_listing_inventory_accounting_records( ); } } - RadrootsActiveOrderStatus::Cancelled => cancelled_order_ids.push(order_id), - RadrootsActiveOrderStatus::Declined => declined_order_ids.push(order_id), - RadrootsActiveOrderStatus::Invalid => { + RadrootsOrderStatus::Cancelled => cancelled_order_ids.push(order_id), + RadrootsOrderStatus::Declined => declined_order_ids.push(order_id), + RadrootsOrderStatus::Invalid => { let mut event_ids = projection_issue_event_ids(&projection.issues); if event_ids.is_empty() { event_ids.extend( @@ -870,14 +869,12 @@ fn reduce_listing_inventory_accounting_records( sort_and_dedup_strings(&mut event_ids); } invalid_event_ids.extend(event_ids.iter().cloned()); - issues.push( - RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { - order_id, - event_ids, - }, - ); + issues.push(RadrootsListingInventoryAccountingIssue::InvalidOrder { + order_id, + event_ids, + }); } - RadrootsActiveOrderStatus::Missing | RadrootsActiveOrderStatus::Requested => {} + RadrootsOrderStatus::Missing | RadrootsOrderStatus::Requested => {} } } @@ -897,10 +894,10 @@ fn reduce_listing_inventory_accounting_records( } } -pub fn canonicalize_active_order_request_for_signer( - mut request: RadrootsTradeOrderRequested, +pub fn canonicalize_order_request_for_signer( + mut request: RadrootsOrderRequest, signer_pubkey: &str, -) -> Result<RadrootsTradeOrderRequested, RadrootsTradeOrderCanonicalizationError> { +) -> Result<RadrootsOrderRequest, RadrootsOrderCanonicalizationError> { let order_id = normalized_required_string(core::mem::take(&mut request.order_id), "order_id")?; let listing_addr_raw = normalized_required_string(core::mem::take(&mut request.listing_addr), "listing_addr")?; @@ -912,7 +909,7 @@ pub fn canonicalize_active_order_request_for_signer( normalized_required_string(core::mem::take(&mut request.buyer_pubkey), "buyer_pubkey")? }; if buyer_pubkey != signer_pubkey { - return Err(RadrootsTradeOrderCanonicalizationError::InvalidBuyerSigner); + return Err(RadrootsOrderCanonicalizationError::InvalidBuyerSigner); } let seller_pubkey = if request.seller_pubkey.trim().is_empty() { @@ -921,7 +918,7 @@ pub fn canonicalize_active_order_request_for_signer( normalized_required_string(core::mem::take(&mut request.seller_pubkey), "seller_pubkey")? }; if seller_pubkey != listing_addr.seller_pubkey { - return Err(RadrootsTradeOrderCanonicalizationError::InvalidSellerListing); + return Err(RadrootsOrderCanonicalizationError::InvalidSellerListing); } canonicalize_items(&mut request.items)?; @@ -933,10 +930,10 @@ pub fn canonicalize_active_order_request_for_signer( Ok(request) } -pub fn canonicalize_active_order_decision_for_signer( - mut decision_event: RadrootsTradeOrderDecisionEvent, +pub fn canonicalize_order_decision_for_signer( + mut decision_event: RadrootsOrderDecision, signer_pubkey: &str, -) -> Result<RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderCanonicalizationError> { +) -> Result<RadrootsOrderDecision, RadrootsOrderCanonicalizationError> { let order_id = normalized_required_string(core::mem::take(&mut decision_event.order_id), "order_id")?; let listing_addr_raw = normalized_required_string( @@ -954,7 +951,7 @@ pub fn canonicalize_active_order_decision_for_signer( )? }; if seller_pubkey != signer_pubkey || seller_pubkey != listing_addr.seller_pubkey { - return Err(RadrootsTradeOrderCanonicalizationError::InvalidSellerListing); + return Err(RadrootsOrderCanonicalizationError::InvalidSellerListing); } let buyer_pubkey = normalized_required_string( @@ -971,9 +968,9 @@ pub fn canonicalize_active_order_decision_for_signer( } #[cfg(feature = "serde_json")] -pub fn radroots_trade_order_economics_digest( - economics: &RadrootsTradeOrderEconomics, -) -> Result<String, RadrootsTradeOrderEconomicsDigestError> { +pub fn radroots_order_economics_digest( + economics: &RadrootsOrderEconomics, +) -> Result<String, RadrootsOrderEconomicsDigestError> { let encoded = serde_json::to_vec(economics)?; let digest = Sha256::digest(encoded); let mut value = String::from("sha256:"); @@ -982,17 +979,15 @@ pub fn radroots_trade_order_economics_digest( } fn unique_request_records( - requests: Vec<RadrootsActiveOrderRequestRecord>, -) -> Vec<RadrootsActiveOrderRequestRecord> { + requests: Vec<RadrootsOrderRequestRecord>, +) -> Vec<RadrootsOrderRequestRecord> { let mut unique = Vec::new(); let mut records = requests; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for request in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderRequestRecord| { - existing.event_id != request.event_id - }) + .all(|existing: &RadrootsOrderRequestRecord| existing.event_id != request.event_id) { unique.push(request); } @@ -1001,17 +996,15 @@ fn unique_request_records( } fn unique_decision_records( - decisions: Vec<RadrootsActiveOrderDecisionRecord>, -) -> Vec<RadrootsActiveOrderDecisionRecord> { + decisions: Vec<RadrootsOrderDecisionRecord>, +) -> Vec<RadrootsOrderDecisionRecord> { let mut unique = Vec::new(); let mut records = decisions; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for decision in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderDecisionRecord| { - existing.event_id != decision.event_id - }) + .all(|existing: &RadrootsOrderDecisionRecord| existing.event_id != decision.event_id) { unique.push(decision); } @@ -1020,15 +1013,15 @@ fn unique_decision_records( } fn unique_revision_proposal_records( - revision_proposals: Vec<RadrootsActiveOrderRevisionProposalRecord>, -) -> Vec<RadrootsActiveOrderRevisionProposalRecord> { + revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>, +) -> Vec<RadrootsOrderRevisionProposalRecord> { let mut unique = Vec::new(); let mut records = revision_proposals; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for proposal in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderRevisionProposalRecord| { + .all(|existing: &RadrootsOrderRevisionProposalRecord| { existing.event_id != proposal.event_id }) { @@ -1039,15 +1032,15 @@ fn unique_revision_proposal_records( } fn unique_revision_decision_records( - revision_decisions: Vec<RadrootsActiveOrderRevisionDecisionRecord>, -) -> Vec<RadrootsActiveOrderRevisionDecisionRecord> { + revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>, +) -> Vec<RadrootsOrderRevisionDecisionRecord> { let mut unique = Vec::new(); let mut records = revision_decisions; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for decision in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderRevisionDecisionRecord| { + .all(|existing: &RadrootsOrderRevisionDecisionRecord| { existing.event_id != decision.event_id }) { @@ -1058,15 +1051,15 @@ fn unique_revision_decision_records( } fn unique_fulfillment_records( - fulfillments: Vec<RadrootsActiveOrderFulfillmentRecord>, -) -> Vec<RadrootsActiveOrderFulfillmentRecord> { + fulfillments: Vec<RadrootsOrderFulfillmentRecord>, +) -> Vec<RadrootsOrderFulfillmentRecord> { let mut unique = Vec::new(); let mut records = fulfillments; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for fulfillment in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderFulfillmentRecord| { + .all(|existing: &RadrootsOrderFulfillmentRecord| { existing.event_id != fulfillment.event_id }) { @@ -1077,15 +1070,15 @@ fn unique_fulfillment_records( } fn unique_cancellation_records( - cancellations: Vec<RadrootsActiveOrderCancellationRecord>, -) -> Vec<RadrootsActiveOrderCancellationRecord> { + cancellations: Vec<RadrootsOrderCancellationRecord>, +) -> Vec<RadrootsOrderCancellationRecord> { let mut unique = Vec::new(); let mut records = cancellations; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for cancellation in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderCancellationRecord| { + .all(|existing: &RadrootsOrderCancellationRecord| { existing.event_id != cancellation.event_id }) { @@ -1096,17 +1089,15 @@ fn unique_cancellation_records( } fn unique_receipt_records( - receipts: Vec<RadrootsActiveOrderReceiptRecord>, -) -> Vec<RadrootsActiveOrderReceiptRecord> { + receipts: Vec<RadrootsOrderReceiptRecord>, +) -> Vec<RadrootsOrderReceiptRecord> { let mut unique = Vec::new(); let mut records = receipts; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for receipt in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderReceiptRecord| { - existing.event_id != receipt.event_id - }) + .all(|existing: &RadrootsOrderReceiptRecord| existing.event_id != receipt.event_id) { unique.push(receipt); } @@ -1115,17 +1106,15 @@ fn unique_receipt_records( } fn unique_payment_records( - payments: Vec<RadrootsActiveOrderPaymentRecord>, -) -> Vec<RadrootsActiveOrderPaymentRecord> { + payments: Vec<RadrootsOrderPaymentEventRecord>, +) -> Vec<RadrootsOrderPaymentEventRecord> { let mut unique = Vec::new(); let mut records = payments; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for payment in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderPaymentRecord| { - existing.event_id != payment.event_id - }) + .all(|existing: &RadrootsOrderPaymentEventRecord| existing.event_id != payment.event_id) { unique.push(payment); } @@ -1134,15 +1123,15 @@ fn unique_payment_records( } fn unique_settlement_records( - settlements: Vec<RadrootsActiveOrderSettlementRecord>, -) -> Vec<RadrootsActiveOrderSettlementRecord> { + settlements: Vec<RadrootsOrderSettlementRecord>, +) -> Vec<RadrootsOrderSettlementRecord> { let mut unique = Vec::new(); let mut records = settlements; records.sort_by(|left, right| left.event_id.cmp(&right.event_id)); for settlement in records { if unique .iter() - .all(|existing: &RadrootsActiveOrderSettlementRecord| { + .all(|existing: &RadrootsOrderSettlementRecord| { existing.event_id != settlement.event_id }) { @@ -1201,13 +1190,13 @@ where } fn listing_order_ids( - requests: &[RadrootsActiveOrderRequestRecord], - decisions: &[RadrootsActiveOrderDecisionRecord], - revision_proposals: &[RadrootsActiveOrderRevisionProposalRecord], - revision_decisions: &[RadrootsActiveOrderRevisionDecisionRecord], - fulfillments: &[RadrootsActiveOrderFulfillmentRecord], - cancellations: &[RadrootsActiveOrderCancellationRecord], - receipts: &[RadrootsActiveOrderReceiptRecord], + requests: &[RadrootsOrderRequestRecord], + decisions: &[RadrootsOrderDecisionRecord], + revision_proposals: &[RadrootsOrderRevisionProposalRecord], + revision_decisions: &[RadrootsOrderRevisionDecisionRecord], + fulfillments: &[RadrootsOrderFulfillmentRecord], + cancellations: &[RadrootsOrderCancellationRecord], + receipts: &[RadrootsOrderReceiptRecord], ) -> Vec<String> { let mut order_ids = Vec::new(); order_ids.extend( @@ -1253,7 +1242,7 @@ fn add_accepted_inventory_reservations_from_economics( bins: &mut [RadrootsListingInventoryBinAccounting], order_id: &str, agreement_event_id: &str, - economics: &RadrootsTradeOrderEconomics, + economics: &RadrootsOrderEconomics, issues: &mut Vec<RadrootsListingInventoryAccountingIssue>, ) { for item in &economics.items { @@ -1280,7 +1269,7 @@ fn add_accepted_inventory_reservations_from_economics( fn add_inventory_reservation( bin: &mut RadrootsListingInventoryBinAccounting, order_id: &str, - decision: &RadrootsActiveOrderDecisionRecord, + decision: &RadrootsOrderDecisionRecord, bin_count: u64, issues: &mut Vec<RadrootsListingInventoryAccountingIssue>, ) { @@ -1344,137 +1333,135 @@ fn finish_inventory_accounting_bins( bins.sort_by(|left, right| left.bin_id.cmp(&right.bin_id)); } -fn projection_issue_event_ids(issues: &[RadrootsActiveOrderReducerIssue]) -> Vec<String> { +fn projection_issue_event_ids(issues: &[RadrootsOrderIssue]) -> Vec<String> { let mut event_ids = Vec::new(); for issue in issues { match issue { - RadrootsActiveOrderReducerIssue::MissingRequest => {} - RadrootsActiveOrderReducerIssue::MultipleRequests { event_ids: ids } - | RadrootsActiveOrderReducerIssue::ConflictingDecisions { event_ids: ids } - | RadrootsActiveOrderReducerIssue::DuplicatePayments { event_ids: ids } - | RadrootsActiveOrderReducerIssue::DuplicateSettlements { event_ids: ids } - | RadrootsActiveOrderReducerIssue::ForkedLifecycle { event_ids: ids } => { + RadrootsOrderIssue::MissingRequest => {} + RadrootsOrderIssue::MultipleRequests { event_ids: ids } + | RadrootsOrderIssue::ConflictingDecisions { event_ids: ids } + | RadrootsOrderIssue::DuplicatePayments { event_ids: ids } + | RadrootsOrderIssue::DuplicateSettlements { event_ids: ids } + | RadrootsOrderIssue::ForkedLifecycle { event_ids: ids } => { event_ids.extend(ids.iter().cloned()); } - RadrootsActiveOrderReducerIssue::RequestPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::RequestOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RequestAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RequestListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::RequestSellerListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::DecisionOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::DecisionListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionMissingInventoryCommitments { event_id } - | RadrootsActiveOrderReducerIssue::DecisionInventoryCommitmentMismatch { event_id } - | RadrootsActiveOrderReducerIssue::DecisionMissingReason { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalWithoutAcceptedDecision { - event_id, - } - | RadrootsActiveOrderReducerIssue::RevisionProposalPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionProposalPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionWithoutProposal { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::RevisionDecisionRevisionIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentWithoutAcceptedDecision { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentStatusNotPublishable { event_id } - | RadrootsActiveOrderReducerIssue::FulfillmentUnsupportedTransition { event_id } - | RadrootsActiveOrderReducerIssue::CancellationWithoutCancellableOrder { event_id } - | RadrootsActiveOrderReducerIssue::CancellationPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::CancellationOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::CancellationListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::CancellationAfterFulfillment { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptWithoutEligibleFulfillment { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::ReceiptPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentWithoutAcceptedAgreement { event_id } - | RadrootsActiveOrderReducerIssue::PaymentPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::PaymentOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::PaymentListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentAgreementMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentQuoteMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentQuoteVersionMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentEconomicsDigestMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentAmountMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentCurrencyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::PaymentAfterCancellation { event_id } - | RadrootsActiveOrderReducerIssue::RevisionAfterPayment { event_id } - | RadrootsActiveOrderReducerIssue::SettlementWithoutValidPayment { event_id } - | RadrootsActiveOrderReducerIssue::SettlementPayloadInvalid { event_id } - | RadrootsActiveOrderReducerIssue::SettlementOrderIdMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementAuthorMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementCounterpartyMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementBuyerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementSellerMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementListingAddressInvalid { event_id } - | RadrootsActiveOrderReducerIssue::SettlementListingMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementRootMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementPreviousMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementPaymentEventMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementAgreementMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementQuoteMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementQuoteVersionMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementEconomicsDigestMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementAmountMismatch { event_id } - | RadrootsActiveOrderReducerIssue::SettlementCurrencyMismatch { event_id } => { + RadrootsOrderIssue::RequestPayloadInvalid { event_id } + | RadrootsOrderIssue::RequestOrderIdMismatch { event_id } + | RadrootsOrderIssue::RequestAuthorMismatch { event_id } + | RadrootsOrderIssue::RequestListingAddressInvalid { event_id } + | RadrootsOrderIssue::RequestSellerListingMismatch { event_id } + | RadrootsOrderIssue::DecisionPayloadInvalid { event_id } + | RadrootsOrderIssue::DecisionOrderIdMismatch { event_id } + | RadrootsOrderIssue::DecisionAuthorMismatch { event_id } + | RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id } + | RadrootsOrderIssue::DecisionBuyerMismatch { event_id } + | RadrootsOrderIssue::DecisionSellerMismatch { event_id } + | RadrootsOrderIssue::DecisionListingAddressInvalid { event_id } + | RadrootsOrderIssue::DecisionListingMismatch { event_id } + | RadrootsOrderIssue::DecisionRootMismatch { event_id } + | RadrootsOrderIssue::DecisionPreviousMismatch { event_id } + | RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id } + | RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id } + | RadrootsOrderIssue::DecisionMissingReason { event_id } + | RadrootsOrderIssue::RevisionProposalWithoutAcceptedDecision { event_id } + | RadrootsOrderIssue::RevisionProposalPayloadInvalid { event_id } + | RadrootsOrderIssue::RevisionProposalOrderIdMismatch { event_id } + | RadrootsOrderIssue::RevisionProposalAuthorMismatch { event_id } + | RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { event_id } + | RadrootsOrderIssue::RevisionProposalBuyerMismatch { event_id } + | RadrootsOrderIssue::RevisionProposalSellerMismatch { event_id } + | RadrootsOrderIssue::RevisionProposalListingAddressInvalid { event_id } + | RadrootsOrderIssue::RevisionProposalListingMismatch { event_id } + | RadrootsOrderIssue::RevisionProposalRootMismatch { event_id } + | RadrootsOrderIssue::RevisionProposalPreviousMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id } + | RadrootsOrderIssue::RevisionDecisionPayloadInvalid { event_id } + | RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionBuyerMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionSellerMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { event_id } + | RadrootsOrderIssue::RevisionDecisionListingMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionRootMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionPreviousMismatch { event_id } + | RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { event_id } + | RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision { event_id } + | RadrootsOrderIssue::FulfillmentPayloadInvalid { event_id } + | RadrootsOrderIssue::FulfillmentOrderIdMismatch { event_id } + | RadrootsOrderIssue::FulfillmentAuthorMismatch { event_id } + | RadrootsOrderIssue::FulfillmentCounterpartyMismatch { event_id } + | RadrootsOrderIssue::FulfillmentBuyerMismatch { event_id } + | RadrootsOrderIssue::FulfillmentSellerMismatch { event_id } + | RadrootsOrderIssue::FulfillmentListingAddressInvalid { event_id } + | RadrootsOrderIssue::FulfillmentListingMismatch { event_id } + | RadrootsOrderIssue::FulfillmentRootMismatch { event_id } + | RadrootsOrderIssue::FulfillmentPreviousMismatch { event_id } + | RadrootsOrderIssue::FulfillmentStatusNotPublishable { event_id } + | RadrootsOrderIssue::FulfillmentUnsupportedTransition { event_id } + | RadrootsOrderIssue::CancellationWithoutCancellableOrder { event_id } + | RadrootsOrderIssue::CancellationPayloadInvalid { event_id } + | RadrootsOrderIssue::CancellationOrderIdMismatch { event_id } + | RadrootsOrderIssue::CancellationAuthorMismatch { event_id } + | RadrootsOrderIssue::CancellationCounterpartyMismatch { event_id } + | RadrootsOrderIssue::CancellationBuyerMismatch { event_id } + | RadrootsOrderIssue::CancellationSellerMismatch { event_id } + | RadrootsOrderIssue::CancellationListingAddressInvalid { event_id } + | RadrootsOrderIssue::CancellationListingMismatch { event_id } + | RadrootsOrderIssue::CancellationRootMismatch { event_id } + | RadrootsOrderIssue::CancellationPreviousMismatch { event_id } + | RadrootsOrderIssue::CancellationAfterFulfillment { event_id } + | RadrootsOrderIssue::ReceiptWithoutEligibleFulfillment { event_id } + | RadrootsOrderIssue::ReceiptPayloadInvalid { event_id } + | RadrootsOrderIssue::ReceiptOrderIdMismatch { event_id } + | RadrootsOrderIssue::ReceiptAuthorMismatch { event_id } + | RadrootsOrderIssue::ReceiptCounterpartyMismatch { event_id } + | RadrootsOrderIssue::ReceiptBuyerMismatch { event_id } + | RadrootsOrderIssue::ReceiptSellerMismatch { event_id } + | RadrootsOrderIssue::ReceiptListingAddressInvalid { event_id } + | RadrootsOrderIssue::ReceiptListingMismatch { event_id } + | RadrootsOrderIssue::ReceiptRootMismatch { event_id } + | RadrootsOrderIssue::ReceiptPreviousMismatch { event_id } + | RadrootsOrderIssue::PaymentWithoutAcceptedAgreement { event_id } + | RadrootsOrderIssue::PaymentPayloadInvalid { event_id } + | RadrootsOrderIssue::PaymentOrderIdMismatch { event_id } + | RadrootsOrderIssue::PaymentAuthorMismatch { event_id } + | RadrootsOrderIssue::PaymentCounterpartyMismatch { event_id } + | RadrootsOrderIssue::PaymentBuyerMismatch { event_id } + | RadrootsOrderIssue::PaymentSellerMismatch { event_id } + | RadrootsOrderIssue::PaymentListingAddressInvalid { event_id } + | RadrootsOrderIssue::PaymentListingMismatch { event_id } + | RadrootsOrderIssue::PaymentRootMismatch { event_id } + | RadrootsOrderIssue::PaymentPreviousMismatch { event_id } + | RadrootsOrderIssue::PaymentAgreementMismatch { event_id } + | RadrootsOrderIssue::PaymentQuoteMismatch { event_id } + | RadrootsOrderIssue::PaymentQuoteVersionMismatch { event_id } + | RadrootsOrderIssue::PaymentEconomicsDigestMismatch { event_id } + | RadrootsOrderIssue::PaymentAmountMismatch { event_id } + | RadrootsOrderIssue::PaymentCurrencyMismatch { event_id } + | RadrootsOrderIssue::PaymentAfterCancellation { event_id } + | RadrootsOrderIssue::RevisionAfterPayment { event_id } + | RadrootsOrderIssue::SettlementWithoutValidPayment { event_id } + | RadrootsOrderIssue::SettlementPayloadInvalid { event_id } + | RadrootsOrderIssue::SettlementOrderIdMismatch { event_id } + | RadrootsOrderIssue::SettlementAuthorMismatch { event_id } + | RadrootsOrderIssue::SettlementCounterpartyMismatch { event_id } + | RadrootsOrderIssue::SettlementBuyerMismatch { event_id } + | RadrootsOrderIssue::SettlementSellerMismatch { event_id } + | RadrootsOrderIssue::SettlementListingAddressInvalid { event_id } + | RadrootsOrderIssue::SettlementListingMismatch { event_id } + | RadrootsOrderIssue::SettlementRootMismatch { event_id } + | RadrootsOrderIssue::SettlementPreviousMismatch { event_id } + | RadrootsOrderIssue::SettlementPaymentEventMismatch { event_id } + | RadrootsOrderIssue::SettlementAgreementMismatch { event_id } + | RadrootsOrderIssue::SettlementQuoteMismatch { event_id } + | RadrootsOrderIssue::SettlementQuoteVersionMismatch { event_id } + | RadrootsOrderIssue::SettlementEconomicsDigestMismatch { event_id } + | RadrootsOrderIssue::SettlementAmountMismatch { event_id } + | RadrootsOrderIssue::SettlementCurrencyMismatch { event_id } => { event_ids.push(event_id.clone()); } - RadrootsActiveOrderReducerIssue::ForkedFulfillments { event_ids: ids } => { + RadrootsOrderIssue::ForkedFulfillments { event_ids: ids } => { event_ids.extend(ids.iter().cloned()); } } @@ -1500,7 +1487,7 @@ fn inventory_issue_sort_key( fn inventory_issue_rank(issue: &RadrootsListingInventoryAccountingIssue) -> u8 { match issue { - RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { .. } => 0, + RadrootsListingInventoryAccountingIssue::InvalidOrder { .. } => 0, RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { .. } => 1, RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { .. } => 2, RadrootsListingInventoryAccountingIssue::OverReserved { .. } => 3, @@ -1509,7 +1496,7 @@ fn inventory_issue_rank(issue: &RadrootsListingInventoryAccountingIssue) -> u8 { fn inventory_issue_id(issue: &RadrootsListingInventoryAccountingIssue) -> &str { match issue { - RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { order_id, .. } => order_id, + RadrootsListingInventoryAccountingIssue::InvalidOrder { order_id, .. } => order_id, RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { bin_id, .. } | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { bin_id, .. } | RadrootsListingInventoryAccountingIssue::OverReserved { bin_id, .. } => bin_id, @@ -1518,33 +1505,33 @@ fn inventory_issue_id(issue: &RadrootsListingInventoryAccountingIssue) -> &str { fn inventory_issue_event_ids(issue: &RadrootsListingInventoryAccountingIssue) -> &[String] { match issue { - RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { event_ids, .. } + RadrootsListingInventoryAccountingIssue::InvalidOrder { event_ids, .. } | RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { event_ids, .. } | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { event_ids, .. } | RadrootsListingInventoryAccountingIssue::OverReserved { event_ids, .. } => event_ids, } } -fn validate_active_request_record( +fn validate_order_request_record( order_id: &str, - request: &RadrootsActiveOrderRequestRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + request: &RadrootsOrderRequestRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if request.payload.validate().is_err() { - issues.push(RadrootsActiveOrderReducerIssue::RequestPayloadInvalid { + issues.push(RadrootsOrderIssue::RequestPayloadInvalid { event_id: request.event_id.clone(), }); valid = false; } if request.payload.order_id != order_id { - issues.push(RadrootsActiveOrderReducerIssue::RequestOrderIdMismatch { + issues.push(RadrootsOrderIssue::RequestOrderIdMismatch { event_id: request.event_id.clone(), }); valid = false; } if request.author_pubkey != request.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::RequestAuthorMismatch { + issues.push(RadrootsOrderIssue::RequestAuthorMismatch { event_id: request.event_id.clone(), }); valid = false; @@ -1552,69 +1539,63 @@ fn validate_active_request_record( match parse_public_listing_addr(&request.payload.listing_addr) { Ok(listing_addr) => { if listing_addr.seller_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RequestSellerListingMismatch { - event_id: request.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RequestSellerListingMismatch { + event_id: request.event_id.clone(), + }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::RequestListingAddressInvalid { - event_id: request.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RequestListingAddressInvalid { + event_id: request.event_id.clone(), + }); valid = false; } } valid } -fn validate_active_decision_record( - request: &RadrootsActiveOrderRequestRecord, - decision: &RadrootsActiveOrderDecisionRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_decision_record( + request: &RadrootsOrderRequestRecord, + decision: &RadrootsOrderDecisionRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if decision_payload_issue(&decision.payload.decision, &decision.event_id, issues) { valid = false; } if decision.payload.validate().is_err() { - issues.push(RadrootsActiveOrderReducerIssue::DecisionPayloadInvalid { + issues.push(RadrootsOrderIssue::DecisionPayloadInvalid { event_id: decision.event_id.clone(), }); valid = false; } if decision.payload.order_id != request.payload.order_id { - issues.push(RadrootsActiveOrderReducerIssue::DecisionOrderIdMismatch { + issues.push(RadrootsOrderIssue::DecisionOrderIdMismatch { event_id: decision.event_id.clone(), }); valid = false; } if decision.author_pubkey != decision.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::DecisionAuthorMismatch { + issues.push(RadrootsOrderIssue::DecisionAuthorMismatch { event_id: decision.event_id.clone(), }); valid = false; } if decision.counterparty_pubkey != request.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::DecisionCounterpartyMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::DecisionCounterpartyMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } if decision.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::DecisionBuyerMismatch { + issues.push(RadrootsOrderIssue::DecisionBuyerMismatch { event_id: decision.event_id.clone(), }); valid = false; } if decision.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::DecisionSellerMismatch { + issues.push(RadrootsOrderIssue::DecisionSellerMismatch { event_id: decision.event_id.clone(), }); valid = false; @@ -1624,101 +1605,85 @@ fn validate_active_decision_record( if decision.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != decision.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::DecisionListingMismatch { + issues.push(RadrootsOrderIssue::DecisionListingMismatch { event_id: decision.event_id.clone(), }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::DecisionListingAddressInvalid { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::DecisionListingAddressInvalid { + event_id: decision.event_id.clone(), + }); valid = false; } } if decision.root_event_id != request.event_id { - issues.push(RadrootsActiveOrderReducerIssue::DecisionRootMismatch { + issues.push(RadrootsOrderIssue::DecisionRootMismatch { event_id: decision.event_id.clone(), }); valid = false; } if decision.prev_event_id != request.event_id { - issues.push(RadrootsActiveOrderReducerIssue::DecisionPreviousMismatch { + issues.push(RadrootsOrderIssue::DecisionPreviousMismatch { event_id: decision.event_id.clone(), }); valid = false; } - if let RadrootsTradeOrderDecision::Accepted { + if let RadrootsOrderDecisionOutcome::Accepted { inventory_commitments, } = &decision.payload.decision && decision.payload.validate().is_ok() && !inventory_commitments_match_request(&request.payload.items, inventory_commitments) { - issues.push( - RadrootsActiveOrderReducerIssue::DecisionInventoryCommitmentMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } valid } -fn validate_active_revision_proposal_record( - request: &RadrootsActiveOrderRequestRecord, - proposal: &RadrootsActiveOrderRevisionProposalRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_revision_proposal_record( + request: &RadrootsOrderRequestRecord, + proposal: &RadrootsOrderRevisionProposalRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if proposal.payload.validate().is_err() { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalPayloadInvalid { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalPayloadInvalid { + event_id: proposal.event_id.clone(), + }); valid = false; } if proposal.payload.order_id != request.payload.order_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalOrderIdMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalOrderIdMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } if proposal.author_pubkey != proposal.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalAuthorMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalAuthorMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } if proposal.counterparty_pubkey != request.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalCounterpartyMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } if proposal.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalBuyerMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalBuyerMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } if proposal.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalSellerMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalSellerMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } match parse_public_listing_addr(&proposal.payload.listing_addr) { @@ -1726,99 +1691,79 @@ fn validate_active_revision_proposal_record( if proposal.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != proposal.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalListingMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalListingMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalListingAddressInvalid { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalListingAddressInvalid { + event_id: proposal.event_id.clone(), + }); valid = false; } } if proposal.root_event_id != request.event_id || proposal.payload.root_event_id != request.event_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalRootMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalRootMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } if proposal.prev_event_id.trim().is_empty() || proposal.prev_event_id == proposal.event_id || proposal.payload.prev_event_id != proposal.prev_event_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalPreviousMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalPreviousMismatch { + event_id: proposal.event_id.clone(), + }); valid = false; } valid } -fn validate_active_revision_decision_record( - request: &RadrootsActiveOrderRequestRecord, - decision: &RadrootsActiveOrderRevisionDecisionRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_revision_decision_record( + request: &RadrootsOrderRequestRecord, + decision: &RadrootsOrderRevisionDecisionRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if decision.payload.validate().is_err() { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionPayloadInvalid { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionPayloadInvalid { + event_id: decision.event_id.clone(), + }); valid = false; } if decision.payload.order_id != request.payload.order_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionOrderIdMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } if decision.author_pubkey != decision.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionAuthorMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionAuthorMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } if decision.counterparty_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionCounterpartyMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } if decision.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionBuyerMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionBuyerMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } if decision.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionSellerMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionSellerMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } match parse_public_listing_addr(&decision.payload.listing_addr) { @@ -1826,97 +1771,83 @@ fn validate_active_revision_decision_record( if decision.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != decision.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionListingMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionListingMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionListingAddressInvalid { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { + event_id: decision.event_id.clone(), + }); valid = false; } } if decision.root_event_id != request.event_id || decision.payload.root_event_id != request.event_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionRootMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionRootMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } if decision.prev_event_id.trim().is_empty() || decision.prev_event_id == decision.event_id || decision.payload.prev_event_id != decision.prev_event_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionPreviousMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionPreviousMismatch { + event_id: decision.event_id.clone(), + }); valid = false; } valid } -fn validate_active_fulfillment_record( - request: &RadrootsActiveOrderRequestRecord, - fulfillment: &RadrootsActiveOrderFulfillmentRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_fulfillment_record( + request: &RadrootsOrderRequestRecord, + fulfillment: &RadrootsOrderFulfillmentRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if !fulfillment.payload.status.is_publishable_update() { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentStatusNotPublishable { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentStatusNotPublishable { + event_id: fulfillment.event_id.clone(), + }); valid = false; } if fulfillment.payload.validate().is_err() { - issues.push(RadrootsActiveOrderReducerIssue::FulfillmentPayloadInvalid { + issues.push(RadrootsOrderIssue::FulfillmentPayloadInvalid { event_id: fulfillment.event_id.clone(), }); valid = false; } if fulfillment.payload.order_id != request.payload.order_id { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentOrderIdMismatch { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentOrderIdMismatch { + event_id: fulfillment.event_id.clone(), + }); valid = false; } if fulfillment.author_pubkey != fulfillment.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::FulfillmentAuthorMismatch { + issues.push(RadrootsOrderIssue::FulfillmentAuthorMismatch { event_id: fulfillment.event_id.clone(), }); valid = false; } if fulfillment.counterparty_pubkey != request.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentCounterpartyMismatch { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentCounterpartyMismatch { + event_id: fulfillment.event_id.clone(), + }); valid = false; } if fulfillment.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::FulfillmentBuyerMismatch { + issues.push(RadrootsOrderIssue::FulfillmentBuyerMismatch { event_id: fulfillment.event_id.clone(), }); valid = false; } if fulfillment.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::FulfillmentSellerMismatch { + issues.push(RadrootsOrderIssue::FulfillmentSellerMismatch { event_id: fulfillment.event_id.clone(), }); valid = false; @@ -1926,25 +1857,21 @@ fn validate_active_fulfillment_record( if fulfillment.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != fulfillment.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentListingMismatch { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentListingMismatch { + event_id: fulfillment.event_id.clone(), + }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentListingAddressInvalid { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentListingAddressInvalid { + event_id: fulfillment.event_id.clone(), + }); valid = false; } } if fulfillment.root_event_id != request.event_id { - issues.push(RadrootsActiveOrderReducerIssue::FulfillmentRootMismatch { + issues.push(RadrootsOrderIssue::FulfillmentRootMismatch { event_id: fulfillment.event_id.clone(), }); valid = false; @@ -1952,66 +1879,54 @@ fn validate_active_fulfillment_record( if fulfillment.prev_event_id.trim().is_empty() || fulfillment.prev_event_id == fulfillment.event_id { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentPreviousMismatch { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentPreviousMismatch { + event_id: fulfillment.event_id.clone(), + }); valid = false; } valid } -fn validate_active_cancellation_record( - request: &RadrootsActiveOrderRequestRecord, - cancellation: &RadrootsActiveOrderCancellationRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_cancellation_record( + request: &RadrootsOrderRequestRecord, + cancellation: &RadrootsOrderCancellationRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if cancellation.payload.validate().is_err() { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationPayloadInvalid { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationPayloadInvalid { + event_id: cancellation.event_id.clone(), + }); valid = false; } if cancellation.payload.order_id != request.payload.order_id { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationOrderIdMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationOrderIdMismatch { + event_id: cancellation.event_id.clone(), + }); valid = false; } if cancellation.author_pubkey != cancellation.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationAuthorMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationAuthorMismatch { + event_id: cancellation.event_id.clone(), + }); valid = false; } if cancellation.counterparty_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationCounterpartyMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationCounterpartyMismatch { + event_id: cancellation.event_id.clone(), + }); valid = false; } if cancellation.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::CancellationBuyerMismatch { + issues.push(RadrootsOrderIssue::CancellationBuyerMismatch { event_id: cancellation.event_id.clone(), }); valid = false; } if cancellation.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationSellerMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationSellerMismatch { + event_id: cancellation.event_id.clone(), + }); valid = false; } match parse_public_listing_addr(&cancellation.payload.listing_addr) { @@ -2019,25 +1934,21 @@ fn validate_active_cancellation_record( if cancellation.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != cancellation.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationListingMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationListingMismatch { + event_id: cancellation.event_id.clone(), + }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationListingAddressInvalid { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationListingAddressInvalid { + event_id: cancellation.event_id.clone(), + }); valid = false; } } if cancellation.root_event_id != request.event_id { - issues.push(RadrootsActiveOrderReducerIssue::CancellationRootMismatch { + issues.push(RadrootsOrderIssue::CancellationRootMismatch { event_id: cancellation.event_id.clone(), }); valid = false; @@ -2045,56 +1956,52 @@ fn validate_active_cancellation_record( if cancellation.prev_event_id.trim().is_empty() || cancellation.prev_event_id == cancellation.event_id { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationPreviousMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationPreviousMismatch { + event_id: cancellation.event_id.clone(), + }); valid = false; } valid } -fn validate_active_receipt_record( - request: &RadrootsActiveOrderRequestRecord, - receipt: &RadrootsActiveOrderReceiptRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_receipt_record( + request: &RadrootsOrderRequestRecord, + receipt: &RadrootsOrderReceiptRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if receipt.payload.validate().is_err() { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptPayloadInvalid { + issues.push(RadrootsOrderIssue::ReceiptPayloadInvalid { event_id: receipt.event_id.clone(), }); valid = false; } if receipt.payload.order_id != request.payload.order_id { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptOrderIdMismatch { + issues.push(RadrootsOrderIssue::ReceiptOrderIdMismatch { event_id: receipt.event_id.clone(), }); valid = false; } if receipt.author_pubkey != receipt.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptAuthorMismatch { + issues.push(RadrootsOrderIssue::ReceiptAuthorMismatch { event_id: receipt.event_id.clone(), }); valid = false; } if receipt.counterparty_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::ReceiptCounterpartyMismatch { - event_id: receipt.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::ReceiptCounterpartyMismatch { + event_id: receipt.event_id.clone(), + }); valid = false; } if receipt.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptBuyerMismatch { + issues.push(RadrootsOrderIssue::ReceiptBuyerMismatch { event_id: receipt.event_id.clone(), }); valid = false; } if receipt.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptSellerMismatch { + issues.push(RadrootsOrderIssue::ReceiptSellerMismatch { event_id: receipt.event_id.clone(), }); valid = false; @@ -2104,29 +2011,27 @@ fn validate_active_receipt_record( if receipt.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != receipt.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptListingMismatch { + issues.push(RadrootsOrderIssue::ReceiptListingMismatch { event_id: receipt.event_id.clone(), }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::ReceiptListingAddressInvalid { - event_id: receipt.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::ReceiptListingAddressInvalid { + event_id: receipt.event_id.clone(), + }); valid = false; } } if receipt.root_event_id != request.event_id { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptRootMismatch { + issues.push(RadrootsOrderIssue::ReceiptRootMismatch { event_id: receipt.event_id.clone(), }); valid = false; } if receipt.prev_event_id.trim().is_empty() || receipt.prev_event_id == receipt.event_id { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptPreviousMismatch { + issues.push(RadrootsOrderIssue::ReceiptPreviousMismatch { event_id: receipt.event_id.clone(), }); valid = false; @@ -2134,35 +2039,35 @@ fn validate_active_receipt_record( valid } -fn reduce_active_payment_settlement_records( - request: &RadrootsActiveOrderRequestRecord, +fn reduce_order_payment_settlement_records( + request: &RadrootsOrderRequestRecord, agreement_event_id: &str, - economics: &RadrootsTradeOrderEconomics, - payments: Vec<RadrootsActiveOrderPaymentRecord>, - settlements: Vec<RadrootsActiveOrderSettlementRecord>, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, -) -> RadrootsActiveOrderPaymentProjection { + economics: &RadrootsOrderEconomics, + payments: Vec<RadrootsOrderPaymentEventRecord>, + settlements: Vec<RadrootsOrderSettlementRecord>, + issues: &mut Vec<RadrootsOrderIssue>, +) -> RadrootsOrderPaymentProjection { let mut valid_payments = Vec::new(); for payment in payments { - if validate_active_payment_record(request, &payment, issues) { + if validate_order_payment_record(request, &payment, issues) { valid_payments.push(payment); } } let mut valid_settlements = Vec::new(); for settlement in settlements { - if validate_active_settlement_record(request, &settlement, issues) { + if validate_order_settlement_record(request, &settlement, issues) { valid_settlements.push(settlement); } } if !issues.is_empty() { - return RadrootsActiveOrderPaymentProjection::invalid(); + return RadrootsOrderPaymentProjection::invalid(); } if valid_payments.is_empty() { record_settlement_without_valid_payment(&valid_settlements, issues); return if issues.is_empty() { - RadrootsActiveOrderPaymentProjection::not_recorded() + RadrootsOrderPaymentProjection::not_recorded() } else { - RadrootsActiveOrderPaymentProjection::invalid() + RadrootsOrderPaymentProjection::invalid() }; } @@ -2185,7 +2090,7 @@ fn reduce_active_payment_settlement_records( .iter() .filter(|payment| !used_payment_event_ids.contains(&payment.event_id)) { - issues.push(RadrootsActiveOrderReducerIssue::PaymentPreviousMismatch { + issues.push(RadrootsOrderIssue::PaymentPreviousMismatch { event_id: payment.event_id.clone(), }); } @@ -2193,17 +2098,14 @@ fn reduce_active_payment_settlement_records( .iter() .filter(|settlement| !used_settlement_event_ids.contains(&settlement.event_id)) { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementWithoutValidPayment { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment { + event_id: settlement.event_id.clone(), + }); } return if issues.is_empty() { - rejected_projection - .unwrap_or_else(RadrootsActiveOrderPaymentProjection::not_recorded) + rejected_projection.unwrap_or_else(RadrootsOrderPaymentProjection::not_recorded) } else { - RadrootsActiveOrderPaymentProjection::invalid() + RadrootsOrderPaymentProjection::invalid() }; } if payment_candidates.len() > 1 { @@ -2212,13 +2114,13 @@ fn reduce_active_payment_settlement_records( .map(|payment| payment.event_id.clone()) .collect::<Vec<_>>(); event_ids.sort(); - issues.push(RadrootsActiveOrderReducerIssue::DuplicatePayments { event_ids }); - return RadrootsActiveOrderPaymentProjection::invalid(); + issues.push(RadrootsOrderIssue::DuplicatePayments { event_ids }); + return RadrootsOrderPaymentProjection::invalid(); } let payment = payment_candidates[0]; - validate_active_payment_agreement_record(payment, agreement_event_id, economics, issues); + validate_order_payment_agreement_record(payment, agreement_event_id, economics, issues); if !issues.is_empty() { - return RadrootsActiveOrderPaymentProjection::invalid(); + return RadrootsOrderPaymentProjection::invalid(); } used_payment_event_ids.push(payment.event_id.clone()); @@ -2236,21 +2138,19 @@ fn reduce_active_payment_settlement_records( .iter() .filter(|settlement| !used_settlement_event_ids.contains(&settlement.event_id)) { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementWithoutValidPayment { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment { + event_id: settlement.event_id.clone(), + }); } return if issues.is_empty() { payment_projection_from_record( payment, - RadrootsActiveOrderPaymentState::Recorded, - RadrootsActiveOrderSettlementState::Pending, + RadrootsOrderPaymentState::Recorded, + RadrootsOrderSettlementState::Pending, None, ) } else { - RadrootsActiveOrderPaymentProjection::invalid() + RadrootsOrderPaymentProjection::invalid() }; } if settlement_candidates.len() > 1 { @@ -2259,22 +2159,22 @@ fn reduce_active_payment_settlement_records( .map(|settlement| settlement.event_id.clone()) .collect::<Vec<_>>(); event_ids.sort(); - issues.push(RadrootsActiveOrderReducerIssue::DuplicateSettlements { event_ids }); - return RadrootsActiveOrderPaymentProjection::invalid(); + issues.push(RadrootsOrderIssue::DuplicateSettlements { event_ids }); + return RadrootsOrderPaymentProjection::invalid(); } let settlement = settlement_candidates[0]; - validate_active_settlement_payment_record(settlement, payment, issues); + validate_order_settlement_payment_record(settlement, payment, issues); if !issues.is_empty() { - return RadrootsActiveOrderPaymentProjection::invalid(); + return RadrootsOrderPaymentProjection::invalid(); } used_settlement_event_ids.push(settlement.event_id.clone()); match settlement.payload.decision { - RadrootsTradeSettlementDecision::Accepted => { + RadrootsOrderSettlementOutcome::Accepted => { for payment in valid_payments .iter() .filter(|payment| !used_payment_event_ids.contains(&payment.event_id)) { - issues.push(RadrootsActiveOrderReducerIssue::PaymentPreviousMismatch { + issues.push(RadrootsOrderIssue::PaymentPreviousMismatch { event_id: payment.event_id.clone(), }); } @@ -2282,28 +2182,26 @@ fn reduce_active_payment_settlement_records( .iter() .filter(|settlement| !used_settlement_event_ids.contains(&settlement.event_id)) { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementWithoutValidPayment { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment { + event_id: settlement.event_id.clone(), + }); } return if issues.is_empty() { payment_projection_from_record( payment, - RadrootsActiveOrderPaymentState::Settled, - RadrootsActiveOrderSettlementState::Accepted, + RadrootsOrderPaymentState::Settled, + RadrootsOrderSettlementState::Accepted, Some(settlement), ) } else { - RadrootsActiveOrderPaymentProjection::invalid() + RadrootsOrderPaymentProjection::invalid() }; } - RadrootsTradeSettlementDecision::Rejected => { + RadrootsOrderSettlementOutcome::Rejected => { rejected_projection = Some(payment_projection_from_record( payment, - RadrootsActiveOrderPaymentState::Rejected, - RadrootsActiveOrderSettlementState::Rejected, + RadrootsOrderPaymentState::Rejected, + RadrootsOrderSettlementState::Rejected, Some(settlement), )); previous_payment_parent = settlement.event_id.clone(); @@ -2313,12 +2211,12 @@ fn reduce_active_payment_settlement_records( } fn payment_projection_from_record( - payment: &RadrootsActiveOrderPaymentRecord, - state: RadrootsActiveOrderPaymentState, - settlement_state: RadrootsActiveOrderSettlementState, - settlement: Option<&RadrootsActiveOrderSettlementRecord>, -) -> RadrootsActiveOrderPaymentProjection { - RadrootsActiveOrderPaymentProjection { + payment: &RadrootsOrderPaymentEventRecord, + state: RadrootsOrderPaymentState, + settlement_state: RadrootsOrderSettlementState, + settlement: Option<&RadrootsOrderSettlementRecord>, +) -> RadrootsOrderPaymentProjection { + RadrootsOrderPaymentProjection { state, settlement_state, payment_event_id: Some(payment.event_id.clone()), @@ -2336,46 +2234,44 @@ fn payment_projection_from_record( } } -fn validate_active_payment_record( - request: &RadrootsActiveOrderRequestRecord, - payment: &RadrootsActiveOrderPaymentRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_payment_record( + request: &RadrootsOrderRequestRecord, + payment: &RadrootsOrderPaymentEventRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if payment.payload.validate().is_err() { - issues.push(RadrootsActiveOrderReducerIssue::PaymentPayloadInvalid { + issues.push(RadrootsOrderIssue::PaymentPayloadInvalid { event_id: payment.event_id.clone(), }); valid = false; } if payment.payload.order_id != request.payload.order_id { - issues.push(RadrootsActiveOrderReducerIssue::PaymentOrderIdMismatch { + issues.push(RadrootsOrderIssue::PaymentOrderIdMismatch { event_id: payment.event_id.clone(), }); valid = false; } if payment.author_pubkey != payment.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::PaymentAuthorMismatch { + issues.push(RadrootsOrderIssue::PaymentAuthorMismatch { event_id: payment.event_id.clone(), }); valid = false; } if payment.counterparty_pubkey != request.payload.seller_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::PaymentCounterpartyMismatch { - event_id: payment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::PaymentCounterpartyMismatch { + event_id: payment.event_id.clone(), + }); valid = false; } if payment.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::PaymentBuyerMismatch { + issues.push(RadrootsOrderIssue::PaymentBuyerMismatch { event_id: payment.event_id.clone(), }); valid = false; } if payment.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::PaymentSellerMismatch { + issues.push(RadrootsOrderIssue::PaymentSellerMismatch { event_id: payment.event_id.clone(), }); valid = false; @@ -2385,25 +2281,23 @@ fn validate_active_payment_record( if payment.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != payment.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::PaymentListingMismatch { + issues.push(RadrootsOrderIssue::PaymentListingMismatch { event_id: payment.event_id.clone(), }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::PaymentListingAddressInvalid { - event_id: payment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::PaymentListingAddressInvalid { + event_id: payment.event_id.clone(), + }); valid = false; } } if payment.root_event_id != request.event_id || payment.payload.root_event_id != request.event_id { - issues.push(RadrootsActiveOrderReducerIssue::PaymentRootMismatch { + issues.push(RadrootsOrderIssue::PaymentRootMismatch { event_id: payment.event_id.clone(), }); valid = false; @@ -2412,7 +2306,7 @@ fn validate_active_payment_record( || payment.prev_event_id == payment.event_id || payment.payload.previous_event_id != payment.prev_event_id { - issues.push(RadrootsActiveOrderReducerIssue::PaymentPreviousMismatch { + issues.push(RadrootsOrderIssue::PaymentPreviousMismatch { event_id: payment.event_id.clone(), }); valid = false; @@ -2420,101 +2314,93 @@ fn validate_active_payment_record( valid } -fn validate_active_payment_agreement_record( - payment: &RadrootsActiveOrderPaymentRecord, +fn validate_order_payment_agreement_record( + payment: &RadrootsOrderPaymentEventRecord, agreement_event_id: &str, - economics: &RadrootsTradeOrderEconomics, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + economics: &RadrootsOrderEconomics, + issues: &mut Vec<RadrootsOrderIssue>, ) { if payment.payload.agreement_event_id != agreement_event_id { - issues.push(RadrootsActiveOrderReducerIssue::PaymentAgreementMismatch { + issues.push(RadrootsOrderIssue::PaymentAgreementMismatch { event_id: payment.event_id.clone(), }); } if payment.payload.quote_id != economics.quote_id { - issues.push(RadrootsActiveOrderReducerIssue::PaymentQuoteMismatch { + issues.push(RadrootsOrderIssue::PaymentQuoteMismatch { event_id: payment.event_id.clone(), }); } if payment.payload.quote_version != economics.quote_version { - issues.push( - RadrootsActiveOrderReducerIssue::PaymentQuoteVersionMismatch { - event_id: payment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::PaymentQuoteVersionMismatch { + event_id: payment.event_id.clone(), + }); } if payment.payload.amount != economics.total.amount { - issues.push(RadrootsActiveOrderReducerIssue::PaymentAmountMismatch { + issues.push(RadrootsOrderIssue::PaymentAmountMismatch { event_id: payment.event_id.clone(), }); } if payment.payload.currency != economics.total.currency || payment.payload.currency != economics.currency { - issues.push(RadrootsActiveOrderReducerIssue::PaymentCurrencyMismatch { + issues.push(RadrootsOrderIssue::PaymentCurrencyMismatch { event_id: payment.event_id.clone(), }); } #[cfg(feature = "serde_json")] - match radroots_trade_order_economics_digest(economics) { + match radroots_order_economics_digest(economics) { Ok(expected_digest) if payment.payload.economics_digest != expected_digest => { - issues.push( - RadrootsActiveOrderReducerIssue::PaymentEconomicsDigestMismatch { - event_id: payment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::PaymentEconomicsDigestMismatch { + event_id: payment.event_id.clone(), + }); } Ok(_) => {} Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::PaymentEconomicsDigestMismatch { - event_id: payment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::PaymentEconomicsDigestMismatch { + event_id: payment.event_id.clone(), + }); } } } -fn validate_active_settlement_record( - request: &RadrootsActiveOrderRequestRecord, - settlement: &RadrootsActiveOrderSettlementRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_settlement_record( + request: &RadrootsOrderRequestRecord, + settlement: &RadrootsOrderSettlementRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { let mut valid = true; if settlement.payload.validate().is_err() { - issues.push(RadrootsActiveOrderReducerIssue::SettlementPayloadInvalid { + issues.push(RadrootsOrderIssue::SettlementPayloadInvalid { event_id: settlement.event_id.clone(), }); valid = false; } if settlement.payload.order_id != request.payload.order_id { - issues.push(RadrootsActiveOrderReducerIssue::SettlementOrderIdMismatch { + issues.push(RadrootsOrderIssue::SettlementOrderIdMismatch { event_id: settlement.event_id.clone(), }); valid = false; } if settlement.author_pubkey != settlement.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::SettlementAuthorMismatch { + issues.push(RadrootsOrderIssue::SettlementAuthorMismatch { event_id: settlement.event_id.clone(), }); valid = false; } if settlement.counterparty_pubkey != request.payload.buyer_pubkey { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementCounterpartyMismatch { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementCounterpartyMismatch { + event_id: settlement.event_id.clone(), + }); valid = false; } if settlement.payload.buyer_pubkey != request.payload.buyer_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::SettlementBuyerMismatch { + issues.push(RadrootsOrderIssue::SettlementBuyerMismatch { event_id: settlement.event_id.clone(), }); valid = false; } if settlement.payload.seller_pubkey != request.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::SettlementSellerMismatch { + issues.push(RadrootsOrderIssue::SettlementSellerMismatch { event_id: settlement.event_id.clone(), }); valid = false; @@ -2524,25 +2410,23 @@ fn validate_active_settlement_record( if settlement.payload.listing_addr != request.payload.listing_addr || listing_addr.seller_pubkey != settlement.payload.seller_pubkey { - issues.push(RadrootsActiveOrderReducerIssue::SettlementListingMismatch { + issues.push(RadrootsOrderIssue::SettlementListingMismatch { event_id: settlement.event_id.clone(), }); valid = false; } } Err(_) => { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementListingAddressInvalid { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementListingAddressInvalid { + event_id: settlement.event_id.clone(), + }); valid = false; } } if settlement.root_event_id != request.event_id || settlement.payload.root_event_id != request.event_id { - issues.push(RadrootsActiveOrderReducerIssue::SettlementRootMismatch { + issues.push(RadrootsOrderIssue::SettlementRootMismatch { event_id: settlement.event_id.clone(), }); valid = false; @@ -2551,91 +2435,77 @@ fn validate_active_settlement_record( || settlement.prev_event_id == settlement.event_id || settlement.payload.previous_event_id != settlement.prev_event_id { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementPreviousMismatch { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementPreviousMismatch { + event_id: settlement.event_id.clone(), + }); valid = false; } valid } -fn validate_active_settlement_payment_record( - settlement: &RadrootsActiveOrderSettlementRecord, - payment: &RadrootsActiveOrderPaymentRecord, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, +fn validate_order_settlement_payment_record( + settlement: &RadrootsOrderSettlementRecord, + payment: &RadrootsOrderPaymentEventRecord, + issues: &mut Vec<RadrootsOrderIssue>, ) { if settlement.payload.payment_event_id != payment.event_id { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementPaymentEventMismatch { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementPaymentEventMismatch { + event_id: settlement.event_id.clone(), + }); } if settlement.payload.agreement_event_id != payment.payload.agreement_event_id { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementAgreementMismatch { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementAgreementMismatch { + event_id: settlement.event_id.clone(), + }); } if settlement.payload.quote_id != payment.payload.quote_id { - issues.push(RadrootsActiveOrderReducerIssue::SettlementQuoteMismatch { + issues.push(RadrootsOrderIssue::SettlementQuoteMismatch { event_id: settlement.event_id.clone(), }); } if settlement.payload.quote_version != payment.payload.quote_version { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementQuoteVersionMismatch { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementQuoteVersionMismatch { + event_id: settlement.event_id.clone(), + }); } if settlement.payload.economics_digest != payment.payload.economics_digest { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementEconomicsDigestMismatch { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementEconomicsDigestMismatch { + event_id: settlement.event_id.clone(), + }); } if settlement.payload.amount != payment.payload.amount { - issues.push(RadrootsActiveOrderReducerIssue::SettlementAmountMismatch { + issues.push(RadrootsOrderIssue::SettlementAmountMismatch { event_id: settlement.event_id.clone(), }); } if settlement.payload.currency != payment.payload.currency { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementCurrencyMismatch { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementCurrencyMismatch { + event_id: settlement.event_id.clone(), + }); } } fn decision_payload_issue( - decision: &RadrootsTradeOrderDecision, + decision: &RadrootsOrderDecisionOutcome, event_id: &str, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + issues: &mut Vec<RadrootsOrderIssue>, ) -> bool { match decision { - RadrootsTradeOrderDecision::Accepted { + RadrootsOrderDecisionOutcome::Accepted { inventory_commitments, } => { if inventory_commitments.is_empty() { - issues.push( - RadrootsActiveOrderReducerIssue::DecisionMissingInventoryCommitments { - event_id: event_id.to_string(), - }, - ); + issues.push(RadrootsOrderIssue::DecisionMissingInventoryCommitments { + event_id: event_id.to_string(), + }); true } else { false } } - RadrootsTradeOrderDecision::Declined { reason } => { + RadrootsOrderDecisionOutcome::Declined { reason } => { if reason.trim().is_empty() { - issues.push(RadrootsActiveOrderReducerIssue::DecisionMissingReason { + issues.push(RadrootsOrderIssue::DecisionMissingReason { event_id: event_id.to_string(), }); true @@ -2647,25 +2517,23 @@ fn decision_payload_issue( } fn record_fulfillment_without_accepted_decision( - fulfillments: &[RadrootsActiveOrderFulfillmentRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + fulfillments: &[RadrootsOrderFulfillmentRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for fulfillment in fulfillments { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentWithoutAcceptedDecision { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision { + event_id: fulfillment.event_id.clone(), + }); } } fn record_revision_proposal_without_accepted_decision( - revision_proposals: &[RadrootsActiveOrderRevisionProposalRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + revision_proposals: &[RadrootsOrderRevisionProposalRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for proposal in revision_proposals { issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalWithoutAcceptedDecision { + RadrootsOrderIssue::RevisionProposalWithoutAcceptedDecision { event_id: proposal.event_id.clone(), }, ); @@ -2673,76 +2541,66 @@ fn record_revision_proposal_without_accepted_decision( } fn record_revision_decision_without_proposal( - revision_decisions: &[RadrootsActiveOrderRevisionDecisionRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + revision_decisions: &[RadrootsOrderRevisionDecisionRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for decision in revision_decisions { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionWithoutProposal { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionWithoutProposal { + event_id: decision.event_id.clone(), + }); } } fn record_cancellation_without_cancellable_order( - cancellations: &[RadrootsActiveOrderCancellationRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + cancellations: &[RadrootsOrderCancellationRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for cancellation in cancellations { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationWithoutCancellableOrder { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationWithoutCancellableOrder { + event_id: cancellation.event_id.clone(), + }); } } fn record_receipt_without_eligible_fulfillment( - receipts: &[RadrootsActiveOrderReceiptRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + receipts: &[RadrootsOrderReceiptRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for receipt in receipts { - issues.push( - RadrootsActiveOrderReducerIssue::ReceiptWithoutEligibleFulfillment { - event_id: receipt.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::ReceiptWithoutEligibleFulfillment { + event_id: receipt.event_id.clone(), + }); } } fn record_payment_without_accepted_agreement( - payments: &[RadrootsActiveOrderPaymentRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + payments: &[RadrootsOrderPaymentEventRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for payment in payments { - issues.push( - RadrootsActiveOrderReducerIssue::PaymentWithoutAcceptedAgreement { - event_id: payment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::PaymentWithoutAcceptedAgreement { + event_id: payment.event_id.clone(), + }); } } fn record_settlement_without_valid_payment( - settlements: &[RadrootsActiveOrderSettlementRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + settlements: &[RadrootsOrderSettlementRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for settlement in settlements { - issues.push( - RadrootsActiveOrderReducerIssue::SettlementWithoutValidPayment { - event_id: settlement.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::SettlementWithoutValidPayment { + event_id: settlement.event_id.clone(), + }); } } fn record_payment_after_cancellation( - payments: &[RadrootsActiveOrderPaymentRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, + payments: &[RadrootsOrderPaymentEventRecord], + issues: &mut Vec<RadrootsOrderIssue>, ) { for payment in payments { - issues.push(RadrootsActiveOrderReducerIssue::PaymentAfterCancellation { + issues.push(RadrootsOrderIssue::PaymentAfterCancellation { event_id: payment.event_id.clone(), }); } @@ -2751,7 +2609,7 @@ fn record_payment_after_cancellation( fn single_lifecycle_child<T>( records: &[T], event_id: impl Fn(&T) -> &String, -) -> Result<Option<T>, RadrootsActiveOrderReducerIssue> +) -> Result<Option<T>, RadrootsOrderIssue> where T: Clone, { @@ -2761,40 +2619,40 @@ where _ => { let mut event_ids = records.iter().map(event_id).cloned().collect::<Vec<_>>(); event_ids.sort(); - Err(RadrootsActiveOrderReducerIssue::ForkedLifecycle { event_ids }) + Err(RadrootsOrderIssue::ForkedLifecycle { event_ids }) } } } fn validated_fulfillment_records( - request: &RadrootsActiveOrderRequestRecord, - fulfillments: Vec<RadrootsActiveOrderFulfillmentRecord>, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, -) -> Vec<RadrootsActiveOrderFulfillmentRecord> { + request: &RadrootsOrderRequestRecord, + fulfillments: Vec<RadrootsOrderFulfillmentRecord>, + issues: &mut Vec<RadrootsOrderIssue>, +) -> Vec<RadrootsOrderFulfillmentRecord> { let mut valid_fulfillments = Vec::new(); for fulfillment in fulfillments { - if validate_active_fulfillment_record(request, &fulfillment, issues) { + if validate_order_fulfillment_record(request, &fulfillment, issues) { valid_fulfillments.push(fulfillment); } } valid_fulfillments } -struct RadrootsActiveRevisionState { +struct RadrootsOrderRevisionState { agreement_event_id: String, lifecycle_parent_event_id: String, - economics: RadrootsTradeOrderEconomics, + economics: RadrootsOrderEconomics, pending_revision_event_id: Option<String>, } -fn active_revision_state( - request: &RadrootsActiveOrderRequestRecord, - decision: &RadrootsActiveOrderDecisionRecord, - revision_proposals: &[RadrootsActiveOrderRevisionProposalRecord], - revision_decisions: &[RadrootsActiveOrderRevisionDecisionRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, -) -> Option<RadrootsActiveRevisionState> { - let mut state = RadrootsActiveRevisionState { +fn order_revision_state( + request: &RadrootsOrderRequestRecord, + decision: &RadrootsOrderDecisionRecord, + revision_proposals: &[RadrootsOrderRevisionProposalRecord], + revision_decisions: &[RadrootsOrderRevisionDecisionRecord], + issues: &mut Vec<RadrootsOrderIssue>, +) -> Option<RadrootsOrderRevisionState> { + let mut state = RadrootsOrderRevisionState { agreement_event_id: decision.event_id.clone(), lifecycle_parent_event_id: decision.event_id.clone(), economics: request.payload.economics.clone(), @@ -2844,31 +2702,27 @@ fn active_revision_state( } }; if revision_decision.payload.revision_id != proposal.payload.revision_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionRevisionIdMismatch { - event_id: revision_decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { + event_id: revision_decision.event_id.clone(), + }); return None; } used_decision_event_ids.push(revision_decision.event_id.clone()); match revision_decision.payload.decision { - RadrootsTradeOrderRevisionDecision::Accepted => { + RadrootsOrderRevisionOutcome::Accepted => { state.agreement_event_id = revision_decision.event_id.clone(); state.economics = proposal.payload.economics; } - RadrootsTradeOrderRevisionDecision::Declined { .. } => {} + RadrootsOrderRevisionOutcome::Declined { .. } => {} } state.lifecycle_parent_event_id = revision_decision.event_id; } for proposal in revision_proposals { if !used_proposal_event_ids.contains(&proposal.event_id) { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionProposalPreviousMismatch { - event_id: proposal.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionProposalPreviousMismatch { + event_id: proposal.event_id.clone(), + }); } } for decision in revision_decisions { @@ -2880,24 +2734,18 @@ fn active_revision_state( .find(|proposal| proposal.event_id == decision.prev_event_id) { if proposal.payload.revision_id != decision.payload.revision_id { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionRevisionIdMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { + event_id: decision.event_id.clone(), + }); } else { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionPreviousMismatch { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionPreviousMismatch { + event_id: decision.event_id.clone(), + }); } } else { - issues.push( - RadrootsActiveOrderReducerIssue::RevisionDecisionWithoutProposal { - event_id: decision.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::RevisionDecisionWithoutProposal { + event_id: decision.event_id.clone(), + }); } } @@ -2906,15 +2754,15 @@ fn active_revision_state( fn latest_fulfillment_record( parent_event_id: &str, - valid_fulfillments: &[RadrootsActiveOrderFulfillmentRecord], - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, -) -> Option<RadrootsActiveOrderFulfillmentRecord> { + valid_fulfillments: &[RadrootsOrderFulfillmentRecord], + issues: &mut Vec<RadrootsOrderIssue>, +) -> Option<RadrootsOrderFulfillmentRecord> { if !issues.is_empty() { return None; } let mut used_event_ids = Vec::new(); let mut previous_event_id = parent_event_id.to_string(); - let mut previous_status = RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled; + let mut previous_status = RadrootsOrderFulfillmentState::AcceptedNotFulfilled; let mut latest = None; loop { @@ -2935,20 +2783,18 @@ fn latest_fulfillment_record( .map(|fulfillment| fulfillment.event_id.clone()) .collect::<Vec<_>>(); event_ids.sort(); - issues.push(RadrootsActiveOrderReducerIssue::ForkedFulfillments { event_ids }); + issues.push(RadrootsOrderIssue::ForkedFulfillments { event_ids }); return None; } let child = children[0]; if matches!( previous_status, - RadrootsActiveTradeFulfillmentState::Delivered - | RadrootsActiveTradeFulfillmentState::SellerCancelled + RadrootsOrderFulfillmentState::Delivered + | RadrootsOrderFulfillmentState::SellerCancelled ) { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentUnsupportedTransition { - event_id: child.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentUnsupportedTransition { + event_id: child.event_id.clone(), + }); return None; } used_event_ids.push(child.event_id.clone()); @@ -2959,11 +2805,9 @@ fn latest_fulfillment_record( for fulfillment in valid_fulfillments { if !used_event_ids.contains(&fulfillment.event_id) { - issues.push( - RadrootsActiveOrderReducerIssue::FulfillmentPreviousMismatch { - event_id: fulfillment.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::FulfillmentPreviousMismatch { + event_id: fulfillment.event_id.clone(), + }); } } latest @@ -2971,11 +2815,11 @@ fn latest_fulfillment_record( fn requested_projection( order_id: &str, - request: &RadrootsActiveOrderRequestRecord, -) -> RadrootsActiveOrderProjection { - RadrootsActiveOrderProjection { + request: &RadrootsOrderRequestRecord, +) -> RadrootsOrderProjection { + RadrootsOrderProjection { order_id: order_id.to_string(), - status: RadrootsActiveOrderStatus::Requested, + status: RadrootsOrderStatus::Requested, request_event_id: Some(request.event_id.clone()), decision_event_id: None, fulfillment_event_id: None, @@ -2986,7 +2830,7 @@ fn requested_projection( receipt_issue: None, receipt_received_at: None, lifecycle_terminal: false, - payment: RadrootsActiveOrderPaymentProjection::not_recorded(), + payment: RadrootsOrderPaymentProjection::not_recorded(), economics: Some(request.payload.economics.clone()), agreement_event_id: None, listing_addr: Some(request.payload.listing_addr.clone()), @@ -2999,19 +2843,17 @@ fn requested_projection( fn requested_cancellation_projection( order_id: &str, - request: &RadrootsActiveOrderRequestRecord, - cancellations: Vec<RadrootsActiveOrderCancellationRecord>, -) -> RadrootsActiveOrderProjection { + request: &RadrootsOrderRequestRecord, + cancellations: Vec<RadrootsOrderCancellationRecord>, +) -> RadrootsOrderProjection { let mut issues = Vec::new(); for cancellation in cancellations .iter() .filter(|cancellation| cancellation.prev_event_id != request.event_id) { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationPreviousMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationPreviousMismatch { + event_id: cancellation.event_id.clone(), + }); } if !issues.is_empty() { return invalid_projection(order_id, Some(request), issues); @@ -3036,19 +2878,19 @@ fn requested_cancellation_projection( fn decided_projection( order_id: &str, - request: &RadrootsActiveOrderRequestRecord, - decision: &RadrootsActiveOrderDecisionRecord, - revision_proposals: Vec<RadrootsActiveOrderRevisionProposalRecord>, - revision_decisions: Vec<RadrootsActiveOrderRevisionDecisionRecord>, - fulfillments: Vec<RadrootsActiveOrderFulfillmentRecord>, - cancellations: Vec<RadrootsActiveOrderCancellationRecord>, - receipts: Vec<RadrootsActiveOrderReceiptRecord>, - payments: Vec<RadrootsActiveOrderPaymentRecord>, - settlements: Vec<RadrootsActiveOrderSettlementRecord>, -) -> RadrootsActiveOrderProjection { + request: &RadrootsOrderRequestRecord, + decision: &RadrootsOrderDecisionRecord, + revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>, + revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>, + fulfillments: Vec<RadrootsOrderFulfillmentRecord>, + cancellations: Vec<RadrootsOrderCancellationRecord>, + receipts: Vec<RadrootsOrderReceiptRecord>, + payments: Vec<RadrootsOrderPaymentEventRecord>, + settlements: Vec<RadrootsOrderSettlementRecord>, +) -> RadrootsOrderProjection { let status = match &decision.payload.decision { - RadrootsTradeOrderDecision::Accepted { .. } => RadrootsActiveOrderStatus::Accepted, - RadrootsTradeOrderDecision::Declined { .. } => RadrootsActiveOrderStatus::Declined, + RadrootsOrderDecisionOutcome::Accepted { .. } => RadrootsOrderStatus::Accepted, + RadrootsOrderDecisionOutcome::Declined { .. } => RadrootsOrderStatus::Declined, }; let mut issues = Vec::new(); let ( @@ -3059,8 +2901,8 @@ fn decided_projection( economics, payment, ) = match status { - RadrootsActiveOrderStatus::Accepted => { - let Some(revision_state) = active_revision_state( + RadrootsOrderStatus::Accepted => { + let Some(revision_state) = order_revision_state( request, decision, &revision_proposals, @@ -3099,7 +2941,7 @@ fn decided_projection( return invalid_projection( order_id, Some(request), - vec![RadrootsActiveOrderReducerIssue::ForkedLifecycle { event_ids }], + vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }], ); } let fulfillment_records = @@ -3122,11 +2964,9 @@ fn decided_projection( for cancellation in cancellations.iter().filter(|cancellation| { cancellation.prev_event_id != revision_state.lifecycle_parent_event_id }) { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationPreviousMismatch { - event_id: cancellation.event_id.clone(), - }, - ); + issues.push(RadrootsOrderIssue::CancellationPreviousMismatch { + event_id: cancellation.event_id.clone(), + }); } if !issues.is_empty() { return invalid_projection(order_id, Some(request), issues); @@ -3139,7 +2979,7 @@ fn decided_projection( order_id, Some(request), issues, - RadrootsActiveOrderPaymentProjection::invalid(), + RadrootsOrderPaymentProjection::invalid(), ); } } @@ -3156,16 +2996,14 @@ fn decided_projection( return invalid_projection( order_id, Some(request), - vec![RadrootsActiveOrderReducerIssue::ForkedLifecycle { event_ids }], + vec![RadrootsOrderIssue::ForkedLifecycle { event_ids }], ); } if latest.is_some() { for cancellation in decision_cancellations { - issues.push( - RadrootsActiveOrderReducerIssue::CancellationAfterFulfillment { - event_id: cancellation.event_id, - }, - ); + issues.push(RadrootsOrderIssue::CancellationAfterFulfillment { + event_id: cancellation.event_id, + }); } if !issues.is_empty() { return invalid_projection(order_id, Some(request), issues); @@ -3188,7 +3026,7 @@ fn decided_projection( } } } - let payment = reduce_active_payment_settlement_records( + let payment = reduce_order_payment_settlement_records( request, &revision_state.agreement_event_id, &revision_state.economics, @@ -3201,7 +3039,7 @@ fn decided_projection( order_id, Some(request), issues, - RadrootsActiveOrderPaymentProjection::invalid(), + RadrootsOrderPaymentProjection::invalid(), ); } let receipt_result = receipt_projection( @@ -3230,14 +3068,13 @@ fn decided_projection( ), None => ( None, - Some(RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled), + Some(RadrootsOrderFulfillmentState::AcceptedNotFulfilled), Some(revision_state.lifecycle_parent_event_id.clone()), ), }; let mut projection_payment = payment; - if projection_payment.state == RadrootsActiveOrderPaymentState::NotRecorded { - projection_payment.settlement_state = - RadrootsActiveOrderSettlementState::NotRequired; + if projection_payment.state == RadrootsOrderPaymentState::NotRecorded { + projection_payment.settlement_state = RadrootsOrderSettlementState::NotRequired; } ( fulfillment_event_id, @@ -3248,7 +3085,7 @@ fn decided_projection( projection_payment, ) } - RadrootsActiveOrderStatus::Declined => { + RadrootsOrderStatus::Declined => { record_revision_proposal_without_accepted_decision(&revision_proposals, &mut issues); record_revision_decision_without_proposal(&revision_decisions, &mut issues); record_payment_without_accepted_agreement(&payments, &mut issues); @@ -3266,7 +3103,7 @@ fn decided_projection( Some(decision.event_id.clone()), None, None, - RadrootsActiveOrderPaymentProjection::not_recorded(), + RadrootsOrderPaymentProjection::not_recorded(), ) } else { record_fulfillment_without_accepted_decision(&fulfillments, &mut issues); @@ -3276,7 +3113,7 @@ fn decided_projection( order_id, Some(request), issues, - RadrootsActiveOrderPaymentProjection::invalid(), + RadrootsOrderPaymentProjection::invalid(), ); } } @@ -3286,10 +3123,10 @@ fn decided_projection( Some(decision.event_id.clone()), None, None, - RadrootsActiveOrderPaymentProjection::not_recorded(), + RadrootsOrderPaymentProjection::not_recorded(), ), }; - RadrootsActiveOrderProjection { + RadrootsOrderProjection { order_id: order_id.to_string(), status, request_event_id: Some(request.event_id.clone()), @@ -3315,15 +3152,15 @@ fn decided_projection( fn receipt_projection( order_id: &str, - request: &RadrootsActiveOrderRequestRecord, - decision: &RadrootsActiveOrderDecisionRecord, + request: &RadrootsOrderRequestRecord, + decision: &RadrootsOrderDecisionRecord, agreement_event_id: &str, - economics: &RadrootsTradeOrderEconomics, - latest_fulfillment: Option<&RadrootsActiveOrderFulfillmentRecord>, - fulfillments: &[RadrootsActiveOrderFulfillmentRecord], - receipts: Vec<RadrootsActiveOrderReceiptRecord>, - issues: &mut Vec<RadrootsActiveOrderReducerIssue>, -) -> Option<RadrootsActiveOrderProjection> { + economics: &RadrootsOrderEconomics, + latest_fulfillment: Option<&RadrootsOrderFulfillmentRecord>, + fulfillments: &[RadrootsOrderFulfillmentRecord], + receipts: Vec<RadrootsOrderReceiptRecord>, + issues: &mut Vec<RadrootsOrderIssue>, +) -> Option<RadrootsOrderProjection> { if receipts.is_empty() { return None; } @@ -3333,8 +3170,7 @@ fn receipt_projection( }; if !matches!( fulfillment.payload.status, - RadrootsActiveTradeFulfillmentState::ReadyForPickup - | RadrootsActiveTradeFulfillmentState::Delivered + RadrootsOrderFulfillmentState::ReadyForPickup | RadrootsOrderFulfillmentState::Delivered ) { record_receipt_without_eligible_fulfillment(&receipts, issues); return None; @@ -3349,8 +3185,8 @@ fn receipt_projection( }; if !matches!( receipt_parent.payload.status, - RadrootsActiveTradeFulfillmentState::ReadyForPickup - | RadrootsActiveTradeFulfillmentState::Delivered + RadrootsOrderFulfillmentState::ReadyForPickup + | RadrootsOrderFulfillmentState::Delivered ) { continue; } @@ -3366,7 +3202,7 @@ fn receipt_projection( } if !fork_event_ids.is_empty() { sort_and_dedup_strings(&mut fork_event_ids); - issues.push(RadrootsActiveOrderReducerIssue::ForkedLifecycle { + issues.push(RadrootsOrderIssue::ForkedLifecycle { event_ids: fork_event_ids, }); return None; @@ -3388,7 +3224,7 @@ fn receipt_projection( )), Ok(None) => { for receipt in receipts { - issues.push(RadrootsActiveOrderReducerIssue::ReceiptPreviousMismatch { + issues.push(RadrootsOrderIssue::ReceiptPreviousMismatch { event_id: receipt.event_id, }); } @@ -3403,15 +3239,15 @@ fn receipt_projection( fn cancelled_projection( order_id: &str, - request: &RadrootsActiveOrderRequestRecord, + request: &RadrootsOrderRequestRecord, decision_event_id: Option<String>, agreement_event_id: Option<String>, - economics: RadrootsTradeOrderEconomics, - cancellation: RadrootsActiveOrderCancellationRecord, -) -> RadrootsActiveOrderProjection { - RadrootsActiveOrderProjection { + economics: RadrootsOrderEconomics, + cancellation: RadrootsOrderCancellationRecord, +) -> RadrootsOrderProjection { + RadrootsOrderProjection { order_id: order_id.to_string(), - status: RadrootsActiveOrderStatus::Cancelled, + status: RadrootsOrderStatus::Cancelled, request_event_id: Some(request.event_id.clone()), decision_event_id, fulfillment_event_id: None, @@ -3422,7 +3258,7 @@ fn cancelled_projection( receipt_issue: None, receipt_received_at: None, lifecycle_terminal: true, - payment: RadrootsActiveOrderPaymentProjection::not_recorded(), + payment: RadrootsOrderPaymentProjection::not_recorded(), economics: Some(economics), agreement_event_id, listing_addr: Some(request.payload.listing_addr.clone()), @@ -3435,19 +3271,19 @@ fn cancelled_projection( fn receipt_terminal_projection( order_id: &str, - request: &RadrootsActiveOrderRequestRecord, - decision: &RadrootsActiveOrderDecisionRecord, + request: &RadrootsOrderRequestRecord, + decision: &RadrootsOrderDecisionRecord, agreement_event_id: &str, - economics: &RadrootsTradeOrderEconomics, - fulfillment: &RadrootsActiveOrderFulfillmentRecord, - receipt: RadrootsActiveOrderReceiptRecord, -) -> RadrootsActiveOrderProjection { + economics: &RadrootsOrderEconomics, + fulfillment: &RadrootsOrderFulfillmentRecord, + receipt: RadrootsOrderReceiptRecord, +) -> RadrootsOrderProjection { let status = if receipt.payload.received { - RadrootsActiveOrderStatus::Completed + RadrootsOrderStatus::Completed } else { - RadrootsActiveOrderStatus::Disputed + RadrootsOrderStatus::Disputed }; - RadrootsActiveOrderProjection { + RadrootsOrderProjection { order_id: order_id.to_string(), status, request_event_id: Some(request.event_id.clone()), @@ -3460,7 +3296,7 @@ fn receipt_terminal_projection( receipt_issue: receipt.payload.issue.clone(), receipt_received_at: Some(receipt.payload.received_at), lifecycle_terminal: true, - payment: RadrootsActiveOrderPaymentProjection::not_recorded(), + payment: RadrootsOrderPaymentProjection::not_recorded(), economics: Some(economics.clone()), agreement_event_id: Some(agreement_event_id.to_string()), listing_addr: Some(request.payload.listing_addr.clone()), @@ -3473,32 +3309,32 @@ fn receipt_terminal_projection( fn invalid_projection( order_id: &str, - request: Option<&RadrootsActiveOrderRequestRecord>, - issues: Vec<RadrootsActiveOrderReducerIssue>, -) -> RadrootsActiveOrderProjection { + request: Option<&RadrootsOrderRequestRecord>, + issues: Vec<RadrootsOrderIssue>, +) -> RadrootsOrderProjection { invalid_projection_with_payment( order_id, request, issues, - RadrootsActiveOrderPaymentProjection::not_recorded(), + RadrootsOrderPaymentProjection::not_recorded(), ) } fn invalid_projection_with_payment( order_id: &str, - request: Option<&RadrootsActiveOrderRequestRecord>, - issues: Vec<RadrootsActiveOrderReducerIssue>, - payment: RadrootsActiveOrderPaymentProjection, -) -> RadrootsActiveOrderProjection { + request: Option<&RadrootsOrderRequestRecord>, + issues: Vec<RadrootsOrderIssue>, + payment: RadrootsOrderPaymentProjection, +) -> RadrootsOrderProjection { let economics = match request { Some(request) if request.payload.validate().is_ok() => { Some(request.payload.economics.clone()) } _ => None, }; - RadrootsActiveOrderProjection { + RadrootsOrderProjection { order_id: order_id.to_string(), - status: RadrootsActiveOrderStatus::Invalid, + status: RadrootsOrderStatus::Invalid, request_event_id: request.map(|request| request.event_id.clone()), decision_event_id: None, fulfillment_event_id: None, @@ -3522,27 +3358,27 @@ fn invalid_projection_with_payment( fn parse_public_listing_addr( listing_addr_raw: &str, -) -> Result<TradeListingAddress, RadrootsTradeOrderCanonicalizationError> { - let listing_addr = TradeListingAddress::parse(listing_addr_raw).map_err(|error| { - RadrootsTradeOrderCanonicalizationError::InvalidListingAddress(error.to_string()) +) -> Result<OrderListingAddress, RadrootsOrderCanonicalizationError> { + let listing_addr = OrderListingAddress::parse(listing_addr_raw).map_err(|error| { + RadrootsOrderCanonicalizationError::InvalidListingAddress(error.to_string()) })?; if u32::from(listing_addr.kind) != KIND_LISTING { - return Err(RadrootsTradeOrderCanonicalizationError::InvalidListingKind); + return Err(RadrootsOrderCanonicalizationError::InvalidListingKind); } Ok(listing_addr) } fn canonicalize_items( - items: &mut Vec<RadrootsTradeOrderItem>, -) -> Result<(), RadrootsTradeOrderCanonicalizationError> { + items: &mut Vec<RadrootsOrderItem>, +) -> Result<(), RadrootsOrderCanonicalizationError> { if items.is_empty() { - return Err(RadrootsTradeOrderCanonicalizationError::MissingItems); + return Err(RadrootsOrderCanonicalizationError::MissingItems); } - let mut canonical_items: Vec<RadrootsTradeOrderItem> = Vec::new(); + let mut canonical_items: Vec<RadrootsOrderItem> = Vec::new(); for (index, item) in items.iter_mut().enumerate() { item.bin_id = normalized_required_string(core::mem::take(&mut item.bin_id), "bin_id")?; if item.bin_count == 0 { - return Err(RadrootsTradeOrderCanonicalizationError::InvalidBinCount { index }); + return Err(RadrootsOrderCanonicalizationError::InvalidBinCount { index }); } if let Some(existing) = canonical_items .iter_mut() @@ -3551,9 +3387,9 @@ fn canonicalize_items( existing.bin_count = existing .bin_count .checked_add(item.bin_count) - .ok_or(RadrootsTradeOrderCanonicalizationError::InvalidBinCount { index })?; + .ok_or(RadrootsOrderCanonicalizationError::InvalidBinCount { index })?; } else { - canonical_items.push(RadrootsTradeOrderItem { + canonical_items.push(RadrootsOrderItem { bin_id: item.bin_id.clone(), bin_count: item.bin_count, }); @@ -3565,13 +3401,13 @@ fn canonicalize_items( } fn canonicalize_decision( - decision: &mut RadrootsTradeOrderDecision, -) -> Result<(), RadrootsTradeOrderCanonicalizationError> { + decision: &mut RadrootsOrderDecisionOutcome, +) -> Result<(), RadrootsOrderCanonicalizationError> { match decision { - RadrootsTradeOrderDecision::Accepted { + RadrootsOrderDecisionOutcome::Accepted { inventory_commitments, } => canonicalize_inventory_commitments(inventory_commitments), - RadrootsTradeOrderDecision::Declined { reason } => { + RadrootsOrderDecisionOutcome::Declined { reason } => { *reason = normalized_required_string(core::mem::take(reason), "reason")?; Ok(()) } @@ -3579,16 +3415,16 @@ fn canonicalize_decision( } fn canonicalize_inventory_commitments( - commitments: &mut [RadrootsTradeInventoryCommitment], -) -> Result<(), RadrootsTradeOrderCanonicalizationError> { + commitments: &mut [RadrootsOrderInventoryCommitment], +) -> Result<(), RadrootsOrderCanonicalizationError> { if commitments.is_empty() { - return Err(RadrootsTradeOrderCanonicalizationError::MissingInventoryCommitments); + return Err(RadrootsOrderCanonicalizationError::MissingInventoryCommitments); } for (index, commitment) in commitments.iter_mut().enumerate() { commitment.bin_id = normalized_required_string(commitment.bin_id.clone(), "bin_id")?; if commitment.bin_count == 0 { return Err( - RadrootsTradeOrderCanonicalizationError::InvalidInventoryCommitmentCount { index }, + RadrootsOrderCanonicalizationError::InvalidInventoryCommitmentCount { index }, ); } } @@ -3602,15 +3438,15 @@ struct NormalizedInventoryCount { } fn inventory_commitments_match_request( - request_items: &[RadrootsTradeOrderItem], - inventory_commitments: &[RadrootsTradeInventoryCommitment], + request_items: &[RadrootsOrderItem], + inventory_commitments: &[RadrootsOrderInventoryCommitment], ) -> bool { normalized_request_item_counts(request_items) == normalized_inventory_commitment_counts(inventory_commitments) } fn normalized_request_item_counts( - items: &[RadrootsTradeOrderItem], + items: &[RadrootsOrderItem], ) -> Option<Vec<NormalizedInventoryCount>> { let mut counts = Vec::new(); for item in items { @@ -3621,7 +3457,7 @@ fn normalized_request_item_counts( } fn normalized_inventory_commitment_counts( - commitments: &[RadrootsTradeInventoryCommitment], + commitments: &[RadrootsOrderInventoryCommitment], ) -> Option<Vec<NormalizedInventoryCount>> { let mut counts = Vec::new(); for commitment in commitments { @@ -3654,10 +3490,10 @@ fn push_normalized_inventory_count( fn normalized_required_string( value: String, field: &'static str, -) -> Result<String, RadrootsTradeOrderCanonicalizationError> { +) -> Result<String, RadrootsOrderCanonicalizationError> { let value = value.trim().to_string(); if value.is_empty() { - return Err(RadrootsTradeOrderCanonicalizationError::EmptyField(field)); + return Err(RadrootsOrderCanonicalizationError::EmptyField(field)); } Ok(value) } @@ -3668,47 +3504,44 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; use radroots_events::kinds::KIND_LISTING; - use radroots_events::trade::{ - RadrootsActiveTradeFulfillmentState, RadrootsTradeBuyerReceipt, - RadrootsTradeFulfillmentUpdated, RadrootsTradeInventoryCommitment, - RadrootsTradeOrderCancelled, RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, - RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomicLine, - RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, - RadrootsTradeOrderRevisionDecision, RadrootsTradeOrderRevisionDecisionEvent, - RadrootsTradeOrderRevisionProposed, RadrootsTradePaymentMethod, - RadrootsTradePaymentRecorded, RadrootsTradePricingBasis, RadrootsTradeSettlementDecision, - RadrootsTradeSettlementDecisionEvent, + use radroots_events::order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, + RadrootsOrderFulfillmentState, RadrootsOrderFulfillmentUpdate, + RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPaymentMethod, + RadrootsOrderPaymentRecord as RadrootsOrderPaymentPayload, RadrootsOrderPricingBasis, + RadrootsOrderReceipt, RadrootsOrderRequest, RadrootsOrderRevisionDecision, + RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, + RadrootsOrderSettlementDecision, RadrootsOrderSettlementOutcome, }; use super::{ - RadrootsActiveOrderCancellationRecord, RadrootsActiveOrderDecisionRecord, - RadrootsActiveOrderFulfillmentRecord, RadrootsActiveOrderPaymentProjection, - RadrootsActiveOrderPaymentRecord, RadrootsActiveOrderPaymentState, - RadrootsActiveOrderProjection, RadrootsActiveOrderReceiptRecord, - RadrootsActiveOrderReducerIssue, RadrootsActiveOrderRequestRecord, - RadrootsActiveOrderRevisionDecisionRecord, RadrootsActiveOrderRevisionProposalRecord, - RadrootsActiveOrderSettlementRecord, RadrootsActiveOrderSettlementState, - RadrootsActiveOrderStatus, RadrootsListingInventoryAccountingIssue, - RadrootsListingInventoryAccountingProjection, RadrootsListingInventoryBinAccounting, - RadrootsListingInventoryBinAvailability, RadrootsListingInventoryOrderReservation, - RadrootsTradeOrderCanonicalizationError, add_inventory_reservation, - canonicalize_active_order_decision_for_signer, - canonicalize_active_order_request_for_signer, projection_issue_event_ids, - radroots_trade_order_economics_digest, - reduce_active_order_events as reduce_active_order_events_with_revisions, + RadrootsListingInventoryAccountingIssue, RadrootsListingInventoryAccountingProjection, + RadrootsListingInventoryBinAccounting, RadrootsListingInventoryBinAvailability, + RadrootsListingInventoryOrderReservation, RadrootsOrderCancellationRecord, + RadrootsOrderCanonicalizationError, RadrootsOrderDecisionRecord, + RadrootsOrderFulfillmentRecord, RadrootsOrderIssue, RadrootsOrderPaymentEventRecord, + RadrootsOrderPaymentProjection, RadrootsOrderPaymentState, RadrootsOrderProjection, + RadrootsOrderReceiptRecord, RadrootsOrderRequestRecord, + RadrootsOrderRevisionDecisionRecord, RadrootsOrderRevisionProposalRecord, + RadrootsOrderSettlementRecord, RadrootsOrderSettlementState, RadrootsOrderStatus, + add_inventory_reservation, canonicalize_order_decision_for_signer, + canonicalize_order_request_for_signer, projection_issue_event_ids, + radroots_order_economics_digest, reduce_listing_inventory_accounting as reduce_listing_inventory_accounting_with_revisions, + reduce_order_events as reduce_order_events_with_revisions, }; const SELLER: &str = "1111111111111111111111111111111111111111111111111111111111111111"; const BUYER: &str = "2222222222222222222222222222222222222222222222222222222222222222"; - fn active_request(buyer_pubkey: &str, seller_pubkey: &str) -> RadrootsTradeOrderRequested { - RadrootsTradeOrderRequested { + fn sample_order_request(buyer_pubkey: &str, seller_pubkey: &str) -> RadrootsOrderRequest { + RadrootsOrderRequest { order_id: " order-1 ".to_string(), listing_addr: format!(" {KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg "), buyer_pubkey: buyer_pubkey.to_string(), seller_pubkey: seller_pubkey.to_string(), - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: " bin-1 ".to_string(), bin_count: 2, }], @@ -3724,17 +3557,13 @@ mod tests { RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) } - fn request_economics( - bin_id: &str, - bin_count: u32, - subtotal: &str, - ) -> RadrootsTradeOrderEconomics { - RadrootsTradeOrderEconomics { + fn request_economics(bin_id: &str, bin_count: u32, subtotal: &str) -> RadrootsOrderEconomics { + RadrootsOrderEconomics { quote_id: "quote-1".to_string(), quote_version: 1, - pricing_basis: RadrootsTradePricingBasis::ListingEvent, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsTradeOrderEconomicItem { + items: vec![RadrootsOrderEconomicItem { bin_id: bin_id.to_string(), bin_count, quantity_amount: decimal("1"), @@ -3743,8 +3572,8 @@ mod tests { unit_price_currency: RadrootsCoreCurrency::USD, line_subtotal: usd(subtotal), }], - discounts: Vec::<RadrootsTradeOrderEconomicLine>::new(), - adjustments: Vec::<RadrootsTradeOrderEconomicLine>::new(), + discounts: Vec::<RadrootsOrderEconomicLine>::new(), + adjustments: Vec::<RadrootsOrderEconomicLine>::new(), subtotal: usd(subtotal), discount_total: usd("0"), adjustment_total: usd("0"), @@ -3752,14 +3581,14 @@ mod tests { } } - fn active_decision(seller_pubkey: &str) -> RadrootsTradeOrderDecisionEvent { - RadrootsTradeOrderDecisionEvent { + fn sample_order_decision(seller_pubkey: &str) -> RadrootsOrderDecision { + RadrootsOrderDecision { order_id: " order-1 ".to_string(), listing_addr: format!(" {KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg "), buyer_pubkey: format!(" {BUYER} "), seller_pubkey: seller_pubkey.to_string(), - decision: RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: " bin-1 ".to_string(), bin_count: 2, }], @@ -3771,13 +3600,13 @@ mod tests { format!("{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") } - fn clean_request_payload() -> RadrootsTradeOrderRequested { - RadrootsTradeOrderRequested { + fn clean_request_payload() -> RadrootsOrderRequest { + RadrootsOrderRequest { order_id: "order-1".to_string(), listing_addr: listing_addr(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: "bin-1".to_string(), bin_count: 2, }], @@ -3785,15 +3614,15 @@ mod tests { } } - fn request_record_with_event_id(event_id: &str) -> RadrootsActiveOrderRequestRecord { - RadrootsActiveOrderRequestRecord { + fn request_record_with_event_id(event_id: &str) -> RadrootsOrderRequestRecord { + RadrootsOrderRequestRecord { event_id: event_id.to_string(), author_pubkey: BUYER.to_string(), payload: clean_request_payload(), } } - fn request_record() -> RadrootsActiveOrderRequestRecord { + fn request_record() -> RadrootsOrderRequestRecord { request_record_with_event_id("request-1") } @@ -3801,7 +3630,7 @@ mod tests { order_id: &str, event_id: &str, bin_count: u32, - ) -> RadrootsActiveOrderRequestRecord { + ) -> RadrootsOrderRequestRecord { let mut request = request_record_with_event_id(event_id); request.payload.order_id = order_id.to_string(); request.payload.items[0].bin_count = bin_count; @@ -3811,8 +3640,8 @@ mod tests { request } - fn decision_payload(decision: RadrootsTradeOrderDecision) -> RadrootsTradeOrderDecisionEvent { - RadrootsTradeOrderDecisionEvent { + fn decision_payload(decision: RadrootsOrderDecisionOutcome) -> RadrootsOrderDecision { + RadrootsOrderDecision { order_id: "order-1".to_string(), listing_addr: listing_addr(), buyer_pubkey: BUYER.to_string(), @@ -3821,15 +3650,15 @@ mod tests { } } - fn accepted_decision_record(event_id: &str) -> RadrootsActiveOrderDecisionRecord { - RadrootsActiveOrderDecisionRecord { + fn accepted_decision_record(event_id: &str) -> RadrootsOrderDecisionRecord { + RadrootsOrderDecisionRecord { event_id: event_id.to_string(), author_pubkey: SELLER.to_string(), counterparty_pubkey: BUYER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: "request-1".to_string(), - payload: decision_payload(RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: "bin-1".to_string(), bin_count: 2, }], @@ -3837,14 +3666,14 @@ mod tests { } } - fn declined_decision_record(event_id: &str) -> RadrootsActiveOrderDecisionRecord { - RadrootsActiveOrderDecisionRecord { + fn declined_decision_record(event_id: &str) -> RadrootsOrderDecisionRecord { + RadrootsOrderDecisionRecord { event_id: event_id.to_string(), author_pubkey: SELLER.to_string(), counterparty_pubkey: BUYER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: "request-1".to_string(), - payload: decision_payload(RadrootsTradeOrderDecision::Declined { + payload: decision_payload(RadrootsOrderDecisionOutcome::Declined { reason: "out_of_stock".to_string(), }), } @@ -3853,15 +3682,15 @@ mod tests { fn fulfillment_record( event_id: &str, prev_event_id: &str, - status: RadrootsActiveTradeFulfillmentState, - ) -> RadrootsActiveOrderFulfillmentRecord { - RadrootsActiveOrderFulfillmentRecord { + status: RadrootsOrderFulfillmentState, + ) -> RadrootsOrderFulfillmentRecord { + RadrootsOrderFulfillmentRecord { event_id: event_id.to_string(), author_pubkey: SELLER.to_string(), counterparty_pubkey: BUYER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), - payload: RadrootsTradeFulfillmentUpdated { + payload: RadrootsOrderFulfillmentUpdate { order_id: "order-1".to_string(), listing_addr: listing_addr(), buyer_pubkey: BUYER.to_string(), @@ -3871,17 +3700,14 @@ mod tests { } } - fn cancellation_record( - event_id: &str, - prev_event_id: &str, - ) -> RadrootsActiveOrderCancellationRecord { - RadrootsActiveOrderCancellationRecord { + fn cancellation_record(event_id: &str, prev_event_id: &str) -> RadrootsOrderCancellationRecord { + RadrootsOrderCancellationRecord { event_id: event_id.to_string(), author_pubkey: BUYER.to_string(), counterparty_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), - payload: RadrootsTradeOrderCancelled { + payload: RadrootsOrderCancellation { order_id: "order-1".to_string(), listing_addr: listing_addr(), buyer_pubkey: BUYER.to_string(), @@ -3895,14 +3721,14 @@ mod tests { event_id: &str, prev_event_id: &str, received: bool, - ) -> RadrootsActiveOrderReceiptRecord { - RadrootsActiveOrderReceiptRecord { + ) -> RadrootsOrderReceiptRecord { + RadrootsOrderReceiptRecord { event_id: event_id.to_string(), author_pubkey: BUYER.to_string(), counterparty_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), - payload: RadrootsTradeBuyerReceipt { + payload: RadrootsOrderReceipt { order_id: "order-1".to_string(), listing_addr: listing_addr(), buyer_pubkey: BUYER.to_string(), @@ -3914,15 +3740,15 @@ mod tests { } } - fn payment_record(event_id: &str, prev_event_id: &str) -> RadrootsActiveOrderPaymentRecord { + fn payment_record(event_id: &str, prev_event_id: &str) -> RadrootsOrderPaymentEventRecord { let economics = request_economics("bin-1", 2, "10"); - RadrootsActiveOrderPaymentRecord { + RadrootsOrderPaymentEventRecord { event_id: event_id.to_string(), author_pubkey: BUYER.to_string(), counterparty_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), - payload: RadrootsTradePaymentRecorded { + payload: RadrootsOrderPaymentPayload { order_id: "order-1".to_string(), listing_addr: listing_addr(), buyer_pubkey: BUYER.to_string(), @@ -3932,10 +3758,10 @@ mod tests { agreement_event_id: "decision-1".to_string(), quote_id: economics.quote_id.clone(), quote_version: economics.quote_version, - economics_digest: radroots_trade_order_economics_digest(&economics).unwrap(), + economics_digest: radroots_order_economics_digest(&economics).unwrap(), amount: economics.total.amount, currency: economics.total.currency, - method: RadrootsTradePaymentMethod::ManualTransfer, + method: RadrootsOrderPaymentMethod::ManualTransfer, reference: Some("manual reference".to_string()), paid_at: Some(1_777_666_000), }, @@ -3945,16 +3771,16 @@ mod tests { fn settlement_record( event_id: &str, payment_event_id: &str, - decision: RadrootsTradeSettlementDecision, - ) -> RadrootsActiveOrderSettlementRecord { + decision: RadrootsOrderSettlementOutcome, + ) -> RadrootsOrderSettlementRecord { let payment = payment_record(payment_event_id, "decision-1"); - RadrootsActiveOrderSettlementRecord { + RadrootsOrderSettlementRecord { event_id: event_id.to_string(), author_pubkey: SELLER.to_string(), counterparty_pubkey: BUYER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: payment_event_id.to_string(), - payload: RadrootsTradeSettlementDecisionEvent { + payload: RadrootsOrderSettlementDecision { order_id: payment.payload.order_id, listing_addr: payment.payload.listing_addr, seller_pubkey: payment.payload.seller_pubkey, @@ -3969,7 +3795,7 @@ mod tests { amount: payment.payload.amount, currency: payment.payload.currency, decision, - reason: (decision == RadrootsTradeSettlementDecision::Rejected) + reason: (decision == RadrootsOrderSettlementOutcome::Rejected) .then(|| "reference mismatch".to_string()), }, } @@ -3980,12 +3806,12 @@ mod tests { event_id: &str, request_event_id: &str, bin_count: u32, - ) -> RadrootsActiveOrderDecisionRecord { + ) -> RadrootsOrderDecisionRecord { let mut decision = accepted_decision_record(event_id); decision.root_event_id = request_event_id.to_string(); decision.prev_event_id = request_event_id.to_string(); decision.payload.order_id = order_id.to_string(); - let RadrootsTradeOrderDecision::Accepted { + let RadrootsOrderDecisionOutcome::Accepted { inventory_commitments, } = &mut decision.payload.decision else { @@ -4007,16 +3833,16 @@ mod tests { prev_event_id: &str, revision_id: &str, bin_count: u32, - ) -> RadrootsActiveOrderRevisionProposalRecord { + ) -> RadrootsOrderRevisionProposalRecord { let subtotal = (RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count)).to_string(); - RadrootsActiveOrderRevisionProposalRecord { + RadrootsOrderRevisionProposalRecord { event_id: event_id.to_string(), author_pubkey: SELLER.to_string(), counterparty_pubkey: BUYER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), - payload: RadrootsTradeOrderRevisionProposed { + payload: RadrootsOrderRevisionProposal { revision_id: revision_id.to_string(), order_id: "order-1".to_string(), listing_addr: listing_addr(), @@ -4024,7 +3850,7 @@ mod tests { seller_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), - items: vec![RadrootsTradeOrderItem { + items: vec![RadrootsOrderItem { bin_id: "bin-1".to_string(), bin_count, }], @@ -4038,15 +3864,15 @@ mod tests { event_id: &str, prev_event_id: &str, revision_id: &str, - decision: RadrootsTradeOrderRevisionDecision, - ) -> RadrootsActiveOrderRevisionDecisionRecord { - RadrootsActiveOrderRevisionDecisionRecord { + decision: RadrootsOrderRevisionOutcome, + ) -> RadrootsOrderRevisionDecisionRecord { + RadrootsOrderRevisionDecisionRecord { event_id: event_id.to_string(), author_pubkey: BUYER.to_string(), counterparty_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), - payload: RadrootsTradeOrderRevisionDecisionEvent { + payload: RadrootsOrderRevisionDecision { revision_id: revision_id.to_string(), order_id: "order-1".to_string(), listing_addr: listing_addr(), @@ -4059,32 +3885,32 @@ mod tests { } } - fn reduce_active_order_events<I, J, K, L, M>( + fn reduce_order_events<I, J, K, L, M>( order_id: &str, requests: I, decisions: J, fulfillments: K, cancellations: L, receipts: M, - ) -> RadrootsActiveOrderProjection + ) -> RadrootsOrderProjection where - I: IntoIterator<Item = RadrootsActiveOrderRequestRecord>, - J: IntoIterator<Item = RadrootsActiveOrderDecisionRecord>, - K: IntoIterator<Item = RadrootsActiveOrderFulfillmentRecord>, - L: IntoIterator<Item = RadrootsActiveOrderCancellationRecord>, - M: IntoIterator<Item = RadrootsActiveOrderReceiptRecord>, + I: IntoIterator<Item = RadrootsOrderRequestRecord>, + J: IntoIterator<Item = RadrootsOrderDecisionRecord>, + K: IntoIterator<Item = RadrootsOrderFulfillmentRecord>, + L: IntoIterator<Item = RadrootsOrderCancellationRecord>, + M: IntoIterator<Item = RadrootsOrderReceiptRecord>, { - reduce_active_order_events_with_revisions( + reduce_order_events_with_revisions( order_id, requests, decisions, - Vec::<RadrootsActiveOrderRevisionProposalRecord>::new(), - Vec::<RadrootsActiveOrderRevisionDecisionRecord>::new(), + Vec::<RadrootsOrderRevisionProposalRecord>::new(), + Vec::<RadrootsOrderRevisionDecisionRecord>::new(), fulfillments, cancellations, receipts, - Vec::<RadrootsActiveOrderPaymentRecord>::new(), - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderPaymentEventRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ) } @@ -4100,11 +3926,11 @@ mod tests { ) -> RadrootsListingInventoryAccountingProjection where I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>, - J: IntoIterator<Item = RadrootsActiveOrderRequestRecord>, - K: IntoIterator<Item = RadrootsActiveOrderDecisionRecord>, - L: IntoIterator<Item = RadrootsActiveOrderFulfillmentRecord>, - M: IntoIterator<Item = RadrootsActiveOrderCancellationRecord>, - N: IntoIterator<Item = RadrootsActiveOrderReceiptRecord>, + J: IntoIterator<Item = RadrootsOrderRequestRecord>, + K: IntoIterator<Item = RadrootsOrderDecisionRecord>, + L: IntoIterator<Item = RadrootsOrderFulfillmentRecord>, + M: IntoIterator<Item = RadrootsOrderCancellationRecord>, + N: IntoIterator<Item = RadrootsOrderReceiptRecord>, { reduce_listing_inventory_accounting_with_revisions( listing_addr, @@ -4112,8 +3938,8 @@ mod tests { bins, requests, decisions, - Vec::<RadrootsActiveOrderRevisionProposalRecord>::new(), - Vec::<RadrootsActiveOrderRevisionDecisionRecord>::new(), + Vec::<RadrootsOrderRevisionProposalRecord>::new(), + Vec::<RadrootsOrderRevisionDecisionRecord>::new(), fulfillments, cancellations, receipts, @@ -4121,9 +3947,9 @@ mod tests { } #[test] - fn canonicalize_active_order_request_sets_authority_and_trims_items() { + fn canonicalize_order_request_sets_authority_and_trims_items() { let request = - canonicalize_active_order_request_for_signer(active_request("", ""), BUYER).unwrap(); + canonicalize_order_request_for_signer(sample_order_request("", ""), BUYER).unwrap(); assert_eq!(request.order_id, "order-1"); assert_eq!( @@ -4136,25 +3962,25 @@ mod tests { } #[test] - fn canonicalize_active_order_request_merges_duplicate_items() { - let mut request = active_request("", ""); + fn canonicalize_order_request_merges_duplicate_items() { + let mut request = sample_order_request("", ""); request.economics.total = usd("12"); request.items = vec![ - RadrootsTradeOrderItem { + RadrootsOrderItem { bin_id: " bin-1 ".to_string(), bin_count: 1, }, - RadrootsTradeOrderItem { + RadrootsOrderItem { bin_id: "bin-1".to_string(), bin_count: 1, }, ]; - let request = canonicalize_active_order_request_for_signer(request, BUYER).unwrap(); + let request = canonicalize_order_request_for_signer(request, BUYER).unwrap(); assert_eq!( request.items, - vec![RadrootsTradeOrderItem { + vec![RadrootsOrderItem { bin_id: "bin-1".to_string(), bin_count: 2, }] @@ -4163,20 +3989,20 @@ mod tests { } #[test] - fn canonicalize_active_order_request_rejects_wrong_buyer_signer() { - let error = canonicalize_active_order_request_for_signer(active_request(SELLER, ""), BUYER) + fn canonicalize_order_request_rejects_wrong_buyer_signer() { + let error = canonicalize_order_request_for_signer(sample_order_request(SELLER, ""), BUYER) .unwrap_err(); assert!(matches!( error, - RadrootsTradeOrderCanonicalizationError::InvalidBuyerSigner + RadrootsOrderCanonicalizationError::InvalidBuyerSigner )); } #[test] - fn canonicalize_active_order_decision_sets_seller_authority_and_commitments() { + fn canonicalize_order_decision_sets_seller_authority_and_commitments() { let decision = - canonicalize_active_order_decision_for_signer(active_decision(""), SELLER).unwrap(); + canonicalize_order_decision_for_signer(sample_order_decision(""), SELLER).unwrap(); assert_eq!(decision.order_id, "order-1"); assert_eq!( @@ -4185,7 +4011,7 @@ mod tests { ); assert_eq!(decision.buyer_pubkey, BUYER); assert_eq!(decision.seller_pubkey, SELLER); - let RadrootsTradeOrderDecision::Accepted { + let RadrootsOrderDecisionOutcome::Accepted { inventory_commitments, } = decision.decision else { @@ -4195,20 +4021,20 @@ mod tests { } #[test] - fn canonicalize_active_order_decision_rejects_wrong_seller_signer() { - let error = canonicalize_active_order_decision_for_signer(active_decision(BUYER), SELLER) + fn canonicalize_order_decision_rejects_wrong_seller_signer() { + let error = canonicalize_order_decision_for_signer(sample_order_decision(BUYER), SELLER) .unwrap_err(); assert!(matches!( error, - RadrootsTradeOrderCanonicalizationError::InvalidSellerListing + RadrootsOrderCanonicalizationError::InvalidSellerListing )); } #[test] - fn canonicalize_active_order_decision_rejects_invalid_commitments() { - let mut decision = active_decision(""); - let RadrootsTradeOrderDecision::Accepted { + fn canonicalize_order_decision_rejects_invalid_commitments() { + let mut decision = sample_order_decision(""); + let RadrootsOrderDecisionOutcome::Accepted { inventory_commitments, } = &mut decision.decision else { @@ -4216,40 +4042,40 @@ mod tests { }; inventory_commitments.clear(); - let error = canonicalize_active_order_decision_for_signer(decision, SELLER).unwrap_err(); + let error = canonicalize_order_decision_for_signer(decision, SELLER).unwrap_err(); assert!(matches!( error, - RadrootsTradeOrderCanonicalizationError::MissingInventoryCommitments + RadrootsOrderCanonicalizationError::MissingInventoryCommitments )); } #[test] - fn canonicalize_active_order_decision_trims_decline_reason() { - let mut decision = active_decision(""); - decision.decision = RadrootsTradeOrderDecision::Declined { + fn canonicalize_order_decision_trims_decline_reason() { + let mut decision = sample_order_decision(""); + decision.decision = RadrootsOrderDecisionOutcome::Declined { reason: " out_of_stock ".to_string(), }; - let decision = canonicalize_active_order_decision_for_signer(decision, SELLER).unwrap(); - let RadrootsTradeOrderDecision::Declined { reason } = decision.decision else { + let decision = canonicalize_order_decision_for_signer(decision, SELLER).unwrap(); + let RadrootsOrderDecisionOutcome::Declined { reason } = decision.decision else { panic!("expected declined decision") }; assert_eq!(reason, "out_of_stock"); } #[test] - fn reduce_active_order_events_reports_missing_without_events() { - let projection = reduce_active_order_events("order-1", [], [], [], [], []); + fn reduce_order_events_reports_missing_without_events() { + let projection = reduce_order_events("order-1", [], [], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Missing); + assert_eq!(projection.status, RadrootsOrderStatus::Missing); assert!(projection.issues.is_empty()); } #[test] - fn reduce_active_order_events_reports_requested_state() { - let projection = reduce_active_order_events("order-1", [request_record()], [], [], [], []); + fn reduce_order_events_reports_requested_state() { + let projection = reduce_order_events("order-1", [request_record()], [], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Requested); + assert_eq!(projection.status, RadrootsOrderStatus::Requested); assert_eq!(projection.request_event_id.as_deref(), Some("request-1")); assert_eq!(projection.last_event_id.as_deref(), Some("request-1")); assert_eq!( @@ -4259,8 +4085,8 @@ mod tests { } #[test] - fn reduce_active_order_events_reports_accepted_state() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_accepted_state() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4269,11 +4095,11 @@ mod tests { [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); assert_eq!(projection.decision_event_id.as_deref(), Some("decision-1")); assert_eq!( projection.fulfillment_status, - Some(RadrootsActiveTradeFulfillmentState::AcceptedNotFulfilled) + Some(RadrootsOrderFulfillmentState::AcceptedNotFulfilled) ); assert_eq!(projection.fulfillment_event_id, None); assert_eq!(projection.last_event_id.as_deref(), Some("decision-1")); @@ -4284,28 +4110,28 @@ mod tests { } #[test] - fn reduce_active_order_events_reports_recorded_payment_state() { - let projection = reduce_active_order_events_with_revisions( + fn reduce_order_events_reports_recorded_payment_state() { + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], - Vec::<RadrootsActiveOrderRevisionProposalRecord>::new(), - Vec::<RadrootsActiveOrderRevisionDecisionRecord>::new(), - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), + Vec::<RadrootsOrderRevisionProposalRecord>::new(), + Vec::<RadrootsOrderRevisionDecisionRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), [payment_record("payment-1", "decision-1")], - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); assert_eq!( projection.payment.state, - RadrootsActiveOrderPaymentState::Recorded + RadrootsOrderPaymentState::Recorded ); assert_eq!( projection.payment.settlement_state, - RadrootsActiveOrderSettlementState::Pending + RadrootsOrderSettlementState::Pending ); assert_eq!( projection.payment.payment_event_id.as_deref(), @@ -4321,32 +4147,29 @@ mod tests { } #[test] - fn reduce_active_order_events_reports_accepted_settlement_state() { - let projection = reduce_active_order_events_with_revisions( + fn reduce_order_events_reports_accepted_settlement_state() { + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], - Vec::<RadrootsActiveOrderRevisionProposalRecord>::new(), - Vec::<RadrootsActiveOrderRevisionDecisionRecord>::new(), - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), + Vec::<RadrootsOrderRevisionProposalRecord>::new(), + Vec::<RadrootsOrderRevisionDecisionRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), [payment_record("payment-1", "decision-1")], [settlement_record( "settlement-1", "payment-1", - RadrootsTradeSettlementDecision::Accepted, + RadrootsOrderSettlementOutcome::Accepted, )], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); - assert_eq!( - projection.payment.state, - RadrootsActiveOrderPaymentState::Settled - ); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); + assert_eq!(projection.payment.state, RadrootsOrderPaymentState::Settled); assert_eq!( projection.payment.settlement_state, - RadrootsActiveOrderSettlementState::Accepted + RadrootsOrderSettlementState::Accepted ); assert_eq!( projection.payment.settlement_event_id.as_deref(), @@ -4357,70 +4180,67 @@ mod tests { } #[test] - fn reduce_active_order_events_rejects_stale_payment_amount() { + fn reduce_order_events_rejects_stale_payment_amount() { let mut payment = payment_record("payment-1", "decision-1"); payment.payload.amount = decimal("9"); - let projection = reduce_active_order_events_with_revisions( + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], - Vec::<RadrootsActiveOrderRevisionProposalRecord>::new(), - Vec::<RadrootsActiveOrderRevisionDecisionRecord>::new(), - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), + Vec::<RadrootsOrderRevisionProposalRecord>::new(), + Vec::<RadrootsOrderRevisionDecisionRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), [payment], - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); - assert_eq!( - projection.payment.state, - RadrootsActiveOrderPaymentState::Invalid - ); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); + assert_eq!(projection.payment.state, RadrootsOrderPaymentState::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::PaymentAmountMismatch { event_id } + RadrootsOrderIssue::PaymentAmountMismatch { event_id } if event_id == "payment-1" ))); } #[test] - fn reduce_active_order_events_keeps_payment_separate_from_receipt() { - let projection = reduce_active_order_events_with_revisions( + fn reduce_order_events_keeps_payment_separate_from_receipt() { + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], - Vec::<RadrootsActiveOrderRevisionProposalRecord>::new(), - Vec::<RadrootsActiveOrderRevisionDecisionRecord>::new(), + Vec::<RadrootsOrderRevisionProposalRecord>::new(), + Vec::<RadrootsOrderRevisionDecisionRecord>::new(), [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Delivered, + RadrootsOrderFulfillmentState::Delivered, )], - Vec::<RadrootsActiveOrderCancellationRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), [receipt_record("receipt-1", "fulfillment-1", true)], [payment_record("payment-1", "decision-1")], - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Completed); + assert_eq!(projection.status, RadrootsOrderStatus::Completed); assert_eq!(projection.receipt_received, Some(true)); assert_eq!( projection.payment.state, - RadrootsActiveOrderPaymentState::Recorded + RadrootsOrderPaymentState::Recorded ); assert_eq!( projection.payment.settlement_state, - RadrootsActiveOrderSettlementState::Pending + RadrootsOrderSettlementState::Pending ); assert!(projection.issues.is_empty()); } #[test] - fn reduce_active_order_events_applies_accepted_revision_agreement() { - let projection = reduce_active_order_events_with_revisions( + fn reduce_order_events_applies_accepted_revision_agreement() { + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4434,16 +4254,16 @@ mod tests { "revision-decision-1", "revision-proposal-1", "revision-1", - RadrootsTradeOrderRevisionDecision::Accepted, + RadrootsOrderRevisionOutcome::Accepted, )], - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), - Vec::<RadrootsActiveOrderPaymentRecord>::new(), - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), + Vec::<RadrootsOrderPaymentEventRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); assert_eq!( projection.agreement_event_id.as_deref(), Some("revision-decision-1") @@ -4460,8 +4280,8 @@ mod tests { } #[test] - fn reduce_active_order_events_preserves_agreement_after_declined_revision() { - let projection = reduce_active_order_events_with_revisions( + fn reduce_order_events_preserves_agreement_after_declined_revision() { + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4475,18 +4295,18 @@ mod tests { "revision-decision-1", "revision-proposal-1", "revision-1", - RadrootsTradeOrderRevisionDecision::Declined { + RadrootsOrderRevisionOutcome::Declined { reason: "keep original order".to_string(), }, )], - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), - Vec::<RadrootsActiveOrderPaymentRecord>::new(), - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), + Vec::<RadrootsOrderPaymentEventRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); assert_eq!(projection.agreement_event_id.as_deref(), Some("decision-1")); assert_eq!( projection.last_event_id.as_deref(), @@ -4500,16 +4320,16 @@ mod tests { } #[test] - fn reduce_active_order_events_rejects_wrong_actor_revision_decision() { + fn reduce_order_events_rejects_wrong_actor_revision_decision() { let mut decision = revision_decision_record( "revision-decision-1", "revision-proposal-1", "revision-1", - RadrootsTradeOrderRevisionDecision::Accepted, + RadrootsOrderRevisionOutcome::Accepted, ); decision.author_pubkey = SELLER.to_string(); - let projection = reduce_active_order_events_with_revisions( + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4520,24 +4340,24 @@ mod tests { 1, )], [decision], - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), - Vec::<RadrootsActiveOrderPaymentRecord>::new(), - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), + Vec::<RadrootsOrderPaymentEventRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::RevisionDecisionAuthorMismatch { event_id } + RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id } if event_id == "revision-decision-1" ))); } #[test] - fn reduce_active_order_events_rejects_stale_revision_decision() { - let projection = reduce_active_order_events_with_revisions( + fn reduce_order_events_rejects_stale_revision_decision() { + let projection = reduce_order_events_with_revisions( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4551,43 +4371,43 @@ mod tests { "revision-decision-1", "unknown-proposal", "revision-1", - RadrootsTradeOrderRevisionDecision::Accepted, + RadrootsOrderRevisionOutcome::Accepted, )], - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), - Vec::<RadrootsActiveOrderPaymentRecord>::new(), - Vec::<RadrootsActiveOrderSettlementRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), + Vec::<RadrootsOrderPaymentEventRecord>::new(), + Vec::<RadrootsOrderSettlementRecord>::new(), ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::RevisionDecisionWithoutProposal { event_id } + RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id } if event_id == "revision-decision-1" ))); } #[test] - fn reduce_active_order_events_rejects_invalid_request_economics() { + fn reduce_order_events_rejects_invalid_request_economics() { let mut request = request_record(); request.payload.economics.total = usd("12"); - let projection = reduce_active_order_events("order-1", [request], [], [], [], []); + let projection = reduce_order_events("order-1", [request], [], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!(projection.economics, None); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::RequestPayloadInvalid { + vec![RadrootsOrderIssue::RequestPayloadInvalid { event_id: "request-1".to_string() }] ); } #[test] - fn reduce_active_order_events_reports_latest_fulfillment_state() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_latest_fulfillment_state() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4595,22 +4415,22 @@ mod tests { fulfillment_record( "fulfillment-2", "fulfillment-1", - RadrootsActiveTradeFulfillmentState::ReadyForPickup, + RadrootsOrderFulfillmentState::ReadyForPickup, ), fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, ), ], [], [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); assert_eq!( projection.fulfillment_status, - Some(RadrootsActiveTradeFulfillmentState::ReadyForPickup) + Some(RadrootsOrderFulfillmentState::ReadyForPickup) ); assert_eq!( projection.fulfillment_event_id.as_deref(), @@ -4620,31 +4440,31 @@ mod tests { } #[test] - fn reduce_active_order_events_keeps_delivered_without_receipt_nonterminal() { - let projection = reduce_active_order_events( + fn reduce_order_events_keeps_delivered_without_receipt_nonterminal() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Delivered, + RadrootsOrderFulfillmentState::Delivered, )], [], [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); assert_eq!( projection.fulfillment_status, - Some(RadrootsActiveTradeFulfillmentState::Delivered) + Some(RadrootsOrderFulfillmentState::Delivered) ); assert!(!projection.lifecycle_terminal); } #[test] - fn reduce_active_order_events_reports_requested_cancellation() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_requested_cancellation() { + let projection = reduce_order_events( "order-1", [request_record()], [], @@ -4653,7 +4473,7 @@ mod tests { [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Cancelled); + assert_eq!(projection.status, RadrootsOrderStatus::Cancelled); assert_eq!(projection.request_event_id.as_deref(), Some("request-1")); assert_eq!( projection.cancellation_event_id.as_deref(), @@ -4663,14 +4483,14 @@ mod tests { assert!(projection.lifecycle_terminal); assert_eq!( projection.payment, - RadrootsActiveOrderPaymentProjection::not_recorded() + RadrootsOrderPaymentProjection::not_recorded() ); assert!(projection.issues.is_empty()); } #[test] - fn reduce_active_order_events_rejects_request_cancellation_decision_fork() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_request_cancellation_decision_fork() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4679,18 +4499,18 @@ mod tests { [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::ForkedLifecycle { + vec![RadrootsOrderIssue::ForkedLifecycle { event_ids: vec!["cancel-1".to_string(), "decision-1".to_string()] }] ); } #[test] - fn reduce_active_order_events_reports_accepted_cancellation_before_fulfillment() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_accepted_cancellation_before_fulfillment() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4699,7 +4519,7 @@ mod tests { [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Cancelled); + assert_eq!(projection.status, RadrootsOrderStatus::Cancelled); assert_eq!(projection.decision_event_id.as_deref(), Some("decision-1")); assert_eq!( projection.cancellation_event_id.as_deref(), @@ -4710,45 +4530,45 @@ mod tests { } #[test] - fn reduce_active_order_events_rejects_cancellation_fulfillment_fork() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_cancellation_fulfillment_fork() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, )], [cancellation_record("cancel-1", "decision-1")], [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::ForkedLifecycle { + vec![RadrootsOrderIssue::ForkedLifecycle { event_ids: vec!["cancel-1".to_string(), "fulfillment-1".to_string()] }] ); } #[test] - fn reduce_active_order_events_reports_completed_buyer_receipt() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_completed_buyer_receipt() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::ReadyForPickup, + RadrootsOrderFulfillmentState::ReadyForPickup, )], [], [receipt_record("receipt-1", "fulfillment-1", true)], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Completed); + assert_eq!(projection.status, RadrootsOrderStatus::Completed); assert_eq!( projection.fulfillment_event_id.as_deref(), Some("fulfillment-1") @@ -4760,13 +4580,13 @@ mod tests { assert!(projection.lifecycle_terminal); assert_eq!( projection.payment, - RadrootsActiveOrderPaymentProjection::not_recorded() + RadrootsOrderPaymentProjection::not_recorded() ); } #[test] - fn reduce_active_order_events_rejects_receipt_fulfillment_fork() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_receipt_fulfillment_fork() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4774,141 +4594,135 @@ mod tests { fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::ReadyForPickup, + RadrootsOrderFulfillmentState::ReadyForPickup, ), fulfillment_record( "fulfillment-2", "fulfillment-1", - RadrootsActiveTradeFulfillmentState::Delivered, + RadrootsOrderFulfillmentState::Delivered, ), ], [], [receipt_record("receipt-1", "fulfillment-1", true)], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::ForkedLifecycle { + vec![RadrootsOrderIssue::ForkedLifecycle { event_ids: vec!["fulfillment-2".to_string(), "receipt-1".to_string()] }] ); } #[test] - fn reduce_active_order_events_reports_disputed_buyer_receipt() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_disputed_buyer_receipt() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Delivered, + RadrootsOrderFulfillmentState::Delivered, )], [], [receipt_record("receipt-1", "fulfillment-1", false)], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Disputed); + assert_eq!(projection.status, RadrootsOrderStatus::Disputed); assert_eq!(projection.receipt_event_id.as_deref(), Some("receipt-1")); assert_eq!(projection.receipt_received, Some(false)); assert_eq!(projection.receipt_issue.as_deref(), Some("damaged items")); assert!(projection.lifecycle_terminal); assert_eq!( projection.payment, - RadrootsActiveOrderPaymentProjection::not_recorded() + RadrootsOrderPaymentProjection::not_recorded() ); } #[test] - fn reduce_active_order_events_rejects_receipt_without_eligible_fulfillment() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_receipt_without_eligible_fulfillment() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, )], [], [receipt_record("receipt-1", "fulfillment-1", true)], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![ - RadrootsActiveOrderReducerIssue::ReceiptWithoutEligibleFulfillment { - event_id: "receipt-1".to_string() - } - ] + vec![RadrootsOrderIssue::ReceiptWithoutEligibleFulfillment { + event_id: "receipt-1".to_string() + }] ); } #[test] - fn reduce_active_order_events_rejects_fulfillment_before_acceptance() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_fulfillment_before_acceptance() { + let projection = reduce_order_events( "order-1", [request_record()], [], [fulfillment_record( "fulfillment-1", "request-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, )], [], [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![ - RadrootsActiveOrderReducerIssue::FulfillmentWithoutAcceptedDecision { - event_id: "fulfillment-1".to_string() - } - ] + vec![RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision { + event_id: "fulfillment-1".to_string() + }] ); } #[test] - fn reduce_active_order_events_rejects_fulfillment_after_decline() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_fulfillment_after_decline() { + let projection = reduce_order_events( "order-1", [request_record()], [declined_decision_record("decision-1")], [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, )], [], [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![ - RadrootsActiveOrderReducerIssue::FulfillmentWithoutAcceptedDecision { - event_id: "fulfillment-1".to_string() - } - ] + vec![RadrootsOrderIssue::FulfillmentWithoutAcceptedDecision { + event_id: "fulfillment-1".to_string() + }] ); } #[test] - fn reduce_active_order_events_rejects_wrong_actor_fulfillment() { + fn reduce_order_events_rejects_wrong_actor_fulfillment() { let mut fulfillment = fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, ); fulfillment.author_pubkey = BUYER.to_string(); - let projection = reduce_active_order_events( + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4917,17 +4731,17 @@ mod tests { [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::FulfillmentAuthorMismatch { event_id } + RadrootsOrderIssue::FulfillmentAuthorMismatch { event_id } if event_id == "fulfillment-1" ))); } #[test] - fn reduce_active_order_events_rejects_forked_fulfillment_chain() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_forked_fulfillment_chain() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4935,30 +4749,30 @@ mod tests { fulfillment_record( "fulfillment-2", "decision-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, ), fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::ReadyForPickup, + RadrootsOrderFulfillmentState::ReadyForPickup, ), ], [], [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::ForkedFulfillments { + vec![RadrootsOrderIssue::ForkedFulfillments { event_ids: vec!["fulfillment-1".to_string(), "fulfillment-2".to_string()] }] ); } #[test] - fn reduce_active_order_events_rejects_terminal_fulfillment_transition() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_terminal_fulfillment_transition() { + let projection = reduce_order_events( "order-1", [request_record()], [accepted_decision_record("decision-1")], @@ -4966,32 +4780,30 @@ mod tests { fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Delivered, + RadrootsOrderFulfillmentState::Delivered, ), fulfillment_record( "fulfillment-2", "fulfillment-1", - RadrootsActiveTradeFulfillmentState::ReadyForPickup, + RadrootsOrderFulfillmentState::ReadyForPickup, ), ], [], [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![ - RadrootsActiveOrderReducerIssue::FulfillmentUnsupportedTransition { - event_id: "fulfillment-2".to_string() - } - ] + vec![RadrootsOrderIssue::FulfillmentUnsupportedTransition { + event_id: "fulfillment-2".to_string() + }] ); } #[test] - fn reduce_active_order_events_reports_declined_state() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_declined_state() { + let projection = reduce_order_events( "order-1", [request_record()], [declined_decision_record("decision-1")], @@ -5000,7 +4812,7 @@ mod tests { [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Declined); + assert_eq!(projection.status, RadrootsOrderStatus::Declined); assert_eq!(projection.decision_event_id.as_deref(), Some("decision-1")); } @@ -5057,11 +4869,11 @@ mod tests { "revision-decision-1", "revision-proposal-1", "revision-1", - RadrootsTradeOrderRevisionDecision::Accepted, + RadrootsOrderRevisionOutcome::Accepted, )], - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), ); assert!(projection.issues.is_empty()); @@ -5088,7 +4900,7 @@ mod tests { [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::SellerCancelled, + RadrootsOrderFulfillmentState::SellerCancelled, )], [], [], @@ -5133,7 +4945,7 @@ mod tests { [fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Delivered, + RadrootsOrderFulfillmentState::Delivered, )], [], [receipt_record("receipt-1", "fulfillment-1", true)], @@ -5157,12 +4969,12 @@ mod tests { fulfillment_record( "fulfillment-2", "decision-1", - RadrootsActiveTradeFulfillmentState::SellerCancelled, + RadrootsOrderFulfillmentState::SellerCancelled, ), fulfillment_record( "fulfillment-1", "decision-1", - RadrootsActiveTradeFulfillmentState::Preparing, + RadrootsOrderFulfillmentState::Preparing, ), ], [], @@ -5176,12 +4988,10 @@ mod tests { ); assert_eq!( projection.issues, - vec![ - RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { - order_id: "order-1".to_string(), - event_ids: vec!["fulfillment-1".to_string(), "fulfillment-2".to_string()], - } - ] + vec![RadrootsListingInventoryAccountingIssue::InvalidOrder { + order_id: "order-1".to_string(), + event_ids: vec!["fulfillment-1".to_string(), "fulfillment-2".to_string()], + }] ); } @@ -5209,9 +5019,9 @@ mod tests { #[test] fn reduce_listing_inventory_accounting_reports_invalid_mismatched_commitment() { - let decision = RadrootsActiveOrderDecisionRecord { - payload: decision_payload(RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + let decision = RadrootsOrderDecisionRecord { + payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: "bin-1".to_string(), bin_count: 1, }], @@ -5234,12 +5044,10 @@ mod tests { assert_eq!(projection.invalid_event_ids, vec!["decision-1".to_string()]); assert_eq!( projection.issues, - vec![ - RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { - order_id: "order-1".to_string(), - event_ids: vec!["decision-1".to_string()], - } - ] + vec![RadrootsListingInventoryAccountingIssue::InvalidOrder { + order_id: "order-1".to_string(), + event_ids: vec!["decision-1".to_string()], + }] ); } @@ -5289,11 +5097,11 @@ mod tests { }, inventory_bin(1), ], - Vec::<RadrootsActiveOrderRequestRecord>::new(), - Vec::<RadrootsActiveOrderDecisionRecord>::new(), - Vec::<RadrootsActiveOrderFulfillmentRecord>::new(), - Vec::<RadrootsActiveOrderCancellationRecord>::new(), - Vec::<RadrootsActiveOrderReceiptRecord>::new(), + Vec::<RadrootsOrderRequestRecord>::new(), + Vec::<RadrootsOrderDecisionRecord>::new(), + Vec::<RadrootsOrderFulfillmentRecord>::new(), + Vec::<RadrootsOrderCancellationRecord>::new(), + Vec::<RadrootsOrderReceiptRecord>::new(), ); assert_eq!(projection.bins[0].available_count, u64::MAX); @@ -5339,33 +5147,31 @@ mod tests { } #[test] - fn reduce_active_order_events_rejects_invalid_decision_actor() { + fn reduce_order_events_rejects_invalid_decision_actor() { let mut decision = accepted_decision_record("decision-1"); decision.author_pubkey = BUYER.to_string(); - let projection = - reduce_active_order_events("order-1", [request_record()], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::DecisionAuthorMismatch { event_id } + RadrootsOrderIssue::DecisionAuthorMismatch { event_id } if event_id == "decision-1" ))); } #[test] - fn reduce_active_order_events_rejects_invalid_decision_counterparty() { + fn reduce_order_events_rejects_invalid_decision_counterparty() { let mut decision = accepted_decision_record("decision-1"); decision.counterparty_pubkey = SELLER.to_string(); - let projection = - reduce_active_order_events("order-1", [request_record()], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::DecisionCounterpartyMismatch { event_id } + RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id } if event_id == "decision-1" ))); } @@ -5390,56 +5196,52 @@ mod tests { assert_eq!(projection.invalid_event_ids, vec!["decision-1".to_string()]); assert_eq!( projection.issues, - vec![ - RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { - order_id: "order-1".to_string(), - event_ids: vec!["decision-1".to_string()], - } - ] + vec![RadrootsListingInventoryAccountingIssue::InvalidOrder { + order_id: "order-1".to_string(), + event_ids: vec!["decision-1".to_string()], + }] ); } #[test] - fn reduce_active_order_events_rejects_invalid_decision_chain() { + fn reduce_order_events_rejects_invalid_decision_chain() { let mut decision = accepted_decision_record("decision-1"); decision.prev_event_id = "request-2".to_string(); - let projection = - reduce_active_order_events("order-1", [request_record()], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::DecisionPreviousMismatch { event_id } + RadrootsOrderIssue::DecisionPreviousMismatch { event_id } if event_id == "decision-1" ))); } #[test] - fn reduce_active_order_events_rejects_missing_commitment() { - let decision = RadrootsActiveOrderDecisionRecord { - payload: decision_payload(RadrootsTradeOrderDecision::Accepted { + fn reduce_order_events_rejects_missing_commitment() { + let decision = RadrootsOrderDecisionRecord { + payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: Vec::new(), }), ..accepted_decision_record("decision-1") }; - let projection = - reduce_active_order_events("order-1", [request_record()], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::DecisionMissingInventoryCommitments { event_id } + RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id } if event_id == "decision-1" ))); } #[test] - fn reduce_active_order_events_rejects_commitment_count_mismatch() { - let decision = RadrootsActiveOrderDecisionRecord { - payload: decision_payload(RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + fn reduce_order_events_rejects_commitment_count_mismatch() { + let decision = RadrootsOrderDecisionRecord { + payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: "bin-1".to_string(), bin_count: 1, }], @@ -5447,22 +5249,21 @@ mod tests { ..accepted_decision_record("decision-1") }; - let projection = - reduce_active_order_events("order-1", [request_record()], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::DecisionInventoryCommitmentMismatch { event_id } + RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id } if event_id == "decision-1" ))); } #[test] - fn reduce_active_order_events_rejects_commitment_bin_mismatch() { - let decision = RadrootsActiveOrderDecisionRecord { - payload: decision_payload(RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + fn reduce_order_events_rejects_commitment_bin_mismatch() { + let decision = RadrootsOrderDecisionRecord { + payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: "bin-2".to_string(), bin_count: 2, }], @@ -5470,36 +5271,33 @@ mod tests { ..accepted_decision_record("decision-1") }; - let projection = - reduce_active_order_events("order-1", [request_record()], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![ - RadrootsActiveOrderReducerIssue::DecisionInventoryCommitmentMismatch { - event_id: "decision-1".to_string() - } - ] + vec![RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { + event_id: "decision-1".to_string() + }] ); } #[test] - fn reduce_active_order_events_matches_normalized_duplicate_bins() { + fn reduce_order_events_matches_normalized_duplicate_bins() { let mut request = request_record(); request.payload.items = vec![ - RadrootsTradeOrderItem { + RadrootsOrderItem { bin_id: " bin-1 ".to_string(), bin_count: 1, }, - RadrootsTradeOrderItem { + RadrootsOrderItem { bin_id: "bin-1".to_string(), bin_count: 1, }, ]; - let decision = RadrootsActiveOrderDecisionRecord { - payload: decision_payload(RadrootsTradeOrderDecision::Accepted { - inventory_commitments: vec![RadrootsTradeInventoryCommitment { + let decision = RadrootsOrderDecisionRecord { + payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { bin_id: "bin-1".to_string(), bin_count: 2, }], @@ -5507,35 +5305,34 @@ mod tests { ..accepted_decision_record("decision-1") }; - let projection = reduce_active_order_events("order-1", [request], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Accepted); + assert_eq!(projection.status, RadrootsOrderStatus::Accepted); assert!(projection.issues.is_empty()); } #[test] - fn reduce_active_order_events_rejects_missing_decline_reason() { - let decision = RadrootsActiveOrderDecisionRecord { - payload: decision_payload(RadrootsTradeOrderDecision::Declined { + fn reduce_order_events_rejects_missing_decline_reason() { + let decision = RadrootsOrderDecisionRecord { + payload: decision_payload(RadrootsOrderDecisionOutcome::Declined { reason: " ".to_string(), }), ..declined_decision_record("decision-1") }; - let projection = - reduce_active_order_events("order-1", [request_record()], [decision], [], [], []); + let projection = reduce_order_events("order-1", [request_record()], [decision], [], [], []); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert!(projection.issues.iter().any(|issue| matches!( issue, - RadrootsActiveOrderReducerIssue::DecisionMissingReason { event_id } + RadrootsOrderIssue::DecisionMissingReason { event_id } if event_id == "decision-1" ))); } #[test] - fn reduce_active_order_events_rejects_conflicting_decisions() { - let projection = reduce_active_order_events( + fn reduce_order_events_rejects_conflicting_decisions() { + let projection = reduce_order_events( "order-1", [request_record()], [ @@ -5547,18 +5344,18 @@ mod tests { [], ); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::ConflictingDecisions { + vec![RadrootsOrderIssue::ConflictingDecisions { event_ids: vec!["decision-1".to_string(), "decision-2".to_string()] }] ); } #[test] - fn reduce_active_order_events_reports_multiple_requests_deterministically() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_multiple_requests_deterministically() { + let projection = reduce_order_events( "order-1", [ request_record_with_event_id("request-2"), @@ -5569,7 +5366,7 @@ mod tests { [], [], ); - let reversed = reduce_active_order_events( + let reversed = reduce_order_events( "order-1", [ request_record_with_event_id("request-1"), @@ -5582,19 +5379,19 @@ mod tests { ); assert_eq!(projection, reversed); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!(projection.request_event_id.as_deref(), Some("request-1")); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::MultipleRequests { + vec![RadrootsOrderIssue::MultipleRequests { event_ids: vec!["request-1".to_string(), "request-2".to_string()] }] ); } #[test] - fn reduce_active_order_events_reports_conflicting_decisions_deterministically() { - let projection = reduce_active_order_events( + fn reduce_order_events_reports_conflicting_decisions_deterministically() { + let projection = reduce_order_events( "order-1", [request_record()], [ @@ -5605,7 +5402,7 @@ mod tests { [], [], ); - let reversed = reduce_active_order_events( + let reversed = reduce_order_events( "order-1", [request_record()], [ @@ -5618,10 +5415,10 @@ mod tests { ); assert_eq!(projection, reversed); - assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid); + assert_eq!(projection.status, RadrootsOrderStatus::Invalid); assert_eq!( projection.issues, - vec![RadrootsActiveOrderReducerIssue::ConflictingDecisions { + vec![RadrootsOrderIssue::ConflictingDecisions { event_ids: vec!["decision-1".to_string(), "decision-2".to_string()] }] ); @@ -5631,15 +5428,15 @@ mod tests { fn projection_issue_event_ids_covers_all_issue_variants() { macro_rules! issue { ($variant:ident, $id:expr) => { - RadrootsActiveOrderReducerIssue::$variant { + RadrootsOrderIssue::$variant { event_id: $id.to_string(), } }; } let issues = vec![ - RadrootsActiveOrderReducerIssue::MissingRequest, - RadrootsActiveOrderReducerIssue::MultipleRequests { + RadrootsOrderIssue::MissingRequest, + RadrootsOrderIssue::MultipleRequests { event_ids: vec!["multi-b".to_string(), "multi-a".to_string()], }, issue!(RequestPayloadInvalid, "request-payload"), @@ -5666,7 +5463,7 @@ mod tests { "decision-commitment-mismatch" ), issue!(DecisionMissingReason, "decision-missing-reason"), - RadrootsActiveOrderReducerIssue::ConflictingDecisions { + RadrootsOrderIssue::ConflictingDecisions { event_ids: vec!["conflict-b".to_string(), "conflict-a".to_string()], }, issue!( @@ -5732,7 +5529,7 @@ mod tests { FulfillmentUnsupportedTransition, "fulfillment-unsupported-transition" ), - RadrootsActiveOrderReducerIssue::ForkedFulfillments { + RadrootsOrderIssue::ForkedFulfillments { event_ids: vec![ "fulfillment-fork-b".to_string(), "fulfillment-fork-a".to_string(), @@ -5795,7 +5592,7 @@ mod tests { issue!(PaymentCurrencyMismatch, "payment-currency"), issue!(PaymentAfterCancellation, "payment-after-cancellation"), issue!(RevisionAfterPayment, "revision-after-payment"), - RadrootsActiveOrderReducerIssue::DuplicatePayments { + RadrootsOrderIssue::DuplicatePayments { event_ids: vec![ "payment-duplicate-b".to_string(), "payment-duplicate-a".to_string(), @@ -5822,13 +5619,13 @@ mod tests { issue!(SettlementEconomicsDigestMismatch, "settlement-digest"), issue!(SettlementAmountMismatch, "settlement-amount"), issue!(SettlementCurrencyMismatch, "settlement-currency"), - RadrootsActiveOrderReducerIssue::DuplicateSettlements { + RadrootsOrderIssue::DuplicateSettlements { event_ids: vec![ "settlement-duplicate-b".to_string(), "settlement-duplicate-a".to_string(), ], }, - RadrootsActiveOrderReducerIssue::ForkedLifecycle { + RadrootsOrderIssue::ForkedLifecycle { event_ids: vec!["lifecycle-b".to_string(), "lifecycle-a".to_string()], }, ]; diff --git a/crates/trade/src/prelude.rs b/crates/trade/src/prelude.rs @@ -1,5 +1,4 @@ pub use crate::listing::*; pub use crate::order::*; -pub use crate::public_trade::*; #[cfg(feature = "serde_json")] pub use crate::validation_receipt::*; diff --git a/crates/trade/src/public_trade.rs b/crates/trade/src/public_trade.rs @@ -1,182 +0,0 @@ -#![forbid(unsafe_code)] - -#[cfg(not(feature = "std"))] -use alloc::string::{String, ToString}; - -use radroots_events::kinds::KIND_LISTING; -use radroots_events::trade::RadrootsTradeMessageType as TradeListingMessageType; -use radroots_events_codec::trade::RadrootsTradeListingAddress as TradeListingAddress; -use thiserror::Error; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CanonicalPublicTradeContext { - pub listing_addr: String, - pub order_id: String, - pub counterparty_pubkey: String, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ExpectedPublicTradeAuthor { - Buyer, - Seller, - Either, -} - -#[derive(Debug, Error)] -pub enum RadrootsPublicTradeCanonicalizationError { - #[error("{0} cannot be empty")] - EmptyField(&'static str), - #[error("invalid listing_addr: {0}")] - InvalidListingAddress(String), - #[error("listing_addr must reference a public NIP-99 listing")] - InvalidListingKind, - #[error("counterparty_pubkey must not match the requested signer identity")] - DuplicateCounterparty, - #[error("{0}")] - InvalidAuthor(String), -} - -pub fn canonicalize_public_trade_context( - listing_addr: String, - order_id: String, - counterparty_pubkey: String, - signer_pubkey: &str, - message_type: TradeListingMessageType, -) -> Result<CanonicalPublicTradeContext, RadrootsPublicTradeCanonicalizationError> { - let listing_addr = normalized_required_string(listing_addr, "listing_addr")?; - let parsed_listing_addr = TradeListingAddress::parse(&listing_addr).map_err(|error| { - RadrootsPublicTradeCanonicalizationError::InvalidListingAddress(error.to_string()) - })?; - if u32::from(parsed_listing_addr.kind) != KIND_LISTING { - return Err(RadrootsPublicTradeCanonicalizationError::InvalidListingKind); - } - - let order_id = normalized_required_string(order_id, "order_id")?; - let counterparty_pubkey = - normalized_required_string(counterparty_pubkey, "counterparty_pubkey")?; - if counterparty_pubkey == signer_pubkey { - return Err(RadrootsPublicTradeCanonicalizationError::DuplicateCounterparty); - } - - validate_expected_author( - &parsed_listing_addr, - message_type, - signer_pubkey, - &counterparty_pubkey, - )?; - - Ok(CanonicalPublicTradeContext { - listing_addr, - order_id, - counterparty_pubkey, - }) -} - -fn validate_expected_author( - listing_addr: &TradeListingAddress, - message_type: TradeListingMessageType, - signer_pubkey: &str, - counterparty_pubkey: &str, -) -> Result<(), RadrootsPublicTradeCanonicalizationError> { - match expected_author(message_type) { - ExpectedPublicTradeAuthor::Seller => { - if signer_pubkey != listing_addr.seller_pubkey { - return Err(RadrootsPublicTradeCanonicalizationError::InvalidAuthor( - format!("{message_type:?} must be authored by the listing seller"), - )); - } - if counterparty_pubkey == listing_addr.seller_pubkey { - return Err(RadrootsPublicTradeCanonicalizationError::InvalidAuthor( - format!("{message_type:?} counterparty must not be the listing seller"), - )); - } - } - ExpectedPublicTradeAuthor::Buyer => { - if signer_pubkey == listing_addr.seller_pubkey { - return Err(RadrootsPublicTradeCanonicalizationError::InvalidAuthor( - format!("{message_type:?} must be authored by the listing buyer"), - )); - } - if counterparty_pubkey != listing_addr.seller_pubkey { - return Err(RadrootsPublicTradeCanonicalizationError::InvalidAuthor( - format!("{message_type:?} counterparty must be the listing seller"), - )); - } - } - ExpectedPublicTradeAuthor::Either => {} - } - Ok(()) -} - -fn expected_author(message_type: TradeListingMessageType) -> ExpectedPublicTradeAuthor { - use TradeListingMessageType as MessageType; - - match message_type { - MessageType::OrderResponse - | MessageType::OrderRevision - | MessageType::OrderRevisionAccept - | MessageType::OrderRevisionDecline - | MessageType::Answer - | MessageType::DiscountOffer - | MessageType::DiscountAccept - | MessageType::DiscountDecline - | MessageType::FulfillmentUpdate => ExpectedPublicTradeAuthor::Seller, - MessageType::Question - | MessageType::DiscountRequest - | MessageType::Cancel - | MessageType::Receipt => ExpectedPublicTradeAuthor::Buyer, - MessageType::OrderRequest - | MessageType::ListingValidateRequest - | MessageType::ListingValidateResult => ExpectedPublicTradeAuthor::Either, - } -} - -fn normalized_required_string( - value: String, - field: &'static str, -) -> Result<String, RadrootsPublicTradeCanonicalizationError> { - let value = value.trim().to_string(); - if value.is_empty() { - return Err(RadrootsPublicTradeCanonicalizationError::EmptyField(field)); - } - Ok(value) -} - -#[cfg(test)] -mod tests { - use radroots_events::kinds::KIND_LISTING; - - use super::canonicalize_public_trade_context; - - #[test] - fn canonicalize_public_trade_context_accepts_seller_authored_message() { - let context = canonicalize_public_trade_context( - format!( - "{KIND_LISTING}:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg" - ), - "order-1".to_string(), - "2222222222222222222222222222222222222222222222222222222222222222".to_string(), - "1111111111111111111111111111111111111111111111111111111111111111", - super::TradeListingMessageType::OrderResponse, - ) - .expect("canonical public trade context"); - - assert_eq!(context.order_id, "order-1"); - } - - #[test] - fn canonicalize_public_trade_context_rejects_wrong_seller_role() { - let err = canonicalize_public_trade_context( - format!( - "{KIND_LISTING}:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg" - ), - "order-1".to_string(), - "3333333333333333333333333333333333333333333333333333333333333333".to_string(), - "2222222222222222222222222222222222222222222222222222222222222222", - super::TradeListingMessageType::OrderResponse, - ) - .expect_err("invalid seller role"); - - assert!(err.to_string().contains("listing seller")); - } -} diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs @@ -10,7 +10,7 @@ use alloc::{ use base64::Engine as _; use radroots_events::{ RadrootsNostrEvent, - kinds::{KIND_TRADE_RECEIPT, KIND_TRADE_VALIDATION_RECEIPT}, + kinds::{KIND_ORDER_RECEIPT, KIND_TRADE_VALIDATION_RECEIPT}, tags::TAG_D, }; use radroots_events_codec::wire::WireEventParts; @@ -434,7 +434,7 @@ pub fn verify_validation_receipt_event( event: &RadrootsNostrEvent, expected: RadrootsValidationReceiptExpectedBinding<'_>, ) -> Result<RadrootsVerifiedValidationReceipt, RadrootsValidationReceiptError> { - if event.kind == KIND_TRADE_RECEIPT { + if event.kind == KIND_ORDER_RECEIPT { return Err(RadrootsValidationReceiptError::BuyerReceiptKind); } if event.kind != KIND_TRADE_VALIDATION_RECEIPT { @@ -715,9 +715,9 @@ mod tests { }; use radroots_events::{ RadrootsNostrEvent, - kinds::{KIND_TRADE_RECEIPT, KIND_TRADE_VALIDATION_RECEIPT}, + kinds::{KIND_ORDER_RECEIPT, KIND_TRADE_VALIDATION_RECEIPT}, }; - use radroots_events_codec::trade::active_trade_buyer_receipt_from_event; + use radroots_events_codec::order::order_receipt_from_event; fn hash32(c: char) -> String { format!("0x{}", c.to_string().repeat(64)) @@ -836,7 +836,7 @@ mod tests { #[test] fn validation_receipt_verifier_rejects_buyer_receipt_kind_3434() { let mut event = sample_validation_receipt_event(); - event.kind = KIND_TRADE_RECEIPT; + event.kind = KIND_ORDER_RECEIPT; assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::BuyerReceiptKind) @@ -850,7 +850,7 @@ mod tests { reject_validation_receipt_as_buyer_receipt(&event), Err(RadrootsValidationReceiptError::ValidationReceiptKind) ); - let buyer_receipt_error = active_trade_buyer_receipt_from_event(&event) + let buyer_receipt_error = order_receipt_from_event(&event) .expect_err("validation receipt must not parse as buyer receipt"); assert!(buyer_receipt_error.to_string().contains("3440")); } diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -689,28 +689,28 @@ const DVM_FEEDBACK_WITNESSES: [EventBoundarySourceWitness; 2] = [ const TRADE_ORDER_REQUESTED_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_ORDER_REQUEST: u32 = 3422;"], + required_fragments: &["pub const KIND_ORDER_REQUEST: u32 = 3422;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub struct RadrootsTradeOrderRequested", - "Self::TradeOrderRequested => KIND_TRADE_ORDER_REQUEST", + "pub struct RadrootsOrderRequest", + "Self::OrderRequested => KIND_ORDER_REQUEST", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_order_request_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_request_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_order_request_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_request_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderRequestRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderRequestRecord", + "pub fn reduce_order_events", ], }, ]; @@ -718,32 +718,29 @@ const TRADE_ORDER_REQUESTED_WITNESSES: [EventBoundarySourceWitness; 5] = [ const TRADE_ORDER_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &[ - "pub const KIND_TRADE_ORDER_RESPONSE: u32 = 3423;", - "pub const KIND_TRADE_ORDER_DECISION: u32 = KIND_TRADE_ORDER_RESPONSE;", - ], + required_fragments: &["pub const KIND_ORDER_DECISION: u32 = 3423;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub enum RadrootsTradeOrderDecision", - "pub struct RadrootsTradeOrderDecisionEvent", - "Self::TradeOrderDecision => KIND_TRADE_ORDER_DECISION", + "pub enum RadrootsOrderDecisionOutcome", + "pub struct RadrootsOrderDecision", + "Self::OrderDecision => KIND_ORDER_DECISION", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_order_decision_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_decision_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_order_decision_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_decision_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderDecisionRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderDecisionRecord", + "pub fn reduce_order_events", ], }, ]; @@ -751,28 +748,28 @@ const TRADE_ORDER_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = [ const TRADE_ORDER_REVISION_PROPOSED_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_ORDER_REVISION: u32 = 3424;"], + required_fragments: &["pub const KIND_ORDER_REVISION_PROPOSAL: u32 = 3424;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub struct RadrootsTradeOrderRevisionProposed", - "Self::TradeOrderRevisionProposed => KIND_TRADE_ORDER_REVISION", + "pub struct RadrootsOrderRevisionProposal", + "Self::OrderRevisionProposed => KIND_ORDER_REVISION_PROPOSAL", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_order_revision_proposal_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_revision_proposal_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_order_revision_proposal_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_revision_proposal_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderRevisionProposalRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderRevisionProposalRecord", + "pub fn reduce_order_events", ], }, ]; @@ -780,29 +777,29 @@ const TRADE_ORDER_REVISION_PROPOSED_WITNESSES: [EventBoundarySourceWitness; 5] = const TRADE_ORDER_REVISION_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_ORDER_REVISION_RESPONSE: u32 = 3425;"], + required_fragments: &["pub const KIND_ORDER_REVISION_DECISION: u32 = 3425;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub enum RadrootsTradeOrderRevisionDecision", - "pub struct RadrootsTradeOrderRevisionDecisionEvent", - "Self::TradeOrderRevisionDecision => KIND_TRADE_ORDER_REVISION_RESPONSE", + "pub enum RadrootsOrderRevisionOutcome", + "pub struct RadrootsOrderRevisionDecision", + "Self::OrderRevisionDecision => KIND_ORDER_REVISION_DECISION", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_order_revision_decision_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_revision_decision_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_order_revision_decision_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_revision_decision_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderRevisionDecisionRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderRevisionDecisionRecord", + "pub fn reduce_order_events", ], }, ]; @@ -810,28 +807,28 @@ const TRADE_ORDER_REVISION_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = const TRADE_ORDER_CANCELLED_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_CANCEL: u32 = 3432;"], + required_fragments: &["pub const KIND_ORDER_CANCELLATION: u32 = 3432;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub struct RadrootsTradeOrderCancelled", - "Self::TradeOrderCancelled => KIND_TRADE_CANCEL", + "pub struct RadrootsOrderCancellation", + "Self::OrderCancelled => KIND_ORDER_CANCELLATION", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_order_cancel_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_cancellation_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_order_cancel_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_cancellation_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderCancellationRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderCancellationRecord", + "pub fn reduce_order_events", ], }, ]; @@ -839,28 +836,28 @@ const TRADE_ORDER_CANCELLED_WITNESSES: [EventBoundarySourceWitness; 5] = [ const TRADE_FULFILLMENT_UPDATED_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_FULFILLMENT_UPDATE: u32 = 3433;"], + required_fragments: &["pub const KIND_ORDER_FULFILLMENT_UPDATE: u32 = 3433;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub struct RadrootsTradeFulfillmentUpdated", - "Self::TradeFulfillmentUpdated => KIND_TRADE_FULFILLMENT_UPDATE", + "pub struct RadrootsOrderFulfillmentUpdate", + "Self::FulfillmentUpdated => KIND_ORDER_FULFILLMENT_UPDATE", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_fulfillment_update_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_fulfillment_update_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_fulfillment_update_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_fulfillment_update_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderFulfillmentRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderFulfillmentRecord", + "pub fn reduce_order_events", ], }, ]; @@ -868,28 +865,28 @@ const TRADE_FULFILLMENT_UPDATED_WITNESSES: [EventBoundarySourceWitness; 5] = [ const TRADE_BUYER_RECEIPT_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_RECEIPT: u32 = 3434;"], + required_fragments: &["pub const KIND_ORDER_RECEIPT: u32 = 3434;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub struct RadrootsTradeBuyerReceipt", - "Self::TradeBuyerReceipt => KIND_TRADE_RECEIPT", + "pub struct RadrootsOrderReceipt", + "Self::BuyerReceipt => KIND_ORDER_RECEIPT", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_buyer_receipt_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_receipt_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_buyer_receipt_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_receipt_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderReceiptRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderReceiptRecord", + "pub fn reduce_order_events", ], }, ]; @@ -897,28 +894,28 @@ const TRADE_BUYER_RECEIPT_WITNESSES: [EventBoundarySourceWitness; 5] = [ const TRADE_PAYMENT_RECORDED_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_PAYMENT_RECORDED: u32 = 3435;"], + required_fragments: &["pub const KIND_ORDER_PAYMENT_RECORD: u32 = 3435;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub struct RadrootsTradePaymentRecorded", - "Self::TradePaymentRecorded => KIND_TRADE_PAYMENT_RECORDED", + "pub struct RadrootsOrderPaymentRecord", + "Self::PaymentRecorded => KIND_ORDER_PAYMENT_RECORD", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_payment_recorded_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_payment_record_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_payment_recorded_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_payment_record_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderPaymentRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderPaymentEventRecord", + "pub fn reduce_order_events", ], }, ]; @@ -926,29 +923,29 @@ const TRADE_PAYMENT_RECORDED_WITNESSES: [EventBoundarySourceWitness; 5] = [ const TRADE_SETTLEMENT_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = [ EventBoundarySourceWitness { relative_path: "crates/events/src/kinds.rs", - required_fragments: &["pub const KIND_TRADE_SETTLEMENT_DECISION: u32 = 3436;"], + required_fragments: &["pub const KIND_ORDER_SETTLEMENT_DECISION: u32 = 3436;"], }, EventBoundarySourceWitness { - relative_path: "crates/events/src/trade.rs", + relative_path: "crates/events/src/order.rs", required_fragments: &[ - "pub enum RadrootsTradeSettlementDecision", - "pub struct RadrootsTradeSettlementDecisionEvent", - "Self::TradeSettlementDecision => KIND_TRADE_SETTLEMENT_DECISION", + "pub enum RadrootsOrderSettlementOutcome", + "pub struct RadrootsOrderSettlementDecision", + "Self::SettlementDecision => KIND_ORDER_SETTLEMENT_DECISION", ], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/encode.rs", - required_fragments: &["pub fn active_trade_settlement_decision_event_build"], + relative_path: "crates/events_codec/src/order/encode.rs", + required_fragments: &["pub fn order_settlement_decision_event_build"], }, EventBoundarySourceWitness { - relative_path: "crates/events_codec/src/trade/decode.rs", - required_fragments: &["pub fn active_trade_settlement_decision_from_event"], + relative_path: "crates/events_codec/src/order/decode.rs", + required_fragments: &["pub fn order_settlement_decision_from_event"], }, EventBoundarySourceWitness { relative_path: "crates/trade/src/order.rs", required_fragments: &[ - "pub struct RadrootsActiveOrderSettlementRecord", - "pub fn reduce_active_order_events", + "pub struct RadrootsOrderSettlementRecord", + "pub fn reduce_order_events", ], }, ]; @@ -4267,7 +4264,7 @@ public = [ "RadrootsNostrEvent", "RadrootsNostrEventRef", "RadrootsNostrEventPtr", - "RadrootsTradeListingAddress", + "RadrootsOrderListingAddress", "RadrootsProfile", "RadrootsFarm", "RadrootsListing", diff --git a/policy/coverage/policy.toml b/policy/coverage/policy.toml @@ -22,9 +22,9 @@ reason = "heavy-development branch coverage gap for publish 0.1.0-alpha.2" fail_under_exec_lines = 90.0 fail_under_functions = 90.0 fail_under_regions = 90.0 -fail_under_branches = 73.638 +fail_under_branches = 70.0 temporary = true -reason = "heavy-development branch coverage gap for publish 0.1.0-alpha.2" +reason = "heavy-development order reducer branch coverage gap after first-pass order refactor" [overrides.radroots_events_codec] fail_under_exec_lines = 90.0 diff --git a/scripts/ci/guard_no_legacy_identifiers.sh b/scripts/ci/guard_no_legacy_identifiers.sh @@ -1,17 +1,37 @@ #!/usr/bin/env bash set -euo pipefail -matches="$( - git grep -nI 'tangle' -- . \ - ':(exclude)AGENTS.md' \ - ':(exclude)scripts/ci/guard_no_legacy_identifiers.sh' || - true -)" +scan_forbidden() { + local label="$1" + local pattern="$2" + shift 2 -if [[ -n $matches ]]; then - echo "legacy identifier 'tangle' is forbidden in tracked oss files" - echo "$matches" - exit 1 -fi + local matches + matches="$( + rg -nI \ + --glob '!AGENTS.md' \ + --glob '!scripts/ci/guard_no_legacy_identifiers.sh' \ + -- "$pattern" "$@" || + true + )" -echo "no legacy 'tangle' identifiers found in tracked oss files" + if [[ -n $matches ]]; then + echo "$label is forbidden in oss source files" + echo "$matches" + exit 1 + fi +} + +scan_forbidden "legacy identifier 'tangle'" "tangle" . + +scan_forbidden \ + "legacy broad trade event identifier" \ + "RadrootsTradeMessageType|RadrootsTradeEnvelope|RadrootsTradeMessagePayload|RadrootsTradeQuestion|RadrootsTradeAnswer|RadrootsTradeDiscount|RadrootsTradeOrder|RadrootsActiveOrder|RadrootsActiveTrade|RadrootsTradeListingParseError|RadrootsTradeDomain|TradeListingParseError|TradeListingEnvelope|TradeListingMessage|KIND_TRADE_ORDER|TRADE_LISTING_KINDS|build_envelope_draft|parse_envelope|public_trade|events::trade::|events_codec::trade::|radroots_sdk::trade::|trade_order_economics_digest|trade_revision|trade_lifecycle|reduce_active_order|canonicalize_active_order|active_trade_|ActiveOrder|active_order|active order|active trade|RADROOTS_TRADE_(LISTING_DOMAIN|ENVELOPE_VERSION)" \ + crates spec scripts + +scan_forbidden \ + "legacy broad trade listing kind constant" \ + "KIND_TRADE_LISTING_(ORDER|QUESTION|ANSWER|DISCOUNT|CANCEL|FULFILLMENT|RECEIPT)" \ + crates spec scripts + +echo "no legacy identifiers found in oss source files" diff --git a/spec/RCLD.md b/spec/RCLD.md @@ -47,7 +47,7 @@ The following decisions are approved and are treated as default design constrain - external SDKs optimize first for third-party app integrations, not for full Radroots internal app parity - publishing is the first-class use case - reading and validation are supported for the same Tier 1 domains, but remain secondary to publishing -- Tier 1 domains are `profile`, `farm`, `listing`, and `trade` +- Tier 1 domains are `profile`, `farm`, `listing`, `order`, and `trade_validation` - the public contract unit is an operation, not a crate - networking and signing remain native to each target language - TypeScript is the first reference SDK for the new contract @@ -75,7 +75,7 @@ The following decisions are approved and are treated as default design constrain The public SDK contract is designed for: -- third-party apps publishing Radroots-compliant profiles, farms, listings, and trade events +- third-party apps publishing Radroots-compliant profiles, farms, listings, and order events - apps that need to parse or validate those supported event families - language SDK maintainers implementing contract-compliant APIs in TypeScript, Python, Swift, and Kotlin @@ -110,10 +110,10 @@ Examples: - `listing.build_tags` - `listing.build_draft` - `listing.parse_event` -- `trade.build_envelope_draft` -- `trade.parse_envelope` -- `trade.parse_listing_address` -- `trade.validate_listing_event` +- `order.build_order_request_draft` +- `order.parse_order_request` +- `order.parse_listing_address` +- `trade_validation.validate_listing_event` ### 2. Shared Types @@ -125,7 +125,7 @@ Examples: - `UnsignedEventDraft` - `RadrootsNostrEvent` - `RadrootsNostrEventRef` -- `RadrootsTradeListingAddress` +- `RadrootsOrderListingAddress` ### 3. Shared Errors @@ -135,7 +135,7 @@ Examples: - event encode errors - listing parse errors -- trade envelope parse errors +- order envelope parse errors - listing validation errors ### 4. Implementation Provenance @@ -150,7 +150,7 @@ Examples: ## Tier 1 Domains And Operations -The initial approved public domains are `profile`, `farm`, `listing`, and `trade`. +The initial approved public domains are `profile`, `farm`, `listing`, `order`, and `trade_validation`. The following operations form the recommended Tier 1 surface. @@ -279,24 +279,24 @@ Determinism: ### Trade -#### `trade.build_envelope_draft` +#### `order.build_order_request_draft` -Purpose: produce an unsigned trade envelope event from typed trade payload input +Purpose: produce an unsigned order envelope event from typed order payload input Rust implementation sources: -- `crates/events_codec/src/trade/encode.rs` +- `crates/events_codec/src/order/encode.rs` Input: - recipient pubkey -- trade message type +- order event type - listing address - optional order id - optional listing event pointer - optional root event id - optional previous event id -- typed trade payload +- typed order payload Output: @@ -306,13 +306,13 @@ Determinism: - required -#### `trade.parse_envelope` +#### `order.parse_order_request` -Purpose: parse a trade event into a typed trade envelope +Purpose: parse a order event into a typed order envelope Rust implementation sources: -- `crates/events_codec/src/trade/decode.rs` +- `crates/events_codec/src/order/decode.rs` Input: @@ -320,19 +320,19 @@ Input: Output: -- typed `RadrootsTradeEnvelope<T>` +- typed `RadrootsOrderEnvelope<T>` Determinism: - required -#### `trade.parse_listing_address` +#### `order.parse_listing_address` Purpose: parse and validate the canonical listing address used by trade flows Rust implementation sources: -- `crates/events_codec/src/trade/decode.rs` +- `crates/events_codec/src/order/decode.rs` Input: @@ -340,13 +340,13 @@ Input: Output: -- `RadrootsTradeListingAddress` +- `RadrootsOrderListingAddress` Determinism: - required -#### `trade.validate_listing_event` +#### `trade_validation.validate_listing_event` Purpose: validate that an event meets Radroots listing contract expectations for trade workflows @@ -379,7 +379,7 @@ Recommended Tier 1 shared types: - `RadrootsNostrEvent` - `RadrootsNostrEventRef` - `RadrootsNostrEventPtr` -- `RadrootsTradeListingAddress` +- `RadrootsOrderListingAddress` - public model types required by Tier 1 operations: - `RadrootsProfile` - `RadrootsFarm` @@ -506,7 +506,7 @@ public = [ "RadrootsNostrEvent", "RadrootsNostrEventRef", "RadrootsNostrEventPtr", - "RadrootsTradeListingAddress", + "RadrootsOrderListingAddress", "RadrootsProfile", "RadrootsFarm", "RadrootsListing", @@ -605,16 +605,16 @@ networking = "native" "listing.build_tags" = "listing.buildTags" "listing.build_draft" = "listing.buildDraft" "listing.parse_event" = "listing.parseEvent" -"trade.build_envelope_draft" = "trade.buildEnvelopeDraft" -"trade.parse_envelope" = "trade.parseEnvelope" -"trade.parse_listing_address" = "trade.parseListingAddress" -"trade.validate_listing_event" = "trade.validateListingEvent" +"order.build_order_request_draft" = "order.buildOrderRequestDraft" +"order.parse_order_request" = "order.parseOrderRequest" +"order.parse_listing_address" = "order.parseListingAddress" +"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" [shared_types] "WireEventParts" = "WireEventParts" "UnsignedEventDraft" = "UnsignedEventDraft" "RadrootsNostrEvent" = "RadrootsNostrEvent" -"RadrootsTradeListingAddress" = "TradeListingAddress" +"RadrootsOrderListingAddress" = "OrderListingAddress" [artifacts] models_dir = "src/generated" @@ -677,8 +677,8 @@ spec/conformance/ build_draft.v1.json parse_event.v1.json trade/ - build_envelope_draft.v1.json - parse_envelope.v1.json + build_order_request_draft.v1.json + parse_order_request.v1.json parse_listing_address.v1.json validate_listing_event.v1.json ``` diff --git a/spec/conformance/vectors/order/build_order_decision_draft.v1.json b/spec/conformance/vectors/order/build_order_decision_draft.v1.json @@ -0,0 +1,36 @@ +{ + "suite": "order", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "order_build_order_decision_draft_accept_minimal_001", + "kind": "order.build_order_decision_draft", + "input": { + "root_event_id": "order-request-event", + "prev_event_id": "order-request-event", + "payload": { + "order_id": "order-1", + "listing_addr": "30402:seller_pubkey:AAAAAAAAAAAAAAAAAAAAAg", + "buyer_pubkey": "buyer_pubkey", + "seller_pubkey": "seller_pubkey", + "decision": { + "decision": "accepted", + "inventory_commitments": [ + { + "bin_id": "bin-1", + "bin_count": 2 + } + ] + } + } + }, + "expected": { + "wire_parts": { + "kind": 3423, + "content_type": "TradeOrderDecision", + "tags_shape": "order_order_decision_tags" + } + } + } + ] +} diff --git a/spec/conformance/vectors/order/build_order_request_draft.v1.json b/spec/conformance/vectors/order/build_order_request_draft.v1.json @@ -0,0 +1,74 @@ +{ + "suite": "order", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "order_build_order_request_draft_minimal_001", + "kind": "order.build_order_request_draft", + "input": { + "listing_event": { + "id": "listing-snapshot", + "relays": "wss://relay.example.com" + }, + "payload": { + "order_id": "order-1", + "listing_addr": "30402:seller_pubkey:AAAAAAAAAAAAAAAAAAAAAg", + "buyer_pubkey": "buyer_pubkey", + "seller_pubkey": "seller_pubkey", + "items": [ + { + "bin_id": "bin-1", + "bin_count": 2 + } + ], + "economics": { + "quote_id": "quote-1", + "quote_version": 1, + "pricing_basis": "listing_event", + "currency": "USD", + "items": [ + { + "bin_id": "bin-1", + "bin_count": 2, + "quantity_amount": "1", + "quantity_unit": "each", + "unit_price_amount": "5", + "unit_price_currency": "USD", + "line_subtotal": { + "amount": "10", + "currency": "USD" + } + } + ], + "discounts": [], + "adjustments": [], + "subtotal": { + "amount": "10", + "currency": "USD" + }, + "discount_total": { + "amount": "0", + "currency": "USD" + }, + "adjustment_total": { + "amount": "0", + "currency": "USD" + }, + "total": { + "amount": "10", + "currency": "USD" + } + } + } + }, + "expected": { + "wire_parts": { + "kind": 3422, + "content_type": "TradeOrderRequested", + "tags_shape": "order_order_request_tags", + "payload_shape": "order_order_request_with_economics" + } + } + } + ] +} diff --git a/spec/conformance/vectors/order/parse_listing_address.v1.json b/spec/conformance/vectors/order/parse_listing_address.v1.json @@ -0,0 +1,16 @@ +{ + "suite": "order", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "order_parse_listing_address_minimal_001", + "kind": "order.parse_listing_address", + "input": { + "listing_addr": "30402:seller_pubkey:AAAAAAAAAAAAAAAAAAAAAg" + }, + "expected": { + "address_shape": "trade_listing_address" + } + } + ] +} diff --git a/spec/conformance/vectors/order/parse_order_decision.v1.json b/spec/conformance/vectors/order/parse_order_decision.v1.json @@ -0,0 +1,22 @@ +{ + "suite": "order", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "order_parse_order_decision_minimal_001", + "kind": "order.parse_order_decision", + "input": { + "event": { + "kind": 3423, + "author": "seller_pubkey", + "content_type": "TradeOrderDecision", + "tags_shape": "order_order_decision_tags" + } + }, + "expected": { + "envelope_shape": "order_order_decision", + "payload_type": "RadrootsOrderDecision" + } + } + ] +} diff --git a/spec/conformance/vectors/order/parse_order_request.v1.json b/spec/conformance/vectors/order/parse_order_request.v1.json @@ -0,0 +1,31 @@ +{ + "suite": "order", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "order_parse_order_request_minimal_001", + "kind": "order.parse_order_request", + "input": { + "event": { + "kind": 3422, + "author": "buyer_pubkey", + "content_type": "TradeOrderRequested", + "tags_shape": "order_order_request_tags", + "payload_shape": "order_order_request_with_economics" + } + }, + "expected": { + "envelope_shape": "order_order_request", + "payload_type": "RadrootsOrderRequest", + "required_payload_fields": [ + "order_id", + "listing_addr", + "buyer_pubkey", + "seller_pubkey", + "items", + "economics" + ] + } + } + ] +} diff --git a/spec/conformance/vectors/trade/.gitkeep b/spec/conformance/vectors/trade/.gitkeep @@ -1 +0,0 @@ - diff --git a/spec/conformance/vectors/trade/build_envelope_draft.v1.json b/spec/conformance/vectors/trade/build_envelope_draft.v1.json @@ -1,23 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_build_envelope_draft_minimal_001", - "kind": "trade.build_envelope_draft", - "input": { - "recipient_pubkey": "recipient_pubkey", - "message_type": "order_request", - "listing_addr": "30402:seller_pubkey:AAAAAAAAAAAAAAAAAAAAAg", - "payload_shape": "trade_message_payload" - }, - "expected": { - "wire_parts": { - "kind": "trade", - "content_shape": "trade_envelope_json", - "tags_shape": "trade_envelope_tags" - } - } - } - ] -} diff --git a/spec/conformance/vectors/trade/build_order_decision_draft.v1.json b/spec/conformance/vectors/trade/build_order_decision_draft.v1.json @@ -1,36 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_build_order_decision_draft_accept_minimal_001", - "kind": "trade.build_order_decision_draft", - "input": { - "root_event_id": "order-request-event", - "prev_event_id": "order-request-event", - "payload": { - "order_id": "order-1", - "listing_addr": "30402:seller_pubkey:AAAAAAAAAAAAAAAAAAAAAg", - "buyer_pubkey": "buyer_pubkey", - "seller_pubkey": "seller_pubkey", - "decision": { - "decision": "accepted", - "inventory_commitments": [ - { - "bin_id": "bin-1", - "bin_count": 2 - } - ] - } - } - }, - "expected": { - "wire_parts": { - "kind": 3423, - "content_type": "TradeOrderDecision", - "tags_shape": "active_trade_order_decision_tags" - } - } - } - ] -} diff --git a/spec/conformance/vectors/trade/build_order_request_draft.v1.json b/spec/conformance/vectors/trade/build_order_request_draft.v1.json @@ -1,74 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_build_order_request_draft_minimal_001", - "kind": "trade.build_order_request_draft", - "input": { - "listing_event": { - "id": "listing-snapshot", - "relays": "wss://relay.example.com" - }, - "payload": { - "order_id": "order-1", - "listing_addr": "30402:seller_pubkey:AAAAAAAAAAAAAAAAAAAAAg", - "buyer_pubkey": "buyer_pubkey", - "seller_pubkey": "seller_pubkey", - "items": [ - { - "bin_id": "bin-1", - "bin_count": 2 - } - ], - "economics": { - "quote_id": "quote-1", - "quote_version": 1, - "pricing_basis": "listing_event", - "currency": "USD", - "items": [ - { - "bin_id": "bin-1", - "bin_count": 2, - "quantity_amount": "1", - "quantity_unit": "each", - "unit_price_amount": "5", - "unit_price_currency": "USD", - "line_subtotal": { - "amount": "10", - "currency": "USD" - } - } - ], - "discounts": [], - "adjustments": [], - "subtotal": { - "amount": "10", - "currency": "USD" - }, - "discount_total": { - "amount": "0", - "currency": "USD" - }, - "adjustment_total": { - "amount": "0", - "currency": "USD" - }, - "total": { - "amount": "10", - "currency": "USD" - } - } - } - }, - "expected": { - "wire_parts": { - "kind": 3422, - "content_type": "TradeOrderRequested", - "tags_shape": "active_trade_order_request_tags", - "payload_shape": "active_trade_order_request_with_economics" - } - } - } - ] -} diff --git a/spec/conformance/vectors/trade/parse_envelope.v1.json b/spec/conformance/vectors/trade/parse_envelope.v1.json @@ -1,20 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_parse_envelope_minimal_001", - "kind": "trade.parse_envelope", - "input": { - "event": { - "kind": "trade", - "content_shape": "trade_envelope_json", - "tags_shape": "trade_envelope_tags" - } - }, - "expected": { - "envelope_shape": "trade_envelope" - } - } - ] -} diff --git a/spec/conformance/vectors/trade/parse_listing_address.v1.json b/spec/conformance/vectors/trade/parse_listing_address.v1.json @@ -1,16 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_parse_listing_address_minimal_001", - "kind": "trade.parse_listing_address", - "input": { - "listing_addr": "30402:seller_pubkey:AAAAAAAAAAAAAAAAAAAAAg" - }, - "expected": { - "address_shape": "trade_listing_address" - } - } - ] -} diff --git a/spec/conformance/vectors/trade/parse_order_decision.v1.json b/spec/conformance/vectors/trade/parse_order_decision.v1.json @@ -1,22 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_parse_order_decision_minimal_001", - "kind": "trade.parse_order_decision", - "input": { - "event": { - "kind": 3423, - "author": "seller_pubkey", - "content_type": "TradeOrderDecision", - "tags_shape": "active_trade_order_decision_tags" - } - }, - "expected": { - "envelope_shape": "active_trade_order_decision", - "payload_type": "RadrootsTradeOrderDecisionEvent" - } - } - ] -} diff --git a/spec/conformance/vectors/trade/parse_order_request.v1.json b/spec/conformance/vectors/trade/parse_order_request.v1.json @@ -1,31 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_parse_order_request_minimal_001", - "kind": "trade.parse_order_request", - "input": { - "event": { - "kind": 3422, - "author": "buyer_pubkey", - "content_type": "TradeOrderRequested", - "tags_shape": "active_trade_order_request_tags", - "payload_shape": "active_trade_order_request_with_economics" - } - }, - "expected": { - "envelope_shape": "active_trade_order_request", - "payload_type": "RadrootsTradeOrderRequested", - "required_payload_fields": [ - "order_id", - "listing_addr", - "buyer_pubkey", - "seller_pubkey", - "items", - "economics" - ] - } - } - ] -} diff --git a/spec/conformance/vectors/trade/validate_listing_event.v1.json b/spec/conformance/vectors/trade/validate_listing_event.v1.json @@ -1,20 +0,0 @@ -{ - "suite": "trade", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "trade_validate_listing_event_minimal_001", - "kind": "trade.validate_listing_event", - "input": { - "event": { - "kind": "listing", - "content_shape": "listing_json_or_markdown", - "tags_shape": "listing_tags_full" - } - }, - "expected": { - "validation_shape": "trade_listing_validation_result" - } - } - ] -} diff --git a/spec/conformance/vectors/trade_validation/validate_listing_event.v1.json b/spec/conformance/vectors/trade_validation/validate_listing_event.v1.json @@ -0,0 +1,20 @@ +{ + "suite": "trade_validation", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "trade_validation_validate_listing_event_minimal_001", + "kind": "trade_validation.validate_listing_event", + "input": { + "event": { + "kind": "listing", + "content_shape": "listing_json_or_markdown", + "tags_shape": "listing_tags_full" + } + }, + "expected": { + "validation_shape": "trade_listing_validation_result" + } + } + ] +} diff --git a/spec/operations.toml b/spec/operations.toml @@ -4,7 +4,7 @@ version = "0.1.0-alpha.2" source = "rust" [public] -domains = ["profile", "farm", "listing", "trade", "social"] +domains = ["profile", "farm", "listing", "order", "trade_validation", "social"] [shared_types] public = [ @@ -13,7 +13,7 @@ public = [ "RadrootsNostrEvent", "RadrootsNostrEventRef", "RadrootsNostrEventPtr", - "RadrootsTradeListingAddress", + "RadrootsOrderListingAddress", "RadrootsProfile", "RadrootsFarm", "RadrootsListing", @@ -24,22 +24,21 @@ public = [ "RadrootsFileMetadata", "RadrootsCalendarDateEvent", "RadrootsCalendarTimeEvent", - "RadrootsTradeEnvelope", - "RadrootsActiveTradeEnvelope", - "RadrootsActiveTradeMessageType", - "RadrootsTradeOrderItem", - "RadrootsTradePricingBasis", - "RadrootsTradeEconomicLineKind", - "RadrootsTradeEconomicActor", - "RadrootsTradeEconomicEffect", - "RadrootsTradeOrderEconomicItem", - "RadrootsTradeOrderEconomicLine", - "RadrootsTradeOrderEconomicTotals", - "RadrootsTradeOrderEconomics", - "RadrootsTradeOrderRequested", - "RadrootsTradeInventoryCommitment", - "RadrootsTradeOrderDecision", - "RadrootsTradeOrderDecisionEvent", + "RadrootsOrderEnvelope", + "RadrootsOrderEventType", + "RadrootsOrderItem", + "RadrootsOrderPricingBasis", + "RadrootsOrderEconomicLineKind", + "RadrootsOrderEconomicActor", + "RadrootsOrderEconomicEffect", + "RadrootsOrderEconomicItem", + "RadrootsOrderEconomicLine", + "RadrootsOrderEconomicTotals", + "RadrootsOrderEconomics", + "RadrootsOrderRequest", + "RadrootsOrderInventoryCommitment", + "RadrootsOrderDecisionOutcome", + "RadrootsOrderDecision", ] [errors] @@ -57,7 +56,7 @@ wasm_crates = ["radroots_events_codec_wasm"] [sdk] rust_package = "radroots_sdk" -primary_domains = ["profile", "farm", "listing", "trade", "social"] +primary_domains = ["profile", "farm", "listing", "order", "trade_validation", "social"] public_surface = "operation_first" [operations.profile_build_draft] @@ -285,160 +284,112 @@ rust_types = ["radroots_events::calendar::RadrootsCalendarTimeEvent"] [operations.social_calendar_time_event_build_tags.conformance] vector = "spec/conformance/vectors/social/mvp.v1.json" -[operations.trade_build_envelope_draft] -domain = "trade" -id = "trade.build_envelope_draft" +[operations.order_build_order_request_draft] +domain = "order" +id = "order.build_order_request_draft" stability = "beta" -inputs = [ - "recipient_pubkey", - "message_type", - "listing_addr", - "order_id?", - "listing_event?", - "root_event_id?", - "prev_event_id?", - "RadrootsTradeMessagePayload", -] +inputs = ["RadrootsOrderRequest", "RadrootsNostrEventPtr"] outputs = ["WireEventParts"] error_class = "encode_error" deterministic = true signing = "native" transport = "native" -[operations.trade_build_envelope_draft.implementation] -rust_modules = ["crates/events_codec/src/trade/encode.rs"] -rust_types = ["radroots_events::trade::RadrootsTradeEnvelope"] - -[operations.trade_build_envelope_draft.conformance] -vector = "spec/conformance/vectors/trade/build_envelope_draft.v1.json" - -[operations.trade_build_order_request_draft] -domain = "trade" -id = "trade.build_order_request_draft" -stability = "beta" -inputs = ["RadrootsTradeOrderRequested", "RadrootsNostrEventPtr"] -outputs = ["WireEventParts"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.trade_build_order_request_draft.implementation] -rust_modules = ["crates/events_codec/src/trade/encode.rs"] +[operations.order_build_order_request_draft.implementation] +rust_modules = ["crates/events_codec/src/order/encode.rs"] rust_types = [ "radroots_events::RadrootsNostrEventPtr", - "radroots_events::trade::RadrootsTradeOrderRequested", + "radroots_events::order::RadrootsOrderRequest", ] -[operations.trade_build_order_request_draft.conformance] -vector = "spec/conformance/vectors/trade/build_order_request_draft.v1.json" +[operations.order_build_order_request_draft.conformance] +vector = "spec/conformance/vectors/order/build_order_request_draft.v1.json" -[operations.trade_build_order_decision_draft] -domain = "trade" -id = "trade.build_order_decision_draft" +[operations.order_build_order_decision_draft] +domain = "order" +id = "order.build_order_decision_draft" stability = "beta" -inputs = ["root_event_id", "prev_event_id", "RadrootsTradeOrderDecisionEvent"] +inputs = ["root_event_id", "prev_event_id", "RadrootsOrderDecision"] outputs = ["WireEventParts"] error_class = "encode_error" deterministic = true signing = "native" transport = "native" -[operations.trade_build_order_decision_draft.implementation] -rust_modules = ["crates/events_codec/src/trade/encode.rs"] -rust_types = ["radroots_events::trade::RadrootsTradeOrderDecisionEvent"] - -[operations.trade_build_order_decision_draft.conformance] -vector = "spec/conformance/vectors/trade/build_order_decision_draft.v1.json" - -[operations.trade_parse_envelope] -domain = "trade" -id = "trade.parse_envelope" -stability = "beta" -inputs = ["RadrootsNostrEvent"] -outputs = ["RadrootsTradeEnvelope"] -error_class = "parse_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.trade_parse_envelope.implementation] -rust_modules = ["crates/events_codec/src/trade/decode.rs"] -rust_types = [ - "radroots_events::RadrootsNostrEvent", - "radroots_events::trade::RadrootsTradeEnvelope", -] +[operations.order_build_order_decision_draft.implementation] +rust_modules = ["crates/events_codec/src/order/encode.rs"] +rust_types = ["radroots_events::order::RadrootsOrderDecision"] -[operations.trade_parse_envelope.conformance] -vector = "spec/conformance/vectors/trade/parse_envelope.v1.json" +[operations.order_build_order_decision_draft.conformance] +vector = "spec/conformance/vectors/order/build_order_decision_draft.v1.json" -[operations.trade_parse_order_request] -domain = "trade" -id = "trade.parse_order_request" +[operations.order_parse_order_request] +domain = "order" +id = "order.parse_order_request" stability = "beta" inputs = ["RadrootsNostrEvent"] -outputs = ["RadrootsActiveTradeEnvelope", "RadrootsTradeOrderRequested"] +outputs = ["RadrootsOrderEnvelope", "RadrootsOrderRequest"] error_class = "parse_error" deterministic = true signing = "native" transport = "native" -[operations.trade_parse_order_request.implementation] -rust_modules = ["crates/events_codec/src/trade/decode.rs"] +[operations.order_parse_order_request.implementation] +rust_modules = ["crates/events_codec/src/order/decode.rs"] rust_types = [ "radroots_events::RadrootsNostrEvent", - "radroots_events::trade::RadrootsActiveTradeEnvelope", - "radroots_events::trade::RadrootsTradeOrderRequested", + "radroots_events::order::RadrootsOrderEnvelope", + "radroots_events::order::RadrootsOrderRequest", ] -[operations.trade_parse_order_request.conformance] -vector = "spec/conformance/vectors/trade/parse_order_request.v1.json" +[operations.order_parse_order_request.conformance] +vector = "spec/conformance/vectors/order/parse_order_request.v1.json" -[operations.trade_parse_order_decision] -domain = "trade" -id = "trade.parse_order_decision" +[operations.order_parse_order_decision] +domain = "order" +id = "order.parse_order_decision" stability = "beta" inputs = ["RadrootsNostrEvent"] -outputs = ["RadrootsActiveTradeEnvelope", "RadrootsTradeOrderDecisionEvent"] +outputs = ["RadrootsOrderEnvelope", "RadrootsOrderDecision"] error_class = "parse_error" deterministic = true signing = "native" transport = "native" -[operations.trade_parse_order_decision.implementation] -rust_modules = ["crates/events_codec/src/trade/decode.rs"] +[operations.order_parse_order_decision.implementation] +rust_modules = ["crates/events_codec/src/order/decode.rs"] rust_types = [ "radroots_events::RadrootsNostrEvent", - "radroots_events::trade::RadrootsActiveTradeEnvelope", - "radroots_events::trade::RadrootsTradeOrderDecisionEvent", + "radroots_events::order::RadrootsOrderEnvelope", + "radroots_events::order::RadrootsOrderDecision", ] -[operations.trade_parse_order_decision.conformance] -vector = "spec/conformance/vectors/trade/parse_order_decision.v1.json" +[operations.order_parse_order_decision.conformance] +vector = "spec/conformance/vectors/order/parse_order_decision.v1.json" -[operations.trade_parse_listing_address] -domain = "trade" -id = "trade.parse_listing_address" +[operations.order_parse_listing_address] +domain = "order" +id = "order.parse_listing_address" stability = "beta" inputs = ["listing_addr"] -outputs = ["RadrootsTradeListingAddress"] +outputs = ["RadrootsOrderListingAddress"] error_class = "address_error" deterministic = true signing = "native" transport = "native" -[operations.trade_parse_listing_address.implementation] -rust_modules = ["crates/events_codec/src/trade/decode.rs"] +[operations.order_parse_listing_address.implementation] +rust_modules = ["crates/events_codec/src/order/decode.rs"] rust_types = [ - "radroots_events_codec::trade::decode::RadrootsTradeListingAddress", + "radroots_events_codec::order::decode::RadrootsOrderListingAddress", ] -[operations.trade_parse_listing_address.conformance] -vector = "spec/conformance/vectors/trade/parse_listing_address.v1.json" +[operations.order_parse_listing_address.conformance] +vector = "spec/conformance/vectors/order/parse_listing_address.v1.json" -[operations.trade_validate_listing_event] -domain = "trade" -id = "trade.validate_listing_event" +[operations.trade_validation_validate_listing_event] +domain = "trade_validation" +id = "trade_validation.validate_listing_event" stability = "beta" inputs = ["RadrootsNostrEvent"] outputs = ["TradeListingValidateResult"] @@ -447,12 +398,12 @@ deterministic = true signing = "native" transport = "native" -[operations.trade_validate_listing_event.implementation] +[operations.trade_validation_validate_listing_event.implementation] rust_modules = ["crates/trade/src/listing/validation.rs"] rust_types = [ "radroots_events::RadrootsNostrEvent", "radroots_trade::listing::validation::RadrootsTradeListing", ] -[operations.trade_validate_listing_event.conformance] -vector = "spec/conformance/vectors/trade/validate_listing_event.v1.json" +[operations.trade_validation_validate_listing_event.conformance] +vector = "spec/conformance/vectors/trade_validation/validate_listing_event.v1.json" diff --git a/spec/sdk-exports/go.toml b/spec/sdk-exports/go.toml @@ -25,14 +25,12 @@ order = 3 "social.file_metadata.build_tags" = "social.FileMetadataBuildTags" "social.calendar_date_event.build_tags" = "social.CalendarDateEventBuildTags" "social.calendar_time_event.build_tags" = "social.CalendarTimeEventBuildTags" -"trade.build_envelope_draft" = "trade.BuildEnvelopeDraft" -"trade.build_order_request_draft" = "trade.BuildOrderRequestDraft" -"trade.build_order_decision_draft" = "trade.BuildOrderDecisionDraft" -"trade.parse_envelope" = "trade.ParseEnvelope" -"trade.parse_order_request" = "trade.ParseOrderRequest" -"trade.parse_order_decision" = "trade.ParseOrderDecision" -"trade.parse_listing_address" = "trade.ParseListingAddress" -"trade.validate_listing_event" = "trade.ValidateListingEvent" +"order.build_order_request_draft" = "order.BuildOrderRequestDraft" +"order.build_order_decision_draft" = "order.BuildOrderDecisionDraft" +"order.parse_order_request" = "order.ParseOrderRequest" +"order.parse_order_decision" = "order.ParseOrderDecision" +"order.parse_listing_address" = "order.ParseListingAddress" +"trade_validation.validate_listing_event" = "tradevalidation.ValidateListingEvent" [shared_types] "WireEventParts" = "WireEventParts" @@ -40,7 +38,7 @@ order = 3 "RadrootsNostrEvent" = "RadrootsNostrEvent" "RadrootsNostrEventRef" = "RadrootsNostrEventRef" "RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" -"RadrootsTradeListingAddress" = "TradeListingAddress" +"RadrootsOrderListingAddress" = "OrderListingAddress" "RadrootsProfile" = "RadrootsProfile" "RadrootsFarm" = "RadrootsFarm" "RadrootsListing" = "RadrootsListing" @@ -51,19 +49,18 @@ order = 3 "RadrootsFileMetadata" = "RadrootsFileMetadata" "RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" "RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" -"RadrootsTradeEnvelope" = "TradeEnvelope" -"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope" -"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType" -"RadrootsTradeOrderItem" = "TradeOrderItem" -"RadrootsTradePricingBasis" = "TradePricingBasis" -"RadrootsTradeEconomicLineKind" = "TradeEconomicLineKind" -"RadrootsTradeEconomicActor" = "TradeEconomicActor" -"RadrootsTradeEconomicEffect" = "TradeEconomicEffect" -"RadrootsTradeOrderEconomicItem" = "TradeOrderEconomicItem" -"RadrootsTradeOrderEconomicLine" = "TradeOrderEconomicLine" -"RadrootsTradeOrderEconomicTotals" = "TradeOrderEconomicTotals" -"RadrootsTradeOrderEconomics" = "TradeOrderEconomics" -"RadrootsTradeOrderRequested" = "TradeOrderRequested" -"RadrootsTradeInventoryCommitment" = "TradeInventoryCommitment" -"RadrootsTradeOrderDecision" = "TradeOrderDecision" -"RadrootsTradeOrderDecisionEvent" = "TradeOrderDecisionEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/spec/sdk-exports/kotlin.toml b/spec/sdk-exports/kotlin.toml @@ -25,14 +25,12 @@ order = 2 "social.file_metadata.build_tags" = "social.fileMetadata.buildTags" "social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags" "social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags" -"trade.build_envelope_draft" = "trade.buildEnvelopeDraft" -"trade.build_order_request_draft" = "trade.buildOrderRequestDraft" -"trade.build_order_decision_draft" = "trade.buildOrderDecisionDraft" -"trade.parse_envelope" = "trade.parseEnvelope" -"trade.parse_order_request" = "trade.parseOrderRequest" -"trade.parse_order_decision" = "trade.parseOrderDecision" -"trade.parse_listing_address" = "trade.parseListingAddress" -"trade.validate_listing_event" = "trade.validateListingEvent" +"order.build_order_request_draft" = "order.buildOrderRequestDraft" +"order.build_order_decision_draft" = "order.buildOrderDecisionDraft" +"order.parse_order_request" = "order.parseOrderRequest" +"order.parse_order_decision" = "order.parseOrderDecision" +"order.parse_listing_address" = "order.parseListingAddress" +"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" [shared_types] "WireEventParts" = "WireEventParts" @@ -40,7 +38,7 @@ order = 2 "RadrootsNostrEvent" = "RadrootsNostrEvent" "RadrootsNostrEventRef" = "RadrootsNostrEventRef" "RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" -"RadrootsTradeListingAddress" = "TradeListingAddress" +"RadrootsOrderListingAddress" = "OrderListingAddress" "RadrootsProfile" = "RadrootsProfile" "RadrootsFarm" = "RadrootsFarm" "RadrootsListing" = "RadrootsListing" @@ -51,19 +49,18 @@ order = 2 "RadrootsFileMetadata" = "RadrootsFileMetadata" "RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" "RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" -"RadrootsTradeEnvelope" = "TradeEnvelope" -"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope" -"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType" -"RadrootsTradeOrderItem" = "TradeOrderItem" -"RadrootsTradePricingBasis" = "TradePricingBasis" -"RadrootsTradeEconomicLineKind" = "TradeEconomicLineKind" -"RadrootsTradeEconomicActor" = "TradeEconomicActor" -"RadrootsTradeEconomicEffect" = "TradeEconomicEffect" -"RadrootsTradeOrderEconomicItem" = "TradeOrderEconomicItem" -"RadrootsTradeOrderEconomicLine" = "TradeOrderEconomicLine" -"RadrootsTradeOrderEconomicTotals" = "TradeOrderEconomicTotals" -"RadrootsTradeOrderEconomics" = "TradeOrderEconomics" -"RadrootsTradeOrderRequested" = "TradeOrderRequested" -"RadrootsTradeInventoryCommitment" = "TradeInventoryCommitment" -"RadrootsTradeOrderDecision" = "TradeOrderDecision" -"RadrootsTradeOrderDecisionEvent" = "TradeOrderDecisionEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/spec/sdk-exports/py.toml b/spec/sdk-exports/py.toml @@ -25,14 +25,12 @@ order = 3 "social.file_metadata.build_tags" = "social_file_metadata_build_tags" "social.calendar_date_event.build_tags" = "social_calendar_date_event_build_tags" "social.calendar_time_event.build_tags" = "social_calendar_time_event_build_tags" -"trade.build_envelope_draft" = "trade_build_envelope_draft" -"trade.build_order_request_draft" = "trade_build_order_request_draft" -"trade.build_order_decision_draft" = "trade_build_order_decision_draft" -"trade.parse_envelope" = "trade_parse_envelope" -"trade.parse_order_request" = "trade_parse_order_request" -"trade.parse_order_decision" = "trade_parse_order_decision" -"trade.parse_listing_address" = "trade_parse_listing_address" -"trade.validate_listing_event" = "trade_validate_listing_event" +"order.build_order_request_draft" = "order_build_order_request_draft" +"order.build_order_decision_draft" = "order_build_order_decision_draft" +"order.parse_order_request" = "order_parse_order_request" +"order.parse_order_decision" = "order_parse_order_decision" +"order.parse_listing_address" = "order_parse_listing_address" +"trade_validation.validate_listing_event" = "trade_validation_validate_listing_event" [shared_types] "WireEventParts" = "WireEventParts" @@ -40,7 +38,7 @@ order = 3 "RadrootsNostrEvent" = "RadrootsNostrEvent" "RadrootsNostrEventRef" = "RadrootsNostrEventRef" "RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" -"RadrootsTradeListingAddress" = "TradeListingAddress" +"RadrootsOrderListingAddress" = "OrderListingAddress" "RadrootsProfile" = "RadrootsProfile" "RadrootsFarm" = "RadrootsFarm" "RadrootsListing" = "RadrootsListing" @@ -51,19 +49,18 @@ order = 3 "RadrootsFileMetadata" = "RadrootsFileMetadata" "RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" "RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" -"RadrootsTradeEnvelope" = "TradeEnvelope" -"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope" -"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType" -"RadrootsTradeOrderItem" = "TradeOrderItem" -"RadrootsTradePricingBasis" = "TradePricingBasis" -"RadrootsTradeEconomicLineKind" = "TradeEconomicLineKind" -"RadrootsTradeEconomicActor" = "TradeEconomicActor" -"RadrootsTradeEconomicEffect" = "TradeEconomicEffect" -"RadrootsTradeOrderEconomicItem" = "TradeOrderEconomicItem" -"RadrootsTradeOrderEconomicLine" = "TradeOrderEconomicLine" -"RadrootsTradeOrderEconomicTotals" = "TradeOrderEconomicTotals" -"RadrootsTradeOrderEconomics" = "TradeOrderEconomics" -"RadrootsTradeOrderRequested" = "TradeOrderRequested" -"RadrootsTradeInventoryCommitment" = "TradeInventoryCommitment" -"RadrootsTradeOrderDecision" = "TradeOrderDecision" -"RadrootsTradeOrderDecisionEvent" = "TradeOrderDecisionEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/spec/sdk-exports/swift.toml b/spec/sdk-exports/swift.toml @@ -25,14 +25,12 @@ order = 2 "social.file_metadata.build_tags" = "social.fileMetadata.buildTags" "social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags" "social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags" -"trade.build_envelope_draft" = "trade.buildEnvelopeDraft" -"trade.build_order_request_draft" = "trade.buildOrderRequestDraft" -"trade.build_order_decision_draft" = "trade.buildOrderDecisionDraft" -"trade.parse_envelope" = "trade.parseEnvelope" -"trade.parse_order_request" = "trade.parseOrderRequest" -"trade.parse_order_decision" = "trade.parseOrderDecision" -"trade.parse_listing_address" = "trade.parseListingAddress" -"trade.validate_listing_event" = "trade.validateListingEvent" +"order.build_order_request_draft" = "order.buildOrderRequestDraft" +"order.build_order_decision_draft" = "order.buildOrderDecisionDraft" +"order.parse_order_request" = "order.parseOrderRequest" +"order.parse_order_decision" = "order.parseOrderDecision" +"order.parse_listing_address" = "order.parseListingAddress" +"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" [shared_types] "WireEventParts" = "WireEventParts" @@ -40,7 +38,7 @@ order = 2 "RadrootsNostrEvent" = "RadrootsNostrEvent" "RadrootsNostrEventRef" = "RadrootsNostrEventRef" "RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" -"RadrootsTradeListingAddress" = "TradeListingAddress" +"RadrootsOrderListingAddress" = "OrderListingAddress" "RadrootsProfile" = "RadrootsProfile" "RadrootsFarm" = "RadrootsFarm" "RadrootsListing" = "RadrootsListing" @@ -51,19 +49,18 @@ order = 2 "RadrootsFileMetadata" = "RadrootsFileMetadata" "RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" "RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" -"RadrootsTradeEnvelope" = "TradeEnvelope" -"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope" -"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType" -"RadrootsTradeOrderItem" = "TradeOrderItem" -"RadrootsTradePricingBasis" = "TradePricingBasis" -"RadrootsTradeEconomicLineKind" = "TradeEconomicLineKind" -"RadrootsTradeEconomicActor" = "TradeEconomicActor" -"RadrootsTradeEconomicEffect" = "TradeEconomicEffect" -"RadrootsTradeOrderEconomicItem" = "TradeOrderEconomicItem" -"RadrootsTradeOrderEconomicLine" = "TradeOrderEconomicLine" -"RadrootsTradeOrderEconomicTotals" = "TradeOrderEconomicTotals" -"RadrootsTradeOrderEconomics" = "TradeOrderEconomics" -"RadrootsTradeOrderRequested" = "TradeOrderRequested" -"RadrootsTradeInventoryCommitment" = "TradeInventoryCommitment" -"RadrootsTradeOrderDecision" = "TradeOrderDecision" -"RadrootsTradeOrderDecisionEvent" = "TradeOrderDecisionEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" diff --git a/spec/sdk-exports/ts.toml b/spec/sdk-exports/ts.toml @@ -26,14 +26,12 @@ order = 1 "social.file_metadata.build_tags" = "social.fileMetadata.buildTags" "social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags" "social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags" -"trade.build_envelope_draft" = "trade.buildEnvelopeDraft" -"trade.build_order_request_draft" = "trade.buildOrderRequestDraft" -"trade.build_order_decision_draft" = "trade.buildOrderDecisionDraft" -"trade.parse_envelope" = "trade.parseEnvelope" -"trade.parse_order_request" = "trade.parseOrderRequest" -"trade.parse_order_decision" = "trade.parseOrderDecision" -"trade.parse_listing_address" = "trade.parseListingAddress" -"trade.validate_listing_event" = "trade.validateListingEvent" +"order.build_order_request_draft" = "order.buildOrderRequestDraft" +"order.build_order_decision_draft" = "order.buildOrderDecisionDraft" +"order.parse_order_request" = "order.parseOrderRequest" +"order.parse_order_decision" = "order.parseOrderDecision" +"order.parse_listing_address" = "order.parseListingAddress" +"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" [shared_types] "WireEventParts" = "WireEventParts" @@ -41,7 +39,7 @@ order = 1 "RadrootsNostrEvent" = "RadrootsNostrEvent" "RadrootsNostrEventRef" = "RadrootsNostrEventRef" "RadrootsNostrEventPtr" = "RadrootsNostrEventPtr" -"RadrootsTradeListingAddress" = "TradeListingAddress" +"RadrootsOrderListingAddress" = "OrderListingAddress" "RadrootsProfile" = "RadrootsProfile" "RadrootsFarm" = "RadrootsFarm" "RadrootsListing" = "RadrootsListing" @@ -52,22 +50,21 @@ order = 1 "RadrootsFileMetadata" = "RadrootsFileMetadata" "RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent" "RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent" -"RadrootsTradeEnvelope" = "TradeEnvelope" -"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope" -"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType" -"RadrootsTradeOrderItem" = "TradeOrderItem" -"RadrootsTradePricingBasis" = "TradePricingBasis" -"RadrootsTradeEconomicLineKind" = "TradeEconomicLineKind" -"RadrootsTradeEconomicActor" = "TradeEconomicActor" -"RadrootsTradeEconomicEffect" = "TradeEconomicEffect" -"RadrootsTradeOrderEconomicItem" = "TradeOrderEconomicItem" -"RadrootsTradeOrderEconomicLine" = "TradeOrderEconomicLine" -"RadrootsTradeOrderEconomicTotals" = "TradeOrderEconomicTotals" -"RadrootsTradeOrderEconomics" = "TradeOrderEconomics" -"RadrootsTradeOrderRequested" = "TradeOrderRequested" -"RadrootsTradeInventoryCommitment" = "TradeInventoryCommitment" -"RadrootsTradeOrderDecision" = "TradeOrderDecision" -"RadrootsTradeOrderDecisionEvent" = "TradeOrderDecisionEvent" +"RadrootsOrderEnvelope" = "OrderEnvelope" +"RadrootsOrderEventType" = "OrderEventType" +"RadrootsOrderItem" = "OrderItem" +"RadrootsOrderPricingBasis" = "OrderPricingBasis" +"RadrootsOrderEconomicLineKind" = "OrderEconomicLineKind" +"RadrootsOrderEconomicActor" = "OrderEconomicActor" +"RadrootsOrderEconomicEffect" = "OrderEconomicEffect" +"RadrootsOrderEconomicItem" = "OrderEconomicItem" +"RadrootsOrderEconomicLine" = "OrderEconomicLine" +"RadrootsOrderEconomicTotals" = "OrderEconomicTotals" +"RadrootsOrderEconomics" = "OrderEconomics" +"RadrootsOrderRequest" = "OrderRequest" +"RadrootsOrderInventoryCommitment" = "OrderInventoryCommitment" +"RadrootsOrderDecisionOutcome" = "OrderDecisionOutcome" +"RadrootsOrderDecision" = "OrderDecision" [artifacts] models_dir = "src/generated"