app

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

commit d6a9fb446ca526617c0ddf1764b92bf11a4aeffa
parent 4e001164f4ef8ef0d3920902f18ed81466417c5d
Author: triesap <tyson@radroots.org>
Date:   Tue, 26 May 2026 04:35:02 +0000

sqlite: require usable order request evidence

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mcrates/shared/sqlite/src/local_interop.rs | 224++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 280 insertions(+), 25 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -4565,6 +4565,7 @@ impl DesktopAppRuntimeState { radroots_sdk::trade::RadrootsActiveTradeMessageType::TradeOrderRequested .kind(), )) + || !signed_order_request_evidence_record_is_usable(&record) { continue; } @@ -8328,6 +8329,24 @@ fn order_sync_payload( .to_string() } +fn signed_order_request_evidence_record_is_usable(record: &LocalEventRecord) -> bool { + if record.status != LocalRecordStatus::Published + || matches!( + record.outbox_status, + PublishOutboxStatus::Pending | PublishOutboxStatus::Failed + ) + { + return false; + } + let Some(relay_delivery_json) = record.relay_delivery_json.as_ref() else { + return false; + }; + let Ok(relay_delivery) = RelayDeliveryEvidence::from_json_value(relay_delivery_json) else { + return false; + }; + matches!(relay_delivery.state.as_str(), "acknowledged" | "observed") +} + #[cfg(test)] mod tests { use std::{ @@ -8394,7 +8413,8 @@ mod tests { use radroots_local_events::{ BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, CANONICAL_RELAY_SET_FINGERPRINT_VERSION, LocalEventRecord, LocalEventRecordInput, LocalEventsStore, LocalRecordFamily, - LocalRecordStatus, PublishOutboxStatus, SourceRuntime, canonical_relay_set_fingerprint, + LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime, + canonical_relay_set_fingerprint, }; use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, @@ -8406,7 +8426,7 @@ mod tests { RadrootsTradeOrderDecision, RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, RadrootsTradePricingBasis, }; - use radroots_sql_core::SqliteExecutor; + use radroots_sql_core::{SqlExecutor, SqliteExecutor}; use serde_json::json; use tokio::net::TcpListener; use tokio::sync::oneshot; @@ -13005,6 +13025,28 @@ mod tests { } #[test] + fn runtime_rejects_seller_order_decision_with_unusable_request_evidence() { + let relay = ThreadedAckRelay::spawn(); + let (runtime, paths, order_id, _product_id, _seller_pubkey, _buyer_pubkey) = + seller_order_decision_runtime("seller_order_unusable_request_evidence", 6, 2); + configure_runtime_relay_ingest(&runtime, &relay); + mark_shared_seller_order_request_evidence_pending(&paths); + + let error = runtime + .prepare_order_accept(order_id) + .expect_err("seller order decision should require usable request evidence"); + + assert!(matches!( + error, + AppSqliteError::InvalidProjection { + reason: "seller order decision requires signed order request evidence" + } + )); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] fn runtime_refreshes_configured_relay_before_seller_order_decision_signing() { let relay = ThreadedAckRelay::spawn(); let (runtime, paths, order_id, product_id, seller_pubkey, buyer_pubkey) = @@ -17193,6 +17235,8 @@ mod tests { relay_set_fingerprint: Some("relay-set".to_owned()), relay_delivery_json: Some(json!({ "state": "acknowledged", + "target_relays": ["wss://relay.example"], + "connected_relays": ["wss://relay.example"], "acknowledged_relays": ["wss://relay.example"] })), }) @@ -17268,6 +17312,15 @@ mod tests { .into_wire_parts(); let record_id = format!("app:signed_event:order-request:{trade_order_id}"); let event_id = format!("event-{record_id}"); + let relay_delivery_json = RelayDeliveryEvidence::acknowledged( + ["wss://relay.example"], + ["wss://relay.example"], + ["wss://relay.example"], + Vec::new(), + ) + .expect("acknowledged relay delivery evidence") + .to_json_value() + .expect("acknowledged relay delivery json"); store .append_record(&LocalEventRecordInput { record_id, @@ -17296,14 +17349,30 @@ mod tests { })), outbox_status: PublishOutboxStatus::Acknowledged, relay_set_fingerprint: Some("relay-set".to_owned()), - relay_delivery_json: Some(json!({ - "state": "acknowledged", - "acknowledged_relays": ["wss://relay.example"] - })), + relay_delivery_json: Some(relay_delivery_json), }) .expect("append signed order request"); } + fn mark_shared_seller_order_request_evidence_pending(paths: &AppDesktopRuntimePaths) { + let database_path = paths + .shared_local_events_database_path() + .expect("shared local events path"); + let executor = + SqliteExecutor::open(database_path.as_path()).expect("open shared local events db"); + executor + .exec( + "UPDATE local_event_record + SET status = 'pending_publish', + outbox_status = 'pending', + relay_set_fingerprint = NULL, + relay_delivery_json = NULL + WHERE record_id = 'app:signed_event:order-request:seller-order-decision-1'", + "[]", + ) + .expect("mark shared order request evidence pending"); + } + fn append_unrelated_signed_event_records(paths: &AppDesktopRuntimePaths, count: usize) { let database_path = paths .shared_local_events_database_path() diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs @@ -219,6 +219,9 @@ impl<'a> AppLocalInteropRepository<'a> { "SELECT event_id, event_kind, + local_status, + outbox_status, + relay_delivery_json, event_pubkey, event_created_at, event_tags_json, @@ -226,6 +229,7 @@ impl<'a> AppLocalInteropRepository<'a> { event_sig FROM local_interop_imports WHERE record_family = 'signed_event' + AND local_status = 'published' AND event_kind = ?1 ORDER BY local_seq ASC, record_id ASC", ) @@ -238,11 +242,14 @@ impl<'a> AppLocalInteropRepository<'a> { Ok(StoredLocalInteropSignedEventEvidence { event_id: row.get(0)?, event_kind: row.get(1)?, - event_pubkey: row.get(2)?, - event_created_at: row.get(3)?, - event_tags_json: row.get(4)?, - event_content: row.get(5)?, - event_sig: row.get(6)?, + local_status: row.get(2)?, + outbox_status: row.get(3)?, + relay_delivery_json: row.get(4)?, + event_pubkey: row.get(5)?, + event_created_at: row.get(6)?, + event_tags_json: row.get(7)?, + event_content: row.get(8)?, + event_sig: row.get(9)?, }) }) .map_err(|source| AppSqliteError::Query { @@ -255,6 +262,9 @@ impl<'a> AppLocalInteropRepository<'a> { operation: "read local interop signed event evidence row", source, })?; + if !signed_event_local_interop_evidence_is_usable(&evidence) { + continue; + } if let Some(event) = signed_event_from_local_interop_evidence(&evidence)? { events.push(event); } @@ -1606,6 +1616,9 @@ struct StoredSignedEventDuplicate { struct StoredLocalInteropSignedEventEvidence { event_id: Option<String>, event_kind: Option<i64>, + local_status: String, + outbox_status: String, + relay_delivery_json: Option<String>, event_pubkey: Option<String>, event_created_at: Option<i64>, event_tags_json: Option<String>, @@ -1791,6 +1804,29 @@ fn signed_event_from_record( })) } +fn signed_event_local_interop_evidence_is_usable( + evidence: &StoredLocalInteropSignedEventEvidence, +) -> bool { + if evidence.local_status != LocalRecordStatus::Published.as_str() + || matches!(evidence.outbox_status.as_str(), "pending" | "failed") + { + return false; + } + let Some(relay_delivery_json) = evidence.relay_delivery_json.as_deref() else { + return false; + }; + let Ok(relay_delivery_value) = serde_json::from_str::<Value>(relay_delivery_json) else { + return false; + }; + let Ok(relay_delivery) = RelayDeliveryEvidence::from_json_value(&relay_delivery_value) else { + return false; + }; + matches!( + relay_delivery.state, + RelayDeliveryState::Acknowledged | RelayDeliveryState::Observed + ) +} + fn signed_event_from_local_interop_evidence( evidence: &StoredLocalInteropSignedEventEvidence, ) -> Result<Option<RadrootsNostrEvent>, AppSqliteError> { @@ -1833,15 +1869,11 @@ fn signed_event_from_local_interop_evidence( let Some(tags_json) = evidence.event_tags_json.as_deref() else { return Ok(None); }; - let tags_value = serde_json::from_str::<Value>(tags_json).map_err(|_| { - AppSqliteError::InvalidProjection { - reason: "local interop signed event tags must decode", - } - })?; + let Ok(tags_value) = serde_json::from_str::<Value>(tags_json) else { + return Ok(None); + }; let Some(tags) = tags_from_json(&tags_value) else { - return Err(AppSqliteError::InvalidProjection { - reason: "local interop signed event tags must be an array", - }); + return Ok(None); }; Ok(Some(RadrootsNostrEvent { id: id.to_owned(), @@ -2318,7 +2350,7 @@ mod tests { }; use radroots_local_events::{ LocalEventRecordInput, LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily, - LocalRecordStatus, PublishOutboxStatus, SourceRuntime, + LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime, }; use radroots_sql_core::SqliteExecutor; use rusqlite::params; @@ -2416,7 +2448,9 @@ mod tests { relay_set_fingerprint: Some("relay-set".to_owned()), relay_delivery_json: Some(json!({ "state": "acknowledged", - "acknowledged_relays": ["ws://127.0.0.1:1234/"] + "target_relays": ["ws://127.0.0.1:1234"], + "connected_relays": ["ws://127.0.0.1:1234"], + "acknowledged_relays": ["ws://127.0.0.1:1234"] })), } } @@ -2803,6 +2837,15 @@ mod tests { source_runtime: SourceRuntime, owner_account_id: Option<&str>, ) -> LocalEventRecordInput { + let relay_delivery_json = RelayDeliveryEvidence::acknowledged( + ["ws://127.0.0.1:1234"], + ["ws://127.0.0.1:1234"], + ["ws://127.0.0.1:1234"], + Vec::new(), + ) + .expect("acknowledged relay evidence") + .to_json_value() + .expect("acknowledged relay evidence json"); LocalEventRecordInput { record_id: record_id.to_owned(), family: LocalRecordFamily::SignedEvent, @@ -2833,10 +2876,7 @@ mod tests { })), outbox_status: PublishOutboxStatus::Acknowledged, relay_set_fingerprint: Some("relay-set".to_owned()), - relay_delivery_json: Some(json!({ - "state": "acknowledged", - "acknowledged_relays": ["ws://127.0.0.1:1234/"] - })), + relay_delivery_json: Some(relay_delivery_json), } } @@ -2947,6 +2987,152 @@ mod tests { } #[test] + fn local_interop_order_request_evidence_requires_usable_delivery_state() { + let app_store = + AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); + let events = local_events_store(); + let listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg"; + let buyer_pubkey = "buyer-pubkey"; + let seller_pubkey = "seller-pubkey"; + let relay_url = "ws://127.0.0.1:1234"; + let build_event = |event_id: &str, order_id_raw: &str| { + let payload = + order_request_payload(order_id_raw, listing_addr, buyer_pubkey, seller_pubkey); + let parts = active_trade_order_request_event_build( + &listing_event_ptr("listing-event-1"), + &payload, + ) + .expect("build order request event"); + event_from_parts(event_id, buyer_pubkey, parts) + }; + let acknowledged_event = build_event("order-request-evidence-ack", "usable-ack"); + events + .append_record(&signed_order_event_record( + "cli:signed_event:order-request:evidence-ack", + &acknowledged_event, + listing_addr, + SourceRuntime::Cli, + None, + )) + .expect("append acknowledged order request evidence"); + + let observed_event = build_event("order-request-evidence-observed", "usable-observed"); + let mut observed_record = signed_order_event_record( + "cli:signed_event:order-request:evidence-observed", + &observed_event, + listing_addr, + SourceRuntime::Cli, + None, + ); + observed_record.outbox_status = PublishOutboxStatus::None; + observed_record.relay_delivery_json = Some( + RelayDeliveryEvidence::observed([relay_url], [relay_url], [relay_url], Vec::new()) + .expect("observed relay evidence") + .to_json_value() + .expect("observed relay evidence json"), + ); + events + .append_record(&observed_record) + .expect("append observed order request evidence"); + + let pending_event = build_event("order-request-evidence-pending", "pending"); + let mut pending_record = signed_order_event_record( + "cli:signed_event:order-request:evidence-pending", + &pending_event, + listing_addr, + SourceRuntime::Cli, + None, + ); + pending_record.status = LocalRecordStatus::PendingPublish; + pending_record.outbox_status = PublishOutboxStatus::Pending; + pending_record.relay_delivery_json = Some( + RelayDeliveryEvidence::pending([relay_url]) + .expect("pending relay evidence") + .to_json_value() + .expect("pending relay evidence json"), + ); + events + .append_record(&pending_record) + .expect("append pending order request evidence"); + + let failed_event = build_event("order-request-evidence-failed", "failed"); + let mut failed_record = signed_order_event_record( + "cli:signed_event:order-request:evidence-failed", + &failed_event, + listing_addr, + SourceRuntime::Cli, + None, + ); + failed_record.outbox_status = PublishOutboxStatus::Failed; + failed_record.relay_delivery_json = Some(json!({ + "state": "failed", + "target_relays": [relay_url], + "connected_relays": [relay_url], + "acknowledged_relays": [], + "failed_relays": [{"relay_url": relay_url, "error": "relay rejected event"}] + })); + events + .append_record(&failed_record) + .expect("append failed order request evidence"); + + let local_only_event = build_event("order-request-evidence-local-only", "local-only"); + let mut local_only_record = signed_order_event_record( + "cli:signed_event:order-request:evidence-local-only", + &local_only_event, + listing_addr, + SourceRuntime::Cli, + None, + ); + local_only_record.outbox_status = PublishOutboxStatus::None; + local_only_record.relay_set_fingerprint = None; + local_only_record.relay_delivery_json = None; + events + .append_record(&local_only_record) + .expect("append local-only order request evidence"); + + let malformed_delivery_event = build_event( + "order-request-evidence-malformed-delivery", + "malformed-delivery", + ); + let mut malformed_delivery_record = signed_order_event_record( + "cli:signed_event:order-request:evidence-malformed-delivery", + &malformed_delivery_event, + listing_addr, + SourceRuntime::Cli, + None, + ); + malformed_delivery_record.relay_delivery_json = Some(json!({ + "state": "acknowledged" + })); + events + .append_record(&malformed_delivery_record) + .expect("append malformed delivery order request evidence"); + + let malformed_event = + build_event("order-request-evidence-malformed-event", "malformed-event"); + let mut malformed_record = signed_order_event_record( + "cli:signed_event:order-request:evidence-malformed-event", + &malformed_event, + listing_addr, + SourceRuntime::Cli, + None, + ); + malformed_record.event_tags_json = Some(json!({"invalid": "tags"})); + events + .append_record(&malformed_record) + .expect("append malformed order request evidence"); + + app_store + .import_shared_local_events_from_store(&events) + .expect("import signed evidence records"); + let signed_evidence = app_store + .load_local_interop_signed_events_by_kind(KIND_ORDER_REQUEST) + .expect("load filtered signed event evidence"); + + assert_eq!(signed_evidence, vec![acknowledged_event, observed_event]); + } + + #[test] fn app_origin_signed_order_request_and_decision_project_to_buyer_orders() { let app_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");