commit 453e6ec28c8952c65202be67e80182af58b943ec
parent 1c16fd4022875fa757f47ffad5c80a25b6047f55
Author: triesap <tyson@radroots.org>
Date: Sun, 19 Jul 2026 23:50:34 +0000
cli: adopt verified event contracts
- migrate listing workflows to classified Operational Listing types
- delegate unsigned listing semantics to the shared validator
- expose valid-stream counts without conflating raw events
- handle fallible conversion and replica outcomes explicitly
Diffstat:
10 files changed, 234 insertions(+), 187 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2113,7 +2113,7 @@ version = "1.0.0-alpha.1"
dependencies = [
"hex",
"radroots_event",
- "radroots_nostr",
+ "radroots_event_codec",
"radroots_transport",
"serde",
"serde_json",
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -6,7 +6,7 @@ use radroots_event::contract::RadrootsActorRole;
use radroots_event::farm::{RadrootsFarm, RadrootsFarmPublicLocation};
use radroots_event::ids::RadrootsAddressableCoordinate;
use radroots_event::kinds::{KIND_FARM, KIND_PROFILE};
-use radroots_event::listing::RadrootsListingPublicLocation;
+use radroots_event::operational_listing::RadrootsOperationalListingPublicLocation;
use radroots_event::profile::{RadrootsAuthoredProfile, RadrootsNip05Identifier};
use radroots_event_codec::d_tag::is_d_tag_base64url;
use radroots_event_codec::profile::authored::authored_profile_to_wire_parts;
@@ -1781,7 +1781,7 @@ fn init_document(
},
listing_defaults: FarmListingDefaults {
delivery_method,
- location: RadrootsListingPublicLocation {
+ location: RadrootsOperationalListingPublicLocation {
primary: location_primary,
city,
region,
diff --git a/src/runtime/farm_config.rs b/src/runtime/farm_config.rs
@@ -2,7 +2,9 @@ use std::fs;
use std::path::{Path, PathBuf};
use radroots_event::farm::RadrootsFarm;
-use radroots_event::listing::{RadrootsListingDeliveryMethod, RadrootsListingPublicLocation};
+use radroots_event::operational_listing::{
+ RadrootsOperationalListingDeliveryMethod, RadrootsOperationalListingPublicLocation,
+};
use radroots_event_codec::d_tag::is_d_tag_base64url;
use serde::{Deserialize, Serialize};
@@ -72,11 +74,13 @@ pub struct FarmConfigSelection {
#[serde(deny_unknown_fields)]
pub struct FarmListingDefaults {
pub delivery_method: String,
- pub location: RadrootsListingPublicLocation,
+ pub location: RadrootsOperationalListingPublicLocation,
}
impl FarmListingDefaults {
- pub fn delivery_method_model(&self) -> Result<RadrootsListingDeliveryMethod, RuntimeError> {
+ pub fn delivery_method_model(
+ &self,
+ ) -> Result<RadrootsOperationalListingDeliveryMethod, RuntimeError> {
parse_delivery_method(self.delivery_method.as_str())
}
}
@@ -371,7 +375,9 @@ fn location_geohash(document: &FarmConfigDocument) -> Option<&str> {
})
}
-fn parse_delivery_method(value: &str) -> Result<RadrootsListingDeliveryMethod, RuntimeError> {
+fn parse_delivery_method(
+ value: &str,
+) -> Result<RadrootsOperationalListingDeliveryMethod, RuntimeError> {
let method = trimmed(value);
if method.is_empty() {
return Err(RuntimeError::Config(
@@ -379,10 +385,10 @@ fn parse_delivery_method(value: &str) -> Result<RadrootsListingDeliveryMethod, R
));
}
Ok(match method {
- "pickup" => RadrootsListingDeliveryMethod::Pickup,
- "local_delivery" => RadrootsListingDeliveryMethod::LocalDelivery,
- "shipping" => RadrootsListingDeliveryMethod::Shipping,
- other => RadrootsListingDeliveryMethod::Other {
+ "pickup" => RadrootsOperationalListingDeliveryMethod::Pickup,
+ "local_delivery" => RadrootsOperationalListingDeliveryMethod::LocalDelivery,
+ "shipping" => RadrootsOperationalListingDeliveryMethod::Shipping,
+ other => RadrootsOperationalListingDeliveryMethod::Other {
method: other.to_owned(),
},
})
@@ -474,7 +480,7 @@ mod tests {
},
listing_defaults: FarmListingDefaults {
delivery_method: "pickup".to_owned(),
- location: RadrootsListingPublicLocation {
+ location: RadrootsOperationalListingPublicLocation {
primary: "San Francisco, CA".to_owned(),
city: Some("San Francisco".to_owned()),
region: Some("CA".to_owned()),
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -12,17 +12,17 @@ use radroots_core::{
};
use radroots_event::contract::RadrootsActorRole;
use radroots_event::farm::RadrootsFarmRef;
-use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId};
-use radroots_event::kinds::KIND_LISTING;
-use radroots_event::listing::{
- RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
- RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingPublicLocation,
- RadrootsListingStatus,
+use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId, RadrootsPublicKey};
+use radroots_event::kinds::KIND_CLASSIFIED_LISTING;
+use radroots_event::operational_listing::{
+ RadrootsOperationalListing, RadrootsOperationalListingAvailability,
+ RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod,
+ RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation,
+ RadrootsOperationalListingStatus,
};
-use radroots_event::trade_validation::RadrootsTradeValidationListingError;
-use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts};
+use radroots_event::trade_validation::RadrootsOperationalListingValidationError;
use radroots_event_codec::d_tag::is_d_tag_base64url;
-use radroots_event_codec::listing::encode::to_wire_parts_with_kind;
+use radroots_event_codec::operational_listing::encode::to_wire_parts_with_kind;
use radroots_replica_store::ReplicaSql;
use radroots_runtime_store::{RuntimeStoreRecord, RuntimeStoreRecordFamily, SourceRuntime};
use radroots_sdk::{
@@ -31,7 +31,9 @@ use radroots_sdk::{
PushOutboxRequest, PushOutboxTargetOutcomeKind, SdkMutationState,
};
use radroots_sql_core::SqlxSqliteExecutor;
-use radroots_trade::listing::{RadrootsListingEditDocumentV1, validation::validate_listing_event};
+use radroots_trade::operational_listing::{
+ RadrootsOperationalListingEditDocumentV1, validate_operational_listing_model,
+};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -230,14 +232,14 @@ struct CanonicalListingDraft {
seller_pubkey: String,
seller_actor_source: String,
farm_d_tag: String,
- listing: RadrootsListing,
+ listing: RadrootsOperationalListing,
}
#[derive(Debug, Clone)]
struct SdkListingPublishInput {
canonical: CanonicalListingDraft,
actor: RadrootsActorContext,
- document: RadrootsListingEditDocumentV1,
+ document: RadrootsOperationalListingEditDocumentV1,
}
#[derive(Debug, Clone)]
@@ -576,8 +578,8 @@ pub fn validate(
match canonicalize_draft(&parsed, &contents, &context) {
Ok(canonical) => {
- let parts = match to_wire_parts_with_kind(&canonical.listing, KIND_LISTING) {
- Ok(parts) => parts,
+ match to_wire_parts_with_kind(&canonical.listing, KIND_CLASSIFIED_LISTING) {
+ Ok(_) => {}
Err(error) => {
return Ok(invalid_validation_view(
args.file.as_path(),
@@ -590,7 +592,7 @@ pub fn validate(
},
));
}
- };
+ }
if let Some(issue) = listing_bound_account_issue(config, &canonical, &contents)? {
return Ok(invalid_validation_view(
args.file.as_path(),
@@ -599,22 +601,7 @@ pub fn validate(
issue,
));
}
- let event = match listing_validation_event(&canonical, parts) {
- Ok(event) => event,
- Err(error) => {
- return Ok(invalid_validation_view(
- args.file.as_path(),
- &parsed,
- &context,
- ListingValidationIssueView {
- field: "listing".to_owned(),
- message: format!("invalid listing event envelope: {error}"),
- line: None,
- },
- ));
- }
- };
- match validate_listing_event(&event) {
+ match validate_operational_listing_draft(&canonical) {
Ok(_) => Ok(ListingValidateView {
state: "valid".to_owned(),
source: LISTING_SOURCE.to_owned(),
@@ -1072,7 +1059,7 @@ fn listing_rebind_selector_error(selector: &str, error: RuntimeError) -> Runtime
}
fn listing_addr(seller_pubkey: &str, listing_id: &str) -> String {
- format!("{KIND_LISTING}:{seller_pubkey}:{listing_id}")
+ format!("{KIND_CLASSIFIED_LISTING}:{seller_pubkey}:{listing_id}")
}
fn load_listing_draft(path: &Path) -> Result<LoadedListingDraft, ListingValidationIssueView> {
@@ -1160,8 +1147,8 @@ fn listing_ready_issues(
canonical: &CanonicalListingDraft,
contents: &str,
) -> Vec<ListingValidationIssueView> {
- let parts = match to_wire_parts_with_kind(&canonical.listing, KIND_LISTING) {
- Ok(parts) => parts,
+ match to_wire_parts_with_kind(&canonical.listing, KIND_CLASSIFIED_LISTING) {
+ Ok(_) => {}
Err(error) => {
return vec![ListingValidationIssueView {
field: "listing".to_owned(),
@@ -1169,18 +1156,8 @@ fn listing_ready_issues(
line: None,
}];
}
- };
- let event = match listing_validation_event(canonical, parts) {
- Ok(event) => event,
- Err(error) => {
- return vec![ListingValidationIssueView {
- field: "listing".to_owned(),
- message: format!("invalid listing event envelope: {error}"),
- line: None,
- }];
- }
- };
- match validate_listing_event(&event) {
+ }
+ match validate_operational_listing_draft(canonical) {
Ok(_) => Vec::new(),
Err(error) => vec![issue_from_trade_validation(error, contents)],
}
@@ -1853,7 +1830,7 @@ fn sdk_listing_publish_input(
[RadrootsActorRole::Seller],
)
.map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?;
- let document = RadrootsListingEditDocumentV1::new(canonical.listing.clone());
+ let document = RadrootsOperationalListingEditDocumentV1::new(canonical.listing.clone());
Ok(SdkListingPublishInput {
canonical,
actor,
@@ -1880,7 +1857,7 @@ fn sdk_prepared_publish_view(
seller_account_id: canonical.seller_account_id.clone(),
seller_pubkey: canonical.seller_pubkey.clone(),
seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_LISTING,
+ event_kind: KIND_CLASSIFIED_LISTING,
dry_run: true,
deduplicated: false,
target_transport_endpoints: Vec::new(),
@@ -1938,7 +1915,7 @@ fn sdk_enqueued_publish_view(
seller_account_id: canonical.seller_account_id.clone(),
seller_pubkey: canonical.seller_pubkey.clone(),
seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_LISTING,
+ event_kind: KIND_CLASSIFIED_LISTING,
dry_run: false,
deduplicated: matches!(enqueue.state, SdkMutationState::AlreadyQueued),
target_transport_endpoints,
@@ -1972,22 +1949,6 @@ fn sdk_plan_event_view(plan: &ListingPublishPlan) -> ListingMutationEventView {
}
}
-fn listing_validation_event(
- canonical: &CanonicalListingDraft,
- parts: radroots_event::wire::RadrootsNip01EventWireParts,
-) -> Result<RadrootsEventEnvelope, String> {
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: "0".repeat(64),
- author: canonical.seller_pubkey.clone(),
- created_at: 0,
- kind: KIND_LISTING,
- tags: parts.tags,
- content: parts.content,
- sig: "0".repeat(128),
- })
- .map_err(|error| error.to_string())
-}
-
fn sdk_push_event_for_listing<'a>(
enqueue: &ListingEnqueueReceipt,
push: &'a PushOutboxReceipt,
@@ -2173,8 +2134,8 @@ fn mutate(
ensure_listing_bound_account(config, &canonical, args.file.as_path())?;
if let Some(status) = operation.listing_status() {
- canonical.listing.availability = Some(RadrootsListingAvailability::Status {
- status: RadrootsListingStatus::Other {
+ canonical.listing.availability = Some(RadrootsOperationalListingAvailability::Status {
+ status: RadrootsOperationalListingStatus::Other {
value: status.to_owned(),
},
});
@@ -2199,7 +2160,7 @@ fn mutate_via_sdk_from_canonical(
[RadrootsActorRole::Seller],
)
.map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?;
- let document = RadrootsListingEditDocumentV1::new(canonical.listing.clone());
+ let document = RadrootsOperationalListingEditDocumentV1::new(canonical.listing.clone());
if config.output.dry_run {
let session = CliSdkSession::connect_memory(config)?;
let plan = session
@@ -2444,7 +2405,7 @@ fn canonicalize_draft(
},
)?;
- let listing = RadrootsListing {
+ let listing = RadrootsOperationalListing {
d_tag: protocol_d_tag(listing_id.as_str(), "listing d_tag").map_err(|error| {
issue_for_field(
contents,
@@ -2457,7 +2418,7 @@ fn canonicalize_draft(
pubkey: seller_pubkey.clone(),
d_tag: farm_d_tag.clone(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: draft.product.key.trim().to_owned(),
title: draft.product.title.trim().to_owned(),
category: draft.product.category.trim().to_owned(),
@@ -2469,7 +2430,7 @@ fn canonicalize_draft(
year: None,
},
primary_bin_id: primary_bin_id.clone(),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: primary_bin_id,
quantity,
price_per_canonical_unit: price,
@@ -2502,7 +2463,7 @@ fn canonicalize_draft(
fn build_availability(
draft: &ListingDraftDocument,
contents: &str,
-) -> Result<RadrootsListingAvailability, ListingValidationIssueView> {
+) -> Result<RadrootsOperationalListingAvailability, ListingValidationIssueView> {
let kind = if draft.availability.kind.trim().is_empty() {
if draft.availability.start.is_some() || draft.availability.end.is_some() {
"window"
@@ -2523,17 +2484,17 @@ fn build_availability(
"missing availability status",
));
}
- Ok(RadrootsListingAvailability::Status {
+ Ok(RadrootsOperationalListingAvailability::Status {
status: match status {
- "active" => RadrootsListingStatus::Active,
- "sold" => RadrootsListingStatus::Sold,
- other => RadrootsListingStatus::Other {
+ "active" => RadrootsOperationalListingStatus::Active,
+ "sold" => RadrootsOperationalListingStatus::Sold,
+ other => RadrootsOperationalListingStatus::Other {
value: other.to_owned(),
},
},
})
}
- "window" => Ok(RadrootsListingAvailability::Window {
+ "window" => Ok(RadrootsOperationalListingAvailability::Window {
start: draft.availability.start,
end: draft.availability.end,
}),
@@ -2548,7 +2509,7 @@ fn build_availability(
fn build_delivery_method(
draft: &ListingDraftDocument,
contents: &str,
-) -> Result<RadrootsListingDeliveryMethod, ListingValidationIssueView> {
+) -> Result<RadrootsOperationalListingDeliveryMethod, ListingValidationIssueView> {
let method = draft.delivery.method.trim();
if method.is_empty() {
return Err(issue_for_field(
@@ -2559,17 +2520,17 @@ fn build_delivery_method(
}
Ok(match method {
- "pickup" => RadrootsListingDeliveryMethod::Pickup,
- "local_delivery" => RadrootsListingDeliveryMethod::LocalDelivery,
- "shipping" => RadrootsListingDeliveryMethod::Shipping,
- other => RadrootsListingDeliveryMethod::Other {
+ "pickup" => RadrootsOperationalListingDeliveryMethod::Pickup,
+ "local_delivery" => RadrootsOperationalListingDeliveryMethod::LocalDelivery,
+ "shipping" => RadrootsOperationalListingDeliveryMethod::Shipping,
+ other => RadrootsOperationalListingDeliveryMethod::Other {
method: other.to_owned(),
},
})
}
-fn build_location(draft: &ListingDraftDocument) -> RadrootsListingPublicLocation {
- RadrootsListingPublicLocation {
+fn build_location(draft: &ListingDraftDocument) -> RadrootsOperationalListingPublicLocation {
+ RadrootsOperationalListingPublicLocation {
primary: draft.location.primary.trim().to_owned(),
city: draft.location.city.clone().and_then(non_empty),
region: draft.location.region.clone().and_then(non_empty),
@@ -2832,48 +2793,62 @@ fn validate_configured_listing_signer(
)
}
+fn validate_operational_listing_draft(
+ canonical: &CanonicalListingDraft,
+) -> Result<(), RadrootsOperationalListingValidationError> {
+ let seller_pubkey = RadrootsPublicKey::parse(canonical.seller_pubkey.as_str())
+ .map_err(|_| RadrootsOperationalListingValidationError::InvalidSeller)?;
+ validate_operational_listing_model(canonical.listing.clone(), &seller_pubkey).map(|_| ())
+}
+
fn issue_from_trade_validation(
- error: RadrootsTradeValidationListingError,
+ error: RadrootsOperationalListingValidationError,
contents: &str,
) -> ListingValidationIssueView {
match error {
- RadrootsTradeValidationListingError::InvalidSeller => issue_for_field(
+ RadrootsOperationalListingValidationError::InvalidSeller => issue_for_field(
contents,
"seller_actor.pubkey",
"listing author does not match the farm pubkey",
),
- RadrootsTradeValidationListingError::MissingTitle => {
+ RadrootsOperationalListingValidationError::MissingTitle => {
issue_for_field(contents, "product.title", "missing listing title")
}
- RadrootsTradeValidationListingError::MissingDescription => {
+ RadrootsOperationalListingValidationError::MissingDescription => {
issue_for_field(contents, "product.summary", "missing listing description")
}
- RadrootsTradeValidationListingError::MissingProductType => {
+ RadrootsOperationalListingValidationError::MissingProductType => {
issue_for_field(contents, "product.category", "missing listing product type")
}
- RadrootsTradeValidationListingError::MissingBins
- | RadrootsTradeValidationListingError::MissingPrimaryBin
- | RadrootsTradeValidationListingError::InvalidBin => {
+ RadrootsOperationalListingValidationError::MissingBins
+ | RadrootsOperationalListingValidationError::MissingPrimaryBin
+ | RadrootsOperationalListingValidationError::InvalidBin => {
issue_for_field(contents, "primary_bin.bin_id", error.to_string())
}
- RadrootsTradeValidationListingError::InvalidPrice => issue_for_field(
+ RadrootsOperationalListingValidationError::MissingPrice
+ | RadrootsOperationalListingValidationError::InvalidPrice => issue_for_field(
contents,
"primary_bin.price_amount",
"invalid listing price",
),
- RadrootsTradeValidationListingError::MissingInventory
- | RadrootsTradeValidationListingError::InvalidInventory => {
+ RadrootsOperationalListingValidationError::MissingInventory
+ | RadrootsOperationalListingValidationError::InvalidInventory => {
issue_for_field(contents, "inventory.available", error.to_string())
}
- RadrootsTradeValidationListingError::MissingAvailability => issue_for_field(
+ RadrootsOperationalListingValidationError::MissingAvailability => issue_for_field(
contents,
"availability.status",
"missing listing availability",
),
- RadrootsTradeValidationListingError::MissingLocation => {
- issue_for_field(contents, "location.primary", "missing listing location")
+ RadrootsOperationalListingValidationError::MissingLocation
+ | RadrootsOperationalListingValidationError::MissingLocationLocality => {
+ issue_for_field(contents, "location.primary", error.to_string())
+ }
+ RadrootsOperationalListingValidationError::MissingLocationGeohash
+ | RadrootsOperationalListingValidationError::InvalidLocationGeohash => {
+ issue_for_field(contents, "location.geohash", error.to_string())
}
- RadrootsTradeValidationListingError::MissingDeliveryMethod => issue_for_field(
+ RadrootsOperationalListingValidationError::MissingDeliveryMethod => issue_for_field(
contents,
"delivery.method",
"missing listing delivery method",
@@ -2976,7 +2951,9 @@ fn authoring_defaults(config: &RuntimeConfig) -> Result<ListingAuthoringDefaults
Ok(defaults)
}
-fn draft_location_from_model(location: &RadrootsListingPublicLocation) -> ListingDraftLocation {
+fn draft_location_from_model(
+ location: &RadrootsOperationalListingPublicLocation,
+) -> ListingDraftLocation {
ListingDraftLocation {
primary: location.primary.clone(),
city: location.city.clone(),
@@ -3296,7 +3273,7 @@ mod tests {
}
#[test]
- fn listing_draft_canonicalization_preserves_discounts() {
+ fn listing_draft_canonicalization_preserves_discounts_and_validates_semantics() {
let seller_pubkey = "a".repeat(64);
let document = ListingDraftDocument {
version: 1,
@@ -3339,8 +3316,8 @@ mod tests {
method: "pickup".to_owned(),
},
location: super::ListingDraftLocation {
- primary: "Asheville".to_owned(),
- city: None,
+ primary: "Farm stand".to_owned(),
+ city: Some("Asheville".to_owned()),
region: None,
country: None,
geohash: "dnqwy".to_owned(),
@@ -3374,6 +3351,17 @@ mod tests {
.len(),
1
);
+ super::validate_operational_listing_draft(&canonical)
+ .expect("canonical listing passes operational validation");
+
+ let mut missing_description = canonical;
+ missing_description.listing.product.summary = Some(" ".to_owned());
+ assert_eq!(
+ super::validate_operational_listing_draft(&missing_description),
+ Err(
+ radroots_event::trade_validation::RadrootsOperationalListingValidationError::MissingDescription
+ )
+ );
}
fn sdk_push_event(
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -1397,6 +1397,7 @@ mod tests {
assert_eq!(session.config().storage_root, config.local.root.join("sdk"));
assert_eq!(status.storage, SdkStorageKind::Directory);
assert_eq!(status.event_store.total_events, 0);
+ assert_eq!(status.event_store.valid_stream_events, 0);
assert_eq!(status.outbox.total_events, 0);
}
diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs
@@ -10,8 +10,8 @@ use crate::view::runtime::{
SignerStatusView, SignerWriteKindReadinessView,
};
use radroots_event::kinds::{
- KIND_FARM, KIND_LISTING, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, KIND_TRADE_PROPOSAL,
- KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
+ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION,
+ KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
};
use radroots_nostr_accounts::prelude::RadrootsNostrAccountStatus;
use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions;
@@ -302,15 +302,15 @@ fn cli_write_kinds() -> [CliWriteKind; 9] {
},
CliWriteKind {
command: "listing.publish",
- event_kind: KIND_LISTING,
+ event_kind: KIND_CLASSIFIED_LISTING,
},
CliWriteKind {
command: "listing.update",
- event_kind: KIND_LISTING,
+ event_kind: KIND_CLASSIFIED_LISTING,
},
CliWriteKind {
command: "listing.withdraw",
- event_kind: KIND_LISTING,
+ event_kind: KIND_CLASSIFIED_LISTING,
},
CliWriteKind {
command: "trade.proposal.submit",
@@ -582,9 +582,9 @@ fn sign_event_permission_for_kind(event_kind: u32) -> String {
#[cfg(test)]
mod tests {
use super::{
- KIND_FARM, KIND_LISTING, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, KIND_TRADE_PROPOSAL,
- KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL, cli_write_kinds,
- myc_managed_account_ref_matches, myc_write_kind_readiness,
+ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION,
+ KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
+ cli_write_kinds, myc_managed_account_ref_matches, myc_write_kind_readiness,
myc_write_kind_readiness_for_permissions, sign_event_permission_for_kind,
};
use radroots_nostr_connect::prelude::{
@@ -644,7 +644,7 @@ mod tests {
for (command, event_kind) in [
("farm.publish", KIND_FARM),
- ("listing.publish", KIND_LISTING),
+ ("listing.publish", KIND_CLASSIFIED_LISTING),
("trade.proposal.submit", KIND_TRADE_PROPOSAL),
] {
let entry = readiness
@@ -666,7 +666,7 @@ mod tests {
Ok(RadrootsNostrConnectPermissions::from(vec![
RadrootsNostrConnectPermission::with_parameter(
RadrootsNostrConnectMethod::SignEvent,
- format!("kind:{KIND_LISTING}"),
+ format!("kind:{KIND_CLASSIFIED_LISTING}"),
),
])),
);
diff --git a/src/runtime/store.rs b/src/runtime/store.rs
@@ -410,7 +410,7 @@ fn sdk_event_store_status_view(
path,
store: sdk_sqlite_status_view(status.store),
total_events: status.total_events,
- projection_eligible_events: status.projection_eligible_events,
+ valid_stream_events: status.valid_stream_events,
transport_observations: status.transport_observations,
last_event_seq: status.last_event_seq,
last_event_updated_at_ms: status.last_event_updated_at_ms,
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -2,11 +2,12 @@ use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use radroots_event::kinds::{
- KIND_FARM, KIND_LIST_SET_APP_CURATION, KIND_LIST_SET_BOOKMARK, KIND_LIST_SET_CALENDAR,
- KIND_LIST_SET_CURATION, KIND_LIST_SET_EMOJI, KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC,
- KIND_LIST_SET_INTEREST, KIND_LIST_SET_KIND_MUTE, KIND_LIST_SET_MEDIA_STARTER_PACK,
- KIND_LIST_SET_PICTURE, KIND_LIST_SET_RELAY, KIND_LIST_SET_RELEASE_ARTIFACT,
- KIND_LIST_SET_STARTER_PACK, KIND_LIST_SET_VIDEO, KIND_LISTING, KIND_PLOT, KIND_PROFILE,
+ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_LIST_SET_APP_CURATION, KIND_LIST_SET_BOOKMARK,
+ KIND_LIST_SET_CALENDAR, KIND_LIST_SET_CURATION, KIND_LIST_SET_EMOJI, KIND_LIST_SET_FOLLOW,
+ KIND_LIST_SET_GENERIC, KIND_LIST_SET_INTEREST, KIND_LIST_SET_KIND_MUTE,
+ KIND_LIST_SET_MEDIA_STARTER_PACK, KIND_LIST_SET_PICTURE, KIND_LIST_SET_RELAY,
+ KIND_LIST_SET_RELEASE_ARTIFACT, KIND_LIST_SET_STARTER_PACK, KIND_LIST_SET_VIDEO, KIND_PLOT,
+ KIND_PROFILE,
};
use radroots_nostr::prelude::{
RadrootsNostrFilter, RadrootsNostrTimestamp, radroots_event_from_nostr, radroots_nostr_kind,
@@ -61,12 +62,12 @@ const RELAY_FETCH_MAX_PAGES: usize = 5;
const MARKET_FRESHNESS_STALE_AFTER_SECONDS: u64 = 15 * 60;
const SYNC_PULL_FRESHNESS_STALE_AFTER_SECONDS: u64 = 30 * 60;
const SYNC_RUN_TABLE: &str = "radroots_cli_sync_run";
-const MARKET_REFRESH_KINDS: &[u32] = &[KIND_PROFILE, KIND_FARM, KIND_LISTING];
+const MARKET_REFRESH_KINDS: &[u32] = &[KIND_PROFILE, KIND_FARM, KIND_CLASSIFIED_LISTING];
const SYNC_PULL_KINDS: &[u32] = &[
KIND_PROFILE,
KIND_FARM,
KIND_PLOT,
- KIND_LISTING,
+ KIND_CLASSIFIED_LISTING,
KIND_LIST_SET_FOLLOW,
KIND_LIST_SET_GENERIC,
KIND_LIST_SET_RELAY,
@@ -1657,9 +1658,26 @@ fn ingest_events(
counts.unsupported_count += 1;
continue;
}
- let event = radroots_event_from_nostr(&event.event);
+ let event = match radroots_event_from_nostr(&event.event) {
+ Ok(event) => event,
+ Err(error) => {
+ counts.failed_count += 1;
+ if counts.first_failure_reason.is_none() {
+ counts.first_failure_reason = Some(error.to_string());
+ }
+ continue;
+ }
+ };
match radroots_replica_ingest_event(executor, &event) {
Ok(RadrootsReplicaIngestOutcome::Applied) => counts.ingested_count += 1,
+ Ok(RadrootsReplicaIngestOutcome::Excluded) => counts.unsupported_count += 1,
+ Ok(RadrootsReplicaIngestOutcome::Rejected) => {
+ counts.failed_count += 1;
+ if counts.first_failure_reason.is_none() {
+ counts.first_failure_reason =
+ Some("event was rejected by the local replica projection".to_owned());
+ }
+ }
Ok(RadrootsReplicaIngestOutcome::Skipped) => counts.skipped_count += 1,
Err(error @ RadrootsReplicaEventsError::Sql(_)) => return Err(error.into()),
Err(error) => {
@@ -1764,12 +1782,14 @@ mod tests {
use radroots_event::farm::{RadrootsFarm, RadrootsFarmRef};
use radroots_event::ids::RadrootsEventId;
- use radroots_event::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_LISTING, KIND_POST};
+ use radroots_event::kinds::{
+ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_LIST_SET_GENERIC, KIND_POST,
+ };
use radroots_event::list::RadrootsListEntry;
use radroots_event::list_set::RadrootsListSet;
use radroots_event::plot::RadrootsPlot;
use radroots_event::profile::RadrootsAuthoredProfile;
- use radroots_event::wire::RadrootsNip01EventWireParts;
+ use radroots_event::wire::{DEFAULT_CONTENT_MAX_BYTES, RadrootsNip01EventWireParts};
use radroots_event_codec::farm::encode as farm_encode;
use radroots_event_codec::list_set::encode as list_set_encode;
use radroots_event_codec::plot::encode as plot_encode;
@@ -2048,6 +2068,7 @@ mod tests {
0,
0,
0,
+ 0,
None,
None,
&["wss://relay-a.example.com", "wss://relay-b.example.com"],
@@ -2081,6 +2102,7 @@ mod tests {
0,
0,
0,
+ 0,
None,
None,
&["wss://relay.example.com"],
@@ -2157,27 +2179,29 @@ mod tests {
}
#[test]
- fn sync_status_reports_sdk_pending_retryable_and_terminal_outbox_counts() {
+ fn sync_status_reports_distinct_raw_valid_and_outbox_counts() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path(), vec!["wss://relay.example.com".to_owned()]);
- let view = sdk_sync_status_view(
- &config,
- sdk_status_receipt(
- 3,
- 4,
- 1,
- 1,
- 2,
- 1,
- 0,
- 1,
- 0,
- Some(1_700_000_010_000),
- Some("auth-required: login".to_owned()),
- &["wss://relay.example.com"],
- ),
+ let receipt = sdk_status_receipt(
+ 3,
+ 2,
+ 4,
+ 1,
+ 1,
+ 2,
+ 1,
+ 0,
+ 1,
+ 0,
+ Some(1_700_000_010_000),
+ Some("auth-required: login".to_owned()),
+ &["wss://relay.example.com"],
);
+ assert_eq!(receipt.event_store.total_events, 3);
+ assert_eq!(receipt.event_store.valid_stream_events, 2);
+
+ let view = sdk_sync_status_view(&config, receipt);
assert_eq!(view.state, "ready");
assert_eq!(view.queue.expected_count, 4);
@@ -2207,6 +2231,7 @@ mod tests {
1,
1,
1,
+ 1,
0,
0,
0,
@@ -2259,6 +2284,7 @@ mod tests {
0,
0,
0,
+ 0,
None,
None,
&["wss://relay.example.com"],
@@ -2387,6 +2413,7 @@ mod tests {
sdk_status_receipt(
2,
2,
+ 2,
0,
1,
1,
@@ -2444,7 +2471,8 @@ mod tests {
reason = "test fixture construction sets every storage status field explicitly"
)]
fn sdk_status_receipt(
- total_events: i64,
+ raw_total_events: i64,
+ valid_stream_events: i64,
outbox_total_events: i64,
pending_events: i64,
retryable_events: i64,
@@ -2465,11 +2493,11 @@ mod tests {
source: SyncStatusSource::SdkCanonicalStores,
observed_at_ms: 1_700_000_030_000,
event_store: SyncEventStoreStatus {
- total_events,
- projection_eligible_events: total_events,
+ total_events: raw_total_events,
+ valid_stream_events,
transport_observations: 0,
- last_event_seq: (total_events > 0).then_some(total_events),
- last_event_updated_at_ms: (total_events > 0).then_some(1_700_000_000_000),
+ last_event_seq: (raw_total_events > 0).then_some(raw_total_events),
+ last_event_updated_at_ms: (raw_total_events > 0).then_some(1_700_000_000_000),
},
outbox: SyncOutboxStatus {
total_events: outbox_total_events,
@@ -2522,7 +2550,7 @@ mod tests {
observed_at_ms: 1_700_000_030_000,
event_store: SyncEventStoreStatus {
total_events: 1,
- projection_eligible_events: 1,
+ valid_stream_events: 1,
transport_observations: 0,
last_event_seq: Some(1),
last_event_updated_at_ms: Some(1_700_000_000_000),
@@ -2687,7 +2715,7 @@ mod tests {
crate::runtime::store::init(&config).expect("store init");
let seller = identity(7);
let seller_pubkey = seller.public_key_hex();
- let listing_addr = format!("{KIND_LISTING}:{seller_pubkey}:{LISTING_D_TAG}");
+ let listing_addr = format!("{KIND_CLASSIFIED_LISTING}:{seller_pubkey}:{LISTING_D_TAG}");
let events = vec![
farm_event(&seller),
plot_event(&seller),
@@ -2927,6 +2955,14 @@ mod tests {
signed_event(
&seller,
RadrootsNip01EventWireParts {
+ kind: KIND_CLASSIFIED_LISTING,
+ content: "x".repeat(DEFAULT_CONTENT_MAX_BYTES + 1),
+ tags: Vec::new(),
+ },
+ ),
+ signed_event(
+ &seller,
+ RadrootsNip01EventWireParts {
kind: KIND_POST,
content: "hello".to_owned(),
tags: Vec::new(),
@@ -2935,7 +2971,7 @@ mod tests {
signed_event(
&seller,
RadrootsNip01EventWireParts {
- kind: KIND_LISTING,
+ kind: KIND_CLASSIFIED_LISTING,
content: "not a listing".to_owned(),
tags: Vec::new(),
},
@@ -2945,15 +2981,15 @@ mod tests {
let view = pull_with_fetcher(&config, fake_fetcher(events)).expect("sync pull ingest");
assert_eq!(view.state, "ready");
- assert_eq!(view.fetched_count, Some(2));
+ assert_eq!(view.fetched_count, Some(3));
assert_eq!(view.ingested_count, Some(0));
assert_eq!(view.unsupported_count, Some(1));
- assert_eq!(view.failed_count, Some(1));
+ assert_eq!(view.failed_count, Some(2));
assert!(
view.reason
.as_deref()
.expect("failure reason")
- .contains("failed ingest")
+ .contains("event envelope content size")
);
}
@@ -3087,7 +3123,7 @@ mod tests {
created_at: u64,
) -> RadrootsNostrEvent {
let mut builder = wire_fixture_builder(RadrootsNip01EventWireParts {
- kind: KIND_LISTING,
+ kind: KIND_CLASSIFIED_LISTING,
content: "# Pasture Eggs".to_owned(),
tags: vec![
vec!["d".to_owned(), LISTING_D_TAG.to_owned()],
diff --git a/src/runtime/trade.rs b/src/runtime/trade.rs
@@ -9,10 +9,9 @@ use radroots_core::{
};
use radroots_event::contract::RadrootsActorRole;
use radroots_event::ids::{
- RadrootsAddressableCoordinate, RadrootsDTag, RadrootsInventoryBinId, RadrootsListingAddress,
- RadrootsPublicKey, RadrootsTradeCandidateId, RadrootsTradeId, RadrootsTradeMutationId,
+ RadrootsClassifiedListingAddress, RadrootsDTag, RadrootsInventoryBinId, RadrootsPublicKey,
+ RadrootsTradeCandidateId, RadrootsTradeId, RadrootsTradeMutationId,
};
-use radroots_event::kinds::KIND_LISTING;
use radroots_event::trade::{
RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION,
RadrootsFulfillmentProfileV1, RadrootsTradeCancellationProfileV1, RadrootsTradeCandidateLineV1,
@@ -378,11 +377,6 @@ fn scaffold_proposal_draft_inner(
})?;
let product = resolve_product(config, args)?;
let parsed_listing = parse_listing_addr(product.listing_addr.as_str())?;
- if parsed_listing.kind != KIND_LISTING {
- return Err(RuntimeError::Config(
- "trade proposal draft requires a public listing address".to_owned(),
- ));
- }
let listing_state =
resolve_active_listing_state(config, product.listing_addr.as_str(), &parsed_listing)?;
let farm_id = resolve_farm_id(config, parsed_listing.seller_pubkey.as_str())?;
@@ -736,7 +730,7 @@ fn candidate_terms(
let line = RadrootsTradeCandidateLineV1 {
line_id: RadrootsDTag::parse("line-1")
.map_err(|error| RuntimeError::Config(format!("invalid line id: {error}")))?,
- listing_addr: RadrootsAddressableCoordinate::parse(product.listing_addr.as_str())
+ listing_addr: RadrootsClassifiedListingAddress::parse(product.listing_addr.as_str())
.map_err(|error| RuntimeError::Config(format!("invalid listing address: {error}")))?,
listing_event_id: listing_state
.last_event_id
@@ -856,7 +850,7 @@ fn resolve_farm_id(
}
fn parse_listing_addr(raw: &str) -> Result<ParsedListingAddress, RuntimeError> {
- let parsed = RadrootsListingAddress::parse(raw)
+ let parsed = RadrootsClassifiedListingAddress::parse(raw)
.map_err(|error| RuntimeError::Config(format!("listing address is invalid: {error}")))?;
let (kind, rest) = parsed
.as_str()
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -1,9 +1,8 @@
use std::process::ExitCode;
use radroots_event::farm::RadrootsFarm;
-use radroots_event::ids::RadrootsListingAddress;
-use radroots_event::kinds::KIND_LISTING;
-use radroots_event::listing::RadrootsListingPublicLocation;
+use radroots_event::ids::RadrootsClassifiedListingAddress;
+use radroots_event::operational_listing::RadrootsOperationalListingPublicLocation;
use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord;
use serde::Serialize;
@@ -614,7 +613,7 @@ pub struct SdkEventStoreStatusView {
pub path: Option<String>,
pub store: SdkSqliteStatusView,
pub total_events: i64,
- pub projection_eligible_events: i64,
+ pub valid_stream_events: i64,
pub transport_observations: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_event_seq: Option<i64>,
@@ -622,6 +621,35 @@ pub struct SdkEventStoreStatusView {
pub last_event_updated_at_ms: Option<i64>,
}
+#[cfg(test)]
+mod sdk_event_store_status_view_tests {
+ use super::{SdkEventStoreStatusView, SdkSqliteStatusView};
+
+ #[test]
+ fn serializes_valid_stream_events_without_legacy_projection_key() {
+ let value = serde_json::to_value(SdkEventStoreStatusView {
+ path: None,
+ store: SdkSqliteStatusView {
+ schema_version: 1,
+ journal_mode: "wal".to_owned(),
+ foreign_keys_enabled: true,
+ busy_timeout_ms: 5_000,
+ integrity_ok: true,
+ integrity_result: "ok".to_owned(),
+ },
+ total_events: 11,
+ valid_stream_events: 7,
+ transport_observations: 3,
+ last_event_seq: Some(11),
+ last_event_updated_at_ms: Some(1_700_000_000_000),
+ })
+ .expect("event store status view");
+
+ assert_eq!(value["valid_stream_events"], 7);
+ assert!(value.get("projection_eligible_events").is_none());
+ }
+}
+
#[derive(Debug, Clone, Serialize)]
pub struct SdkOutboxStatusView {
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1118,7 +1146,7 @@ pub struct FarmSelectionView {
#[derive(Debug, Clone, Serialize)]
pub struct FarmListingDefaultsView {
pub delivery_method: String,
- pub location: RadrootsListingPublicLocation,
+ pub location: RadrootsOperationalListingPublicLocation,
}
#[derive(Debug, Clone, Serialize)]
@@ -1208,13 +1236,7 @@ impl MarketReadinessView {
price_per_amount: f64,
) -> Self {
let protocol_valid = listing_addr.is_some_and(|listing_addr| {
- RadrootsListingAddress::parse(listing_addr).is_ok_and(|parsed| {
- parsed
- .as_str()
- .split_once(':')
- .and_then(|(kind, _)| kind.parse::<u32>().ok())
- == Some(KIND_LISTING)
- })
+ RadrootsClassifiedListingAddress::parse(listing_addr).is_ok()
});
let marketplace_eligible = protocol_valid
&& title.is_some_and(|title| !title.trim().is_empty())