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:
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"