app

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

commit 368e49bf3cd33e4869e9f01aebd2eb7b1fb87318
parent 8f7e8764fc3696d79fa764bbf8efa1cd8ddbe3c3
Author: triesap <tyson@radroots.org>
Date:   Tue, 26 May 2026 17:43:21 +0000

sqlite: gate network app listing projection reuse

Diffstat:
Mcrates/shared/sqlite/src/local_interop.rs | 436+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 396 insertions(+), 40 deletions(-)

diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs @@ -736,13 +736,21 @@ impl<'a> AppLocalInteropRepository<'a> { .as_deref() .or(signed_farm_pubkey.as_deref()) .or(record.owner_pubkey.as_deref()); - let mut existing_projection = - self.existing_listing_projection(record.listing_addr.as_deref())?; + let app_shaped_network_listing = record.source_runtime == SourceRuntime::Network + && parse_app_d_tag_uuid(farm_key.as_str()).is_some() + && parse_app_d_tag_uuid(listing_key.as_str()).is_some(); + let mut existing_projection = if app_shaped_network_listing { + None + } else { + self.existing_listing_projection(record.listing_addr.as_deref())? + }; if existing_projection.is_none() { existing_projection = self.existing_app_origin_listing_projection( - record.source_runtime, + record, farm_key.as_str(), listing_key.as_str(), + listing_pubkey, + tags, )?; } let (farm_id, product_id) = if let Some(existing_projection) = existing_projection { @@ -1502,11 +1510,13 @@ impl<'a> AppLocalInteropRepository<'a> { fn existing_app_origin_listing_projection( &self, - source_runtime: SourceRuntime, + record: &LocalEventRecord, farm_key: &str, listing_key: &str, + listing_pubkey: Option<&str>, + tags: Option<&Value>, ) -> Result<Option<ExistingListingProjection>, AppSqliteError> { - if source_runtime != SourceRuntime::Network { + if record.source_runtime != SourceRuntime::Network { return Ok(None); } let Some(farm_id) = parse_app_d_tag_uuid(farm_key).map(FarmId::from) else { @@ -1515,30 +1525,77 @@ impl<'a> AppLocalInteropRepository<'a> { let Some(product_id) = parse_app_d_tag_uuid(listing_key).map(ProductId::from) else { return Ok(None); }; - let Some((product_id, farm_id, title, unit_label, listing_bin_id)) = self - .connection - .query_row( - "SELECT id, farm_id, title, unit_label, listing_bin_id - FROM products - WHERE id = ?1 - AND farm_id = ?2 + let Some(listing_addr) = record + .listing_addr + .as_deref() + .map(str::trim) + .filter(|listing_addr| !listing_addr.is_empty()) + else { + return Ok(None); + }; + let Some(listing_addr_parts) = listing_address_parts(listing_addr) else { + return Ok(None); + }; + let Some(event_pubkey) = record + .event_pubkey + .as_deref() + .map(str::trim) + .filter(|event_pubkey| !event_pubkey.is_empty()) + else { + return Ok(None); + }; + if listing_addr_parts.kind != KIND_LISTING + || listing_addr_parts.pubkey != event_pubkey + || listing_addr_parts.d_tag != listing_key + || listing_pubkey.map(str::trim) != Some(event_pubkey) + || !signed_farm_address_matches(tags, farm_key, event_pubkey) + { + return Ok(None); + } + let Some((product_id, farm_id, title, unit_label, listing_bin_id, evidence_farm_key)) = + self.connection + .query_row( + "SELECT + products.id, + products.farm_id, + products.title, + products.unit_label, + products.listing_bin_id, + local_interop_imports.farm_key + FROM local_interop_imports + JOIN products ON products.id = local_interop_imports.projected_id + WHERE local_interop_imports.projected_kind = 'listing' + AND local_interop_imports.projected_id = ?1 + AND local_interop_imports.source_runtime = 'app' + AND local_interop_imports.farm_key = ?2 + AND local_interop_imports.listing_addr = ?3 + AND local_interop_imports.owner_pubkey = ?4 + AND products.id = ?1 + AND products.farm_id = ?5 LIMIT 1", - params![product_id.to_string(), farm_id.to_string()], - |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<String>>(4)?, - )) - }, - ) - .optional() - .map_err(|source| AppSqliteError::Query { - operation: "load existing app-origin listing projection", - source, - })? + params![ + product_id.to_string(), + farm_key, + listing_addr, + event_pubkey, + farm_id.to_string(), + ], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, Option<String>>(4)?, + row.get::<_, Option<String>>(5)?, + )) + }, + ) + .optional() + .map_err(|source| AppSqliteError::Query { + operation: "load existing app-origin listing projection", + source, + })? else { return Ok(None); }; @@ -1556,7 +1613,7 @@ impl<'a> AppLocalInteropRepository<'a> { title, unit_label, listing_bin_id, - farm_key: Some(farm_key.to_owned()), + farm_key: evidence_farm_key, })) } @@ -2418,6 +2475,36 @@ fn tag_index_value(tags: Option<&Value>, tag_name: &str, index: usize) -> Option }) } +fn signed_farm_address_matches(tags: Option<&Value>, farm_key: &str, seller_pubkey: &str) -> bool { + let Some(address) = tag_index_value(tags, "a", 1) else { + return false; + }; + address_d_tag(address.as_str()).as_deref() == Some(farm_key) + && address_pubkey(address.as_str()).as_deref() == Some(seller_pubkey) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct ListingAddressParts<'a> { + kind: i64, + pubkey: &'a str, + d_tag: &'a str, +} + +fn listing_address_parts(address: &str) -> Option<ListingAddressParts<'_>> { + let mut parts = address.trim().split(':'); + let kind = parts.next()?.parse::<i64>().ok()?; + let pubkey = parts.next()?.trim(); + let d_tag = parts.next()?.trim(); + if parts.next().is_some() || pubkey.is_empty() || d_tag.is_empty() { + return None; + } + Some(ListingAddressParts { + kind, + pubkey, + d_tag, + }) +} + fn address_d_tag(address: &str) -> Option<String> { address .rsplit(':') @@ -4110,7 +4197,7 @@ mod tests { } #[test] - fn network_app_origin_listing_reuses_existing_app_product_without_local_interop_import() { + fn network_app_origin_listing_cannot_claim_app_product_without_app_owned_evidence() { let app_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); let events = local_events_store(); @@ -4151,14 +4238,24 @@ mod tests { .connection() .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) .expect("product count"); - let product: (String, String, String, Option<i64>) = app_store + let app_product: (String, Option<i64>) = app_store .connection() .query_row( - "SELECT id, farm_id, title, stock_count FROM products", - [], + "SELECT title, stock_count FROM products WHERE id = ?1", + [product_uuid.to_string()], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .expect("load app product"); + let network_product_id = + deterministic_product_id(Some("app-seller-pubkey"), listing_key.as_str()); + let network_product: (String, String, String, Option<i64>) = app_store + .connection() + .query_row( + "SELECT id, farm_id, title, stock_count FROM products WHERE id = ?1", + [network_product_id.to_string()], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), ) - .expect("load product"); + .expect("load network product"); let buyer_listings = app_store .load_buyer_listings("relay app", &BTreeSet::new()) .expect("buyer listings should load"); @@ -4168,13 +4265,18 @@ mod tests { .expect("network app-origin listing import"); assert_eq!(report.imported_records, 1); - assert_eq!(product_count, 1); - assert_eq!(product.0, product_uuid.to_string()); - assert_eq!(product.1, farm_uuid.to_string()); - assert_eq!(product.2, "Relay App Eggs"); - assert_eq!(product.3, Some(11)); + assert_eq!(product_count, 2); + assert_eq!(app_product.0, "Origin Eggs"); + assert_eq!(app_product.1, Some(3)); + assert_ne!(network_product_id.as_uuid(), product_uuid); + assert_ne!(network_product.1, farm_uuid.to_string()); + assert_eq!(network_product.2, "Relay App Eggs"); + assert_eq!(network_product.3, Some(11)); assert_eq!(buyer_listings.rows.len(), 1); - assert_eq!(buyer_listings.rows[0].product_id.as_uuid(), product_uuid); + assert_eq!( + buyer_listings.rows[0].product_id.as_uuid(), + network_product_id.as_uuid() + ); assert_eq!( listing_import.source_runtime, SourceRuntime::Network.as_str() @@ -4185,11 +4287,265 @@ mod tests { ); assert_eq!( listing_import.projected_id.as_deref(), + Some(network_product_id.to_string().as_str()) + ); + } + + #[test] + fn network_app_origin_listing_reuses_app_product_with_matching_app_owned_evidence() { + let app_store = + AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); + let events = local_events_store(); + let farm_uuid = Uuid::from_u128(0x79797979797949799797979797979797); + let product_uuid = Uuid::from_u128(0x89898989898949898989898989898989); + let farm_key = app_d_tag_from_uuid(farm_uuid); + let listing_key = app_d_tag_from_uuid(product_uuid); + let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); + let app_farm_record = app_local_work_record( + "app:local_work:farm:network-claim-gate", + farm_key.as_str(), + json!({ + "record_kind": "farm_config_v1", + "document": { + "selection": { + "account": "seller-account", + "farm_d_tag": farm_key + }, + "profile": { + "display_name": "App Farm" + }, + "farm": { + "d_tag": farm_key, + "name": "App Farm" + } + } + }), + ); + let mut app_listing_record = app_local_work_record( + "app:local_work:listing:network-claim-gate", + farm_key.as_str(), + json!({ + "record_kind": "listing_draft_v1", + "document": { + "listing": { + "d_tag": listing_key, + "farm_d_tag": farm_key + }, + "seller_actor": { + "account_id": "seller-account", + "pubkey": "app-seller-pubkey" + }, + "product": { + "key": listing_key, + "title": "App Draft Eggs", + "summary": "Fresh app-origin eggs" + }, + "primary_bin": { + "quantity_unit": "each", + "price_amount": "7", + "price_currency": "USD" + }, + "inventory": { + "available": "12" + } + } + }), + ); + app_listing_record.listing_addr = Some(listing_addr.clone()); + events + .append_record(&app_farm_record) + .expect("append app farm local work"); + events + .append_record(&app_listing_record) + .expect("append app listing local work"); + app_store + .import_shared_local_events_from_store(&events) + .expect("import app local work"); + let mut network_listing = signed_market_listing_record( + "network-app-origin-matching-evidence", + "app-seller-pubkey", + farm_key.as_str(), + listing_key.as_str(), + "Relay App Eggs", + "11", + "active", + "pickup", + "App farmstand pickup", + 4_102_444_800, + 4_102_531_200, + LocalRecordStatus::Published, + PublishOutboxStatus::Acknowledged, + ); + network_listing.source_runtime = SourceRuntime::Network; + network_listing.owner_account_id = None; + events + .append_record(&network_listing) + .expect("append network app-origin listing"); + + let report = app_store + .import_shared_local_events_from_store(&events) + .expect("import network app-origin listing"); + let product_count: i64 = app_store + .connection() + .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .expect("product count"); + let product: (String, String, String, Option<i64>) = app_store + .connection() + .query_row( + "SELECT id, farm_id, title, stock_count FROM products", + [], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), + ) + .expect("load product"); + let imported = app_store + .load_local_interop_records() + .expect("load imported records"); + let listing_import = imported + .iter() + .find(|record| record.record_id == "network-app-origin-matching-evidence") + .expect("network app-origin listing import"); + + assert_eq!(report.imported_records, 1); + assert_eq!(product_count, 1); + assert_eq!(product.0, product_uuid.to_string()); + assert_eq!(product.1, farm_uuid.to_string()); + assert_eq!(product.2, "Relay App Eggs"); + assert_eq!(product.3, Some(11)); + assert_eq!( + listing_import.source_runtime, + SourceRuntime::Network.as_str() + ); + assert_eq!( + listing_import.projected_id.as_deref(), Some(product_uuid.to_string().as_str()) ); } #[test] + fn network_app_origin_listing_requires_matching_event_pubkey_for_app_product_reuse() { + let app_store = + AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); + let events = local_events_store(); + let farm_uuid = Uuid::from_u128(0x7a7a7a7a7a7a4a7a9a7a7a7a7a7a7a7a); + let product_uuid = Uuid::from_u128(0x8a8a8a8a8a8a4a8aaa8a8a8a8a8a8a8a); + let farm_key = app_d_tag_from_uuid(farm_uuid); + let listing_key = app_d_tag_from_uuid(product_uuid); + let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); + let app_farm_record = app_local_work_record( + "app:local_work:farm:network-foreign-claim", + farm_key.as_str(), + json!({ + "record_kind": "farm_config_v1", + "document": { + "selection": { + "account": "seller-account", + "farm_d_tag": farm_key + }, + "profile": { + "display_name": "App Farm" + }, + "farm": { + "d_tag": farm_key, + "name": "App Farm" + } + } + }), + ); + let mut app_listing_record = app_local_work_record( + "app:local_work:listing:network-foreign-claim", + farm_key.as_str(), + json!({ + "record_kind": "listing_draft_v1", + "document": { + "listing": { + "d_tag": listing_key, + "farm_d_tag": farm_key + }, + "seller_actor": { + "account_id": "seller-account", + "pubkey": "app-seller-pubkey" + }, + "product": { + "key": listing_key, + "title": "App Draft Eggs", + "summary": "Fresh app-origin eggs" + }, + "primary_bin": { + "quantity_unit": "each", + "price_amount": "7", + "price_currency": "USD" + }, + "inventory": { + "available": "12" + } + } + }), + ); + app_listing_record.listing_addr = Some(listing_addr.clone()); + events + .append_record(&app_farm_record) + .expect("append app farm local work"); + events + .append_record(&app_listing_record) + .expect("append app listing local work"); + app_store + .import_shared_local_events_from_store(&events) + .expect("import app local work"); + let mut network_listing = signed_market_listing_record( + "network-app-origin-foreign-event-pubkey", + "app-seller-pubkey", + farm_key.as_str(), + listing_key.as_str(), + "Foreign Relay App Eggs", + "11", + "active", + "pickup", + "App farmstand pickup", + 4_102_444_800, + 4_102_531_200, + LocalRecordStatus::Published, + PublishOutboxStatus::Acknowledged, + ); + network_listing.source_runtime = SourceRuntime::Network; + network_listing.owner_account_id = None; + network_listing.event_pubkey = Some("foreign-seller-pubkey".to_owned()); + events + .append_record(&network_listing) + .expect("append foreign network app-origin listing"); + + app_store + .import_shared_local_events_from_store(&events) + .expect("import network app-origin listing"); + let product_count: i64 = app_store + .connection() + .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .expect("product count"); + let app_product: (String, Option<i64>) = app_store + .connection() + .query_row( + "SELECT title, stock_count FROM products WHERE id = ?1", + [product_uuid.to_string()], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .expect("load app product"); + let foreign_product_id = + deterministic_product_id(Some("foreign-seller-pubkey"), listing_key.as_str()); + let foreign_product_count: i64 = app_store + .connection() + .query_row( + "SELECT COUNT(*) FROM products WHERE id = ?1", + [foreign_product_id.to_string()], + |row| row.get(0), + ) + .expect("foreign product count"); + + assert_eq!(product_count, 2); + assert_eq!(app_product.0, "App Draft Eggs"); + assert_eq!(app_product.1, Some(12)); + assert_eq!(foreign_product_count, 1); + } + + #[test] fn app_signed_duplicate_replaces_network_listing_product_projection() { let app_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");