lib

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

commit fc25603d9a99b19dfa5b5295e57f3c285f5a269f
parent d2d2b7ba342c3215dbf40fbfa7132cd895a5594c
Author: triesap <tyson@radroots.org>
Date:   Fri, 12 Jun 2026 22:03:58 -0700

events: add typed commercial protocol identifiers

- add validated protocol value types for order, listing, bin, quote, digest, address, pubkey, event, and signature identifiers
- convert listing and order payload fields to typed identifiers while preserving wire-v1 string serialization
- update codec and trade fixtures to parse typed identifiers at test and decode boundaries
- add a guard against raw commercial identifier String regressions in active payload structs

Diffstat:
Mcrates/events/src/ids.rs | 46+++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/events/src/listing.rs | 11++++++-----
Mcrates/events/src/order.rs | 192+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/events/src/order_economics.rs | 8+++++---
Mcrates/events_codec/src/farm/mod.rs | 15++++++++++++---
Mcrates/events_codec/src/listing/decode.rs | 8+++++++-
Mcrates/events_codec/src/listing/tags.rs | 71++++++++++++++++++++++-------------------------------------------------
Mcrates/events_codec/src/order/decode.rs | 118+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mcrates/events_codec/tests/domain_encode_non_serde.rs | 39++++++++++++---------------------------
Mcrates/events_codec/tests/listing.rs | 27+++++++++++++++++----------
Mcrates/events_codec/tests/structured_encode_default.rs | 15++++++++++++---
Mcrates/events_codec/tests/tag_builders.rs | 30+++++++++++++-----------------
Mcrates/trade/src/listing/codec.rs | 18++++++++++++++----
Mcrates/trade/src/listing/price_ext.rs | 11++++++++---
Mcrates/trade/src/listing/publish.rs | 15++++++++++++---
Mcrates/trade/src/listing/validation.rs | 23+++++++++++++++--------
Mcrates/trade/src/order.rs | 178+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mscripts/ci/guard_no_legacy_identifiers.sh | 30++++++++++++++++++++++++++++++
18 files changed, 504 insertions(+), 351 deletions(-)

