lib

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

commit ab8cc47b6b29989767c1b1ecb46c41f9a94afcc8
parent c1a3bbd5e1c66c57233880c0c2ce61b520c301fd
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 18:26:47 -0700

trade: validate canonical listing draft construction

Diffstat:
Mcrates/trade/src/listing/draft.rs | 202++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/trade/src/listing/mutation.rs | 9+--------
2 files changed, 134 insertions(+), 77 deletions(-)

diff --git a/crates/trade/src/listing/draft.rs b/crates/trade/src/listing/draft.rs @@ -47,17 +47,31 @@ pub struct RadrootsCanonicalListingDraft { impl RadrootsCanonicalListingDraft { pub fn new( - listing: RadrootsListing, + mut listing: RadrootsListing, seller_pubkey: RadrootsPublicKey, - public_listing_addr: RadrootsListingAddress, - draft_listing_addr: RadrootsListingAddress, - ) -> Self { - Self { + ) -> Result<Self, RadrootsListingDraftError> { + let farm_pubkey = RadrootsPublicKey::parse(listing.farm.pubkey.as_str()) + .map_err(RadrootsListingDraftError::InvalidSellerPubkey)?; + if farm_pubkey != seller_pubkey { + return Err(RadrootsListingDraftError::FarmPubkeyMismatch { + expected_pubkey: seller_pubkey, + actual_pubkey: farm_pubkey, + }); + } + listing.farm.pubkey = farm_pubkey.as_str().to_string(); + validate_listing_bins(&listing)?; + + let public_listing_addr = + listing_addr(KIND_LISTING, &seller_pubkey, listing.d_tag.as_str())?; + let draft_listing_addr = + listing_addr(KIND_LISTING_DRAFT, &seller_pubkey, listing.d_tag.as_str())?; + + Ok(Self { listing, seller_pubkey, public_listing_addr, draft_listing_addr, - } + }) } } @@ -82,36 +96,11 @@ pub enum RadrootsListingDraftError { DuplicateBinId { bin_id: RadrootsInventoryBinId }, } -pub fn canonicalize_listing_draft( - actor: &RadrootsActorContext, - mut document: RadrootsListingDraftDocumentV1, -) -> Result<RadrootsCanonicalListingDraft, RadrootsListingDraftError> { - if !actor.satisfies(RadrootsActorRole::Seller) { - return Err(RadrootsListingDraftError::ActorRoleUnsatisfied { - required_role: RadrootsActorRole::Seller, - }); - } - - let seller_pubkey = actor.pubkey().clone(); - let farm_pubkey = document.listing.farm.pubkey.as_str(); - if farm_pubkey.is_empty() { - document.listing.farm.pubkey = seller_pubkey.as_str().to_string(); - } else { - let farm_pubkey = RadrootsPublicKey::parse(farm_pubkey) - .map_err(RadrootsListingDraftError::InvalidSellerPubkey)?; - if farm_pubkey != seller_pubkey { - return Err(RadrootsListingDraftError::FarmPubkeyMismatch { - expected_pubkey: seller_pubkey, - actual_pubkey: farm_pubkey, - }); - } - document.listing.farm.pubkey = farm_pubkey.as_str().to_string(); - } - - let primary_bin_id = document.listing.primary_bin_id.clone(); +fn validate_listing_bins(listing: &RadrootsListing) -> Result<(), RadrootsListingDraftError> { + let primary_bin_id = listing.primary_bin_id.clone(); let mut seen_bin_ids = Vec::new(); let mut primary_bin_found = false; - for bin in &document.listing.bins { + for bin in &listing.bins { if seen_bin_ids .iter() .any(|seen_bin_id| seen_bin_id == &bin.bin_id) @@ -129,26 +118,35 @@ pub fn canonicalize_listing_draft( if !primary_bin_found { return Err(RadrootsListingDraftError::MissingPrimaryBin { primary_bin_id }); } + Ok(()) +} - let public_listing_addr = RadrootsListingAddress::parse(format!( - "{KIND_LISTING}:{}:{}", - seller_pubkey.as_str(), - document.listing.d_tag.as_str() - )) - .map_err(RadrootsListingDraftError::InvalidListingAddress)?; - let draft_listing_addr = RadrootsListingAddress::parse(format!( - "{KIND_LISTING_DRAFT}:{}:{}", - seller_pubkey.as_str(), - document.listing.d_tag.as_str() - )) - .map_err(RadrootsListingDraftError::InvalidListingAddress)?; - - Ok(RadrootsCanonicalListingDraft::new( - document.listing, - seller_pubkey, - public_listing_addr, - draft_listing_addr, - )) +fn listing_addr( + kind: u32, + seller_pubkey: &RadrootsPublicKey, + d_tag: &str, +) -> Result<RadrootsListingAddress, RadrootsListingDraftError> { + RadrootsListingAddress::parse(format!("{kind}:{}:{d_tag}", seller_pubkey.as_str())) + .map_err(RadrootsListingDraftError::InvalidListingAddress) +} + +pub fn canonicalize_listing_draft( + actor: &RadrootsActorContext, + mut document: RadrootsListingDraftDocumentV1, +) -> Result<RadrootsCanonicalListingDraft, RadrootsListingDraftError> { + if !actor.satisfies(RadrootsActorRole::Seller) { + return Err(RadrootsListingDraftError::ActorRoleUnsatisfied { + required_role: RadrootsActorRole::Seller, + }); + } + + let seller_pubkey = actor.pubkey().clone(); + let farm_pubkey = document.listing.farm.pubkey.as_str(); + if farm_pubkey.is_empty() { + document.listing.farm.pubkey = seller_pubkey.as_str().to_string(); + } + + RadrootsCanonicalListingDraft::new(document.listing, seller_pubkey) } #[cfg(test)] @@ -254,26 +252,20 @@ mod tests { #[test] fn canonical_draft_carries_seller_listing_and_addresses() { let seller_pubkey = RadrootsPublicKey::parse(SELLER).expect("seller"); - let public_listing_addr = RadrootsListingAddress::parse(format!( - "{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg" - )) - .expect("public listing address"); - let draft_listing_addr = RadrootsListingAddress::parse(format!( - "{KIND_LISTING_DRAFT}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg" - )) - .expect("draft listing address"); let listing = listing(); - let canonical = RadrootsCanonicalListingDraft::new( - listing, - seller_pubkey.clone(), - public_listing_addr.clone(), - draft_listing_addr.clone(), - ); + let canonical = + RadrootsCanonicalListingDraft::new(listing, seller_pubkey.clone()).expect("canonical"); assert_eq!(canonical.seller_pubkey, seller_pubkey); - assert_eq!(canonical.public_listing_addr, public_listing_addr); - assert_eq!(canonical.draft_listing_addr, draft_listing_addr); + assert_eq!( + canonical.public_listing_addr.as_str(), + format!("{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") + ); + assert_eq!( + canonical.draft_listing_addr.as_str(), + format!("{KIND_LISTING_DRAFT}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") + ); assert_eq!(canonical.listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); } @@ -343,6 +335,40 @@ mod tests { } #[test] + fn canonical_draft_new_rejects_mismatched_farm_pubkey() { + let mut listing = listing(); + listing.farm.pubkey = OTHER.to_string(); + + let error = RadrootsCanonicalListingDraft::new( + listing, + RadrootsPublicKey::parse(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert!(matches!( + error, + RadrootsListingDraftError::FarmPubkeyMismatch { .. } + )); + } + + #[test] + fn canonical_draft_new_rejects_empty_farm_pubkey() { + let mut listing = listing(); + listing.farm.pubkey.clear(); + + let error = RadrootsCanonicalListingDraft::new( + listing, + RadrootsPublicKey::parse(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert!(matches!( + error, + RadrootsListingDraftError::InvalidSellerPubkey(_) + )); + } + + #[test] fn canonicalize_listing_draft_rejects_missing_primary_bin() { let mut listing = listing(); listing.primary_bin_id = bin_id("bin-2"); @@ -359,6 +385,25 @@ mod tests { } #[test] + fn canonical_draft_new_rejects_missing_primary_bin() { + let mut listing = listing(); + listing.primary_bin_id = bin_id("bin-2"); + + let error = RadrootsCanonicalListingDraft::new( + listing, + RadrootsPublicKey::parse(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert_eq!( + error, + RadrootsListingDraftError::MissingPrimaryBin { + primary_bin_id: bin_id("bin-2") + } + ); + } + + #[test] fn canonicalize_listing_draft_rejects_duplicate_bin_ids() { let mut listing = listing(); listing.bins.push(listing.bins[0].clone()); @@ -373,4 +418,23 @@ mod tests { } ); } + + #[test] + fn canonical_draft_new_rejects_duplicate_bin_ids() { + let mut listing = listing(); + listing.bins.push(listing.bins[0].clone()); + + let error = RadrootsCanonicalListingDraft::new( + listing, + RadrootsPublicKey::parse(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert_eq!( + error, + RadrootsListingDraftError::DuplicateBinId { + bin_id: bin_id("bin-1") + } + ); + } } diff --git a/crates/trade/src/listing/mutation.rs b/crates/trade/src/listing/mutation.rs @@ -240,15 +240,8 @@ mod tests { RadrootsCanonicalListingDraft::new( listing(), RadrootsPublicKey::parse(SELLER).expect("seller"), - RadrootsListingAddress::parse(format!( - "{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg" - )) - .expect("public listing address"), - RadrootsListingAddress::parse(format!( - "{KIND_LISTING_DRAFT}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg" - )) - .expect("draft listing address"), ) + .expect("canonical listing draft") } #[test]