commit 9b99c76042412f066c96e70e3a0744ad3eb53f1d
parent 30bfac2ff8dfe5604bd1b1b30912ba29e2356419
Author: triesap <tyson@radroots.org>
Date: Sun, 5 Jul 2026 02:54:36 +0000
runtime: keep trade projections root aware
Diffstat:
3 files changed, 319 insertions(+), 39 deletions(-)
diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs
@@ -9534,6 +9534,7 @@ fn insert_seller_order_request_evidence(
let app_order_id = projected_order_id_from_trade_request(
payload.order_id.as_str(),
payload.buyer_pubkey.as_str(),
+ event.id.as_str(),
);
if app_order_id != *order_id {
return;
@@ -19840,8 +19841,6 @@ mod tests {
let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
let listing_event_id = signed_listing_event_id(label);
let trade_order_id = format!("{label}-trade-order");
- let order_id =
- projected_order_id_from_trade_request(trade_order_id.as_str(), buyer_pubkey.as_str());
append_app_signed_listing_record(
&paths,
"linked-seller-account",
@@ -19860,6 +19859,11 @@ mod tests {
seller_pubkey,
2,
);
+ let order_id = projected_order_id_from_trade_request(
+ trade_order_id.as_str(),
+ buyer_pubkey.as_str(),
+ request_event_id.as_str(),
+ );
let decision_event_id = if append_decision {
append_signed_order_decision_record(
&paths,
@@ -19914,7 +19918,6 @@ mod tests {
let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned();
let product_id = ProductId::new();
let trade_order_id = "seller-order-decision-1";
- let order_id = projected_order_id_from_trade_request(trade_order_id, buyer_pubkey.as_str());
let farm_key = super::d_tag_from_uuid(farm_id.as_uuid());
let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
@@ -19928,7 +19931,7 @@ mod tests {
listing_event_id.as_str(),
stock_count,
);
- append_signed_order_request_record(
+ let request_event_id = append_signed_order_request_record(
&paths,
trade_order_id,
listing_addr.as_str(),
@@ -19937,6 +19940,11 @@ mod tests {
seller_pubkey.as_str(),
order_quantity,
);
+ let order_id = projected_order_id_from_trade_request(
+ trade_order_id,
+ buyer_pubkey.as_str(),
+ request_event_id.as_str(),
+ );
(
runtime,
@@ -19976,7 +19984,6 @@ mod tests {
let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned();
let product_id = ProductId::new();
let trade_order_id = "seller-order-decision-1";
- let order_id = projected_order_id_from_trade_request(trade_order_id, buyer_pubkey.as_str());
let farm_key = super::d_tag_from_uuid(farm_id.as_uuid());
let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
@@ -19990,7 +19997,7 @@ mod tests {
listing_event_id.as_str(),
stock_count,
);
- append_verified_signed_order_request_record(
+ let request_event_id = append_verified_signed_order_request_record(
&paths,
trade_order_id,
listing_addr.as_str(),
@@ -19999,6 +20006,11 @@ mod tests {
seller_pubkey.as_str(),
order_quantity,
);
+ let order_id = projected_order_id_from_trade_request(
+ trade_order_id,
+ buyer_pubkey.as_str(),
+ request_event_id.as_str(),
+ );
(
runtime,
@@ -20294,7 +20306,7 @@ mod tests {
buyer_pubkey: &str,
seller_pubkey: &str,
order_quantity: u32,
- ) {
+ ) -> String {
assert_eq!(buyer_pubkey, SDK_TEST_BUYER_PUBLIC_KEY_HEX);
let database_path = paths
.shared_local_events_database_path()
@@ -20334,6 +20346,7 @@ mod tests {
.sign_with_keys(&keys)
.expect("order request event should sign");
let event = radroots_event_from_nostr(&signed_event);
+ let stored_event_id = event.id.clone();
let record_id = format!("app:signed_event:order-request:{trade_order_id}");
let relay_delivery_json = RelayDeliveryEvidence::acknowledged(
["wss://relay.example"],
@@ -20372,6 +20385,7 @@ mod tests {
relay_delivery_json: Some(relay_delivery_json),
})
.expect("append verified signed order request");
+ stored_event_id
}
fn signed_order_request_economics(
diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs
@@ -1609,6 +1609,133 @@ fn app_sdk_trade_propose_request_stays_product_shaped() {
}
#[test]
+fn app_sdk_trade_mutation_requests_stay_locator_and_resync_owned() {
+ let source = read_source_path(app_root().join("crates/runtime/src/sdk.rs").as_path());
+
+ for request_struct in [
+ "AppSdkTradeProposeRequest",
+ "AppSdkTradeDecisionRequest",
+ "AppSdkTradeRevisionProposalRequest",
+ "AppSdkTradeRevisionDecisionRequest",
+ "AppSdkTradeCancellationRequest",
+ ] {
+ let request = struct_block(source.as_str(), request_struct);
+ assert!(
+ !request.contains("evidence_events"),
+ "{request_struct} must not expose caller-owned evidence events"
+ );
+ }
+
+ for request_struct in [
+ "AppSdkTradeDecisionRequest",
+ "AppSdkTradeRevisionProposalRequest",
+ "AppSdkTradeRevisionDecisionRequest",
+ "AppSdkTradeCancellationRequest",
+ ] {
+ let request = struct_block(source.as_str(), request_struct);
+ assert!(
+ request.contains("pub locator: RadrootsTradeLocator"),
+ "{request_struct} must require a root-capable SDK trade locator"
+ );
+ }
+
+ assert!(source.contains("sdk.trades().buyer().propose_trade(sdk_request)"));
+ for (label, start, end, required_count) in [
+ (
+ "trade decision",
+ "fn trade_decision_with_sdk(",
+ "fn trade_revision_propose_with_sdk(",
+ 2usize,
+ ),
+ (
+ "trade revision proposal",
+ "fn trade_revision_propose_with_sdk(",
+ "fn trade_revision_decide_with_sdk(",
+ 1usize,
+ ),
+ (
+ "trade revision decision",
+ "fn trade_revision_decide_with_sdk(",
+ "fn trade_cancel_with_sdk(",
+ 1usize,
+ ),
+ (
+ "trade cancellation",
+ "fn trade_cancel_with_sdk(",
+ "fn sdk_actor_context(",
+ 1usize,
+ ),
+ ] {
+ let segment = source_segment(source.as_str(), start, end);
+ assert!(
+ segment
+ .matches("TradeEvidenceMode::ResyncBeforeMutation")
+ .count()
+ >= required_count,
+ "{label} must use SDK resync evidence mode before mutation"
+ );
+ assert!(
+ !segment.contains("TradeEvidenceMode::LocalOnly"),
+ "{label} must not use local-only evidence for app mutation authority"
+ );
+ }
+}
+
+#[test]
+fn app_store_active_order_projection_is_root_aware_and_non_authoritative() {
+ let source = read_source_path(app_root().join("crates/store/src/interop.rs").as_path());
+ let project_active_order = source_segment(
+ source.as_str(),
+ "fn project_active_order(",
+ "fn upsert_order_request(",
+ );
+ assert!(project_active_order.contains("current_evidence.root_event_id()"));
+ assert!(project_active_order.contains("root_event_id.as_str()"));
+ assert!(!project_active_order.contains("current_evidence.order_id()"));
+
+ let load_active_order_evidence = source_segment(
+ source.as_str(),
+ "fn load_active_order_evidence(",
+ "fn replace_order_request_lines(",
+ );
+ assert!(load_active_order_evidence.contains("record.root_event_id() == root_event_id"));
+ assert!(!load_active_order_evidence.contains("record.order_id()"));
+
+ let active_evidence_identity = source_segment(
+ source.as_str(),
+ "fn root_event_id(&self)",
+ "struct ActiveOrderEvidenceBuckets",
+ );
+ assert!(active_evidence_identity.contains("Self::Request(record) => record.event_id.as_str()"));
+ assert!(
+ active_evidence_identity
+ .contains("Self::Decision(record) => record.root_event_id.as_str()")
+ );
+
+ let projected_order_id = source_segment(
+ source.as_str(),
+ "pub fn projected_order_id_from_trade_request(",
+ "fn active_order_revision_status(",
+ );
+ assert!(projected_order_id.contains("root_event_id: &str"));
+ assert!(projected_order_id.contains("radroots-cli-order-root"));
+
+ for forbidden in [
+ "TradeAcceptRequest::new",
+ "TradeDeclineRequest::new",
+ "TradeRevisionProposalRequest::new",
+ "TradeRevisionDecisionRequest::new",
+ "TradeCancelRequest::new",
+ "TradeEvidenceMode::",
+ ] {
+ assert!(
+ !source.contains(forbidden),
+ "store projection must not become SDK mutation authority through `{forbidden}`"
+ );
+ }
+}
+
+#[test]
fn app_production_sources_do_not_suppress_dead_code() {
let forbidden = ["#[allow(", "dead_code", ")]"].concat();
@@ -2395,6 +2522,17 @@ fn struct_block<'source>(source: &'source str, struct_name: &str) -> &'source st
panic!("struct `{struct_name}` body is not closed");
}
+fn source_segment<'source>(source: &'source str, start: &str, end: &str) -> &'source str {
+ let start_index = source
+ .find(start)
+ .unwrap_or_else(|| panic!("missing source segment start `{start}`"));
+ let end_index = source[start_index..]
+ .find(end)
+ .map(|index| start_index + index)
+ .unwrap_or_else(|| panic!("missing source segment end `{end}`"));
+ &source[start_index..end_index]
+}
+
fn app_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs
@@ -1151,17 +1151,18 @@ impl<'a> AppLocalInteropRepository<'a> {
current_evidence: ActiveOrderEvidence,
) -> Result<(), AppSqliteError> {
if let ActiveOrderEvidence::Request(request) = ¤t_evidence {
- let order_id = self.upsert_order_request(record, &request.payload)?;
+ let order_id =
+ self.upsert_order_request(record, &request.payload, request.event_id.as_str())?;
self.attach_validation_receipts_for_request(
request.event_id.as_str(),
request.payload.order_id.as_str(),
order_id,
)?;
}
- let mut evidence = self.load_active_order_evidence(current_evidence.order_id())?;
+ let mut evidence = self.load_active_order_evidence(current_evidence.root_event_id())?;
evidence.push(current_evidence);
dedupe_active_order_evidence(&mut evidence);
- let Some((raw_order_id, buyer_pubkey)) = evidence
+ let Some((raw_order_id, buyer_pubkey, root_event_id)) = evidence
.first()
.map(ActiveOrderEvidence::order_projection_identity)
else {
@@ -1169,7 +1170,12 @@ impl<'a> AppLocalInteropRepository<'a> {
};
let raw_order_id = raw_order_id.to_owned();
let buyer_pubkey = buyer_pubkey.to_owned();
- let order_id = projected_order_id(raw_order_id.as_str(), buyer_pubkey.as_str());
+ let root_event_id = root_event_id.to_owned();
+ let order_id = projected_order_id(
+ raw_order_id.as_str(),
+ buyer_pubkey.as_str(),
+ root_event_id.as_str(),
+ );
let buckets = ActiveOrderEvidenceBuckets::from_evidence(evidence);
let requests = buckets.requests.clone();
let revision_proposals = buckets.revision_proposals.clone();
@@ -1219,6 +1225,7 @@ impl<'a> AppLocalInteropRepository<'a> {
&self,
record: &LocalEventRecord,
payload: &RadrootsOrderRequest,
+ root_event_id: &str,
) -> Result<OrderId, AppSqliteError> {
let existing_listing =
self.existing_listing_projection(Some(payload.listing_addr.as_str()))?;
@@ -1231,7 +1238,11 @@ impl<'a> AppLocalInteropRepository<'a> {
)
};
self.ensure_farm_exists(farm_id)?;
- let order_id = projected_order_id(payload.order_id.as_str(), payload.buyer_pubkey.as_str());
+ let order_id = projected_order_id(
+ payload.order_id.as_str(),
+ payload.buyer_pubkey.as_str(),
+ root_event_id,
+ );
let order_number = existing_order_number(self.connection, order_id)?
.unwrap_or_else(|| deterministic_order_number(payload.order_id.as_str()));
self.connection
@@ -1415,6 +1426,7 @@ impl<'a> AppLocalInteropRepository<'a> {
projected_order_id(
envelope.payload.order_id.as_str(),
envelope.payload.buyer_pubkey.as_str(),
+ tags.root_event_id.as_str(),
),
))
}
@@ -1546,7 +1558,7 @@ impl<'a> AppLocalInteropRepository<'a> {
fn load_active_order_evidence(
&self,
- order_id: &str,
+ root_event_id: &str,
) -> Result<Vec<ActiveOrderEvidence>, AppSqliteError> {
let mut evidence = Vec::new();
for kind in ACTIVE_ORDER_EVENT_KINDS {
@@ -1554,7 +1566,7 @@ impl<'a> AppLocalInteropRepository<'a> {
let Some(record) = active_order_evidence_from_event(&event) else {
continue;
};
- if record.order_id() == order_id {
+ if record.root_event_id() == root_event_id {
evidence.push(record);
}
}
@@ -2802,37 +2814,42 @@ impl ActiveOrderEvidence {
}
}
- fn order_id(&self) -> &str {
+ fn root_event_id(&self) -> &str {
match self {
- Self::Request(record) => record.payload.order_id.as_str(),
- Self::Decision(record) => record.payload.order_id.as_str(),
- Self::RevisionProposal(record) => record.payload.order_id.as_str(),
- Self::RevisionDecision(record) => record.payload.order_id.as_str(),
- Self::Cancellation(record) => record.payload.order_id.as_str(),
+ Self::Request(record) => record.event_id.as_str(),
+ Self::Decision(record) => record.root_event_id.as_str(),
+ Self::RevisionProposal(record) => record.root_event_id.as_str(),
+ Self::RevisionDecision(record) => record.root_event_id.as_str(),
+ Self::Cancellation(record) => record.root_event_id.as_str(),
}
}
- fn order_projection_identity(&self) -> (&str, &str) {
+ fn order_projection_identity(&self) -> (&str, &str, &str) {
match self {
Self::Request(record) => (
record.payload.order_id.as_str(),
record.payload.buyer_pubkey.as_str(),
+ record.event_id.as_str(),
),
Self::Decision(record) => (
record.payload.order_id.as_str(),
record.payload.buyer_pubkey.as_str(),
+ record.root_event_id.as_str(),
),
Self::RevisionProposal(record) => (
record.payload.order_id.as_str(),
record.payload.buyer_pubkey.as_str(),
+ record.root_event_id.as_str(),
),
Self::RevisionDecision(record) => (
record.payload.order_id.as_str(),
record.payload.buyer_pubkey.as_str(),
+ record.root_event_id.as_str(),
),
Self::Cancellation(record) => (
record.payload.order_id.as_str(),
record.payload.buyer_pubkey.as_str(),
+ record.root_event_id.as_str(),
),
}
}
@@ -3271,18 +3288,20 @@ fn tags_from_json(value: &Value) -> Option<Vec<Vec<String>>> {
})
}
-pub fn projected_order_id_from_trade_request(order_id: &str, buyer_pubkey: &str) -> OrderId {
- order_id.parse().unwrap_or_else(|_| {
- OrderId::from(deterministic_uuid(
- "radroots-cli-order",
- Some(buyer_pubkey),
- order_id,
- ))
- })
+pub fn projected_order_id_from_trade_request(
+ order_id: &str,
+ buyer_pubkey: &str,
+ root_event_id: &str,
+) -> OrderId {
+ OrderId::from(deterministic_uuid(
+ "radroots-cli-order-root",
+ Some(buyer_pubkey),
+ format!("{}:{}", root_event_id.trim(), order_id.trim()).as_str(),
+ ))
}
-fn projected_order_id(order_id: &str, buyer_pubkey: &str) -> OrderId {
- projected_order_id_from_trade_request(order_id, buyer_pubkey)
+fn projected_order_id(order_id: &str, buyer_pubkey: &str, root_event_id: &str) -> OrderId {
+ projected_order_id_from_trade_request(order_id, buyer_pubkey, root_event_id)
}
fn active_order_revision_status(
@@ -4646,7 +4665,11 @@ mod tests {
events,
buyer_context: BuyerContext::account("acct_validation"),
seller_farm_id: deterministic_farm_id(Some(seller_pubkey.as_str()), farm_key),
- order_id: projected_order_id(order_id_raw.as_str(), buyer_pubkey.as_str()),
+ order_id: projected_order_id(
+ order_id_raw.as_str(),
+ buyer_pubkey.as_str(),
+ request_event_id.as_str(),
+ ),
order_id_raw,
listing_addr,
buyer_pubkey,
@@ -4856,7 +4879,7 @@ mod tests {
.import_shared_local_events_from_store(&events)
.expect("import signed order request");
let farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
- let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey, event.id.as_str());
let orders = app_store
.load_orders_list(
farm_id,
@@ -5116,7 +5139,7 @@ mod tests {
.import_shared_local_events_from_store(&events)
.expect("import app order request");
let buyer_context = BuyerContext::account("acct_buyer");
- let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id.as_str());
let farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
let buyer_orders = app_store
.load_buyer_orders(&buyer_context)
@@ -5208,6 +5231,110 @@ mod tests {
}
#[test]
+ fn duplicate_app_order_request_roots_project_to_distinct_buyer_orders() {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ let farm_key = "CCCCCCCCCCCCCCCCCCCCCC";
+ let listing_key = "AAAAAAAAAAAAAAAAAAAAAy";
+ let seller_pubkey = test_pubkey("duplicate-root-seller-pubkey");
+ let seller_pubkey = seller_pubkey.as_str();
+ let buyer_pubkey = test_pubkey("duplicate-root-buyer-pubkey");
+ let buyer_pubkey = buyer_pubkey.as_str();
+ let order_id_raw = "duplicate-root-order";
+ let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
+ events
+ .append_record(&signed_market_listing_record(
+ "duplicate-root-listing",
+ seller_pubkey,
+ farm_key,
+ listing_key,
+ "Duplicate Root Eggs",
+ "9",
+ "active",
+ "pickup",
+ "North barn pickup",
+ 4_102_444_800,
+ 4_102_531_200,
+ LocalRecordStatus::Published,
+ PublishOutboxStatus::Acknowledged,
+ ))
+ .expect("append duplicate-root listing");
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import duplicate-root listing");
+
+ let request_payload = order_request_payload(
+ order_id_raw,
+ listing_addr.as_str(),
+ buyer_pubkey,
+ seller_pubkey,
+ );
+ let first_request_parts = order_request_event_build(
+ &listing_event_ptr("duplicate-root-listing-event"),
+ &request_payload,
+ )
+ .expect("build duplicate-root order request");
+ let first_request_event = event_from_parts(
+ "duplicate-root-request-first",
+ buyer_pubkey,
+ first_request_parts,
+ );
+ let second_request_parts = order_request_event_build(
+ &listing_event_ptr("duplicate-root-listing-event"),
+ &request_payload,
+ )
+ .expect("build second duplicate-root order request");
+ let second_request_event = event_from_parts(
+ "duplicate-root-request-second",
+ buyer_pubkey,
+ second_request_parts,
+ );
+ for (record_id, event) in [
+ (
+ "app:signed_event:duplicate-root:first",
+ &first_request_event,
+ ),
+ (
+ "app:signed_event:duplicate-root:second",
+ &second_request_event,
+ ),
+ ] {
+ events
+ .append_record(&signed_order_event_record(
+ record_id,
+ event,
+ listing_addr.as_str(),
+ SourceRuntime::App,
+ Some("acct_duplicate_root"),
+ ))
+ .expect("append duplicate-root request");
+ }
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import duplicate-root requests");
+
+ let first_order_id =
+ projected_order_id(order_id_raw, buyer_pubkey, first_request_event.id.as_str());
+ let second_order_id =
+ projected_order_id(order_id_raw, buyer_pubkey, second_request_event.id.as_str());
+ let buyer_context = BuyerContext::account("acct_duplicate_root");
+ let buyer_orders = app_store
+ .load_buyer_orders(&buyer_context)
+ .expect("load duplicate-root buyer orders");
+ let projected_ids = buyer_orders
+ .rows
+ .iter()
+ .map(|row| row.order_id)
+ .collect::<Vec<_>>();
+
+ assert_ne!(first_order_id, second_order_id);
+ assert_eq!(buyer_orders.rows.len(), 2);
+ assert!(projected_ids.contains(&first_order_id));
+ assert!(projected_ids.contains(&second_order_id));
+ }
+
+ #[test]
fn app_origin_signed_order_request_and_decline_project_to_buyer_orders() {
let app_store =
AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
@@ -5270,7 +5397,7 @@ mod tests {
.import_shared_local_events_from_store(&events)
.expect("import app order request");
let buyer_context = BuyerContext::account("acct_buyer");
- let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id.as_str());
let buyer_orders = app_store
.load_buyer_orders(&buyer_context)
.expect("load buyer orders after request");
@@ -5764,7 +5891,7 @@ mod tests {
.import_shared_local_events_from_store(&events)
.expect("import request after receipt");
- let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id.as_str());
let buyer_context = BuyerContext::account("acct_validation_out_of_order");
let buyer_detail = app_store
.load_buyer_order_detail(&buyer_context, order_id)
@@ -5979,7 +6106,7 @@ mod tests {
.expect("import revision proposal");
let seller_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
- let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id.as_str());
let buyer_context = BuyerContext::account("acct_revision");
let seller_orders = app_store
.load_orders_list(
@@ -6160,7 +6287,7 @@ mod tests {
.expect("import cancellation");
let seller_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
- let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id.as_str());
let buyer_context = BuyerContext::account("acct_cancel");
let buyer_detail = app_store
.load_buyer_order_detail(&buyer_context, order_id)
@@ -6301,7 +6428,8 @@ mod tests {
app_store
.import_shared_local_events_from_store(&events)
.expect("import conflicting decisions");
- let order_id = projected_order_id(order_id_raw, buyer_pubkey);
+ let order_id =
+ projected_order_id(order_id_raw, buyer_pubkey, request_event.id.as_str());
let detail = app_store
.load_order_detail(
deterministic_farm_id(Some(seller_pubkey), farm_key),