diff --git a/crates/events/src/ids.rs b/crates/events/src/ids.rs @@ -6,7 +6,7 @@ use alloc::{string::String, string::ToString, vec::Vec}; #[cfg(feature = "std")] use std::{string::String, vec::Vec}; -use core::{borrow::Borrow, fmt, str::FromStr}; +use core::{borrow::Borrow, fmt, ops::Deref, str::FromStr}; #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsIdParseError { @@ -67,6 +67,15 @@ macro_rules! validated_string_id { } } + impl Deref for $name { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + self.as_str() + } + } + impl Borrow<str> for $name { #[inline] fn borrow(&self) -> &str { @@ -74,6 +83,41 @@ macro_rules! validated_string_id { } } + impl From<$name> for String { + #[inline] + fn from(value: $name) -> Self { + value.into_string() + } + } + + impl PartialEq<&str> for $name { + #[inline] + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } + } + + impl PartialEq<$name> for &str { + #[inline] + fn eq(&self, other: &$name) -> bool { + *self == other.as_str() + } + } + + impl PartialEq<String> for $name { + #[inline] + fn eq(&self, other: &String) -> bool { + self.as_str() == other.as_str() + } + } + + impl PartialEq<$name> for String { + #[inline] + fn eq(&self, other: &$name) -> bool { + self.as_str() == other.as_str() + } + } + impl fmt::Display for $name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) diff --git a/crates/events/src/listing.rs b/crates/events/src/listing.rs @@ -4,6 +4,7 @@ use radroots_core::{ }; use crate::farm::RadrootsFarmRef; +use crate::ids::{RadrootsDTag, RadrootsInventoryBinId}; use crate::plot::RadrootsPlotRef; use crate::resource_area::RadrootsResourceAreaRef; @@ -54,7 +55,7 @@ pub enum RadrootsListingDeliveryMethod { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct RadrootsListing { - pub d_tag: String, + pub d_tag: RadrootsDTag, #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "Option::is_none") @@ -63,7 +64,7 @@ pub struct RadrootsListing { #[cfg_attr(feature = "serde", serde(default))] pub farm: RadrootsFarmRef, pub product: RadrootsListingProduct, - pub primary_bin_id: String, + pub primary_bin_id: RadrootsInventoryBinId, pub bins: Vec<RadrootsListingBin>, pub resource_area: Option<RadrootsResourceAreaRef>, pub plot: Option<RadrootsPlotRef>, @@ -98,7 +99,7 @@ pub struct RadrootsListingProductTagKeys; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct RadrootsListingBin { - pub bin_id: String, + pub bin_id: RadrootsInventoryBinId, pub quantity: RadrootsCoreQuantity, pub price_per_canonical_unit: RadrootsCoreQuantityPrice, pub display_amount: Option<RadrootsCoreDecimal>, @@ -150,7 +151,7 @@ mod tests { use crate::kinds::{KIND_LISTING_DRAFT, is_listing_kind}; let listing = super::RadrootsListing { - d_tag: "listing-draft".to_string(), + d_tag: "listing-draft".parse().unwrap(), published_at: Some(1_700_000_000), farm: RadrootsFarmRef::default(), product: super::RadrootsListingProduct { @@ -164,7 +165,7 @@ mod tests { profile: None, year: None, }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: "bin-1".parse().unwrap(), bins: vec![], resource_area: None, plot: None, diff --git a/crates/events/src/order.rs b/crates/events/src/order.rs @@ -6,6 +6,10 @@ use alloc::{ vec::Vec, }; +use crate::ids::{ + RadrootsEconomicsDigest, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, + RadrootsOrderQuoteId, RadrootsOrderRevisionId, +}; use crate::kinds::*; pub use crate::order_economics::*; #[cfg(test)] @@ -158,8 +162,8 @@ impl RadrootsOrderEconomics { #[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 order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, pub buyer_pubkey: String, pub seller_pubkey: String, pub items: Vec<RadrootsOrderItem>, @@ -181,9 +185,9 @@ impl RadrootsOrderRequest { #[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 revision_id: RadrootsOrderRevisionId, + pub order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, pub buyer_pubkey: String, pub seller_pubkey: String, pub root_event_id: String, @@ -229,9 +233,9 @@ impl RadrootsOrderRevisionOutcome { #[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 revision_id: RadrootsOrderRevisionId, + pub order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, pub buyer_pubkey: String, pub seller_pubkey: String, pub root_event_id: String, @@ -255,7 +259,7 @@ impl RadrootsOrderRevisionDecision { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsOrderInventoryCommitment { - pub bin_id: String, + pub bin_id: RadrootsInventoryBinId, pub bin_count: u32, } @@ -285,8 +289,8 @@ impl RadrootsOrderDecisionOutcome { #[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 order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, pub buyer_pubkey: String, pub seller_pubkey: String, pub decision: RadrootsOrderDecisionOutcome, @@ -324,8 +328,8 @@ impl RadrootsOrderFulfillmentState { #[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 order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, pub buyer_pubkey: String, pub seller_pubkey: String, pub status: RadrootsOrderFulfillmentState, @@ -348,8 +352,8 @@ impl RadrootsOrderFulfillmentUpdate { #[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 order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, pub buyer_pubkey: String, pub seller_pubkey: String, pub reason: String, @@ -368,8 +372,8 @@ impl RadrootsOrderCancellation { #[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 order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, pub buyer_pubkey: String, pub seller_pubkey: String, pub received: bool, @@ -409,16 +413,16 @@ pub enum RadrootsOrderPaymentMethod { #[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 order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, 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_id: RadrootsOrderQuoteId, pub quote_version: u32, - pub economics_digest: String, + pub economics_digest: RadrootsEconomicsDigest, pub amount: RadrootsCoreDecimal, pub currency: RadrootsCoreCurrency, pub method: RadrootsOrderPaymentMethod, @@ -461,17 +465,17 @@ pub enum RadrootsOrderSettlementOutcome { #[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 order_id: RadrootsOrderId, + pub listing_addr: RadrootsListingAddress, 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_id: RadrootsOrderQuoteId, pub quote_version: u32, - pub economics_digest: String, + pub economics_digest: RadrootsEconomicsDigest, pub amount: RadrootsCoreDecimal, pub currency: RadrootsCoreCurrency, pub decision: RadrootsOrderSettlementOutcome, @@ -1061,18 +1065,44 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; - fn sample_listing_addr() -> String { - "30402:pubkey:AAAAAAAAAAAAAAAAAAAAAg".into() + fn sample_pubkey() -> String { + "0".repeat(64) + } + + fn sample_listing_addr() -> RadrootsListingAddress { + format!("30402:{}:AAAAAAAAAAAAAAAAAAAAAg", sample_pubkey()) + .parse() + .unwrap() + } + + fn order_id(raw: &str) -> RadrootsOrderId { + raw.parse().unwrap() + } + + fn revision_id(raw: &str) -> RadrootsOrderRevisionId { + raw.parse().unwrap() + } + + fn quote_id(raw: &str) -> RadrootsOrderQuoteId { + raw.parse().unwrap() + } + + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() + } + + fn digest(raw: &str) -> RadrootsEconomicsDigest { + raw.parse().unwrap() } fn sample_order_request() -> RadrootsOrderRequest { RadrootsOrderRequest { - order_id: "order-1".into(), + order_id: order_id("order-1"), listing_addr: sample_listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), items: vec![RadrootsOrderItem { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), bin_count: 2, }], economics: sample_bound_order_economics(), @@ -1089,13 +1119,13 @@ mod tests { fn sample_order_economics() -> RadrootsOrderEconomics { RadrootsOrderEconomics { - quote_id: "quote-1".into(), + quote_id: quote_id("quote-1"), quote_version: 1, pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, items: vec![ RadrootsOrderEconomicItem { - bin_id: "bin-a".into(), + bin_id: bin_id("bin-a"), bin_count: 2, quantity_amount: decimal("1.5"), quantity_unit: RadrootsCoreUnit::Each, @@ -1104,7 +1134,7 @@ mod tests { line_subtotal: usd("12"), }, RadrootsOrderEconomicItem { - bin_id: "bin-b".into(), + bin_id: bin_id("bin-b"), bin_count: 1, quantity_amount: decimal("2"), quantity_unit: RadrootsCoreUnit::Each, @@ -1148,12 +1178,12 @@ mod tests { fn sample_bound_order_economics() -> RadrootsOrderEconomics { RadrootsOrderEconomics { - quote_id: "quote-bound-1".into(), + quote_id: quote_id("quote-bound-1"), quote_version: 1, pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), bin_count: 2, quantity_amount: decimal("1"), quantity_unit: RadrootsCoreUnit::Each, @@ -1172,14 +1202,14 @@ mod tests { fn sample_inventory_commitment() -> RadrootsOrderInventoryCommitment { RadrootsOrderInventoryCommitment { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), bin_count: 2, } } fn sample_order_decision() -> RadrootsOrderDecision { RadrootsOrderDecision { - order_id: "order-1".into(), + order_id: order_id("order-1"), listing_addr: sample_listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), @@ -1191,7 +1221,7 @@ mod tests { fn sample_order_fulfillment_update() -> RadrootsOrderFulfillmentUpdate { RadrootsOrderFulfillmentUpdate { - order_id: "order-1".into(), + order_id: order_id("order-1"), listing_addr: sample_listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), @@ -1201,7 +1231,7 @@ mod tests { fn sample_order_cancellation() -> RadrootsOrderCancellation { RadrootsOrderCancellation { - order_id: "order-1".into(), + order_id: order_id("order-1"), listing_addr: sample_listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), @@ -1211,7 +1241,7 @@ mod tests { fn sample_order_buyer_receipt(received: bool) -> RadrootsOrderReceipt { RadrootsOrderReceipt { - order_id: "order-1".into(), + order_id: order_id("order-1"), listing_addr: sample_listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), @@ -1223,15 +1253,15 @@ mod tests { fn sample_order_revision_proposal() -> RadrootsOrderRevisionProposal { RadrootsOrderRevisionProposal { - revision_id: "rev-1".into(), - order_id: "order-1".into(), + revision_id: revision_id("rev-1"), + order_id: order_id("order-1"), 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_id: bin_id("bin-1"), bin_count: 2, }], economics: sample_bound_order_economics(), @@ -1243,8 +1273,8 @@ mod tests { decision: RadrootsOrderRevisionOutcome, ) -> RadrootsOrderRevisionDecision { RadrootsOrderRevisionDecision { - revision_id: "rev-1".into(), - order_id: "order-1".into(), + revision_id: revision_id("rev-1"), + order_id: order_id("order-1"), listing_addr: sample_listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), @@ -1256,16 +1286,16 @@ mod tests { fn sample_payment_recorded() -> RadrootsOrderPaymentRecord { RadrootsOrderPaymentRecord { - order_id: "order-1".into(), + order_id: order_id("order-1"), 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_id: quote_id("quote-1"), quote_version: 1, - economics_digest: "economics-digest".into(), + economics_digest: digest("economics-digest"), amount: decimal("16"), currency: RadrootsCoreCurrency::USD, method: RadrootsOrderPaymentMethod::ManualTransfer, @@ -1279,7 +1309,7 @@ mod tests { reason: Option<&str>, ) -> RadrootsOrderSettlementDecision { RadrootsOrderSettlementDecision { - order_id: "order-1".into(), + order_id: order_id("order-1"), listing_addr: sample_listing_addr(), seller_pubkey: "seller".into(), buyer_pubkey: "buyer".into(), @@ -1287,9 +1317,9 @@ mod tests { previous_event_id: "previous-event".into(), agreement_event_id: "agreement-event".into(), payment_event_id: "payment-event".into(), - quote_id: "quote-1".into(), + quote_id: quote_id("quote-1"), quote_version: 1, - economics_digest: "economics-digest".into(), + economics_digest: digest("economics-digest"), amount: decimal("16"), currency: RadrootsCoreCurrency::USD, decision, @@ -1461,11 +1491,11 @@ mod tests { 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(); + let mut missing_buyer_pubkey = sample_order_request(); + missing_buyer_pubkey.buyer_pubkey = " ".into(); assert_eq!( - missing_order_id.validate().unwrap_err(), - RadrootsOrderPayloadError::EmptyField("order_id") + missing_buyer_pubkey.validate().unwrap_err(), + RadrootsOrderPayloadError::EmptyField("buyer_pubkey") ); let mut missing_items = sample_order_request(); @@ -1482,15 +1512,8 @@ mod tests { 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(); + mismatched_economic_item.economics.items[0].bin_id = bin_id("bin-other"); assert_eq!( mismatched_economic_item.validate().unwrap_err(), RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { @@ -1708,8 +1731,8 @@ mod tests { ); let invalid_order_items = [RadrootsOrderItem { - bin_id: " ".into(), - bin_count: 1, + bin_id: bin_id("bin-1"), + bin_count: 0, }]; assert_eq!( validate_order_economics_binding(&invalid_order_items, &economics).unwrap_err(), @@ -1720,11 +1743,11 @@ mod tests { let duplicate_counts = normalized_order_item_counts(&[ RadrootsOrderItem { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), bin_count: 1, }, RadrootsOrderItem { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), bin_count: 2, }, ]) @@ -1733,25 +1756,18 @@ mod tests { 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_id: bin_id("bin-1"), bin_count: 0, }]) .is_none() ); let sorted_counts = normalized_order_item_counts(&[ RadrootsOrderItem { - bin_id: "bin-b".into(), + bin_id: bin_id("bin-b"), bin_count: 1, }, RadrootsOrderItem { - bin_id: "bin-a".into(), + bin_id: bin_id("bin-a"), bin_count: 1, }, ]) @@ -1887,7 +1903,7 @@ mod tests { let accepted_with_zero_count = RadrootsOrderDecision { decision: RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), bin_count: 0, }], }, @@ -1912,15 +1928,6 @@ mod tests { 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(()) @@ -1941,15 +1948,6 @@ mod tests { 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] @@ -2151,7 +2149,7 @@ mod tests { assert_eq!(json["order_id"], serde_json::json!("order-1")); assert_eq!( json["listing_addr"], - serde_json::json!("30402:pubkey:AAAAAAAAAAAAAAAAAAAAAg") + serde_json::json!(sample_listing_addr().as_str()) ); assert_eq!(json["payload"]["items"][0]["bin_id"], "bin-1"); } @@ -2163,7 +2161,7 @@ mod tests { domain: RadrootsCommercialDomain::Listing, message_type: RadrootsOrderEventType::OrderRequested, order_id: "order-1".into(), - listing_addr: sample_listing_addr(), + listing_addr: sample_listing_addr().into_string(), payload: sample_order_request(), }; let invalid_version_err = invalid_version.validate().unwrap_err(); diff --git a/crates/events/src/order_economics.rs b/crates/events/src/order_economics.rs @@ -7,10 +7,12 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; +use crate::ids::{RadrootsInventoryBinId, RadrootsOrderQuoteId}; + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsOrderItem { - pub bin_id: String, + pub bin_id: RadrootsInventoryBinId, pub bin_count: u32, } @@ -49,7 +51,7 @@ pub enum RadrootsOrderEconomicEffect { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsOrderEconomicItem { - pub bin_id: String, + pub bin_id: RadrootsInventoryBinId, pub bin_count: u32, pub quantity_amount: RadrootsCoreDecimal, pub quantity_unit: RadrootsCoreUnit, @@ -81,7 +83,7 @@ pub struct RadrootsOrderEconomicTotals { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsOrderEconomics { - pub quote_id: String, + pub quote_id: RadrootsOrderQuoteId, pub quote_version: u32, pub pricing_basis: RadrootsOrderPricingBasis, pub currency: RadrootsCoreCurrency, diff --git a/crates/events_codec/src/farm/mod.rs b/crates/events_codec/src/farm/mod.rs @@ -18,9 +18,18 @@ mod tests { RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon, }; + use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}; use radroots_events::plot::RadrootsPlot; + fn d_tag(raw: &str) -> RadrootsDTag { + raw.parse().unwrap() + } + + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() + } + #[test] fn farm_tags_include_required_fields() { let farm = RadrootsFarm { @@ -294,7 +303,7 @@ mod tests { #[test] fn farm_listings_list_set_uses_listing_addresses() { let listings = vec![RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), published_at: None, farm: RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), @@ -311,9 +320,9 @@ mod tests { profile: None, year: None, }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each, diff --git a/crates/events_codec/src/listing/decode.rs b/crates/events_codec/src/listing/decode.rs @@ -13,6 +13,7 @@ use radroots_core::{ use radroots_events::{ RadrootsNostrEvent, farm::RadrootsFarmRef, + ids::{RadrootsDTag, RadrootsInventoryBinId}, kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA, is_listing_kind}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, @@ -492,6 +493,10 @@ pub fn listing_from_event_parts( return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN)); } + let d_tag = RadrootsDTag::parse(&d_tag).map_err(|_| EventParseError::InvalidTag(TAG_D))?; + let primary_bin_id = RadrootsInventoryBinId::parse(&primary_bin_id) + .map_err(|_| EventParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN))?; + Ok(RadrootsListing { d_tag, published_at, @@ -689,7 +694,8 @@ fn build_bins(mut drafts: Vec<BinDraft>) -> Result<Vec<RadrootsListingBin>, Even return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRICE)); } bins.push(RadrootsListingBin { - bin_id: draft.bin_id, + bin_id: RadrootsInventoryBinId::parse(&draft.bin_id) + .map_err(|_| EventParseError::InvalidTag(TAG_RADROOTS_BIN))?, quantity, price_per_canonical_unit: price, display_amount: draft.display_amount, diff --git a/crates/events_codec/src/listing/tags.rs b/crates/events_codec/src/listing/tags.rs @@ -154,7 +154,7 @@ pub fn listing_tags_with_options( tags.push(vec![ TAG_RADROOTS_PRIMARY_BIN.to_string(), - listing.primary_bin_id.clone(), + listing.primary_bin_id.to_string(), ]); let mut bins: Vec<&RadrootsListingBin> = listing.bins.iter().collect(); @@ -336,7 +336,7 @@ fn tag_listing_bin(bin: &RadrootsListingBin) -> Result<Vec<String>, EventEncodeE } let mut tag = Vec::with_capacity(7); tag.push(TAG_RADROOTS_BIN.to_string()); - tag.push(bin.bin_id.clone()); + tag.push(bin.bin_id.to_string()); tag.push(bin.quantity.amount.to_string()); tag.push(unit.code().to_string()); if let Some(amount) = bin.display_amount.as_ref() { @@ -371,7 +371,7 @@ fn tag_listing_price(bin: &RadrootsListingBin) -> Result<Vec<String>, EventEncod } let mut tag = Vec::with_capacity(8); tag.push(TAG_RADROOTS_PRICE.to_string()); - tag.push(bin.bin_id.clone()); + tag.push(bin.bin_id.to_string()); tag.push(price.amount.amount.to_string()); tag.push(price.amount.currency.as_str().to_string()); tag.push(price.quantity.amount.to_string()); @@ -629,6 +629,7 @@ mod tests { RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; + use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::listing::{ RadrootsListingImageSize, RadrootsListingProduct, RadrootsListingStatus, }; @@ -646,6 +647,14 @@ mod tests { RadrootsCoreDecimal::from_str(value).expect("valid decimal") } + fn d_tag(raw: &str) -> RadrootsDTag { + raw.parse().unwrap() + } + + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() + } + fn base_product() -> RadrootsListingProduct { RadrootsListingProduct { key: "coffee".to_string(), @@ -662,7 +671,7 @@ mod tests { fn base_bin() -> RadrootsListingBin { RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new(decimal("1000"), RadrootsCoreUnit::MassG) .with_label("bag"), price_per_canonical_unit: RadrootsCoreQuantityPrice::new( @@ -682,14 +691,14 @@ mod tests { fn base_listing() -> RadrootsListing { RadrootsListing { - d_tag: TEST_D_TAG.to_string(), + d_tag: d_tag(TEST_D_TAG), published_at: None, farm: RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), d_tag: TEST_FARM_D_TAG.to_string(), }, product: base_product(), - primary_bin_id: "bin-1".to_string(), + primary_bin_id: bin_id("bin-1"), bins: vec![base_bin()], resource_area: Some(RadrootsResourceAreaRef { pubkey: TEST_PUBKEY_HEX.to_string(), @@ -1083,19 +1092,6 @@ mod tests { let generic_tag = tag_listing_price_generic(&total); assert_eq!(generic_tag[0], "price"); - let mut bad_bin = base_bin(); - bad_bin.bin_id = "".to_string(); - let err = tag_listing_bin(&bad_bin).expect_err("empty bin_id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("bin_id") - )); - let err = tag_listing_price(&bad_bin).expect_err("empty bin_id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("bin_id") - )); - let mut non_canonical = base_bin(); non_canonical.quantity = RadrootsCoreQuantity::new(decimal("1"), RadrootsCoreUnit::MassKg); let err = tag_listing_bin(&non_canonical).expect_err("non canonical quantity"); @@ -1189,16 +1185,6 @@ mod tests { fn listing_tags_propagate_bin_and_resource_area_validation_errors() { let mut listing = base_listing(); listing.discounts = None; - listing.bins[0].bin_id = "".to_string(); - let err = listing_tags_with_options(&listing, ListingTagOptions::default()) - .expect_err("empty bin id should fail listing tags"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("bin_id") - )); - - let mut listing = base_listing(); - listing.discounts = None; listing.bins[0].price_per_canonical_unit = RadrootsCoreQuantityPrice::new( RadrootsCoreMoney::new(decimal("10"), RadrootsCoreCurrency::USD), RadrootsCoreQuantity::new(decimal("2"), RadrootsCoreUnit::MassG), @@ -1429,24 +1415,11 @@ mod tests { fn listing_tags_required_field_errors() { let mut listing = base_listing(); - listing.d_tag = "".to_string(); - let err = listing_tags(&listing).expect_err("missing d"); - assert!(matches!(err, EventEncodeError::EmptyRequiredField("d"))); - - listing = base_listing(); - listing.d_tag = "listing:invalid".to_string(); + listing.d_tag = d_tag("listing:invalid"); let err = listing_tags(&listing).expect_err("invalid d"); assert!(matches!(err, EventEncodeError::InvalidField("d"))); listing = base_listing(); - listing.primary_bin_id = "".to_string(); - let err = listing_tags(&listing).expect_err("missing primary bin"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("primary_bin_id") - )); - - listing = base_listing(); listing.bins.clear(); let err = listing_tags(&listing).expect_err("missing bins"); assert!(matches!(err, EventEncodeError::EmptyRequiredField("bins"))); @@ -1596,13 +1569,13 @@ mod tests { listing.discounts = None; let mut primary = base_bin(); - primary.bin_id = "bin-1".to_string(); + primary.bin_id = bin_id("bin-1"); primary.display_label = None; primary.quantity = primary.quantity.clone().with_label("fallback-label"); let mut secondary = base_bin(); - secondary.bin_id = "bin-2".to_string(); - listing.primary_bin_id = "bin-1".to_string(); + secondary.bin_id = bin_id("bin-2"); + listing.primary_bin_id = bin_id("bin-1"); listing.bins = vec![secondary, primary]; let tags = listing_tags(&listing).expect("listing tags"); @@ -1616,12 +1589,12 @@ mod tests { let mut listing_without_primary_match = base_listing(); listing_without_primary_match.discounts = None; let mut first = base_bin(); - first.bin_id = "bin-2".to_string(); + first.bin_id = bin_id("bin-2"); first.display_label = None; first.quantity = RadrootsCoreQuantity::new(decimal("1000"), RadrootsCoreUnit::MassG); let mut second = base_bin(); - second.bin_id = "bin-1".to_string(); - listing_without_primary_match.primary_bin_id = "bin-missing".to_string(); + second.bin_id = bin_id("bin-1"); + listing_without_primary_match.primary_bin_id = bin_id("bin-missing"); listing_without_primary_match.bins = vec![first, second]; let tags = listing_tags(&listing_without_primary_match).expect("listing tags"); diff --git a/crates/events_codec/src/order/decode.rs b/crates/events_codec/src/order/decode.rs @@ -607,6 +607,10 @@ mod tests { }; use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, + ids::{ + RadrootsEconomicsDigest, RadrootsInventoryBinId, RadrootsListingAddress, + RadrootsOrderId, RadrootsOrderQuoteId, RadrootsOrderRevisionId, + }, kinds::{ KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_FULFILLMENT_UPDATE, KIND_ORDER_PAYMENT_RECORD, KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST, @@ -627,14 +631,48 @@ mod tests { tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, }; + fn seller_pubkey() -> String { + "a".repeat(64) + } + + fn listing_addr() -> RadrootsListingAddress { + format!("30402:{}:AAAAAAAAAAAAAAAAAAAAAg", seller_pubkey()) + .parse() + .unwrap() + } + + fn listing_addr_wire() -> String { + listing_addr().into_string() + } + + fn order_id(raw: &str) -> RadrootsOrderId { + raw.parse().unwrap() + } + + fn revision_id(raw: &str) -> RadrootsOrderRevisionId { + raw.parse().unwrap() + } + + fn quote_id(raw: &str) -> RadrootsOrderQuoteId { + raw.parse().unwrap() + } + + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() + } + + fn digest(raw: &str) -> RadrootsEconomicsDigest { + raw.parse().unwrap() + } + fn order_request() -> RadrootsOrderRequest { RadrootsOrderRequest { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + order_id: order_id("order-1"), + listing_addr: listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), items: vec![RadrootsOrderItem { - bin_id: "lb".into(), + bin_id: bin_id("lb"), bin_count: 3, }], economics: request_economics(), @@ -651,12 +689,12 @@ mod tests { fn request_economics() -> RadrootsOrderEconomics { RadrootsOrderEconomics { - quote_id: "quote-1".into(), + quote_id: quote_id("quote-1"), quote_version: 1, pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, items: vec![RadrootsOrderEconomicItem { - bin_id: "lb".into(), + bin_id: bin_id("lb"), bin_count: 3, quantity_amount: decimal("1"), quantity_unit: RadrootsCoreUnit::Each, @@ -675,13 +713,13 @@ mod tests { fn order_decision() -> RadrootsOrderDecision { RadrootsOrderDecision { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + order_id: order_id("order-1"), + listing_addr: listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), decision: RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "lb".into(), + bin_id: bin_id("lb"), bin_count: 3, }], }, @@ -690,7 +728,7 @@ mod tests { fn order_revision_proposal() -> RadrootsOrderRevisionProposal { let mut economics = request_economics(); - economics.quote_id = "revision-quote-1".into(); + economics.quote_id = quote_id("revision-quote-1"); economics.quote_version = 2; economics.items[0].bin_count = 4; economics.items[0].line_subtotal = usd("20"); @@ -698,15 +736,15 @@ mod tests { economics.total = usd("20"); economics.canonicalize(); RadrootsOrderRevisionProposal { - revision_id: "rev-1".into(), - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + revision_id: revision_id("rev-1"), + order_id: order_id("order-1"), + listing_addr: listing_addr(), 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_id: bin_id("lb"), bin_count: 4, }], economics, @@ -718,9 +756,9 @@ mod tests { decision: RadrootsOrderRevisionOutcome, ) -> RadrootsOrderRevisionDecision { RadrootsOrderRevisionDecision { - revision_id: "rev-1".into(), - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + revision_id: revision_id("rev-1"), + order_id: order_id("order-1"), + listing_addr: listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), root_event_id: "root-event".into(), @@ -731,8 +769,8 @@ mod tests { fn order_fulfillment_update() -> RadrootsOrderFulfillmentUpdate { RadrootsOrderFulfillmentUpdate { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + order_id: order_id("order-1"), + listing_addr: listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), status: RadrootsOrderFulfillmentState::ReadyForPickup, @@ -741,8 +779,8 @@ mod tests { fn order_cancelled() -> RadrootsOrderCancellation { RadrootsOrderCancellation { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + order_id: order_id("order-1"), + listing_addr: listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), reason: "changed plans".into(), @@ -751,8 +789,8 @@ mod tests { fn order_buyer_receipt(received: bool) -> RadrootsOrderReceipt { RadrootsOrderReceipt { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + order_id: order_id("order-1"), + listing_addr: listing_addr(), buyer_pubkey: "buyer".into(), seller_pubkey: "seller".into(), received, @@ -763,16 +801,16 @@ mod tests { fn order_payment_recorded() -> RadrootsOrderPaymentRecord { RadrootsOrderPaymentRecord { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + order_id: order_id("order-1"), + listing_addr: listing_addr(), 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_id: quote_id("quote-1"), quote_version: 1, - economics_digest: "digest-1".into(), + economics_digest: digest("digest-1"), amount: decimal("15"), currency: RadrootsCoreCurrency::USD, method: RadrootsOrderPaymentMethod::Cash, @@ -785,17 +823,17 @@ mod tests { decision: RadrootsOrderSettlementOutcome, ) -> RadrootsOrderSettlementDecision { RadrootsOrderSettlementDecision { - order_id: "order-1".into(), - listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + order_id: order_id("order-1"), + listing_addr: listing_addr(), 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_id: quote_id("quote-1"), quote_version: 1, - economics_digest: "digest-1".into(), + economics_digest: digest("digest-1"), amount: decimal("15"), currency: RadrootsCoreCurrency::USD, decision, @@ -832,13 +870,7 @@ mod tests { ); 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[1], vec!["a".to_string(), listing_addr_wire()]); assert_eq!( built.tags[2], vec![TAG_D.to_string(), "order-1".to_string()] @@ -1162,7 +1194,7 @@ mod tests { 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(); + payload.economics.items[0].bin_id = bin_id("other-bin"); let envelope = RadrootsOrderEnvelope::new( RadrootsOrderEventType::OrderRequested, payload.listing_addr.clone(), @@ -1393,12 +1425,8 @@ mod tests { ), ] { let payload = serde_json::json!({}); - let envelope = RadrootsOrderEnvelope::new( - message_type, - "30402:seller:AAAAAAAAAAAAAAAAAAAAAg", - "order-1", - &payload, - ); + let envelope = + RadrootsOrderEnvelope::new(message_type, listing_addr_wire(), "order-1", &payload); let event = RadrootsNostrEvent { id: "event-id".into(), author: "seller".into(), @@ -1406,7 +1434,7 @@ mod tests { kind, tags: vec![ vec!["p".into(), "buyer".into()], - vec!["a".into(), "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into()], + vec!["a".into(), listing_addr_wire()], vec![TAG_D.into(), "order-1".into()], vec![TAG_E_ROOT.into(), "root-event".into()], vec![TAG_E_PREV.into(), "prev-event".into()], diff --git a/crates/events_codec/tests/domain_encode_non_serde.rs b/crates/events_codec/tests/domain_encode_non_serde.rs @@ -14,6 +14,7 @@ use radroots_events::{ RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon, }, + ids::{RadrootsDTag, RadrootsInventoryBinId}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, @@ -55,6 +56,14 @@ fn decimal(value: &str) -> RadrootsCoreDecimal { RadrootsCoreDecimal::from_str(value).expect("valid decimal") } +fn listing_d_tag(raw: &str) -> RadrootsDTag { + raw.parse().unwrap() +} + +fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() +} + fn sample_gcs(geohash: &str) -> RadrootsGcsLocation { RadrootsGcsLocation { lat: 37.0, @@ -157,7 +166,7 @@ fn sample_listing() -> RadrootsListing { ); RadrootsListing { - d_tag: VALID_DOC_D_TAG.to_string(), + d_tag: listing_d_tag(VALID_DOC_D_TAG), published_at: None, farm: RadrootsFarmRef { pubkey: VALID_PUBKEY.to_string(), @@ -174,9 +183,9 @@ fn sample_listing() -> RadrootsListing { profile: None, year: None, }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), quantity, price_per_canonical_unit, display_amount: None, @@ -842,15 +851,6 @@ fn listing_encode_paths() { })); let mut invalid = sample_listing(); - invalid.bins[0].bin_id = " ".to_string(); - let err = listing_tags_with_options(&invalid, ListingTagOptions::default()) - .expect_err("empty bin_id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("bin_id") - )); - - let mut invalid = sample_listing(); invalid.bins[0].display_price = None; invalid.bins[0].display_price_unit = Some(RadrootsCoreUnit::Each); let err = listing_tags_with_options(&invalid, ListingTagOptions::default()) @@ -956,21 +956,6 @@ fn listing_encode_paths() { )); let mut invalid = sample_listing(); - invalid.d_tag = " ".to_string(); - let err = - listing_tags_with_options(&invalid, ListingTagOptions::default()).expect_err("empty d"); - assert!(matches!(err, EventEncodeError::EmptyRequiredField("d"))); - - let mut invalid = sample_listing(); - invalid.primary_bin_id = " ".to_string(); - let err = listing_tags_with_options(&invalid, ListingTagOptions::default()) - .expect_err("empty primary_bin_id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("primary_bin_id") - )); - - let mut invalid = sample_listing(); invalid.bins.clear(); let err = listing_tags_with_options(&invalid, ListingTagOptions::default()).expect_err("empty bins"); diff --git a/crates/events_codec/tests/listing.rs b/crates/events_codec/tests/listing.rs @@ -8,6 +8,7 @@ use radroots_core::{ use radroots_events::tags::{TAG_D, TAG_PUBLISHED_AT}; use radroots_events::{ farm::RadrootsFarmRef, + ids::{RadrootsDTag, RadrootsInventoryBinId}, kinds::{KIND_LISTING, KIND_LISTING_DRAFT, KIND_POST}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, @@ -25,6 +26,14 @@ use radroots_events_codec::listing::tags::{ }; use std::str::FromStr; +fn listing_d_tag(raw: &str) -> RadrootsDTag { + raw.parse().unwrap() +} + +fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() +} + fn sample_listing(d_tag: &str) -> RadrootsListing { let quantity = RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each); @@ -34,7 +43,7 @@ fn sample_listing(d_tag: &str) -> RadrootsListing { ); RadrootsListing { - d_tag: d_tag.to_string(), + d_tag: listing_d_tag(d_tag), published_at: None, farm: RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), @@ -51,9 +60,9 @@ fn sample_listing(d_tag: &str) -> RadrootsListing { profile: None, year: None, }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), quantity, price_per_canonical_unit: price, display_amount: None, @@ -80,7 +89,7 @@ fn sample_listing_full(d_tag: &str) -> RadrootsListing { let display_price = RadrootsCoreDecimal::from_str("10").unwrap(); RadrootsListing { - d_tag: d_tag.to_string(), + d_tag: listing_d_tag(d_tag), published_at: None, farm: RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), @@ -97,9 +106,9 @@ fn sample_listing_full(d_tag: &str) -> RadrootsListing { profile: Some("standard".to_string()), year: Some("2024".to_string()), }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new(qty_amount, RadrootsCoreUnit::MassG), price_per_canonical_unit: RadrootsCoreQuantityPrice::new( RadrootsCoreMoney::new(price_amount, RadrootsCoreCurrency::USD), @@ -148,9 +157,7 @@ fn sample_listing_full(d_tag: &str) -> RadrootsListing { #[test] fn listing_build_tags_requires_d_tag() { - let listing = sample_listing(""); - let err = listing_build_tags(&listing).unwrap_err(); - assert!(matches!(err, EventEncodeError::EmptyRequiredField("d"))); + assert!(RadrootsDTag::parse("").is_err()); } #[test] @@ -376,7 +383,7 @@ fn listing_build_tags_includes_listing_fields() { fn listing_tags_full_uses_single_generic_price_for_primary_bin() { let mut listing = sample_listing_full("AAAAAAAAAAAAAAAAAAAAAw"); listing.bins.push(RadrootsListingBin { - bin_id: "bin-2".to_string(), + bin_id: bin_id("bin-2"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from_str("500").unwrap(), RadrootsCoreUnit::MassG, diff --git a/crates/events_codec/tests/structured_encode_default.rs b/crates/events_codec/tests/structured_encode_default.rs @@ -11,6 +11,7 @@ use radroots_events::farm::{ RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon, }; +use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::list_set::RadrootsListSet; use radroots_events::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}; use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation, RadrootsPlotRef}; @@ -44,6 +45,14 @@ use test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX; const TEST_PUBKEY_HEX: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX; +fn listing_d_tag(raw: &str) -> RadrootsDTag { + raw.parse().unwrap() +} + +fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() +} + fn sample_gcs() -> RadrootsGcsLocation { RadrootsGcsLocation { lat: 37.0, @@ -87,7 +96,7 @@ fn sample_listing(d_tag: &str) -> RadrootsListing { quantity.clone(), ); RadrootsListing { - d_tag: d_tag.to_string(), + d_tag: listing_d_tag(d_tag), published_at: None, farm: RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), @@ -104,9 +113,9 @@ fn sample_listing(d_tag: &str) -> RadrootsListing { profile: None, year: None, }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), quantity, price_per_canonical_unit: price, display_amount: None, diff --git a/crates/events_codec/tests/tag_builders.rs b/crates/events_codec/tests/tag_builders.rs @@ -18,6 +18,7 @@ use radroots_events::farm::{ use radroots_events::follow::{RadrootsFollow, RadrootsFollowProfile}; use radroots_events::geochat::RadrootsGeoChat; use radroots_events::gift_wrap::{RadrootsGiftWrap, RadrootsGiftWrapRecipient}; +use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::job::{JobFeedbackStatus, JobInputType, JobPaymentRequest}; use radroots_events::job_feedback::RadrootsJobFeedback; use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest}; @@ -60,6 +61,14 @@ fn cdn_url(path: &str) -> String { format!("{CDN_PRIMARY_HTTPS}/{path}") } +fn d_tag(raw: &str) -> RadrootsDTag { + raw.parse().unwrap() +} + +fn bin_id(raw: &str) -> RadrootsInventoryBinId { + raw.parse().unwrap() +} + fn sample_social_target(id: &str) -> RadrootsSocialTarget { RadrootsSocialTarget::Event { id: id.to_string(), @@ -113,7 +122,7 @@ fn sample_listing() -> RadrootsListing { ); RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), published_at: None, farm: RadrootsFarmRef { pubkey: TEST_NPUB.to_string(), @@ -130,9 +139,9 @@ fn sample_listing() -> RadrootsListing { profile: None, year: None, }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), quantity, price_per_canonical_unit: price, display_amount: None, @@ -665,24 +674,11 @@ fn listing_and_message_builders_cover_optional_shapes() { #[test] fn listing_builder_rejects_required_field_errors() { let mut listing = sample_listing(); - listing.d_tag = " ".to_string(); - let err = listing_build_tags(&listing).expect_err("empty listing d_tag"); - assert!(matches!(err, EventEncodeError::EmptyRequiredField("d"))); - - let mut listing = sample_listing(); - listing.d_tag = "invalid".to_string(); + listing.d_tag = d_tag("listing:invalid"); let err = listing_build_tags(&listing).expect_err("invalid listing d_tag"); assert!(matches!(err, EventEncodeError::InvalidField("d"))); let mut listing = sample_listing(); - listing.primary_bin_id = " ".to_string(); - let err = listing_build_tags(&listing).expect_err("empty primary bin id"); - assert!(matches!( - err, - EventEncodeError::EmptyRequiredField("primary_bin_id") - )); - - let mut listing = sample_listing(); listing.bins.clear(); let err = listing_build_tags(&listing).expect_err("empty bins"); assert!(matches!(err, EventEncodeError::EmptyRequiredField("bins"))); diff --git a/crates/trade/src/listing/codec.rs b/crates/trade/src/listing/codec.rs @@ -8,6 +8,7 @@ use radroots_core::{ RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::farm::RadrootsFarmRef; +use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, @@ -95,7 +96,8 @@ pub fn listing_from_event_parts( { if let Ok(mut listing) = serde_json::from_str::<RadrootsListing>(content) { if listing.d_tag.trim().is_empty() { - listing.d_tag = d_tag; + listing.d_tag = RadrootsDTag::parse(&d_tag) + .map_err(|_| ListingParseError::InvalidTag(TAG_D.to_string()))?; } else if listing.d_tag != d_tag { return Err(ListingParseError::InvalidTag(TAG_D.to_string())); } @@ -173,6 +175,8 @@ fn listing_from_tags( if !is_d_tag_base64url(&d_tag) { return Err(ListingParseError::InvalidTag(TAG_D.to_string())); } + let d_tag = RadrootsDTag::parse(&d_tag) + .map_err(|_| ListingParseError::InvalidTag(TAG_D.to_string()))?; let mut product = RadrootsListingProduct { key: String::new(), title: String::new(), @@ -448,6 +452,8 @@ fn listing_from_tags( let primary_bin_id = primary_bin_id .and_then(|v| clean_value(&v)) .ok_or_else(|| ListingParseError::MissingTag(TAG_RADROOTS_PRIMARY_BIN.to_string()))?; + let primary_bin_id = RadrootsInventoryBinId::parse(&primary_bin_id) + .map_err(|_| ListingParseError::InvalidTag(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(ListingParseError::InvalidTag( @@ -634,6 +640,10 @@ mod tests { "AAAAAAAAAAAAAAAAAAAAAg".to_string() } + fn d_tag(raw: &str) -> RadrootsDTag { + RadrootsDTag::parse(raw).expect("d tag") + } + fn base_event_tags() -> Vec<Vec<String>> { vec![ vec![TAG_D.into(), listing_d_tag()], @@ -844,7 +854,6 @@ mod tests { #[test] fn listing_from_event_parts_uses_json_content_and_backfills_tags() { let mut listing = parse_base_listing_from_tags(); - listing.d_tag = String::new(); listing.farm.pubkey = String::new(); listing.farm.d_tag = String::new(); listing.resource_area = None; @@ -877,7 +886,7 @@ mod tests { let tags = base_event_tags(); let mut listing = parse_base_listing_from_tags(); - listing.d_tag = "AAAAAAAAAAAAAAAAAAAAAw".into(); + listing.d_tag = d_tag("AAAAAAAAAAAAAAAAAAAAAw"); let err = listing_from_event_parts(&tags, &serde_json::to_string(&listing).unwrap()).unwrap_err(); assert_eq!(parse_error_tag(err), TAG_D.to_string()); @@ -2429,7 +2438,8 @@ fn build_bins(mut drafts: Vec<BinDraft>) -> Result<Vec<RadrootsListingBin>, List )); } let bin = RadrootsListingBin { - bin_id: draft.bin_id, + bin_id: RadrootsInventoryBinId::parse(&draft.bin_id) + .map_err(|_| ListingParseError::InvalidTag(TAG_RADROOTS_BIN.to_string()))?, quantity, price_per_canonical_unit: price, display_amount: draft.display_amount, diff --git a/crates/trade/src/listing/price_ext.rs b/crates/trade/src/listing/price_ext.rs @@ -94,11 +94,16 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreQuantityPriceError, RadrootsCoreUnit, }; + use radroots_events::ids::RadrootsInventoryBinId; use radroots_events::listing::RadrootsListingBin; + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + RadrootsInventoryBinId::parse(raw).expect("bin id") + } + fn valid_bin() -> RadrootsListingBin { RadrootsListingBin { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from(2u32), RadrootsCoreUnit::MassG, @@ -118,7 +123,7 @@ mod tests { #[test] fn try_subtotal_for_rejects_unit_mismatch() { let bin = RadrootsListingBin { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::MassG, @@ -182,7 +187,7 @@ mod tests { #[test] fn try_total_for_count_propagates_subtotal_errors() { let bin = RadrootsListingBin { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::MassG, diff --git a/crates/trade/src/listing/publish.rs b/crates/trade/src/listing/publish.rs @@ -64,6 +64,7 @@ mod tests { RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::farm::RadrootsFarmRef; + use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, @@ -73,9 +74,17 @@ mod tests { canonicalize_listing_for_seller, resolve_listing_kind, validate_listing_for_seller, }; + fn d_tag(raw: &str) -> RadrootsDTag { + RadrootsDTag::parse(raw).expect("d tag") + } + + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + RadrootsInventoryBinId::parse(raw).expect("bin id") + } + fn base_listing() -> RadrootsListing { RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), published_at: None, farm: RadrootsFarmRef { pubkey: String::new(), @@ -92,9 +101,9 @@ mod tests { profile: None, year: None, }, - primary_bin_id: "bin-1".into(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from(1000u32), RadrootsCoreUnit::MassG, diff --git a/crates/trade/src/listing/validation.rs b/crates/trade/src/listing/validation.rs @@ -172,6 +172,7 @@ mod tests { use radroots_events::{ RadrootsNostrEvent, farm::RadrootsFarmRef, + ids::{RadrootsDTag, RadrootsInventoryBinId}, kinds::{KIND_LISTING, KIND_LISTING_DRAFT}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, @@ -179,9 +180,17 @@ mod tests { }, }; + fn d_tag(raw: &str) -> RadrootsDTag { + RadrootsDTag::parse(raw).expect("d tag") + } + + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + RadrootsInventoryBinId::parse(raw).expect("bin id") + } + fn base_listing() -> RadrootsListing { RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), published_at: None, farm: RadrootsFarmRef { pubkey: "seller".into(), @@ -198,9 +207,9 @@ mod tests { profile: None, year: None, }, - primary_bin_id: "bin-1".into(), + primary_bin_id: bin_id("bin-1"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".into(), + bin_id: bin_id("bin-1"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from(1000u32), RadrootsCoreUnit::MassG, @@ -249,7 +258,7 @@ mod tests { created_at: 0, kind: KIND_LISTING, tags: vec![ - vec!["d".into(), listing.d_tag.clone()], + vec!["d".into(), listing.d_tag.to_string()], vec!["p".into(), listing.farm.pubkey.clone()], vec![ "a".into(), @@ -397,15 +406,13 @@ mod tests { #[test] fn validate_listing_rejects_missing_primary_bin_id() { - let mut listing = base_listing(); - listing.primary_bin_id = " ".into(); - assert_validation_err(listing, TradeListingValidationError::MissingPrimaryBin); + assert!(RadrootsInventoryBinId::parse(" ").is_err()); } #[test] fn validate_listing_rejects_primary_bin_not_found() { let mut listing = base_listing(); - listing.primary_bin_id = "missing".into(); + listing.primary_bin_id = bin_id("missing"); assert_validation_err(listing, TradeListingValidationError::MissingPrimaryBin); } diff --git a/crates/trade/src/order.rs b/crates/trade/src/order.rs @@ -7,6 +7,7 @@ use alloc::{ }; use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDecimal}; +use radroots_events::ids::RadrootsListingAddress; use radroots_events::kinds::KIND_LISTING; use radroots_events::order::{ RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, @@ -898,9 +899,8 @@ pub fn canonicalize_order_request_for_signer( mut request: RadrootsOrderRequest, signer_pubkey: &str, ) -> 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")?; + let order_id = request.order_id.clone(); + let listing_addr_raw = request.listing_addr.to_string(); let listing_addr = parse_public_listing_addr(&listing_addr_raw)?; let buyer_pubkey = if request.buyer_pubkey.trim().is_empty() { @@ -924,7 +924,10 @@ pub fn canonicalize_order_request_for_signer( canonicalize_items(&mut request.items)?; request.economics.canonicalize(); request.order_id = order_id; - request.listing_addr = listing_addr.as_str(); + request.listing_addr = + RadrootsListingAddress::parse(listing_addr.as_str()).map_err(|error| { + RadrootsOrderCanonicalizationError::InvalidListingAddress(error.to_string()) + })?; request.buyer_pubkey = buyer_pubkey; request.seller_pubkey = seller_pubkey; Ok(request) @@ -934,12 +937,8 @@ pub fn canonicalize_order_decision_for_signer( mut decision_event: RadrootsOrderDecision, signer_pubkey: &str, ) -> 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( - core::mem::take(&mut decision_event.listing_addr), - "listing_addr", - )?; + let order_id = decision_event.order_id.clone(); + let listing_addr_raw = decision_event.listing_addr.to_string(); let listing_addr = parse_public_listing_addr(&listing_addr_raw)?; let seller_pubkey = if decision_event.seller_pubkey.trim().is_empty() { @@ -961,7 +960,10 @@ pub fn canonicalize_order_decision_for_signer( canonicalize_decision(&mut decision_event.decision)?; decision_event.order_id = order_id; - decision_event.listing_addr = listing_addr.as_str(); + decision_event.listing_addr = + RadrootsListingAddress::parse(listing_addr.as_str()).map_err(|error| { + RadrootsOrderCanonicalizationError::InvalidListingAddress(error.to_string()) + })?; decision_event.buyer_pubkey = buyer_pubkey; decision_event.seller_pubkey = seller_pubkey; Ok(decision_event) @@ -1202,37 +1204,37 @@ fn listing_order_ids( order_ids.extend( requests .iter() - .map(|request| request.payload.order_id.clone()), + .map(|request| request.payload.order_id.to_string()), ); order_ids.extend( decisions .iter() - .map(|decision| decision.payload.order_id.clone()), + .map(|decision| decision.payload.order_id.to_string()), ); order_ids.extend( revision_proposals .iter() - .map(|proposal| proposal.payload.order_id.clone()), + .map(|proposal| proposal.payload.order_id.to_string()), ); order_ids.extend( revision_decisions .iter() - .map(|decision| decision.payload.order_id.clone()), + .map(|decision| decision.payload.order_id.to_string()), ); order_ids.extend( fulfillments .iter() - .map(|fulfillment| fulfillment.payload.order_id.clone()), + .map(|fulfillment| fulfillment.payload.order_id.to_string()), ); order_ids.extend( cancellations .iter() - .map(|cancellation| cancellation.payload.order_id.clone()), + .map(|cancellation| cancellation.payload.order_id.to_string()), ); order_ids.extend( receipts .iter() - .map(|receipt| receipt.payload.order_id.clone()), + .map(|receipt| receipt.payload.order_id.to_string()), ); sort_and_dedup_strings(&mut order_ids); order_ids @@ -1257,7 +1259,7 @@ fn add_accepted_inventory_reservations_from_economics( } else { issues.push( RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { - bin_id: item.bin_id.clone(), + bin_id: item.bin_id.to_string(), event_ids: vec![agreement_event_id.to_string()], }, ); @@ -2222,9 +2224,9 @@ fn payment_projection_from_record( payment_event_id: Some(payment.event_id.clone()), settlement_event_id: settlement.map(|settlement| settlement.event_id.clone()), agreement_event_id: Some(payment.payload.agreement_event_id.clone()), - quote_id: Some(payment.payload.quote_id.clone()), + quote_id: Some(payment.payload.quote_id.to_string()), quote_version: Some(payment.payload.quote_version), - economics_digest: Some(payment.payload.economics_digest.clone()), + economics_digest: Some(payment.payload.economics_digest.to_string()), amount: Some(payment.payload.amount), currency: Some(payment.payload.currency), method: Some(payment.payload.method), @@ -2833,7 +2835,7 @@ fn requested_projection( payment: RadrootsOrderPaymentProjection::not_recorded(), economics: Some(request.payload.economics.clone()), agreement_event_id: None, - listing_addr: Some(request.payload.listing_addr.clone()), + listing_addr: Some(request.payload.listing_addr.to_string()), buyer_pubkey: Some(request.payload.buyer_pubkey.clone()), seller_pubkey: Some(request.payload.seller_pubkey.clone()), last_event_id: Some(request.event_id.clone()), @@ -3142,7 +3144,7 @@ fn decided_projection( payment, economics, agreement_event_id, - listing_addr: Some(request.payload.listing_addr.clone()), + listing_addr: Some(request.payload.listing_addr.to_string()), buyer_pubkey: Some(request.payload.buyer_pubkey.clone()), seller_pubkey: Some(request.payload.seller_pubkey.clone()), last_event_id, @@ -3261,7 +3263,7 @@ fn cancelled_projection( payment: RadrootsOrderPaymentProjection::not_recorded(), economics: Some(economics), agreement_event_id, - listing_addr: Some(request.payload.listing_addr.clone()), + listing_addr: Some(request.payload.listing_addr.to_string()), buyer_pubkey: Some(request.payload.buyer_pubkey.clone()), seller_pubkey: Some(request.payload.seller_pubkey.clone()), last_event_id: Some(cancellation.event_id), @@ -3299,7 +3301,7 @@ fn receipt_terminal_projection( 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()), + listing_addr: Some(request.payload.listing_addr.to_string()), buyer_pubkey: Some(request.payload.buyer_pubkey.clone()), seller_pubkey: Some(request.payload.seller_pubkey.clone()), last_event_id: Some(receipt.event_id), @@ -3348,7 +3350,7 @@ fn invalid_projection_with_payment( payment, economics, agreement_event_id: None, - listing_addr: request.map(|request| request.payload.listing_addr.clone()), + listing_addr: request.map(|request| request.payload.listing_addr.to_string()), buyer_pubkey: request.map(|request| request.payload.buyer_pubkey.clone()), seller_pubkey: request.map(|request| request.payload.seller_pubkey.clone()), last_event_id: request.map(|request| request.event_id.clone()), @@ -3376,7 +3378,6 @@ fn canonicalize_items( } 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(RadrootsOrderCanonicalizationError::InvalidBinCount { index }); } @@ -3421,7 +3422,6 @@ fn canonicalize_inventory_commitments( 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( RadrootsOrderCanonicalizationError::InvalidInventoryCommitmentCount { index }, @@ -3503,6 +3503,10 @@ mod tests { use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; + use radroots_events::ids::{ + RadrootsEconomicsDigest, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, + RadrootsOrderQuoteId, RadrootsOrderRevisionId, + }; use radroots_events::kinds::KIND_LISTING; use radroots_events::order::{ RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, @@ -3535,14 +3539,38 @@ mod tests { const SELLER: &str = "1111111111111111111111111111111111111111111111111111111111111111"; const BUYER: &str = "2222222222222222222222222222222222222222222222222222222222222222"; + fn order_id(raw: &str) -> RadrootsOrderId { + RadrootsOrderId::parse(raw).expect("order id") + } + + fn order_revision_id(raw: &str) -> RadrootsOrderRevisionId { + RadrootsOrderRevisionId::parse(raw).expect("revision id") + } + + fn order_quote_id(raw: &str) -> RadrootsOrderQuoteId { + RadrootsOrderQuoteId::parse(raw).expect("quote id") + } + + fn bin_id(raw: &str) -> RadrootsInventoryBinId { + RadrootsInventoryBinId::parse(raw).expect("bin id") + } + + fn listing_address() -> RadrootsListingAddress { + RadrootsListingAddress::parse(listing_addr()).expect("listing address") + } + + fn economics_digest(raw: impl AsRef<str>) -> RadrootsEconomicsDigest { + RadrootsEconomicsDigest::parse(raw.as_ref()).expect("economics digest") + } + 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 "), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: buyer_pubkey.to_string(), seller_pubkey: seller_pubkey.to_string(), items: vec![RadrootsOrderItem { - bin_id: " bin-1 ".to_string(), + bin_id: bin_id("bin-1"), bin_count: 2, }], economics: request_economics("bin-1", 2, "10"), @@ -3557,14 +3585,18 @@ mod tests { RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) } - fn request_economics(bin_id: &str, bin_count: u32, subtotal: &str) -> RadrootsOrderEconomics { + fn request_economics( + raw_bin_id: &str, + bin_count: u32, + subtotal: &str, + ) -> RadrootsOrderEconomics { RadrootsOrderEconomics { - quote_id: "quote-1".to_string(), + quote_id: order_quote_id("quote-1"), quote_version: 1, pricing_basis: RadrootsOrderPricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, items: vec![RadrootsOrderEconomicItem { - bin_id: bin_id.to_string(), + bin_id: bin_id(raw_bin_id), bin_count, quantity_amount: decimal("1"), quantity_unit: RadrootsCoreUnit::Each, @@ -3583,13 +3615,13 @@ mod tests { fn sample_order_decision(seller_pubkey: &str) -> RadrootsOrderDecision { RadrootsOrderDecision { - order_id: " order-1 ".to_string(), - listing_addr: format!(" {KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg "), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: format!(" {BUYER} "), seller_pubkey: seller_pubkey.to_string(), decision: RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: " bin-1 ".to_string(), + bin_id: bin_id("bin-1"), bin_count: 2, }], }, @@ -3602,12 +3634,12 @@ mod tests { fn clean_request_payload() -> RadrootsOrderRequest { RadrootsOrderRequest { - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), items: vec![RadrootsOrderItem { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 2, }], economics: request_economics("bin-1", 2, "10"), @@ -3627,12 +3659,12 @@ mod tests { } fn request_record_for( - order_id: &str, + raw_order_id: &str, event_id: &str, bin_count: u32, ) -> RadrootsOrderRequestRecord { let mut request = request_record_with_event_id(event_id); - request.payload.order_id = order_id.to_string(); + request.payload.order_id = order_id(raw_order_id); request.payload.items[0].bin_count = bin_count; let subtotal = (RadrootsCoreDecimal::from(5u32) * RadrootsCoreDecimal::from(bin_count)).to_string(); @@ -3642,8 +3674,8 @@ mod tests { fn decision_payload(decision: RadrootsOrderDecisionOutcome) -> RadrootsOrderDecision { RadrootsOrderDecision { - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), decision, @@ -3659,7 +3691,7 @@ mod tests { prev_event_id: "request-1".to_string(), payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 2, }], }), @@ -3691,8 +3723,8 @@ mod tests { root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), payload: RadrootsOrderFulfillmentUpdate { - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), status, @@ -3708,8 +3740,8 @@ mod tests { root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), payload: RadrootsOrderCancellation { - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), reason: "changed plans".to_string(), @@ -3729,8 +3761,8 @@ mod tests { root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), payload: RadrootsOrderReceipt { - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), received, @@ -3749,8 +3781,8 @@ mod tests { root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), payload: RadrootsOrderPaymentPayload { - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), @@ -3758,7 +3790,9 @@ mod tests { agreement_event_id: "decision-1".to_string(), quote_id: economics.quote_id.clone(), quote_version: economics.quote_version, - economics_digest: radroots_order_economics_digest(&economics).unwrap(), + economics_digest: economics_digest( + radroots_order_economics_digest(&economics).unwrap(), + ), amount: economics.total.amount, currency: economics.total.currency, method: RadrootsOrderPaymentMethod::ManualTransfer, @@ -3802,7 +3836,7 @@ mod tests { } fn accepted_decision_record_for( - order_id: &str, + raw_order_id: &str, event_id: &str, request_event_id: &str, bin_count: u32, @@ -3810,7 +3844,7 @@ mod tests { 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(); + decision.payload.order_id = order_id(raw_order_id); let RadrootsOrderDecisionOutcome::Accepted { inventory_commitments, } = &mut decision.payload.decision @@ -3843,15 +3877,15 @@ mod tests { root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), payload: RadrootsOrderRevisionProposal { - revision_id: revision_id.to_string(), - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + revision_id: order_revision_id(revision_id), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), items: vec![RadrootsOrderItem { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count, }], economics: request_economics("bin-1", bin_count, &subtotal), @@ -3873,9 +3907,9 @@ mod tests { root_event_id: "request-1".to_string(), prev_event_id: prev_event_id.to_string(), payload: RadrootsOrderRevisionDecision { - revision_id: revision_id.to_string(), - order_id: "order-1".to_string(), - listing_addr: listing_addr(), + revision_id: order_revision_id(revision_id), + order_id: order_id("order-1"), + listing_addr: listing_address(), buyer_pubkey: BUYER.to_string(), seller_pubkey: SELLER.to_string(), root_event_id: "request-1".to_string(), @@ -3967,11 +4001,11 @@ mod tests { request.economics.total = usd("12"); request.items = vec![ RadrootsOrderItem { - bin_id: " bin-1 ".to_string(), + bin_id: bin_id("bin-1"), bin_count: 1, }, RadrootsOrderItem { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 1, }, ]; @@ -3981,7 +4015,7 @@ mod tests { assert_eq!( request.items, vec![RadrootsOrderItem { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 2, }] ); @@ -5022,7 +5056,7 @@ mod tests { let decision = RadrootsOrderDecisionRecord { payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 1, }], }), @@ -5242,7 +5276,7 @@ mod tests { let decision = RadrootsOrderDecisionRecord { payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 1, }], }), @@ -5264,7 +5298,7 @@ mod tests { let decision = RadrootsOrderDecisionRecord { payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-2".to_string(), + bin_id: bin_id("bin-2"), bin_count: 2, }], }), @@ -5287,18 +5321,18 @@ mod tests { let mut request = request_record(); request.payload.items = vec![ RadrootsOrderItem { - bin_id: " bin-1 ".to_string(), + bin_id: bin_id("bin-1"), bin_count: 1, }, RadrootsOrderItem { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 1, }, ]; let decision = RadrootsOrderDecisionRecord { payload: decision_payload(RadrootsOrderDecisionOutcome::Accepted { inventory_commitments: vec![RadrootsOrderInventoryCommitment { - bin_id: "bin-1".to_string(), + bin_id: bin_id("bin-1"), bin_count: 2, }], }), diff --git a/scripts/ci/guard_no_legacy_identifiers.sh b/scripts/ci/guard_no_legacy_identifiers.sh @@ -22,6 +22,34 @@ scan_forbidden() { fi } +scan_raw_commercial_identifier_fields() { + local files + files="$(rg --files crates/events/src crates/events_codec/src crates/trade/src -g '*.rs')" + + local matches + matches="$( + awk ' + /^pub struct / { + struct_name = $3 + sub(/\{.*/, "", struct_name) + sub(/<.*/, "", struct_name) + } + /^}/ { struct_name = "" } + /^[[:space:]]*pub (order_id|listing_addr|revision_id|quote_id|primary_bin_id|bin_id|economics_digest): String,/ { + if (struct_name != "RadrootsOrderEnvelope" && struct_name != "BinDraft" && struct_name !~ /Projection|Accounting|Availability|Reservation|Issue|NormalizedInventoryCount|TradeListing|ValidationReceiptTags/) { + print FILENAME ":" FNR ":" $0 + } + } + ' $files || true + )" + + if [[ -n $matches ]]; then + echo "raw commercial protocol identifier String fields are forbidden in active payload structs" + echo "$matches" + exit 1 + fi +} + scan_forbidden "legacy identifier 'tangle'" "tangle" . scan_forbidden \ @@ -34,4 +62,6 @@ scan_forbidden \ "KIND_TRADE_LISTING_(ORDER|QUESTION|ANSWER|DISCOUNT|CANCEL|FULFILLMENT|RECEIPT)" \ crates spec scripts +scan_raw_commercial_identifier_fields + echo "no legacy identifiers found in oss source files"