commit d85a5f64f1c25835b1ce46bf596dfece18dd18c0
parent 927e08fac32a77b9e104e6d2203b3c0b61dc28c3
Author: triesap <tyson@radroots.org>
Date: Sat, 3 Jan 2026 15:20:13 +0000
events: add resource area and harvest cap event support
- Add resource_area and resource_cap modules with encode/decode and tests
- Parse and emit radroots:resource_area and radroots:plot listing tags
- Extend listing models/bindings with optional resource_area and plot refs
- Fix nostr client timestamp handling and event stream error tolerance
Diffstat:
27 files changed, 1241 insertions(+), 81 deletions(-)
diff --git a/events-codec/src/farm/mod.rs b/events-codec/src/farm/mod.rs
@@ -178,6 +178,8 @@ mod tests {
display_price: None,
display_price_unit: None,
}],
+ resource_area: None,
+ plot: None,
discounts: None,
inventory_available: None,
availability: None,
diff --git a/events-codec/src/lib.rs b/events-codec/src/lib.rs
@@ -16,6 +16,8 @@ pub mod app_data;
pub mod document;
pub mod coop;
pub mod farm;
+pub mod resource_area;
+pub mod resource_cap;
pub mod gift_wrap;
pub mod message;
pub mod message_file;
diff --git a/events-codec/src/listing/decode.rs b/events-codec/src/listing/decode.rs
@@ -5,9 +5,11 @@ use alloc::{string::{String, ToString}, vec::Vec};
use radroots_events::{
RadrootsNostrEvent,
- kinds::KIND_FARM,
+ kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA},
kinds::KIND_LISTING,
listing::{RadrootsListing, RadrootsListingEventIndex, RadrootsListingEventMetadata, RadrootsListingFarmRef},
+ plot::RadrootsPlotRef,
+ resource_area::RadrootsResourceAreaRef,
tags::TAG_D,
};
@@ -16,6 +18,8 @@ use crate::error::EventParseError;
const DEFAULT_KIND: u32 = KIND_LISTING;
const TAG_A: &str = "a";
const TAG_P: &str = "p";
+const TAG_RADROOTS_RESOURCE_AREA: &str = "radroots:resource_area";
+const TAG_RADROOTS_PLOT: &str = "radroots:plot";
fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> {
let tag = tags
@@ -33,49 +37,114 @@ fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> {
}
fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsListingFarmRef, EventParseError> {
+ for tag in tags.iter().filter(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_A)) {
+ let value = tag
+ .get(1)
+ .map(|s| s.to_string())
+ .ok_or(EventParseError::InvalidTag(TAG_A))?;
+ let mut parts = value.splitn(3, ':');
+ let kind = parts
+ .next()
+ .and_then(|v| v.parse::<u32>().ok())
+ .ok_or(EventParseError::InvalidTag(TAG_A))?;
+ if kind != KIND_FARM {
+ continue;
+ }
+ let pubkey = parts
+ .next()
+ .ok_or(EventParseError::InvalidTag(TAG_A))?
+ .to_string();
+ let d_tag = parts
+ .next()
+ .ok_or(EventParseError::InvalidTag(TAG_A))?
+ .to_string();
+ if pubkey.trim().is_empty() || d_tag.trim().is_empty() {
+ return Err(EventParseError::InvalidTag(TAG_A));
+ }
+ return Ok(RadrootsListingFarmRef { pubkey, d_tag });
+ }
+ Err(EventParseError::MissingTag(TAG_A))
+}
+
+fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, EventParseError> {
+ let tag = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_P))
+ .ok_or(EventParseError::MissingTag(TAG_P))?;
+ let value = tag
+ .get(1)
+ .map(|s| s.to_string())
+ .ok_or(EventParseError::InvalidTag(TAG_P))?;
+ if value.trim().is_empty() {
+ return Err(EventParseError::InvalidTag(TAG_P));
+ }
+ Ok(value)
+}
+
+fn parse_resource_area(tags: &[Vec<String>]) -> Result<Option<RadrootsResourceAreaRef>, EventParseError> {
let tag = tags
.iter()
- .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_A))
- .ok_or(EventParseError::MissingTag(TAG_A))?;
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_RADROOTS_RESOURCE_AREA));
+ let Some(tag) = tag else {
+ return Ok(None);
+ };
let value = tag
.get(1)
.map(|s| s.to_string())
- .ok_or(EventParseError::InvalidTag(TAG_A))?;
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))?;
let mut parts = value.splitn(3, ':');
let kind = parts
.next()
.and_then(|v| v.parse::<u32>().ok())
- .ok_or(EventParseError::InvalidTag(TAG_A))?;
- if kind != KIND_FARM {
- return Err(EventParseError::InvalidTag(TAG_A));
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))?;
+ if kind != KIND_RESOURCE_AREA {
+ return Err(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA));
}
let pubkey = parts
.next()
- .ok_or(EventParseError::InvalidTag(TAG_A))?
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))?
.to_string();
let d_tag = parts
.next()
- .ok_or(EventParseError::InvalidTag(TAG_A))?
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))?
.to_string();
if pubkey.trim().is_empty() || d_tag.trim().is_empty() {
- return Err(EventParseError::InvalidTag(TAG_A));
+ return Err(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA));
}
- Ok(RadrootsListingFarmRef { pubkey, d_tag })
+ Ok(Some(RadrootsResourceAreaRef { pubkey, d_tag }))
}
-fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, EventParseError> {
+fn parse_plot_ref(tags: &[Vec<String>]) -> Result<Option<RadrootsPlotRef>, EventParseError> {
let tag = tags
.iter()
- .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_P))
- .ok_or(EventParseError::MissingTag(TAG_P))?;
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_RADROOTS_PLOT));
+ let Some(tag) = tag else {
+ return Ok(None);
+ };
let value = tag
.get(1)
.map(|s| s.to_string())
- .ok_or(EventParseError::InvalidTag(TAG_P))?;
- if value.trim().is_empty() {
- return Err(EventParseError::InvalidTag(TAG_P));
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))?;
+ let mut parts = value.splitn(3, ':');
+ let kind = parts
+ .next()
+ .and_then(|v| v.parse::<u32>().ok())
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))?;
+ if kind != KIND_PLOT {
+ return Err(EventParseError::InvalidTag(TAG_RADROOTS_PLOT));
}
- Ok(value)
+ let pubkey = parts
+ .next()
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))?
+ .to_string();
+ let d_tag = parts
+ .next()
+ .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))?
+ .to_string();
+ if pubkey.trim().is_empty() || d_tag.trim().is_empty() {
+ return Err(EventParseError::InvalidTag(TAG_RADROOTS_PLOT));
+ }
+ Ok(Some(RadrootsPlotRef { pubkey, d_tag }))
}
pub fn listing_from_event(
@@ -95,6 +164,8 @@ pub fn listing_from_event(
let d_tag = parse_d_tag(tags)?;
let farm_ref = parse_farm_ref(tags)?;
let farm_pubkey = parse_farm_pubkey(tags)?;
+ let resource_area = parse_resource_area(tags)?;
+ let plot = parse_plot_ref(tags)?;
let mut listing: RadrootsListing =
serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?;
@@ -113,6 +184,28 @@ pub fn listing_from_event(
return Err(EventParseError::InvalidTag(TAG_P));
}
+ if let Some(tag_area) = resource_area {
+ match listing.resource_area.as_ref() {
+ None => listing.resource_area = Some(tag_area),
+ Some(area) => {
+ if area.pubkey != tag_area.pubkey || area.d_tag != tag_area.d_tag {
+ return Err(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA));
+ }
+ }
+ }
+ }
+
+ if let Some(tag_plot) = plot {
+ match listing.plot.as_ref() {
+ None => listing.plot = Some(tag_plot),
+ Some(existing) => {
+ if existing.pubkey != tag_plot.pubkey || existing.d_tag != tag_plot.d_tag {
+ return Err(EventParseError::InvalidTag(TAG_RADROOTS_PLOT));
+ }
+ }
+ }
+ }
+
Ok(listing)
}
diff --git a/events-codec/src/listing/tags.rs b/events-codec/src/listing/tags.rs
@@ -11,7 +11,9 @@ use radroots_events::listing::{
RadrootsListingBin, RadrootsListingImage, RadrootsListingLocation,
RadrootsListingStatus,
};
-use radroots_events::kinds::KIND_FARM;
+use radroots_events::plot::RadrootsPlotRef;
+use radroots_events::resource_area::RadrootsResourceAreaRef;
+use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA};
use radroots_events::tags::TAG_D;
use crate::error::EventEncodeError;
@@ -21,6 +23,8 @@ const TAG_RADROOTS_BIN: &str = "radroots:bin";
const TAG_RADROOTS_PRICE: &str = "radroots:price";
const TAG_RADROOTS_DISCOUNT: &str = "radroots:discount";
const TAG_RADROOTS_PRIMARY_BIN: &str = "radroots:primary_bin";
+const TAG_RADROOTS_RESOURCE_AREA: &str = "radroots:resource_area";
+const TAG_RADROOTS_PLOT: &str = "radroots:plot";
const TAG_LOCATION: &str = "location";
const TAG_IMAGE: &str = "image";
const TAG_GEOHASH: &str = "g";
@@ -104,6 +108,12 @@ pub fn listing_tags_with_options(
let mut tags: Vec<Vec<String>> = Vec::new();
tags.push(vec![TAG_D.to_string(), d_tag.to_string()]);
push_farm_tags(&mut tags, &listing.farm)?;
+ if let Some(resource_area) = listing.resource_area.as_ref() {
+ tags.push(tag_listing_resource_area(resource_area)?);
+ }
+ if let Some(plot) = listing.plot.as_ref() {
+ tags.push(tag_listing_plot(plot)?);
+ }
let product = &listing.product;
push_tag_value(&mut tags, "key", &product.key);
@@ -244,6 +254,40 @@ fn push_farm_tags(
Ok(())
}
+fn tag_listing_resource_area(
+ area: &RadrootsResourceAreaRef,
+) -> Result<Vec<String>, EventEncodeError> {
+ if area.pubkey.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("resource_area.pubkey"));
+ }
+ if area.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("resource_area.d_tag"));
+ }
+ let mut address = String::new();
+ address.push_str(&KIND_RESOURCE_AREA.to_string());
+ address.push(':');
+ address.push_str(&area.pubkey);
+ address.push(':');
+ address.push_str(&area.d_tag);
+ Ok(vec![TAG_RADROOTS_RESOURCE_AREA.to_string(), address])
+}
+
+fn tag_listing_plot(plot: &RadrootsPlotRef) -> Result<Vec<String>, EventEncodeError> {
+ if plot.pubkey.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("plot.pubkey"));
+ }
+ if plot.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("plot.d_tag"));
+ }
+ let mut address = String::new();
+ address.push_str(&KIND_PLOT.to_string());
+ address.push(':');
+ address.push_str(&plot.pubkey);
+ address.push(':');
+ address.push_str(&plot.d_tag);
+ Ok(vec![TAG_RADROOTS_PLOT.to_string(), address])
+}
+
fn tag_listing_bin(bin: &RadrootsListingBin) -> Result<Vec<String>, EventEncodeError> {
if bin.bin_id.trim().is_empty() {
return Err(EventEncodeError::EmptyRequiredField("bin_id"));
diff --git a/events-codec/src/resource_area/decode.rs b/events-codec/src/resource_area/decode.rs
@@ -0,0 +1,106 @@
+#![cfg(feature = "serde_json")]
+
+#[cfg(not(feature = "std"))]
+use alloc::{string::{String, ToString}, vec::Vec};
+
+use radroots_events::{
+ RadrootsNostrEvent,
+ kinds::KIND_RESOURCE_AREA,
+ resource_area::{RadrootsResourceArea, RadrootsResourceAreaEventIndex, RadrootsResourceAreaEventMetadata},
+ tags::TAG_D,
+};
+
+use crate::error::EventParseError;
+
+const DEFAULT_KIND: u32 = KIND_RESOURCE_AREA;
+
+fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> {
+ let tag = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D))
+ .ok_or(EventParseError::MissingTag(TAG_D))?;
+ let value = tag
+ .get(1)
+ .map(|s| s.to_string())
+ .ok_or(EventParseError::InvalidTag(TAG_D))?;
+ if value.trim().is_empty() {
+ return Err(EventParseError::InvalidTag(TAG_D));
+ }
+ Ok(value)
+}
+
+pub fn resource_area_from_event(
+ kind: u32,
+ tags: &[Vec<String>],
+ content: &str,
+) -> Result<RadrootsResourceArea, EventParseError> {
+ if kind != DEFAULT_KIND {
+ return Err(EventParseError::InvalidKind {
+ expected: "30370",
+ got: kind,
+ });
+ }
+ if content.trim().is_empty() {
+ return Err(EventParseError::InvalidJson("content"));
+ }
+ let d_tag = parse_d_tag(tags)?;
+ let mut area: RadrootsResourceArea =
+ serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?;
+
+ if area.d_tag.trim().is_empty() {
+ area.d_tag = d_tag;
+ } else if area.d_tag != d_tag {
+ return Err(EventParseError::InvalidTag(TAG_D));
+ }
+
+ Ok(area)
+}
+
+pub fn metadata_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+) -> Result<RadrootsResourceAreaEventMetadata, EventParseError> {
+ let area = resource_area_from_event(kind, &tags, &content)?;
+ Ok(RadrootsResourceAreaEventMetadata {
+ id,
+ author,
+ published_at,
+ kind,
+ area,
+ })
+}
+
+pub fn index_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+ sig: String,
+) -> Result<RadrootsResourceAreaEventIndex, EventParseError> {
+ let metadata = metadata_from_event(
+ id.clone(),
+ author.clone(),
+ published_at,
+ kind,
+ content.clone(),
+ tags.clone(),
+ )?;
+ Ok(RadrootsResourceAreaEventIndex {
+ event: RadrootsNostrEvent {
+ id,
+ author,
+ created_at: published_at,
+ kind,
+ content,
+ tags,
+ sig,
+ },
+ metadata,
+ })
+}
diff --git a/events-codec/src/resource_area/encode.rs b/events-codec/src/resource_area/encode.rs
@@ -0,0 +1,93 @@
+#[cfg(not(feature = "std"))]
+use alloc::{string::{String, ToString}, vec::Vec};
+
+use radroots_events::{
+ kinds::KIND_RESOURCE_AREA,
+ resource_area::{RadrootsResourceArea, RadrootsResourceAreaRef},
+ tags::TAG_D,
+};
+
+use crate::error::EventEncodeError;
+
+#[cfg(feature = "serde_json")]
+use crate::wire::WireEventParts;
+
+const TAG_T: &str = "t";
+const TAG_G: &str = "g";
+const TAG_A: &str = "a";
+const TAG_P: &str = "p";
+
+fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) {
+ let mut tag = Vec::with_capacity(2);
+ tag.push(key.to_string());
+ tag.push(value.to_string());
+ tags.push(tag);
+}
+
+fn resource_area_address(area: &RadrootsResourceAreaRef) -> Result<String, EventEncodeError> {
+ if area.pubkey.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("resource_area.pubkey"));
+ }
+ if area.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("resource_area.d_tag"));
+ }
+ let mut addr = String::new();
+ addr.push_str(&KIND_RESOURCE_AREA.to_string());
+ addr.push(':');
+ addr.push_str(&area.pubkey);
+ addr.push(':');
+ addr.push_str(&area.d_tag);
+ Ok(addr)
+}
+
+pub fn resource_area_build_tags(
+ area: &RadrootsResourceArea,
+) -> Result<Vec<Vec<String>>, EventEncodeError> {
+ if area.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("d_tag"));
+ }
+ if area.name.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("name"));
+ }
+ let geohash = area.location.gcs.geohash.trim();
+ if geohash.is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("location.gcs.geohash"));
+ }
+ let mut tags = Vec::new();
+ push_tag(&mut tags, TAG_D, &area.d_tag);
+ if let Some(items) = area.tags.as_ref() {
+ for item in items.iter().filter(|v| !v.trim().is_empty()) {
+ push_tag(&mut tags, TAG_T, item);
+ }
+ }
+ push_tag(&mut tags, TAG_G, geohash);
+ Ok(tags)
+}
+
+pub fn resource_area_ref_tags(
+ area: &RadrootsResourceAreaRef,
+) -> Result<Vec<Vec<String>>, EventEncodeError> {
+ let addr = resource_area_address(area)?;
+ let mut tags = Vec::with_capacity(2);
+ push_tag(&mut tags, TAG_P, &area.pubkey);
+ push_tag(&mut tags, TAG_A, &addr);
+ Ok(tags)
+}
+
+#[cfg(feature = "serde_json")]
+pub fn to_wire_parts(area: &RadrootsResourceArea) -> Result<WireEventParts, EventEncodeError> {
+ to_wire_parts_with_kind(area, KIND_RESOURCE_AREA)
+}
+
+#[cfg(feature = "serde_json")]
+pub fn to_wire_parts_with_kind(
+ area: &RadrootsResourceArea,
+ kind: u32,
+) -> Result<WireEventParts, EventEncodeError> {
+ if kind != KIND_RESOURCE_AREA {
+ return Err(EventEncodeError::InvalidKind(kind));
+ }
+ let tags = resource_area_build_tags(area)?;
+ let content = serde_json::to_string(area).map_err(|_| EventEncodeError::Json)?;
+ Ok(WireEventParts { kind, content, tags })
+}
diff --git a/events-codec/src/resource_area/list_sets.rs b/events-codec/src/resource_area/list_sets.rs
@@ -0,0 +1,150 @@
+#![forbid(unsafe_code)]
+
+#[cfg(not(feature = "std"))]
+use alloc::{format, string::{String, ToString}, vec, vec::Vec};
+
+use radroots_events::farm::RadrootsFarmRef;
+use radroots_events::kinds::{KIND_FARM, KIND_PLOT};
+use radroots_events::list::RadrootsListEntry;
+use radroots_events::list_set::RadrootsListSet;
+use radroots_events::plot::RadrootsPlotRef;
+
+use crate::error::EventEncodeError;
+
+fn resource_list_set_id(area_id: &str, suffix: &str) -> Result<String, EventEncodeError> {
+ let area_id = area_id.trim();
+ if area_id.is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("area_id"));
+ }
+ if suffix.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("list_set_suffix"));
+ }
+ Ok(format!("resource:{area_id}:{suffix}"))
+}
+
+fn list_entries<I, S>(tag: &str, values: I) -> Result<Vec<RadrootsListEntry>, EventEncodeError>
+where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+{
+ let mut entries = Vec::new();
+ for value in values {
+ let value = value.as_ref().trim();
+ if value.is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("entry.values"));
+ }
+ entries.push(RadrootsListEntry {
+ tag: tag.to_string(),
+ values: vec![value.to_string()],
+ });
+ }
+ Ok(entries)
+}
+
+fn farm_address(farm: &RadrootsFarmRef) -> Result<String, EventEncodeError> {
+ if farm.pubkey.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("farm.pubkey"));
+ }
+ if farm.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("farm.d_tag"));
+ }
+ let mut addr = String::new();
+ addr.push_str(&KIND_FARM.to_string());
+ addr.push(':');
+ addr.push_str(&farm.pubkey);
+ addr.push(':');
+ addr.push_str(&farm.d_tag);
+ Ok(addr)
+}
+
+fn plot_address(plot: &RadrootsPlotRef) -> Result<String, EventEncodeError> {
+ if plot.pubkey.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("plot.pubkey"));
+ }
+ if plot.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("plot.d_tag"));
+ }
+ let mut addr = String::new();
+ addr.push_str(&KIND_PLOT.to_string());
+ addr.push(':');
+ addr.push_str(&plot.pubkey);
+ addr.push(':');
+ addr.push_str(&plot.d_tag);
+ Ok(addr)
+}
+
+pub fn resource_area_members_farms_list_set<I>(
+ area_id: &str,
+ farms: I,
+) -> Result<RadrootsListSet, EventEncodeError>
+where
+ I: IntoIterator<Item = RadrootsFarmRef>,
+{
+ let mut entries = Vec::new();
+ for farm in farms {
+ let address = farm_address(&farm)?;
+ entries.push(RadrootsListEntry {
+ tag: "a".to_string(),
+ values: vec![address],
+ });
+ entries.push(RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![farm.pubkey],
+ });
+ }
+ Ok(RadrootsListSet {
+ d_tag: resource_list_set_id(area_id, "members.farms")?,
+ content: String::new(),
+ entries,
+ title: None,
+ description: None,
+ image: None,
+ })
+}
+
+pub fn resource_area_members_plots_list_set<I>(
+ area_id: &str,
+ plots: I,
+) -> Result<RadrootsListSet, EventEncodeError>
+where
+ I: IntoIterator<Item = RadrootsPlotRef>,
+{
+ let mut entries = Vec::new();
+ for plot in plots {
+ let address = plot_address(&plot)?;
+ entries.push(RadrootsListEntry {
+ tag: "a".to_string(),
+ values: vec![address],
+ });
+ entries.push(RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![plot.pubkey],
+ });
+ }
+ Ok(RadrootsListSet {
+ d_tag: resource_list_set_id(area_id, "members.plots")?,
+ content: String::new(),
+ entries,
+ title: None,
+ description: None,
+ image: None,
+ })
+}
+
+pub fn resource_area_stewards_list_set<I, S>(
+ area_id: &str,
+ stewards: I,
+) -> Result<RadrootsListSet, EventEncodeError>
+where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+{
+ Ok(RadrootsListSet {
+ d_tag: resource_list_set_id(area_id, "members.stewards")?,
+ content: String::new(),
+ entries: list_entries("p", stewards)?,
+ title: None,
+ description: None,
+ image: None,
+ })
+}
diff --git a/events-codec/src/resource_area/mod.rs b/events-codec/src/resource_area/mod.rs
@@ -0,0 +1,120 @@
+#![forbid(unsafe_code)]
+
+pub mod encode;
+pub mod decode;
+pub mod list_sets;
+
+#[cfg(test)]
+mod tests {
+ use radroots_events::farm::{RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon};
+ use radroots_events::resource_area::{RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef};
+ use crate::resource_area::encode::{resource_area_build_tags, resource_area_ref_tags};
+ use crate::resource_area::list_sets::{
+ resource_area_members_farms_list_set,
+ resource_area_members_plots_list_set,
+ resource_area_stewards_list_set,
+ };
+ use radroots_events::farm::RadrootsFarmRef;
+ use radroots_events::plot::RadrootsPlotRef;
+
+ fn sample_location() -> RadrootsResourceAreaLocation {
+ RadrootsResourceAreaLocation {
+ primary: None,
+ city: None,
+ region: None,
+ country: None,
+ gcs: RadrootsGcsLocation {
+ lat: -4.527,
+ lng: 129.898,
+ geohash: "pmb5v".to_string(),
+ point: RadrootsGeoJsonPoint {
+ r#type: "Point".to_string(),
+ coordinates: [129.898, -4.527],
+ },
+ polygon: RadrootsGeoJsonPolygon {
+ r#type: "Polygon".to_string(),
+ coordinates: vec![vec![
+ [129.898, -4.527],
+ [129.899, -4.527],
+ [129.899, -4.528],
+ [129.898, -4.527],
+ ]],
+ },
+ accuracy: None,
+ altitude: None,
+ tag_0: None,
+ label: None,
+ area: None,
+ elevation: None,
+ soil: None,
+ climate: None,
+ gc_id: None,
+ gc_name: None,
+ gc_admin1_id: None,
+ gc_admin1_name: None,
+ gc_country_id: None,
+ gc_country_name: None,
+ },
+ }
+ }
+
+ #[test]
+ fn resource_area_tags_include_required_fields() {
+ let area = RadrootsResourceArea {
+ d_tag: "area-1".to_string(),
+ name: "Banda Grove".to_string(),
+ about: None,
+ location: sample_location(),
+ tags: Some(vec!["nutmeg".to_string()]),
+ };
+
+ let tags = resource_area_build_tags(&area).expect("tags");
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("d")));
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("g")));
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("t")));
+ }
+
+ #[test]
+ fn resource_area_ref_tags_include_p_and_a() {
+ let area_ref = RadrootsResourceAreaRef {
+ pubkey: "area_pubkey".to_string(),
+ d_tag: "area-1".to_string(),
+ };
+
+ let tags = resource_area_ref_tags(&area_ref).expect("ref tags");
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("p")));
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("a")));
+ }
+
+ #[test]
+ fn resource_area_list_sets_include_expected_tags() {
+ let farms = resource_area_members_farms_list_set(
+ "area-1",
+ [RadrootsFarmRef {
+ pubkey: "farm_pubkey".to_string(),
+ d_tag: "farm-1".to_string(),
+ }],
+ )
+ .expect("farm members");
+ assert_eq!(farms.d_tag, "resource:area-1:members.farms");
+ assert!(farms.entries.iter().any(|entry| entry.tag == "a"));
+ assert!(farms.entries.iter().any(|entry| entry.tag == "p"));
+
+ let plots = resource_area_members_plots_list_set(
+ "area-1",
+ [RadrootsPlotRef {
+ pubkey: "farm_pubkey".to_string(),
+ d_tag: "plot-1".to_string(),
+ }],
+ )
+ .expect("plot members");
+ assert_eq!(plots.d_tag, "resource:area-1:members.plots");
+ assert!(plots.entries.iter().any(|entry| entry.tag == "a"));
+ assert!(plots.entries.iter().any(|entry| entry.tag == "p"));
+
+ let stewards = resource_area_stewards_list_set("area-1", ["steward_pubkey"])
+ .expect("stewards");
+ assert_eq!(stewards.d_tag, "resource:area-1:members.stewards");
+ assert!(stewards.entries.iter().any(|entry| entry.tag == "p"));
+ }
+}
diff --git a/events-codec/src/resource_cap/decode.rs b/events-codec/src/resource_cap/decode.rs
@@ -0,0 +1,106 @@
+#![cfg(feature = "serde_json")]
+
+#[cfg(not(feature = "std"))]
+use alloc::{string::{String, ToString}, vec::Vec};
+
+use radroots_events::{
+ RadrootsNostrEvent,
+ kinds::KIND_RESOURCE_HARVEST_CAP,
+ resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestCapEventIndex, RadrootsResourceHarvestCapEventMetadata},
+ tags::TAG_D,
+};
+
+use crate::error::EventParseError;
+
+const DEFAULT_KIND: u32 = KIND_RESOURCE_HARVEST_CAP;
+
+fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> {
+ let tag = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D))
+ .ok_or(EventParseError::MissingTag(TAG_D))?;
+ let value = tag
+ .get(1)
+ .map(|s| s.to_string())
+ .ok_or(EventParseError::InvalidTag(TAG_D))?;
+ if value.trim().is_empty() {
+ return Err(EventParseError::InvalidTag(TAG_D));
+ }
+ Ok(value)
+}
+
+pub fn resource_harvest_cap_from_event(
+ kind: u32,
+ tags: &[Vec<String>],
+ content: &str,
+) -> Result<RadrootsResourceHarvestCap, EventParseError> {
+ if kind != DEFAULT_KIND {
+ return Err(EventParseError::InvalidKind {
+ expected: "30371",
+ got: kind,
+ });
+ }
+ if content.trim().is_empty() {
+ return Err(EventParseError::InvalidJson("content"));
+ }
+ let d_tag = parse_d_tag(tags)?;
+ let mut cap: RadrootsResourceHarvestCap =
+ serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?;
+
+ if cap.d_tag.trim().is_empty() {
+ cap.d_tag = d_tag;
+ } else if cap.d_tag != d_tag {
+ return Err(EventParseError::InvalidTag(TAG_D));
+ }
+
+ Ok(cap)
+}
+
+pub fn metadata_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+) -> Result<RadrootsResourceHarvestCapEventMetadata, EventParseError> {
+ let cap = resource_harvest_cap_from_event(kind, &tags, &content)?;
+ Ok(RadrootsResourceHarvestCapEventMetadata {
+ id,
+ author,
+ published_at,
+ kind,
+ cap,
+ })
+}
+
+pub fn index_from_event(
+ id: String,
+ author: String,
+ published_at: u32,
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+ sig: String,
+) -> Result<RadrootsResourceHarvestCapEventIndex, EventParseError> {
+ let metadata = metadata_from_event(
+ id.clone(),
+ author.clone(),
+ published_at,
+ kind,
+ content.clone(),
+ tags.clone(),
+ )?;
+ Ok(RadrootsResourceHarvestCapEventIndex {
+ event: RadrootsNostrEvent {
+ id,
+ author,
+ created_at: published_at,
+ kind,
+ content,
+ tags,
+ sig,
+ },
+ metadata,
+ })
+}
diff --git a/events-codec/src/resource_cap/encode.rs b/events-codec/src/resource_cap/encode.rs
@@ -0,0 +1,97 @@
+#[cfg(not(feature = "std"))]
+use alloc::{string::{String, ToString}, vec::Vec};
+
+use radroots_events::{
+ kinds::KIND_RESOURCE_AREA,
+ resource_cap::RadrootsResourceHarvestCap,
+ tags::TAG_D,
+};
+
+use crate::error::EventEncodeError;
+
+#[cfg(feature = "serde_json")]
+use crate::wire::WireEventParts;
+#[cfg(feature = "serde_json")]
+use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP;
+
+const TAG_A: &str = "a";
+const TAG_P: &str = "p";
+const TAG_T: &str = "t";
+const TAG_KEY: &str = "key";
+const TAG_CATEGORY: &str = "category";
+const TAG_START: &str = "start";
+const TAG_END: &str = "end";
+
+fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) {
+ let mut tag = Vec::with_capacity(2);
+ tag.push(key.to_string());
+ tag.push(value.to_string());
+ tags.push(tag);
+}
+
+fn resource_area_address(cap: &RadrootsResourceHarvestCap) -> Result<String, EventEncodeError> {
+ let area = &cap.resource_area;
+ if area.pubkey.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("resource_area.pubkey"));
+ }
+ if area.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("resource_area.d_tag"));
+ }
+ let mut addr = String::new();
+ addr.push_str(&KIND_RESOURCE_AREA.to_string());
+ addr.push(':');
+ addr.push_str(&area.pubkey);
+ addr.push(':');
+ addr.push_str(&area.d_tag);
+ Ok(addr)
+}
+
+pub fn resource_harvest_cap_build_tags(
+ cap: &RadrootsResourceHarvestCap,
+) -> Result<Vec<Vec<String>>, EventEncodeError> {
+ if cap.d_tag.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("d_tag"));
+ }
+ if cap.product.key.trim().is_empty() {
+ return Err(EventEncodeError::EmptyRequiredField("product.key"));
+ }
+ let mut tags = Vec::new();
+ push_tag(&mut tags, TAG_D, &cap.d_tag);
+ let addr = resource_area_address(cap)?;
+ push_tag(&mut tags, TAG_A, &addr);
+ push_tag(&mut tags, TAG_P, &cap.resource_area.pubkey);
+ push_tag(&mut tags, TAG_KEY, &cap.product.key);
+ if let Some(category) = cap.product.category.as_deref() {
+ if !category.trim().is_empty() {
+ push_tag(&mut tags, TAG_CATEGORY, category);
+ }
+ }
+ push_tag(&mut tags, TAG_START, &cap.start.to_string());
+ push_tag(&mut tags, TAG_END, &cap.end.to_string());
+ if let Some(items) = cap.tags.as_ref() {
+ for item in items.iter().filter(|v| !v.trim().is_empty()) {
+ push_tag(&mut tags, TAG_T, item);
+ }
+ }
+ Ok(tags)
+}
+
+#[cfg(feature = "serde_json")]
+pub fn to_wire_parts(
+ cap: &RadrootsResourceHarvestCap,
+) -> Result<WireEventParts, EventEncodeError> {
+ to_wire_parts_with_kind(cap, KIND_RESOURCE_HARVEST_CAP)
+}
+
+#[cfg(feature = "serde_json")]
+pub fn to_wire_parts_with_kind(
+ cap: &RadrootsResourceHarvestCap,
+ kind: u32,
+) -> Result<WireEventParts, EventEncodeError> {
+ if kind != KIND_RESOURCE_HARVEST_CAP {
+ return Err(EventEncodeError::InvalidKind(kind));
+ }
+ let tags = resource_harvest_cap_build_tags(cap)?;
+ let content = serde_json::to_string(cap).map_err(|_| EventEncodeError::Json)?;
+ Ok(WireEventParts { kind, content, tags })
+}
diff --git a/events-codec/src/resource_cap/mod.rs b/events-codec/src/resource_cap/mod.rs
@@ -0,0 +1,44 @@
+#![forbid(unsafe_code)]
+
+pub mod encode;
+pub mod decode;
+
+#[cfg(test)]
+mod tests {
+ use radroots_core::{RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreUnit};
+ use radroots_events::resource_area::RadrootsResourceAreaRef;
+ use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct};
+ use crate::resource_cap::encode::resource_harvest_cap_build_tags;
+
+ #[test]
+ fn resource_harvest_cap_tags_include_required_fields() {
+ let cap = RadrootsResourceHarvestCap {
+ d_tag: "cap-2025".to_string(),
+ resource_area: RadrootsResourceAreaRef {
+ pubkey: "area_pubkey".to_string(),
+ d_tag: "area-1".to_string(),
+ },
+ product: RadrootsResourceHarvestProduct {
+ key: "nutmeg".to_string(),
+ category: Some("spice".to_string()),
+ },
+ start: 1735689600,
+ end: 1767225600,
+ cap_quantity: RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(100000u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ display_amount: None,
+ display_unit: None,
+ display_label: None,
+ tags: Some(vec!["community".to_string()]),
+ };
+
+ let tags = resource_harvest_cap_build_tags(&cap).expect("tags");
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("d")));
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("a")));
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("key")));
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("start")));
+ assert!(tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("end")));
+ }
+}
diff --git a/events-codec/src/tag_builders.rs b/events-codec/src/tag_builders.rs
@@ -12,6 +12,8 @@ use radroots_events::{
coop::RadrootsCoop,
follow::RadrootsFollow,
farm::RadrootsFarm,
+ resource_area::RadrootsResourceArea,
+ resource_cap::RadrootsResourceHarvestCap,
gift_wrap::RadrootsGiftWrap,
job_feedback::RadrootsJobFeedback,
job_request::RadrootsJobRequest,
@@ -35,6 +37,8 @@ use crate::document::encode::document_build_tags;
use crate::coop::encode::coop_build_tags;
use crate::follow::encode::follow_build_tags;
use crate::farm::encode::farm_build_tags;
+use crate::resource_area::encode::resource_area_build_tags;
+use crate::resource_cap::encode::resource_harvest_cap_build_tags;
use crate::job::encode::JobEncodeError;
use crate::job::feedback::encode::job_feedback_build_tags;
use crate::job::request::encode::job_request_build_tags;
@@ -118,6 +122,22 @@ impl RadrootsEventTagBuilder for RadrootsFarm {
}
}
+impl RadrootsEventTagBuilder for RadrootsResourceArea {
+ type Error = EventEncodeError;
+
+ fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> {
+ resource_area_build_tags(self)
+ }
+}
+
+impl RadrootsEventTagBuilder for RadrootsResourceHarvestCap {
+ type Error = EventEncodeError;
+
+ fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> {
+ resource_harvest_cap_build_tags(self)
+ }
+}
+
impl RadrootsEventTagBuilder for RadrootsCoop {
type Error = EventEncodeError;
diff --git a/events-codec/tests/listing.rs b/events-codec/tests/listing.rs
@@ -56,6 +56,8 @@ fn sample_listing(d_tag: &str) -> RadrootsListing {
display_price: None,
display_price_unit: None,
}],
+ resource_area: None,
+ plot: None,
discounts: None,
inventory_available: None,
availability: None,
@@ -105,6 +107,8 @@ fn sample_listing_full(d_tag: &str) -> RadrootsListing {
)),
display_price_unit: Some(RadrootsCoreUnit::MassKg),
}],
+ resource_area: None,
+ plot: None,
discounts: Some(vec![RadrootsCoreDiscount {
scope: RadrootsCoreDiscountScope::Bin,
threshold: RadrootsCoreDiscountThreshold::BinCount {
diff --git a/events/bindings/ts/src/kinds.ts b/events/bindings/ts/src/kinds.ts
@@ -42,6 +42,8 @@ export const KIND_FARM = 30340;
export const KIND_PLOT = 30350;
export const KIND_COOP = 30360;
export const KIND_DOCUMENT = 30361;
+export const KIND_RESOURCE_AREA = 30370;
+export const KIND_RESOURCE_HARVEST_CAP = 30371;
export const KIND_APP_DATA = 30078;
export const KIND_LISTING = 30402;
export const KIND_APPLICATION_HANDLER = 31990;
diff --git a/events/bindings/ts/src/schemas.ts b/events/bindings/ts/src/schemas.ts
@@ -18,37 +18,7 @@ export const radroots_listing_location_schema = z.object({
geohash: z.string().optional()
});
-export const radroots_listing_discount_schema = z.union([
- z.object({
- kind: z.literal("quantity"),
- amount: z.object({
- ref_quantity: z.string(),
- threshold: z.any(),
- value: z.any()
- })
- }),
- z.object({
- kind: z.literal("mass"),
- amount: z.object({
- threshold: z.any(),
- value: z.any()
- })
- }),
- z.object({
- kind: z.literal("subtotal"),
- amount: z.object({
- threshold: z.any(),
- value: z.any()
- })
- }),
- z.object({
- kind: z.literal("total"),
- amount: z.object({
- total_min: z.any(),
- value: z.any()
- })
- })
-]);
+export const radroots_listing_discount_schema = z.any();
export const radroots_listing_price_schema = z.object({
amount: z.any(),
@@ -56,9 +26,20 @@ export const radroots_listing_price_schema = z.object({
});
export const radroots_listing_quantity_schema = z.object({
- value: z.any(),
- label: z.string().optional(),
- count: z.number().optional()
+ amount: z.any(),
+ unit: z.any(),
+ label: z.string().optional()
+});
+
+export const radroots_listing_bin_schema = z.object({
+ bin_id: z.string(),
+ quantity: z.any(),
+ price_per_canonical_unit: z.any(),
+ display_amount: z.number().optional(),
+ display_unit: z.any().optional(),
+ display_label: z.string().optional(),
+ display_price: z.any().optional(),
+ display_price_unit: z.any().optional()
});
export const radroots_listing_product_schema = z.object({
@@ -76,9 +57,12 @@ export const radroots_listing_product_schema = z.object({
export const radroots_listing_schema = z.object({
d_tag: z.string(),
product: radroots_listing_product_schema,
- quantities: z.array(radroots_listing_quantity_schema),
- prices: z.array(radroots_listing_price_schema),
+ primary_bin_id: z.string(),
+ bins: z.array(radroots_listing_bin_schema),
discounts: z.array(radroots_listing_discount_schema).optional(),
+ inventory_available: z.number().optional(),
+ availability: z.any().optional(),
+ delivery_method: z.any().optional(),
location: radroots_listing_location_schema.optional(),
images: z.array(radroots_listing_image_schema).optional()
});
diff --git a/events/bindings/ts/src/types.ts b/events/bindings/ts/src/types.ts
@@ -106,7 +106,7 @@ export type RadrootsListSetEventIndex = { event: RadrootsNostrEvent, metadata: R
export type RadrootsListSetEventMetadata = { id: string, author: string, published_at: number, kind: number, list_set: RadrootsListSet, };
-export type RadrootsListing = { d_tag: string, farm: RadrootsListingFarmRef, product: RadrootsListingProduct, primary_bin_id: string, bins: Array<RadrootsListingBin>, discounts?: RadrootsCoreDiscount[] | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, };
+export type RadrootsListing = { d_tag: string, farm: RadrootsListingFarmRef, product: RadrootsListingProduct, primary_bin_id: string, bins: Array<RadrootsListingBin>, resource_area?: RadrootsResourceAreaRef | null, plot?: RadrootsPlotRef | null, discounts?: RadrootsCoreDiscount[] | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, };
export type RadrootsListingAvailability = { "kind": "window", "amount": { start?: number | null, end?: number | null, } } | { "kind": "status", "amount": { status: RadrootsListingStatus, } };
@@ -162,6 +162,8 @@ export type RadrootsPlotEventMetadata = { id: string, author: string, published_
export type RadrootsPlotLocation = { primary?: string | null, city?: string | null, region?: string | null, country?: string | null, gcs: RadrootsGcsLocation, };
+export type RadrootsPlotRef = { pubkey: string, d_tag: string, };
+
export type RadrootsPost = { content: string, };
export type RadrootsPostEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsPostEventMetadata, };
@@ -184,6 +186,24 @@ export type RadrootsReactionEventMetadata = { id: string, author: string, publis
export type RadrootsRelayDocument = { name?: string | null, description?: string | null, pubkey?: string | null, contact?: string | null, supported_nips?: number[] | null, software?: string | null, version?: string | null, };
+export type RadrootsResourceArea = { d_tag: string, name: string, about?: string | null, location: RadrootsResourceAreaLocation, tags?: string[] | null, };
+
+export type RadrootsResourceAreaEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsResourceAreaEventMetadata, };
+
+export type RadrootsResourceAreaEventMetadata = { id: string, author: string, published_at: number, kind: number, area: RadrootsResourceArea, };
+
+export type RadrootsResourceAreaLocation = { primary?: string | null, city?: string | null, region?: string | null, country?: string | null, gcs: RadrootsGcsLocation, };
+
+export type RadrootsResourceAreaRef = { pubkey: string, d_tag: string, };
+
+export type RadrootsResourceHarvestCap = { d_tag: string, resource_area: RadrootsResourceAreaRef, product: RadrootsResourceHarvestProduct, start: bigint, end: bigint, cap_quantity: RadrootsCoreQuantity, display_amount?: RadrootsCoreDecimal | null, display_unit?: RadrootsCoreUnit | null, display_label?: string | null, tags?: string[] | null, };
+
+export type RadrootsResourceHarvestCapEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsResourceHarvestCapEventMetadata, };
+
+export type RadrootsResourceHarvestCapEventMetadata = { id: string, author: string, published_at: number, kind: number, cap: RadrootsResourceHarvestCap, };
+
+export type RadrootsResourceHarvestProduct = { key: string, category?: string | null, };
+
export type RadrootsSeal = { content: string, };
export type RadrootsSealEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsSealEventMetadata, };
diff --git a/events/src/kinds.rs b/events/src/kinds.rs
@@ -42,6 +42,8 @@ pub const KIND_FARM: u32 = 30340;
pub const KIND_PLOT: u32 = 30350;
pub const KIND_COOP: u32 = 30360;
pub const KIND_DOCUMENT: u32 = 30361;
+pub const KIND_RESOURCE_AREA: u32 = 30370;
+pub const KIND_RESOURCE_HARVEST_CAP: u32 = 30371;
pub const KIND_APP_DATA: u32 = 30078;
pub const KIND_LISTING: u32 = 30402;
pub const KIND_APPLICATION_HANDLER: u32 = 31990;
@@ -171,6 +173,8 @@ mod kinds_constants_tests {
("KIND_PLOT", KIND_PLOT),
("KIND_COOP", KIND_COOP),
("KIND_DOCUMENT", KIND_DOCUMENT),
+ ("KIND_RESOURCE_AREA", KIND_RESOURCE_AREA),
+ ("KIND_RESOURCE_HARVEST_CAP", KIND_RESOURCE_HARVEST_CAP),
("KIND_APP_DATA", KIND_APP_DATA),
("KIND_LISTING", KIND_LISTING),
("KIND_APPLICATION_HANDLER", KIND_APPLICATION_HANDLER),
diff --git a/events/src/lib.rs b/events/src/lib.rs
@@ -24,6 +24,8 @@ pub mod list_set;
pub mod app_data;
pub mod coop;
pub mod farm;
+pub mod resource_area;
+pub mod resource_cap;
pub mod message;
pub mod plot;
pub mod message_file;
diff --git a/events/src/listing.rs b/events/src/listing.rs
@@ -6,6 +6,8 @@ use radroots_core::{
use ts_rs::TS;
use crate::RadrootsNostrEvent;
+use crate::plot::RadrootsPlotRef;
+use crate::resource_area::RadrootsResourceAreaRef;
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
@@ -88,6 +90,16 @@ pub struct RadrootsListing {
pub bins: Vec<RadrootsListingBin>,
#[cfg_attr(
feature = "ts-rs",
+ ts(optional, type = "RadrootsResourceAreaRef | null")
+ )]
+ pub resource_area: Option<RadrootsResourceAreaRef>,
+ #[cfg_attr(
+ feature = "ts-rs",
+ ts(optional, type = "RadrootsPlotRef | null")
+ )]
+ pub plot: Option<RadrootsPlotRef>,
+ #[cfg_attr(
+ feature = "ts-rs",
ts(optional, type = "RadrootsCoreDiscount[] | null")
)]
pub discounts: Option<Vec<RadrootsCoreDiscount>>,
diff --git a/events/src/plot.rs b/events/src/plot.rs
@@ -30,6 +30,15 @@ pub struct RadrootsPlotEventMetadata {
#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
+pub struct RadrootsPlotRef {
+ pub pubkey: String,
+ pub d_tag: String,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
pub struct RadrootsPlot {
pub d_tag: String,
pub farm: RadrootsFarmRef,
diff --git a/events/src/resource_area.rs b/events/src/resource_area.rs
@@ -0,0 +1,69 @@
+#![forbid(unsafe_code)]
+
+use crate::RadrootsNostrEvent;
+use crate::farm::RadrootsGcsLocation;
+#[cfg(feature = "ts-rs")]
+use ts_rs::TS;
+
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceAreaEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsResourceAreaEventMetadata,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceAreaEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub kind: u32,
+ pub area: RadrootsResourceArea,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceArea {
+ pub d_tag: String,
+ pub name: String,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub about: Option<String>,
+ pub location: RadrootsResourceAreaLocation,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string[] | null"))]
+ pub tags: Option<Vec<String>>,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceAreaRef {
+ pub pubkey: String,
+ pub d_tag: String,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceAreaLocation {
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub primary: Option<String>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub city: Option<String>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub region: Option<String>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub country: Option<String>,
+ pub gcs: RadrootsGcsLocation,
+}
diff --git a/events/src/resource_cap.rs b/events/src/resource_cap.rs
@@ -0,0 +1,64 @@
+#![forbid(unsafe_code)]
+
+use radroots_core::{RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreUnit};
+
+use crate::RadrootsNostrEvent;
+use crate::resource_area::RadrootsResourceAreaRef;
+#[cfg(feature = "ts-rs")]
+use ts_rs::TS;
+
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceHarvestCapEventIndex {
+ pub event: RadrootsNostrEvent,
+ pub metadata: RadrootsResourceHarvestCapEventMetadata,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceHarvestCapEventMetadata {
+ pub id: String,
+ pub author: String,
+ pub published_at: u32,
+ pub kind: u32,
+ pub cap: RadrootsResourceHarvestCap,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceHarvestProduct {
+ pub key: String,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub category: Option<String>,
+}
+
+#[cfg_attr(feature = "ts-rs", derive(TS))]
+#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug)]
+pub struct RadrootsResourceHarvestCap {
+ pub d_tag: String,
+ pub resource_area: RadrootsResourceAreaRef,
+ pub product: RadrootsResourceHarvestProduct,
+ pub start: u64,
+ pub end: u64,
+ #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsCoreQuantity"))]
+ pub cap_quantity: RadrootsCoreQuantity,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "RadrootsCoreDecimal | null"))]
+ pub display_amount: Option<RadrootsCoreDecimal>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "RadrootsCoreUnit | null"))]
+ pub display_unit: Option<RadrootsCoreUnit>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))]
+ pub display_label: Option<String>,
+ #[cfg_attr(feature = "ts-rs", ts(optional, type = "string[] | null"))]
+ pub tags: Option<Vec<String>>,
+}
diff --git a/net-core/src/nostr_client/manager.rs b/net-core/src/nostr_client/manager.rs
@@ -43,7 +43,7 @@ impl NostrClientManager {
async move {
use futures::StreamExt;
- let mut since = since_unix.unwrap_or_else(|| RadrootsNostrTimestamp::now().as_u64());
+ let mut since = since_unix.unwrap_or_else(|| RadrootsNostrTimestamp::now().as_secs());
loop {
let filter = radroots_nostr_post_events_filter(None, Some(since));
@@ -59,9 +59,13 @@ impl NostrClientManager {
}
};
- while let Some(event) = stream.next().await {
+ while let Some((_, event)) = stream.next().await {
+ let event = match event {
+ Ok(event) => event,
+ Err(_) => continue,
+ };
let meta = radroots_nostr::event_adapters::to_post_event_metadata(&event);
- let ts = event.created_at.as_u64();
+ let ts = event.created_at.as_secs();
since = ts.saturating_add(1);
let _ = inner.post_events_tx.send(meta);
}
diff --git a/trade/bindings/ts/package.json b/trade/bindings/ts/package.json
@@ -23,7 +23,7 @@
"build:cjs": "tsc -p tsconfig.cjs.json",
"build": "npm run build:esm && npm run build:cjs",
"prebuild": "npm run clean && npm run prepend-imports",
- "prepend-imports": "bash -c 'f=./src/types.ts; line=\"import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from \\\"@radroots/core-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")' && bash -c 'f=./src/types.ts; old=\"import type { RadrootsListingDiscount, RadrootsListingImage, RadrootsNostrEventPtr } from \\\"@radroots/events-bindings\\\";\"; if grep -qxF \"$old\" \"$f\"; then grep -vxF \"$old\" \"$f\" > \"$f.tmp\" && mv \"$f.tmp\" \"$f\"; fi; line=\"import type { RadrootsListingImage, RadrootsNostrEventPtr } from \\\"@radroots/events-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")'",
+ "prepend-imports": "bash -c 'f=./src/types.ts; line=\"import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from \\\"@radroots/core-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")' && bash -c 'f=./src/types.ts; old=\"import type { RadrootsListingDiscount, RadrootsListingImage, RadrootsNostrEventPtr } from \\\"@radroots/events-bindings\\\";\"; if grep -qxF \"$old\" \"$f\"; then grep -vxF \"$old\" \"$f\" > \"$f.tmp\" && mv \"$f.tmp\" \"$f\"; fi; old2=\"import type { RadrootsListingImage, RadrootsNostrEventPtr } from \\\"@radroots/events-bindings\\\";\"; if grep -qxF \"$old2\" \"$f\"; then grep -vxF \"$old2\" \"$f\" > \"$f.tmp\" && mv \"$f.tmp\" \"$f\"; fi; line=\"import type { RadrootsListingImage, RadrootsNostrEventPtr, RadrootsPlotRef, RadrootsResourceAreaRef } from \\\"@radroots/events-bindings\\\";\"; grep -qxF \"$line\" \"$f\" || (echo -e \"$line\\n\\n$(cat $f)\" > \"$f\")'",
"clean": "rimraf dist",
"dev": "npm run watch",
"watch": "tsc -w"
diff --git a/trade/bindings/ts/src/types.ts b/trade/bindings/ts/src/types.ts
@@ -1,10 +1,10 @@
-import type { RadrootsListingImage, RadrootsNostrEventPtr } from "@radroots/events-bindings";
+import type { RadrootsListingImage, RadrootsNostrEventPtr, RadrootsPlotRef, RadrootsResourceAreaRef } from "@radroots/events-bindings";
import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit } from "@radroots/core-bindings";
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type RadrootsListing = { d_tag: string, farm: RadrootsListingFarmRef, product: RadrootsListingProduct, primary_bin_id: string, bins: Array<RadrootsListingBin>, discounts?: RadrootsCoreDiscount[] | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, };
+export type RadrootsListing = { d_tag: string, farm: RadrootsListingFarmRef, product: RadrootsListingProduct, primary_bin_id: string, bins: Array<RadrootsListingBin>, resource_area?: RadrootsResourceAreaRef | null, plot?: RadrootsPlotRef | null, discounts?: RadrootsCoreDiscount[] | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, };
export type RadrootsListingAvailability = { "kind": "window", "amount": { start?: number | null, end?: number | null, } } | { "kind": "status", "amount": { status: RadrootsListingStatus, } };
diff --git a/trade/src/listing/codec.rs b/trade/src/listing/codec.rs
@@ -13,7 +13,9 @@ use radroots_events::listing::{
RadrootsListingImageSize, RadrootsListingLocation, RadrootsListingProduct,
RadrootsListingStatus,
};
-use radroots_events::kinds::KIND_FARM;
+use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA};
+use radroots_events::plot::RadrootsPlotRef;
+use radroots_events::resource_area::RadrootsResourceAreaRef;
use radroots_events::tags::TAG_D;
use radroots_events_codec::error::EventEncodeError;
use radroots_events_codec::listing::tags::{listing_tags_with_options, ListingTagOptions};
@@ -25,6 +27,8 @@ const TAG_RADROOTS_BIN: &str = "radroots:bin";
const TAG_RADROOTS_PRICE: &str = "radroots:price";
const TAG_RADROOTS_DISCOUNT: &str = "radroots:discount";
const TAG_RADROOTS_PRIMARY_BIN: &str = "radroots:primary_bin";
+const TAG_RADROOTS_RESOURCE_AREA: &str = "radroots:resource_area";
+const TAG_RADROOTS_PLOT: &str = "radroots:plot";
const TAG_LOCATION: &str = "location";
const TAG_IMAGE: &str = "image";
const TAG_GEOHASH: &str = "g";
@@ -107,6 +111,8 @@ pub fn listing_from_event_parts(
let d_tag = parse_d_tag(tags)?;
let farm_ref = parse_farm_ref(tags)?;
let farm_pubkey = parse_farm_pubkey(tags)?;
+ let resource_area = parse_resource_area(tags)?;
+ let plot = parse_plot_ref(tags)?;
if !content.trim().is_empty() {
#[cfg(feature = "serde_json")]
@@ -127,12 +133,36 @@ pub fn listing_from_event_parts(
if listing.farm.pubkey != farm_pubkey {
return Err(TradeListingParseError::InvalidTag(TAG_P.to_string()));
}
+ if let Some(tag_area) = resource_area {
+ match listing.resource_area.as_ref() {
+ None => listing.resource_area = Some(tag_area),
+ Some(area) => {
+ if area.pubkey != tag_area.pubkey || area.d_tag != tag_area.d_tag {
+ return Err(TradeListingParseError::InvalidTag(
+ TAG_RADROOTS_RESOURCE_AREA.to_string(),
+ ));
+ }
+ }
+ }
+ }
+ if let Some(tag_plot) = plot {
+ match listing.plot.as_ref() {
+ None => listing.plot = Some(tag_plot),
+ Some(existing) => {
+ if existing.pubkey != tag_plot.pubkey || existing.d_tag != tag_plot.d_tag {
+ return Err(TradeListingParseError::InvalidTag(
+ TAG_RADROOTS_PLOT.to_string(),
+ ));
+ }
+ }
+ }
+ }
return Ok(listing);
}
}
}
- listing_from_tags(tags, d_tag, farm_ref, farm_pubkey)
+ listing_from_tags(tags, d_tag, farm_ref, farm_pubkey, resource_area, plot)
}
pub fn listing_tags_build(listing: &RadrootsListing) -> Result<Vec<Vec<String>>, TradeListingParseError> {
@@ -155,6 +185,8 @@ fn listing_from_tags(
d_tag: String,
farm_ref: RadrootsListingFarmRef,
farm_pubkey: String,
+ resource_area: Option<RadrootsResourceAreaRef>,
+ plot: Option<RadrootsPlotRef>,
) -> Result<RadrootsListing, TradeListingParseError> {
let mut product = RadrootsListingProduct {
key: String::new(),
@@ -450,6 +482,8 @@ fn listing_from_tags(
product,
primary_bin_id,
bins,
+ resource_area,
+ plot,
discounts: if discounts.is_empty() { None } else { Some(discounts) },
inventory_available,
availability,
@@ -460,49 +494,120 @@ fn listing_from_tags(
}
fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsListingFarmRef, TradeListingParseError> {
+ for tag in tags.iter().filter(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_A)) {
+ let value = tag
+ .get(1)
+ .map(|s| s.to_string())
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?;
+ let mut parts = value.splitn(3, ':');
+ let kind = parts
+ .next()
+ .and_then(|v| v.parse::<u32>().ok())
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?;
+ if kind != KIND_FARM {
+ continue;
+ }
+ let pubkey = parts
+ .next()
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?
+ .to_string();
+ let d_tag = parts
+ .next()
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?
+ .to_string();
+ if pubkey.trim().is_empty() || d_tag.trim().is_empty() {
+ return Err(TradeListingParseError::InvalidTag(TAG_A.to_string()));
+ }
+ return Ok(RadrootsListingFarmRef { pubkey, d_tag });
+ }
+ Err(TradeListingParseError::MissingTag(TAG_A.to_string()))
+}
+
+fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, TradeListingParseError> {
let tag = tags
.iter()
- .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_A))
- .ok_or_else(|| TradeListingParseError::MissingTag(TAG_A.to_string()))?;
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_P))
+ .ok_or_else(|| TradeListingParseError::MissingTag(TAG_P.to_string()))?;
let value = tag
.get(1)
.map(|s| s.to_string())
- .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?;
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_P.to_string()))?;
+ if value.trim().is_empty() {
+ return Err(TradeListingParseError::InvalidTag(TAG_P.to_string()));
+ }
+ Ok(value)
+}
+
+fn parse_resource_area(
+ tags: &[Vec<String>],
+) -> Result<Option<RadrootsResourceAreaRef>, TradeListingParseError> {
+ let tag = tags
+ .iter()
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_RADROOTS_RESOURCE_AREA));
+ let Some(tag) = tag else {
+ return Ok(None);
+ };
+ let value = tag
+ .get(1)
+ .map(|s| s.to_string())
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))?;
let mut parts = value.splitn(3, ':');
let kind = parts
.next()
.and_then(|v| v.parse::<u32>().ok())
- .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?;
- if kind != KIND_FARM {
- return Err(TradeListingParseError::InvalidTag(TAG_A.to_string()));
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))?;
+ if kind != KIND_RESOURCE_AREA {
+ return Err(TradeListingParseError::InvalidTag(
+ TAG_RADROOTS_RESOURCE_AREA.to_string(),
+ ));
}
let pubkey = parts
.next()
- .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))?
.to_string();
let d_tag = parts
.next()
- .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_A.to_string()))?
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA.to_string()))?
.to_string();
if pubkey.trim().is_empty() || d_tag.trim().is_empty() {
- return Err(TradeListingParseError::InvalidTag(TAG_A.to_string()));
+ return Err(TradeListingParseError::InvalidTag(
+ TAG_RADROOTS_RESOURCE_AREA.to_string(),
+ ));
}
- Ok(RadrootsListingFarmRef { pubkey, d_tag })
+ Ok(Some(RadrootsResourceAreaRef { pubkey, d_tag }))
}
-fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, TradeListingParseError> {
+fn parse_plot_ref(tags: &[Vec<String>]) -> Result<Option<RadrootsPlotRef>, TradeListingParseError> {
let tag = tags
.iter()
- .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_P))
- .ok_or_else(|| TradeListingParseError::MissingTag(TAG_P.to_string()))?;
+ .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_RADROOTS_PLOT));
+ let Some(tag) = tag else {
+ return Ok(None);
+ };
let value = tag
.get(1)
.map(|s| s.to_string())
- .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_P.to_string()))?;
- if value.trim().is_empty() {
- return Err(TradeListingParseError::InvalidTag(TAG_P.to_string()));
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?;
+ let mut parts = value.splitn(3, ':');
+ let kind = parts
+ .next()
+ .and_then(|v| v.parse::<u32>().ok())
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?;
+ if kind != KIND_PLOT {
+ return Err(TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()));
}
- Ok(value)
+ let pubkey = parts
+ .next()
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?
+ .to_string();
+ let d_tag = parts
+ .next()
+ .ok_or_else(|| TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()))?
+ .to_string();
+ if pubkey.trim().is_empty() || d_tag.trim().is_empty() {
+ return Err(TradeListingParseError::InvalidTag(TAG_RADROOTS_PLOT.to_string()));
+ }
+ Ok(Some(RadrootsPlotRef { pubkey, d_tag }))
}
#[cfg(test)]
@@ -551,6 +656,8 @@ mod tests {
"listing-1".to_string(),
farm_ref(),
"seller".to_string(),
+ None,
+ None,
)
.expect("listing");
diff --git a/trade/src/listing/validation.rs b/trade/src/listing/validation.rs
@@ -319,6 +319,8 @@ mod tests {
display_price: None,
display_price_unit: None,
}],
+ resource_area: None,
+ plot: None,
discounts: None,
inventory_available: Some(RadrootsCoreDecimal::from(5u32)),
availability: Some(RadrootsListingAvailability::Status {