cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 9+++++++++
Msrc/ops/exec/basket.rs | 19+++++++++++++------
Msrc/runtime/listing.rs | 18+++++++++++-------
Msrc/runtime/order.rs | 42++++++++++++++++++++++++++++++++----------
Msrc/view/runtime.rs | 11++++++++---
Mtests/support/mod.rs | 20+++++++++++---------
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 {