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:
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,
- ¶ms,
- 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"