commit 570ca1634bad63d9086edc405596d2e5ae320632
parent 4235d0f202e49fee1a71b66cc131803bf1994b4c
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 02:40:52 -0700
target: align listing publish contracts
- replace deleted trade listing publish helpers with canonical listing validation
- migrate order listing address call sites to canonical validated IDs
- update local test fixtures that decompose listing addresses
- validate with nix run .#check and nix run .#test
Diffstat:
6 files changed, 84 insertions(+), 35 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3513,6 +3513,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
+name = "radroots_authority"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "radroots_events",
+ "thiserror 1.0.69",
+]
+
+[[package]]
name = "radroots_cli"
version = "0.1.0"
dependencies = [
@@ -3827,6 +3835,7 @@ version = "0.1.0-alpha.2"
dependencies = [
"base64 0.22.1",
"hex",
+ "radroots_authority",
"radroots_core",
"radroots_events",
"radroots_events_codec",
diff --git a/src/ops/exec/basket.rs b/src/ops/exec/basket.rs
@@ -1315,8 +1315,8 @@ mod tests {
use std::path::{Path, PathBuf};
use radroots_events::RadrootsNostrEvent;
+ use radroots_events::ids::RadrootsListingAddress;
use radroots_events::kinds::{KIND_FARM, KIND_LISTING};
- use radroots_events_codec::order::RadrootsOrderListingAddress;
use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
use radroots_runtime_paths::RadrootsMigrationReport;
use radroots_secret_vault::RadrootsSecretBackend;
@@ -1770,22 +1770,22 @@ mod tests {
fn seed_current_listing(config: &RuntimeConfig) {
crate::runtime::store::init(config).expect("store init");
- let parsed = RadrootsOrderListingAddress::parse(LISTING_ADDR).expect("listing addr");
+ let (seller_pubkey, listing_id) = listing_addr_parts(LISTING_ADDR);
let event = RadrootsNostrEvent {
id: "2".repeat(64),
- author: parsed.seller_pubkey.clone(),
+ author: seller_pubkey.clone(),
created_at: 1,
kind: KIND_LISTING,
tags: vec![
- vec!["d".to_owned(), parsed.listing_id],
+ vec!["d".to_owned(), listing_id],
vec![
"a".to_owned(),
format!(
"{}:{}:{}",
- KIND_FARM, parsed.seller_pubkey, "AAAAAAAAAAAAAAAAAAAAAA"
+ KIND_FARM, seller_pubkey, "AAAAAAAAAAAAAAAAAAAAAA"
),
],
- vec!["p".to_owned(), parsed.seller_pubkey],
+ vec!["p".to_owned(), seller_pubkey],
vec!["key".to_owned(), "pasture-eggs".to_owned()],
vec!["title".to_owned(), "Market Eggs".to_owned()],
vec!["category".to_owned(), "eggs".to_owned()],
@@ -1827,6 +1827,13 @@ mod tests {
);
}
+ fn listing_addr_parts(listing_addr: &str) -> (String, String) {
+ let parsed = RadrootsListingAddress::parse(listing_addr).expect("listing addr");
+ let (_, rest) = parsed.as_str().split_once(':').expect("listing addr kind");
+ let (seller_pubkey, listing_id) = rest.split_once(':').expect("listing addr parts");
+ (seller_pubkey.to_owned(), listing_id.to_owned())
+ }
+
fn duplicate_current_listing_row(config: &RuntimeConfig) {
let executor = SqliteExecutor::open(&config.local.replica_db_path).expect("open replica");
let params = json!(["33333333-3333-3333-3333-333333333333", LISTING_ADDR]).to_string();
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -27,7 +27,6 @@ use radroots_nostr::prelude::{RadrootsNostrEvent as SignedNostrEvent, radroots_e
use radroots_replica_db::{ReplicaSql, migrations};
use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
use radroots_sql_core::SqliteExecutor;
-use radroots_trade::listing::publish::validate_listing_for_seller;
use radroots_trade::listing::validation::validate_listing_event;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -2616,12 +2615,17 @@ fn build_listing_event_draft(
) -> Result<(ListingMutationEventDraft, String), RuntimeError> {
let parts = to_wire_parts_with_kind(&canonical.listing, KIND_LISTING)
.map_err(|error| RuntimeError::Config(format!("invalid listing contract: {error}")))?;
- let validated = validate_listing_for_seller(
- canonical.listing.clone(),
- canonical.seller_pubkey.as_str(),
- KIND_LISTING,
- )
- .map_err(|error| RuntimeError::Config(format!("invalid listing contract: {error}")))?;
+ let event = RadrootsNostrEvent {
+ id: String::new(),
+ author: canonical.seller_pubkey.clone(),
+ created_at: 0,
+ kind: parts.kind,
+ tags: parts.tags.clone(),
+ content: parts.content.clone(),
+ sig: String::new(),
+ };
+ let validated = validate_listing_event(&event)
+ .map_err(|error| RuntimeError::Config(format!("invalid listing contract: {error}")))?;
Ok((
ListingMutationEventDraft {
event: ListingMutationEventView {
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -37,11 +37,11 @@ use radroots_events::order::{
use radroots_events_codec::d_tag::is_d_tag_base64url;
use radroots_events_codec::listing::decode::listing_from_event;
use radroots_events_codec::order::{
- RadrootsOrderListingAddress, order_cancellation_event_build, order_cancellation_from_event,
- order_decision_event_build, order_envelope_from_event, order_event_context_from_tags,
- order_fulfillment_update_event_build, order_fulfillment_update_from_event,
- order_payment_record_event_build, order_payment_record_from_event, order_receipt_event_build,
- order_receipt_from_event, order_request_from_event, order_revision_decision_event_build,
+ order_cancellation_event_build, order_cancellation_from_event, order_decision_event_build,
+ order_envelope_from_event, order_event_context_from_tags, order_fulfillment_update_event_build,
+ order_fulfillment_update_from_event, order_payment_record_event_build,
+ order_payment_record_from_event, order_receipt_event_build, order_receipt_from_event,
+ order_request_from_event, order_revision_decision_event_build,
order_revision_decision_from_event, order_revision_proposal_event_build,
order_revision_proposal_from_event, order_settlement_decision_event_build,
order_settlement_decision_from_event,
@@ -6783,7 +6783,7 @@ fn current_inventory_listing_from_receipt(
}
fn current_inventory_listing_from_parts(
- parsed: RadrootsOrderListingAddress,
+ parsed: ParsedListingAddress,
receipt: DirectRelayFetchReceipt,
) -> Result<Option<ResolvedInventoryListing>, RuntimeError> {
let mut candidates = Vec::new();
@@ -8813,7 +8813,7 @@ fn order_request_filter(
}
fn listing_event_filter(
- listing_addr: &RadrootsOrderListingAddress,
+ listing_addr: &ParsedListingAddress,
) -> Result<RadrootsNostrFilter, RuntimeError> {
let filter = RadrootsNostrFilter::new()
.kind(radroots_nostr_kind(KIND_LISTING as u16))
@@ -9061,7 +9061,7 @@ fn resolve_trade_product_by_listing_addr(
fn resolve_active_listing_event_id(
config: &RuntimeConfig,
listing_addr: &str,
- parsed: &RadrootsOrderListingAddress,
+ parsed: &ParsedListingAddress,
) -> Result<Option<String>, RuntimeError> {
if !config.local.replica_db_path.exists() {
return Ok(None);
@@ -12437,8 +12437,30 @@ fn draft_lookup_path(config: &RuntimeConfig, lookup: &str) -> PathBuf {
drafts_dir(config).join(file_name)
}
-fn parse_listing_addr(raw: &str) -> Result<RadrootsOrderListingAddress, String> {
- RadrootsOrderListingAddress::parse(raw).map_err(|error| error.to_string())
+#[derive(Debug, Clone)]
+struct ParsedListingAddress {
+ kind: u32,
+ seller_pubkey: String,
+ listing_id: String,
+}
+
+fn parse_listing_addr(raw: &str) -> Result<ParsedListingAddress, String> {
+ let parsed = RadrootsListingAddress::parse(raw).map_err(|error| error.to_string())?;
+ let (kind, rest) = parsed
+ .as_str()
+ .split_once(':')
+ .ok_or_else(|| "listing address has invalid format".to_owned())?;
+ let (seller_pubkey, listing_id) = rest
+ .split_once(':')
+ .ok_or_else(|| "listing address has invalid format".to_owned())?;
+ let kind = kind
+ .parse::<u32>()
+ .map_err(|_| "listing address kind is invalid".to_owned())?;
+ Ok(ParsedListingAddress {
+ kind,
+ seller_pubkey: seller_pubkey.to_owned(),
+ listing_id: listing_id.to_owned(),
+ })
}
fn issue(field: impl Into<String>, message: impl Into<String>) -> OrderIssueView {
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -4,13 +4,13 @@ use std::process::ExitCode;
use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDecimal};
use radroots_events::farm::RadrootsFarm;
+use radroots_events::ids::RadrootsListingAddress;
use radroots_events::kinds::KIND_LISTING;
use radroots_events::listing::RadrootsListingLocation;
use radroots_events::order::{
RadrootsOrderEconomics, RadrootsOrderPaymentMethod, RadrootsOrderSettlementOutcome,
};
use radroots_events::profile::RadrootsProfile;
-use radroots_events_codec::order::RadrootsOrderListingAddress;
use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord;
use serde::Serialize;
@@ -1086,8 +1086,13 @@ impl MarketReadinessView {
price_per_amount: f64,
) -> Self {
let protocol_valid = listing_addr.is_some_and(|listing_addr| {
- RadrootsOrderListingAddress::parse(listing_addr)
- .is_ok_and(|parsed| parsed.kind == KIND_LISTING)
+ RadrootsListingAddress::parse(listing_addr).is_ok_and(|parsed| {
+ parsed
+ .as_str()
+ .split_once(':')
+ .and_then(|(kind, _)| kind.parse::<u32>().ok())
+ == Some(KIND_LISTING)
+ })
});
let marketplace_eligible = protocol_valid
&& title.is_some_and(|title| !title.trim().is_empty())
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
@@ -7,8 +7,8 @@ use std::sync::Mutex;
use assert_cmd::prelude::*;
use radroots_events::RadrootsNostrEvent;
+use radroots_events::ids::RadrootsListingAddress;
use radroots_events::kinds::{KIND_FARM, KIND_LISTING};
-use radroots_events_codec::order::RadrootsOrderListingAddress;
use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
use radroots_local_events::{
LocalEventRecord, LocalEventRecordInput, LocalEventsStore, LocalRecordFamily,
@@ -240,9 +240,7 @@ pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str)
let db_path = store["result"]["path"]
.as_str()
.expect("replica db path from store init");
- let parsed = RadrootsOrderListingAddress::parse(listing_addr).expect("listing addr");
- let seller_pubkey = parsed.seller_pubkey.clone();
- let listing_id = parsed.listing_id.clone();
+ let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr);
let event_id = "2".repeat(64);
let event = RadrootsNostrEvent {
id: event_id.clone(),
@@ -411,11 +409,8 @@ pub fn replace_latest_listing_event_id(
listing_addr: &str,
event_id: &str,
) {
- let parsed = RadrootsOrderListingAddress::parse(listing_addr).expect("listing addr");
- let key = format!(
- "{}:{}:{}",
- KIND_LISTING, parsed.seller_pubkey, parsed.listing_id
- );
+ let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr);
+ let key = format!("{KIND_LISTING}:{seller_pubkey}:{listing_id}");
let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
let params = serde_json::to_string(&vec![event_id, key.as_str()]).expect("update params");
executor
@@ -426,6 +421,13 @@ pub fn replace_latest_listing_event_id(
.expect("update latest listing event id");
}
+fn listing_addr_parts(listing_addr: &str) -> (String, String) {
+ let parsed = RadrootsListingAddress::parse(listing_addr).expect("listing addr");
+ let (_, rest) = parsed.as_str().split_once(':').expect("listing addr kind");
+ let (seller_pubkey, listing_id) = rest.split_once(':').expect("listing addr parts");
+ (seller_pubkey.to_owned(), listing_id.to_owned())
+}
+
pub fn create_listing_draft(sandbox: &RadrootsCliSandbox, key: &str) -> PathBuf {
let accounts = sandbox.json_success(&["--format", "json", "account", "list"]);
if accounts["result"]["count"].as_u64().unwrap_or_default() == 0 {