app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit e17ba7818e59a9e5b1f0d846d72be34a9defab25
parent 1c06d7881ad72bccee9d27342801e5869e5223d4
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 20:55:47 -0700

runtime: use shared trade event kind constants

- replace desktop runtime trade and listing kind literals with radroots_events constants
- route store interop listing and farm kind aliases through shared constants
- add source guard coverage for production event-kind literals
- validate formatting, app checks, guard tests, and store interop tests

Diffstat:
MCargo.lock | 1+
Mcrates/desktop/Cargo.toml | 1+
Mcrates/desktop/src/runtime.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++------------------
Mcrates/desktop/src/source_guards.rs | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/store/src/interop.rs | 14++++++++------
5 files changed, 136 insertions(+), 24 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -5070,6 +5070,7 @@ dependencies = [ "radroots_app_ui", "radroots_app_view", "radroots_core", + "radroots_events", "radroots_events_codec", "radroots_identity", "radroots_local_events", diff --git a/crates/desktop/Cargo.toml b/crates/desktop/Cargo.toml @@ -29,6 +29,7 @@ radroots_app_sqlite.workspace = true radroots_app_state.workspace = true radroots_app_sync.workspace = true radroots_app_ui.workspace = true +radroots_events.workspace = true radroots_local_events.workspace = true radroots_sql_core.workspace = true radroots_trade.workspace = true diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -64,6 +64,12 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; +use radroots_events::kinds::{ + KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_PROFILE, KIND_TRADE_CANCEL, + KIND_TRADE_FULFILLMENT_UPDATE, KIND_TRADE_ORDER_DECISION, KIND_TRADE_ORDER_REQUEST, + KIND_TRADE_ORDER_REVISION, KIND_TRADE_ORDER_REVISION_RESPONSE, KIND_TRADE_PAYMENT_RECORDED, + KIND_TRADE_RECEIPT, KIND_TRADE_SETTLEMENT_DECISION, +}; use radroots_events_codec::trade::{ active_trade_event_context_from_tags, active_trade_payment_recorded_from_event, active_trade_settlement_decision_from_event, @@ -145,7 +151,19 @@ const APP_DIRECT_RELAY_INGEST_SCOPE_KEY: &str = "direct_relay_ingest"; const APP_DIRECT_RELAY_INGEST_STALE_AFTER_SECONDS: i64 = 900; const APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE: u32 = 250; const APP_DIRECT_RELAY_INGEST_KINDS: &[u16] = &[ - 0, 30340, 30402, 30403, 3422, 3423, 3424, 3425, 3432, 3433, 3434, 3435, 3436, + KIND_PROFILE as u16, + KIND_FARM as u16, + KIND_LISTING as u16, + KIND_LISTING_DRAFT as u16, + KIND_TRADE_ORDER_REQUEST as u16, + KIND_TRADE_ORDER_DECISION as u16, + KIND_TRADE_ORDER_REVISION as u16, + KIND_TRADE_ORDER_REVISION_RESPONSE as u16, + KIND_TRADE_CANCEL as u16, + KIND_TRADE_FULFILLMENT_UPDATE as u16, + KIND_TRADE_RECEIPT as u16, + KIND_TRADE_PAYMENT_RECORDED as u16, + KIND_TRADE_SETTLEMENT_DECISION as u16, ]; #[derive(Debug, Default)] @@ -5428,7 +5446,7 @@ impl DesktopAppRuntimeState { continue; } match event.kind { - 3423 => { + KIND_TRADE_ORDER_DECISION => { let envelope = radroots_sdk::trade::parse_order_decision(&event).map_err(|_| { AppSqliteError::InvalidProjection { @@ -5445,7 +5463,7 @@ impl DesktopAppRuntimeState { payload: envelope.payload, }); } - 3424 => { + KIND_TRADE_ORDER_REVISION => { let Ok(envelope) = radroots_sdk::trade::parse_order_revision_proposal(&event) else { return Err(AppSqliteError::InvalidProjection { @@ -5464,7 +5482,7 @@ impl DesktopAppRuntimeState { payload: envelope.payload, }); } - 3425 => { + KIND_TRADE_ORDER_REVISION_RESPONSE => { let Ok(envelope) = radroots_sdk::trade::parse_order_revision_decision(&event) else { return Err(AppSqliteError::InvalidProjection { @@ -5483,7 +5501,7 @@ impl DesktopAppRuntimeState { payload: envelope.payload, }); } - 3432 => { + KIND_TRADE_CANCEL => { let Ok(envelope) = radroots_sdk::trade::parse_order_cancellation(&event) else { return Err(AppSqliteError::InvalidProjection { reason: "order lifecycle evidence is invalid", @@ -5501,7 +5519,7 @@ impl DesktopAppRuntimeState { payload: envelope.payload, }); } - 3433 => { + KIND_TRADE_FULFILLMENT_UPDATE => { let Ok(envelope) = radroots_sdk::trade::parse_fulfillment_update(&event) else { return Err(AppSqliteError::InvalidProjection { reason: "order lifecycle evidence is invalid", @@ -5519,7 +5537,7 @@ impl DesktopAppRuntimeState { payload: envelope.payload, }); } - 3434 => { + KIND_TRADE_RECEIPT => { let Ok(envelope) = radroots_sdk::trade::parse_buyer_receipt(&event) else { return Err(AppSqliteError::InvalidProjection { reason: "order lifecycle evidence is invalid", @@ -5535,7 +5553,7 @@ impl DesktopAppRuntimeState { payload: envelope.payload, }); } - 3435 => { + KIND_TRADE_PAYMENT_RECORDED => { let envelope = active_trade_payment_recorded_from_event(&event).map_err(|_| { AppSqliteError::InvalidProjection { @@ -5552,7 +5570,7 @@ impl DesktopAppRuntimeState { payload: envelope.payload, }); } - 3436 => { + KIND_TRADE_SETTLEMENT_DECISION => { let envelope = active_trade_settlement_decision_from_event(&event).map_err(|_| { AppSqliteError::InvalidProjection { @@ -5667,7 +5685,16 @@ impl DesktopAppRuntimeState { ) -> Result<Vec<radroots_sdk::RadrootsNostrEvent>, AppSqliteError> { let mut events = Vec::new(); let mut seen_event_ids = BTreeSet::new(); - let kinds = [3423_u32, 3424, 3425, 3432, 3433, 3434, 3435, 3436]; + let kinds = [ + KIND_TRADE_ORDER_DECISION, + KIND_TRADE_ORDER_REVISION, + KIND_TRADE_ORDER_REVISION_RESPONSE, + KIND_TRADE_CANCEL, + KIND_TRADE_FULFILLMENT_UPDATE, + KIND_TRADE_RECEIPT, + KIND_TRADE_PAYMENT_RECORDED, + KIND_TRADE_SETTLEMENT_DECISION, + ]; if let Some(sqlite_store) = self.sqlite_store.as_ref() { for kind in kinds { @@ -5876,7 +5903,7 @@ impl DesktopAppRuntimeState { let owner_pubkey = self.local_events_owner_pubkey(account); let listing_addr = owner_pubkey .as_ref() - .map(|pubkey| format!("30402:{pubkey}:{listing_d_tag}")); + .map(|pubkey| format!("{KIND_LISTING}:{pubkey}:{listing_d_tag}")); let exportability = local_work_exportability(owner_pubkey.as_deref()); let farm_setup = self.state_store.farm_setup_projection(); let farm_rules = self.state_store.farm_rules_projection(); @@ -6951,8 +6978,8 @@ fn direct_relay_event_records( fn direct_relay_event_farm_id(kind: u16, tags: &[Vec<String>]) -> Option<String> { match kind { - 30340 => relay_event_tag_value(tags, "d", 1), - 30402 | 30403 => { + kind if kind == KIND_FARM as u16 => relay_event_tag_value(tags, "d", 1), + kind if kind == KIND_LISTING as u16 || kind == KIND_LISTING_DRAFT as u16 => { relay_event_tag_value(tags, "a", 1).and_then(|address| relay_address_d_tag(&address)) } _ => None, @@ -6965,7 +6992,9 @@ fn direct_relay_event_listing_addr( listing_d_tag: Option<&str>, ) -> Option<String> { match kind { - 30402 | 30403 => listing_d_tag.map(|d_tag| format!("{kind}:{event_pubkey}:{d_tag}")), + kind if kind == KIND_LISTING as u16 || kind == KIND_LISTING_DRAFT as u16 => { + listing_d_tag.map(|d_tag| format!("{kind}:{event_pubkey}:{d_tag}")) + } _ => None, } } @@ -7639,15 +7668,15 @@ fn d_tag_from_uuid(uuid: Uuid) -> String { fn signed_event_farm_id(receipt: &AppPublishedOperationReceipt) -> Option<String> { match receipt.event_kind { - 30340 => signed_event_tag_value(&receipt.event_tags_json, "d", 1), - 30402 => signed_event_tag_value(&receipt.event_tags_json, "a", 1) + KIND_FARM => signed_event_tag_value(&receipt.event_tags_json, "d", 1), + KIND_LISTING => signed_event_tag_value(&receipt.event_tags_json, "a", 1) .and_then(|address| signed_event_address_d_tag(address.as_str())), _ => None, } } fn signed_event_listing_addr(receipt: &AppPublishedOperationReceipt) -> Option<String> { - if receipt.event_kind != 30402 { + if receipt.event_kind != KIND_LISTING { return None; } let pubkey = receipt.event_pubkey.trim(); @@ -7655,7 +7684,7 @@ fn signed_event_listing_addr(receipt: &AppPublishedOperationReceipt) -> Option<S return None; } signed_event_tag_value(&receipt.event_tags_json, "d", 1) - .map(|d_tag| format!("30402:{pubkey}:{d_tag}")) + .map(|d_tag| format!("{KIND_LISTING}:{pubkey}:{d_tag}")) } fn signed_event_tag_value( diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -858,6 +858,30 @@ const FORBIDDEN_PAYMENT_ACTION_COPY_TERMS: &[&str] = &[ "payment provider", ]; +const FORBIDDEN_PRODUCTION_EVENT_KIND_LITERALS: &[(&str, &str)] = &[ + ("30340", "KIND_FARM"), + ("30402", "KIND_LISTING"), + ("30403", "KIND_LISTING_DRAFT"), + ("3422", "KIND_TRADE_ORDER_REQUEST"), + ("3423", "KIND_TRADE_ORDER_DECISION"), + ("3424", "KIND_TRADE_ORDER_REVISION"), + ("3425", "KIND_TRADE_ORDER_REVISION_RESPONSE"), + ("3426", "KIND_TRADE_QUESTION"), + ("3427", "KIND_TRADE_ANSWER"), + ("3428", "KIND_TRADE_DISCOUNT_REQUEST"), + ("3429", "KIND_TRADE_DISCOUNT_OFFER"), + ("3430", "KIND_TRADE_DISCOUNT_ACCEPT"), + ("3431", "KIND_TRADE_FORBIDDEN_3431"), + ("3432", "KIND_TRADE_CANCEL"), + ("3433", "KIND_TRADE_FULFILLMENT_UPDATE"), + ("3434", "KIND_TRADE_RECEIPT"), + ("3435", "KIND_TRADE_PAYMENT_RECORDED"), + ("3436", "KIND_TRADE_SETTLEMENT_DECISION"), + ("3440", "KIND_TRADE_VALIDATION_RECEIPT"), +]; + +const TEST_MODULE_SENTINEL: &str = "\n#[cfg(test)]\nmod tests {"; + #[test] fn desktop_menu_source_uses_localized_copy_paths() { assert_eq!( @@ -980,6 +1004,32 @@ fn desktop_sources_do_not_expose_reserved_payment_action_copy() { } } +#[test] +fn app_production_trade_event_kinds_use_shared_constants() { + assert_production_source_omits_event_kind_literals( + "crates/desktop/src/runtime.rs", + include_str!("runtime.rs"), + ); + + let store_interop_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|path| path.parent()) + .expect("desktop crate should live under app crates directory") + .join("crates/store/src/interop.rs"); + let store_interop_source = + fs::read_to_string(store_interop_path.as_path()).unwrap_or_else(|error| { + panic!( + "failed to read app store interop source {}: {error}", + store_interop_path.display() + ) + }); + + assert_production_source_omits_event_kind_literals( + "crates/store/src/interop.rs", + store_interop_source.as_str(), + ); +} + fn extract_string_literals(source: &str) -> BTreeSet<&str> { let mut literals = BTreeSet::new(); let bytes = source.as_bytes(); @@ -1002,6 +1052,35 @@ fn extract_string_literals(source: &str) -> BTreeSet<&str> { literals } +fn assert_production_source_omits_event_kind_literals(path: &str, source: &str) { + let production_source = production_source_without_tests(source); + for (literal, constant_name) in FORBIDDEN_PRODUCTION_EVENT_KIND_LITERALS { + assert!( + !contains_numeric_token(production_source, literal), + "{path} uses raw event kind {literal}; use shared {constant_name} instead" + ); + } +} + +fn production_source_without_tests(source: &str) -> &str { + source + .split_once(TEST_MODULE_SENTINEL) + .map_or(source, |(production_source, _)| production_source) +} + +fn contains_numeric_token(source: &str, literal: &str) -> bool { + source.match_indices(literal).any(|(start, _)| { + let end = start + literal.len(); + let before_ok = start == 0 || !is_rust_identifier_byte(source.as_bytes()[start - 1]); + let after_ok = end == source.len() || !source.as_bytes()[end].is_ascii_digit(); + before_ok && after_ok + }) +} + +fn is_rust_identifier_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} + fn contains_reserved_payment_action_term(value: &str, term: &str) -> bool { if term.contains(' ') || term.contains('-') { return value.contains(term); diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs @@ -9,9 +9,11 @@ use radroots_app_view::{ use radroots_events::{ RadrootsNostrEvent, kinds::{ - KIND_TRADE_CANCEL, KIND_TRADE_FULFILLMENT_UPDATE, KIND_TRADE_ORDER_REQUEST, - KIND_TRADE_ORDER_RESPONSE, KIND_TRADE_ORDER_REVISION, KIND_TRADE_ORDER_REVISION_RESPONSE, - KIND_TRADE_PAYMENT_RECORDED, KIND_TRADE_RECEIPT, KIND_TRADE_SETTLEMENT_DECISION, + KIND_FARM as RADROOTS_KIND_FARM, KIND_LISTING as RADROOTS_KIND_LISTING, + KIND_LISTING_DRAFT as RADROOTS_KIND_LISTING_DRAFT, KIND_TRADE_CANCEL, + KIND_TRADE_FULFILLMENT_UPDATE, KIND_TRADE_ORDER_REQUEST, KIND_TRADE_ORDER_RESPONSE, + KIND_TRADE_ORDER_REVISION, KIND_TRADE_ORDER_REVISION_RESPONSE, KIND_TRADE_PAYMENT_RECORDED, + KIND_TRADE_RECEIPT, KIND_TRADE_SETTLEMENT_DECISION, }, trade::{ RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, @@ -48,9 +50,9 @@ use crate::{AppSqliteError, AppSqliteStore}; const LOCAL_EVENTS_BATCH_LIMIT: u32 = 500; const APP_LOCAL_INTEROP_CURSOR_ID: &str = "radroots_app_sqlite_projection_v1"; -const KIND_FARM: i64 = 30340; -const KIND_LISTING: i64 = 30402; -const KIND_LISTING_DRAFT: i64 = 30403; +const KIND_FARM: i64 = RADROOTS_KIND_FARM as i64; +const KIND_LISTING: i64 = RADROOTS_KIND_LISTING as i64; +const KIND_LISTING_DRAFT: i64 = RADROOTS_KIND_LISTING_DRAFT as i64; const KIND_ORDER_REQUEST: i64 = KIND_TRADE_ORDER_REQUEST as i64; const KIND_ORDER_DECISION: i64 = KIND_TRADE_ORDER_RESPONSE as i64; const KIND_ORDER_REVISION: i64 = KIND_TRADE_ORDER_REVISION as i64;