app

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

commit 3904feb68bedd027809f3e120f68c0a0a07a9901
parent 0448b20daf02fa8c4e852ccba672540494698817
Author: triesap <tyson@radroots.org>
Date:   Mon, 25 May 2026 04:49:28 +0000

runtime: bind receipts to payload account

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/shared/sync/src/lib.rs | 1+
2 files changed, 163 insertions(+), 16 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -4396,12 +4396,6 @@ impl DesktopAppRuntimeState { source, })?; let timestamp = current_runtime_time_ms()?; - let owner_account_id = self - .state_store - .identity_projection() - .selected_account - .as_ref() - .map(|account| account.account.account_id.clone()); for receipt in receipts { let source_record = receipt @@ -4417,6 +4411,15 @@ impl DesktopAppRuntimeState { }) .transpose()? .flatten(); + if source_record + .as_ref() + .and_then(|record| record.owner_account_id.as_deref()) + .is_some_and(|owner_account_id| owner_account_id != receipt.source_account_id) + { + return Err(AppSqliteError::InvalidProjection { + reason: "published operation source account does not match local event owner", + }); + } let farm_id = source_record .as_ref() .and_then(|record| record.farm_id.clone()) @@ -4432,7 +4435,7 @@ impl DesktopAppRuntimeState { source_runtime: SourceRuntime::App, created_at_ms: i64::from(receipt.event_created_at) * 1_000, inserted_at_ms: timestamp, - owner_account_id: owner_account_id.clone(), + owner_account_id: Some(receipt.source_account_id.clone()), owner_pubkey: Some(receipt.event_pubkey.clone()), farm_id, listing_addr, @@ -5279,10 +5282,19 @@ fn published_operation_receipt( "direct relay app sync received non-relay receipt", )); }; - let source_local_event_id = match payload { - AppPublishPayload::FarmProfile(payload) => payload.context.source_local_event_id.clone(), - AppPublishPayload::Listing(payload) => payload.context.source_local_event_id.clone(), - AppPublishPayload::OrderRequest(payload) => payload.context.source_local_event_id.clone(), + let (source_account_id, source_local_event_id) = match payload { + AppPublishPayload::FarmProfile(payload) => ( + payload.context.account_id.clone(), + payload.context.source_local_event_id.clone(), + ), + AppPublishPayload::Listing(payload) => ( + payload.context.account_id.clone(), + payload.context.source_local_event_id.clone(), + ), + AppPublishPayload::OrderRequest(payload) => ( + payload.context.account_id.clone(), + payload.context.source_local_event_id.clone(), + ), }; let failed_relays = relay_receipt .failed_relays @@ -5306,6 +5318,7 @@ fn published_operation_receipt( Ok(AppPublishedOperationReceipt { operation_key: operation_key.to_owned(), + source_account_id, source_local_event_id, event_id: relay_receipt.event_id, event_kind: relay_receipt.event_kind, @@ -7056,11 +7069,11 @@ mod tests { }; use radroots_app_sync::{ AppFarmProfilePublishPayload, AppOrderRequestPublishPayload, AppPublishContext, - AppPublishPayload, AppSyncRequest, AppSyncResult, AppSyncRunStatus, AppSyncTransport, - AppSyncTransportError, PendingSyncOperation, PendingSyncOperationState, - RecordedAppSyncTransport, SyncAggregateRef, SyncCheckpointState, SyncCheckpointStatus, - SyncConflict, SyncConflictKind, SyncConflictResolutionStatus, SyncConflictSeverity, - SyncOperationKind, SyncTrigger, + AppPublishPayload, AppPublishedOperationReceipt, AppSyncRequest, AppSyncResult, + AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, PendingSyncOperation, + PendingSyncOperationState, RecordedAppSyncTransport, SyncAggregateRef, SyncCheckpointState, + SyncCheckpointStatus, SyncConflict, SyncConflictKind, SyncConflictResolutionStatus, + SyncConflictSeverity, SyncOperationKind, SyncTrigger, }; use radroots_identity::RadrootsIdentity; use radroots_local_events::{ @@ -7246,6 +7259,10 @@ mod tests { assert_eq!(result.published_receipts.len(), 1); assert_eq!(result.published_receipts[0].event_kind, 30340); assert_eq!( + result.published_receipts[0].source_account_id, + account_id.to_string() + ); + assert_eq!( result.published_receipts[0] .source_local_event_id .as_deref(), @@ -8845,6 +8862,104 @@ mod tests { } #[test] + fn runtime_published_receipts_record_payload_account_owner() { + let (runtime, paths) = bootstrapped_runtime("published_receipt_payload_owner"); + assert!( + runtime + .generate_local_account(Some("First".to_owned())) + .expect("first account should generate") + ); + let payload_account_id = runtime + .summary() + .settings_account_projection + .selected_account + .as_ref() + .expect("first selected account") + .account + .account_id + .clone(); + assert!( + runtime + .generate_local_account(Some("Second".to_owned())) + .expect("second account should generate") + ); + let selected_account_id = runtime + .summary() + .settings_account_projection + .selected_account + .as_ref() + .expect("second selected account") + .account + .account_id + .clone(); + assert_ne!(payload_account_id, selected_account_id); + + let receipt = + app_published_operation_receipt(payload_account_id.clone(), None, "event-app-owner"); + runtime + .lock_state() + .record_published_sync_receipts(&[receipt]) + .expect("published receipt should record"); + + let records = shared_local_event_records(&paths); + let signed_record = records + .iter() + .find(|record| record.record_id == "app:signed_event:event-app-owner") + .expect("signed event record"); + assert_eq!( + signed_record.owner_account_id.as_deref(), + Some(payload_account_id.as_str()) + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] + fn runtime_published_receipts_reject_conflicting_source_owner() { + let (runtime, paths) = bootstrapped_runtime("published_receipt_owner_conflict"); + 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"); + let store = LocalEventsStore::new(executor); + store.migrate_up().expect("migrate shared local events"); + store + .append_record(&local_work_record( + "app:local_work:conflict-source", + "other-account", + "farm-key", + None, + json!({"record_kind": "farm_config_v1"}), + )) + .expect("append conflicting source record"); + let receipt = app_published_operation_receipt( + "payload-account".to_owned(), + Some("app:local_work:conflict-source".to_owned()), + "event-app-owner-conflict", + ); + + let error = runtime + .lock_state() + .record_published_sync_receipts(&[receipt]) + .expect_err("conflicting source owner should fail closed"); + + assert!(matches!( + error, + AppSqliteError::InvalidProjection { + reason: "published operation source account does not match local event owner" + } + )); + assert!( + shared_local_event_records(&paths) + .iter() + .all(|record| record.record_id != "app:signed_event:event-app-owner-conflict") + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] fn runtime_app_local_work_without_resolved_pubkey_is_non_exportable() { let (runtime, paths) = bootstrapped_runtime("app_local_work_unresolved_pubkey"); let farm_id = FarmId::new(); @@ -14555,6 +14670,37 @@ mod tests { } } + fn app_published_operation_receipt( + source_account_id: String, + source_local_event_id: Option<String>, + event_id: &str, + ) -> AppPublishedOperationReceipt { + let event_pubkey = "1111111111111111111111111111111111111111111111111111111111111111"; + AppPublishedOperationReceipt { + operation_key: "farm:upsert".to_owned(), + source_account_id, + source_local_event_id, + event_id: event_id.to_owned(), + event_kind: 30340, + event_pubkey: event_pubkey.to_owned(), + event_created_at: 1_774_000_000, + event_tags_json: json!([["d", "farm-key"]]), + event_content: "{}".to_owned(), + event_sig: "signature".to_owned(), + raw_event_json: json!({ + "id": event_id, + "kind": 30340, + "pubkey": event_pubkey, + "content": "{}" + }), + relay_set_fingerprint: "relay-set".to_owned(), + relay_delivery_json: json!({ + "state": "acknowledged", + "acknowledged_relays": ["ws://127.0.0.1:1234/"] + }), + } + } + fn shared_local_event_records(paths: &AppDesktopRuntimePaths) -> Vec<LocalEventRecord> { let database_path = paths .shared_local_events_database_path() diff --git a/crates/shared/sync/src/lib.rs b/crates/shared/sync/src/lib.rs @@ -394,6 +394,7 @@ impl AppSyncResult { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct AppPublishedOperationReceipt { pub operation_key: String, + pub source_account_id: String, pub source_local_event_id: Option<String>, pub event_id: String, pub event_kind: u32,