lib

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

commit 6d0441a8b62df67a0744f125e5f95c8cf3c6397c
parent c1a47c298393571893298dae6cd8d50c79cdbbc6
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 02:09:22 -0700

trade: add listing draft model

- add typed listing draft document and canonical draft models
- expose listing draft types through the listing module
- reserve precise draft error variants for canonicalization follow-up
- validate with cargo fmt, check, and tests for radroots_trade

Diffstat:
Acrates/trade/src/listing/draft.rs | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/trade/src/listing/mod.rs | 4++++
2 files changed, 181 insertions(+), 0 deletions(-)

diff --git a/crates/trade/src/listing/draft.rs b/crates/trade/src/listing/draft.rs @@ -0,0 +1,177 @@ +#![forbid(unsafe_code)] + +use radroots_events::{ + ids::{RadrootsIdParseError, RadrootsListingAddress, RadrootsPublicKey}, + listing::RadrootsListing, +}; +use thiserror::Error; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsListingDraftDocumentV1 { + pub listing: RadrootsListing, +} + +impl RadrootsListingDraftDocumentV1 { + pub fn new(listing: RadrootsListing) -> Self { + Self { listing } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsCanonicalListingDraft { + pub seller_pubkey: RadrootsPublicKey, + pub listing_addr: RadrootsListingAddress, + pub document: RadrootsListingDraftDocumentV1, +} + +impl RadrootsCanonicalListingDraft { + pub fn new( + seller_pubkey: RadrootsPublicKey, + listing_addr: RadrootsListingAddress, + document: RadrootsListingDraftDocumentV1, + ) -> Self { + Self { + seller_pubkey, + listing_addr, + document, + } + } +} + +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum RadrootsListingDraftError { + #[error("invalid listing draft seller pubkey: {0}")] + InvalidSellerPubkey(RadrootsIdParseError), + #[error("invalid listing draft address: {0}")] + InvalidListingAddress(RadrootsIdParseError), +} + +#[cfg(test)] +mod tests { + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, + RadrootsCoreQuantityPrice, RadrootsCoreUnit, + }; + use radroots_events::{ + farm::RadrootsFarmRef, + ids::{RadrootsDTag, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsPublicKey}, + kinds::KIND_LISTING_DRAFT, + listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}, + }; + + use super::{ + RadrootsCanonicalListingDraft, RadrootsListingDraftDocumentV1, RadrootsListingDraftError, + }; + + const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + 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 listing() -> RadrootsListing { + RadrootsListing { + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), + published_at: None, + farm: RadrootsFarmRef { + pubkey: SELLER.to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + product: RadrootsListingProduct { + key: "coffee".to_string(), + title: "Coffee".to_string(), + category: "coffee".to_string(), + summary: Some("Single origin coffee".to_string()), + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: bin_id("bin-1"), + bins: vec![RadrootsListingBin { + bin_id: bin_id("bin-1"), + quantity: RadrootsCoreQuantity::new( + RadrootsCoreDecimal::from(1000u32), + RadrootsCoreUnit::MassG, + ), + price_per_canonical_unit: RadrootsCoreQuantityPrice { + amount: RadrootsCoreMoney::new( + RadrootsCoreDecimal::from(20u32), + RadrootsCoreCurrency::USD, + ), + quantity: RadrootsCoreQuantity::new( + RadrootsCoreDecimal::from(1u32), + RadrootsCoreUnit::MassG, + ), + }, + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: None, + availability: None, + delivery_method: None, + location: None, + images: None, + } + } + + #[test] + fn draft_document_wraps_listing() { + let document = RadrootsListingDraftDocumentV1::new(listing()); + + assert_eq!(document.listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); + assert_eq!(document.listing.product.title, "Coffee"); + } + + #[test] + fn canonical_draft_carries_seller_and_address() { + let seller_pubkey = RadrootsPublicKey::parse(SELLER).expect("seller"); + let listing_addr = RadrootsListingAddress::parse(format!( + "{KIND_LISTING_DRAFT}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg" + )) + .expect("listing address"); + let document = RadrootsListingDraftDocumentV1::new(listing()); + + let canonical = RadrootsCanonicalListingDraft::new( + seller_pubkey.clone(), + listing_addr.clone(), + document, + ); + + assert_eq!(canonical.seller_pubkey, seller_pubkey); + assert_eq!(canonical.listing_addr, listing_addr); + assert_eq!( + canonical.document.listing.d_tag.as_str(), + "AAAAAAAAAAAAAAAAAAAAAg" + ); + } + + #[test] + fn listing_draft_error_variants_are_precise() { + assert!(matches!( + RadrootsListingDraftError::InvalidSellerPubkey( + RadrootsPublicKey::parse("bad").unwrap_err() + ), + RadrootsListingDraftError::InvalidSellerPubkey(_) + )); + assert!(matches!( + RadrootsListingDraftError::InvalidListingAddress( + RadrootsListingAddress::parse("bad").unwrap_err() + ), + RadrootsListingDraftError::InvalidListingAddress(_) + )); + } +} diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs @@ -1,4 +1,5 @@ mod codec; +pub mod draft; pub mod model; pub mod price_ext; pub mod publish; @@ -6,6 +7,9 @@ pub mod validation; use radroots_events::{RadrootsNostrEvent, kinds::is_listing_kind, listing::RadrootsListing}; +pub use self::draft::{ + RadrootsCanonicalListingDraft, RadrootsListingDraftDocumentV1, RadrootsListingDraftError, +}; pub use radroots_events::order::RadrootsListingParseError as ListingParseError; pub fn parse_listing_event(