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:
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;