app

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

commit 908de111a715b71161b5fb8d4a4c3d36be5b8b79
parent 5c695d9197f36945772e05c820085671b61a24a4
Author: triesap <tyson@radroots.org>
Date:   Thu, 18 Jun 2026 23:59:01 -0700

app: route order lifecycle through sdk

- add app SDK runtime commands for revision, cancellation, fulfillment, and receipt writes
- feed request and lifecycle evidence into SDK order enqueue calls before publishing follow-on events
- record migration receipts for lifecycle writes and refresh projections through the SDK-backed path
- narrow sync metadata and source guards so lifecycle direct publish calls cannot re-enter production paths

Diffstat:
Mcrates/desktop/src/runtime.rs | 618++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mcrates/desktop/src/source_guards.rs | 92++++++++++---------------------------------------------------------------------
Mcrates/runtime/src/lib.rs | 18++++++++++--------
Mcrates/runtime/src/sdk.rs | 529+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/sync/src/publish.rs | 37++++++++++++++++++++++---------------
5 files changed, 1023 insertions(+), 271 deletions(-)

diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -9,11 +9,13 @@ use chrono::{DateTime, Duration, Utc}; use radroots_app_core::{ AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode, AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics, - AppSdkFarmPublishRequest, AppSdkLifecycleState, AppSdkOrderDecisionRequest, - AppSdkOrderSubmitRequest, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy, AppSdkRuntime, - AppSdkRuntimeError, AppSdkRuntimeIssue, AppSdkRuntimeStatus, AppSdkStoragePaths, - AppSdkWorkflowReceipt, AppSharedAccountsPaths, PackDayExportWriteError, - prepare_pack_day_export_bundle_at_data_root, + AppSdkFarmPublishRequest, AppSdkLifecycleState, AppSdkOrderCancellationRequest, + AppSdkOrderDecisionRequest, AppSdkOrderFulfillmentUpdateRequest, + AppSdkOrderReceiptRecordRequest, AppSdkOrderRevisionDecisionRequest, + AppSdkOrderRevisionProposalRequest, AppSdkOrderSubmitRequest, AppSdkProjectionLifecycleState, + AppSdkRelayUrlPolicy, AppSdkRuntime, AppSdkRuntimeError, AppSdkRuntimeIssue, + AppSdkRuntimeStatus, AppSdkStoragePaths, AppSdkWorkflowReceipt, AppSharedAccountsPaths, + PackDayExportWriteError, prepare_pack_day_export_bundle_at_data_root, shared_local_events_database_path_from_shared_accounts, write_prepared_pack_day_export_bundle, }; use radroots_app_remote_signer::{ @@ -117,9 +119,11 @@ use radroots_sdk::protocol::order::{ RadrootsOrderRevisionProposal, }; use radroots_sdk::{ - FARM_PUBLISH_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND, - RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, SdkPublishReceipt, - SdkTransportMode, SdkTransportReceipt, SignerConfig, + FARM_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, + ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND, + ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, + ORDER_SUBMIT_OPERATION_KIND, RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, + SdkPublishReceipt, SdkTransportMode, SdkTransportReceipt, SignerConfig, }; use radroots_sql_core::SqliteExecutor; use radroots_trade::listing::parse_public_listing_address; @@ -258,6 +262,7 @@ struct ResolvedAppOrderFulfillmentEvidence { #[derive(Clone, Debug, Eq, PartialEq)] struct ResolvedAppOrderLifecycleEvidence { + evidence_events: Vec<SdkRadrootsNostrEvent>, status: RadrootsOrderStatus, payment_state: RadrootsOrderPaymentState, agreement_event_id: Option<String>, @@ -2954,15 +2959,14 @@ impl DesktopAppRuntimeState { status: RadrootsOrderFulfillmentState, ) -> Result<bool, AppSqliteError> { let payload = self.prepare_seller_order_fulfillment(order_id, status)?; - let operation = PendingSyncOperation::from_publish_payload( - AppPublishPayload::OrderFulfillment(payload), - current_utc_timestamp(), - ) - .map_err(|_| AppSqliteError::InvalidProjection { - reason: "seller order fulfillment publish payload must serialize", - })?; - let _ = self.enqueue_selected_account_sync_operation_once(operation)?; - self.attempt_sync(SyncTrigger::ManualRefresh) + let source_record_id = order_fulfillment_sdk_source_record_id(&payload); + self.enqueue_order_fulfillment_payload_via_sdk( + &payload, + AppSdkMigrationReceiptSourceKind::LocalOutbox, + source_record_id.as_str(), + )?; + let _ = self.refresh_selected_account_sync()?; + Ok(true) } fn prepare_seller_order_revision_proposal( @@ -3105,15 +3109,14 @@ impl DesktopAppRuntimeState { ) -> Result<bool, AppSqliteError> { let payload = self.prepare_seller_order_revision_proposal(order_id, items, economics, reason)?; - let operation = PendingSyncOperation::from_publish_payload( - AppPublishPayload::OrderRevisionProposal(payload), - current_utc_timestamp(), - ) - .map_err(|_| AppSqliteError::InvalidProjection { - reason: "seller order revision publish payload must serialize", - })?; - let _ = self.enqueue_selected_account_sync_operation_once(operation)?; - self.attempt_sync(SyncTrigger::ManualRefresh) + let source_record_id = order_revision_proposal_sdk_source_record_id(&payload); + self.enqueue_order_revision_proposal_payload_via_sdk( + &payload, + AppSdkMigrationReceiptSourceKind::LocalOutbox, + source_record_id.as_str(), + )?; + let _ = self.refresh_selected_account_sync()?; + Ok(true) } fn prepare_buyer_order_revision_decision( @@ -3246,15 +3249,14 @@ impl DesktopAppRuntimeState { decision: RadrootsOrderRevisionOutcome, ) -> Result<bool, AppSqliteError> { let payload = self.prepare_buyer_order_revision_decision(order_id, decision)?; - let operation = PendingSyncOperation::from_publish_payload( - AppPublishPayload::OrderRevisionDecision(payload), - current_utc_timestamp(), - ) - .map_err(|_| AppSqliteError::InvalidProjection { - reason: "buyer order revision publish payload must serialize", - })?; - let _ = self.enqueue_selected_account_sync_operation_once(operation)?; - self.attempt_sync(SyncTrigger::ManualRefresh) + let source_record_id = order_revision_decision_sdk_source_record_id(&payload); + self.enqueue_order_revision_decision_payload_via_sdk( + &payload, + AppSdkMigrationReceiptSourceKind::LocalOutbox, + source_record_id.as_str(), + )?; + let _ = self.refresh_selected_account_sync()?; + Ok(true) } fn prepare_buyer_order_cancellation( @@ -3372,15 +3374,14 @@ impl DesktopAppRuntimeState { order_id: OrderId, ) -> Result<bool, AppSqliteError> { let payload = self.prepare_buyer_order_cancellation(order_id)?; - let operation = PendingSyncOperation::from_publish_payload( - AppPublishPayload::OrderCancellation(payload), - current_utc_timestamp(), - ) - .map_err(|_| AppSqliteError::InvalidProjection { - reason: "buyer order cancellation publish payload must serialize", - })?; - let _ = self.enqueue_selected_account_sync_operation_once(operation)?; - self.attempt_sync(SyncTrigger::ManualRefresh) + let source_record_id = order_cancellation_sdk_source_record_id(&payload); + self.enqueue_order_cancellation_payload_via_sdk( + &payload, + AppSdkMigrationReceiptSourceKind::LocalOutbox, + source_record_id.as_str(), + )?; + let _ = self.refresh_selected_account_sync()?; + Ok(true) } fn prepare_buyer_order_receipt( @@ -3491,15 +3492,14 @@ impl DesktopAppRuntimeState { outcome: AppOrderReceiptOutcome, ) -> Result<bool, AppSqliteError> { let payload = self.prepare_buyer_order_receipt(order_id, outcome)?; - let operation = PendingSyncOperation::from_publish_payload( - AppPublishPayload::OrderReceipt(payload), - current_utc_timestamp(), - ) - .map_err(|_| AppSqliteError::InvalidProjection { - reason: "buyer order receipt publish payload must serialize", - })?; - let _ = self.enqueue_selected_account_sync_operation_once(operation)?; - self.attempt_sync(SyncTrigger::ManualRefresh) + let source_record_id = order_receipt_sdk_source_record_id(&payload); + self.enqueue_order_receipt_payload_via_sdk( + &payload, + AppSdkMigrationReceiptSourceKind::LocalOutbox, + source_record_id.as_str(), + )?; + let _ = self.refresh_selected_account_sync()?; + Ok(true) } fn start_order_recovery( @@ -5017,37 +5017,6 @@ impl DesktopAppRuntimeState { self.refresh_selected_account_sync() } - fn enqueue_selected_account_sync_operation_once( - &mut self, - operation: PendingSyncOperation, - ) -> Result<bool, AppSqliteError> { - let Some(account_id) = self - .state_store - .identity_projection() - .selected_account - .as_ref() - .map(|account| account.account.account_id.clone()) - else { - return Ok(false); - }; - let already_enqueued = { - let Some(sqlite_store) = self.sqlite_store.as_ref() else { - return Ok(false); - }; - let existing = sqlite_store.load_pending_sync_operations(account_id.as_str())?; - existing.iter().any(|pending| { - pending.operation.aggregate == operation.aggregate - && pending.operation.operation == operation.operation - && pending.operation.payload_json == operation.payload_json - }) - }; - if already_enqueued { - return self.refresh_selected_account_sync(); - } - - self.enqueue_selected_account_sync_operations(vec![operation]) - } - fn enqueue_selected_account_product_publish_operation( &mut self, product_id: ProductId, @@ -5384,6 +5353,298 @@ impl DesktopAppRuntimeState { } } + fn enqueue_order_revision_proposal_payload_via_sdk( + &self, + payload: &AppOrderRevisionProposalPublishPayload, + source_kind: AppSdkMigrationReceiptSourceKind, + source_record_id: &str, + ) -> Result<(), AppSqliteError> { + let operation_kind = ORDER_REVISION_PROPOSAL_OPERATION_KIND; + let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?; + let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?; + let actor_pubkey = self + .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderRevisionProposal( + payload.clone(), + )) + .and_then(|identity| { + let actor_pubkey = identity.public_key_hex(); + let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?; + let request = AppSdkOrderRevisionProposalRequest { + actor_account_id: payload.context.account_id.clone(), + actor_pubkey: actor_pubkey.clone(), + signer_keys: identity.into_keys(), + evidence_events: lifecycle.evidence_events, + root_event: order_lifecycle_sdk_event_ptr( + payload.request_event_id.as_str(), + target_relays.as_slice(), + "order revision proposal requires request event id", + )?, + previous_event: order_lifecycle_sdk_event_ptr( + payload.prev_event_id.as_str(), + target_relays.as_slice(), + "order revision proposal requires previous event id", + )?, + proposal: order_revision_proposal_publish_payload_to_sdk_revision(payload)?, + relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()), + target_relays, + idempotency_key: Some(sdk_idempotency_key(source_record_id)), + }; + self.enqueue_app_sdk_order_revision_proposal(request) + .map(|receipt| (actor_pubkey, receipt)) + .map_err(sync_transport_error_from_sdk_runtime_error) + }); + match actor_pubkey { + Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success( + source_kind, + source_record_id, + operation_kind, + actor_pubkey.as_str(), + &receipt, + ), + Err(error) => self.record_app_sdk_migration_failure( + source_kind, + source_record_id, + operation_kind, + None, + sync_transport_error_detail_json(&error), + ), + } + } + + fn enqueue_order_revision_decision_payload_via_sdk( + &self, + payload: &AppOrderRevisionDecisionPublishPayload, + source_kind: AppSdkMigrationReceiptSourceKind, + source_record_id: &str, + ) -> Result<(), AppSqliteError> { + let operation_kind = ORDER_REVISION_DECISION_OPERATION_KIND; + let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?; + let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?; + let actor_pubkey = self + .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderRevisionDecision( + payload.clone(), + )) + .and_then(|identity| { + let actor_pubkey = identity.public_key_hex(); + let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?; + let request = AppSdkOrderRevisionDecisionRequest { + actor_account_id: payload.context.account_id.clone(), + actor_pubkey: actor_pubkey.clone(), + signer_keys: identity.into_keys(), + evidence_events: lifecycle.evidence_events, + root_event: order_lifecycle_sdk_event_ptr( + payload.request_event_id.as_str(), + target_relays.as_slice(), + "order revision decision requires request event id", + )?, + previous_event: order_lifecycle_sdk_event_ptr( + payload.prev_event_id.as_str(), + target_relays.as_slice(), + "order revision decision requires previous event id", + )?, + decision: order_revision_decision_publish_payload_to_sdk_revision_decision( + payload, + )?, + relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()), + target_relays, + idempotency_key: Some(sdk_idempotency_key(source_record_id)), + }; + self.enqueue_app_sdk_order_revision_decision(request) + .map(|receipt| (actor_pubkey, receipt)) + .map_err(sync_transport_error_from_sdk_runtime_error) + }); + match actor_pubkey { + Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success( + source_kind, + source_record_id, + operation_kind, + actor_pubkey.as_str(), + &receipt, + ), + Err(error) => self.record_app_sdk_migration_failure( + source_kind, + source_record_id, + operation_kind, + None, + sync_transport_error_detail_json(&error), + ), + } + } + + fn enqueue_order_cancellation_payload_via_sdk( + &self, + payload: &AppOrderCancellationPublishPayload, + source_kind: AppSdkMigrationReceiptSourceKind, + source_record_id: &str, + ) -> Result<(), AppSqliteError> { + let operation_kind = ORDER_CANCELLATION_OPERATION_KIND; + let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?; + let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?; + let actor_pubkey = self + .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderCancellation( + payload.clone(), + )) + .and_then(|identity| { + let actor_pubkey = identity.public_key_hex(); + let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?; + let request = AppSdkOrderCancellationRequest { + actor_account_id: payload.context.account_id.clone(), + actor_pubkey: actor_pubkey.clone(), + signer_keys: identity.into_keys(), + evidence_events: lifecycle.evidence_events, + root_event: order_lifecycle_sdk_event_ptr( + payload.request_event_id.as_str(), + target_relays.as_slice(), + "order cancellation requires request event id", + )?, + previous_event: order_lifecycle_sdk_event_ptr( + payload.prev_event_id.as_str(), + target_relays.as_slice(), + "order cancellation requires previous event id", + )?, + cancellation: order_cancellation_publish_payload_to_sdk_cancellation(payload)?, + relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()), + target_relays, + idempotency_key: Some(sdk_idempotency_key(source_record_id)), + }; + self.enqueue_app_sdk_order_cancellation(request) + .map(|receipt| (actor_pubkey, receipt)) + .map_err(sync_transport_error_from_sdk_runtime_error) + }); + match actor_pubkey { + Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success( + source_kind, + source_record_id, + operation_kind, + actor_pubkey.as_str(), + &receipt, + ), + Err(error) => self.record_app_sdk_migration_failure( + source_kind, + source_record_id, + operation_kind, + None, + sync_transport_error_detail_json(&error), + ), + } + } + + fn enqueue_order_fulfillment_payload_via_sdk( + &self, + payload: &AppOrderFulfillmentPublishPayload, + source_kind: AppSdkMigrationReceiptSourceKind, + source_record_id: &str, + ) -> Result<(), AppSqliteError> { + let operation_kind = ORDER_FULFILLMENT_UPDATE_OPERATION_KIND; + let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?; + let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?; + let actor_pubkey = self + .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderFulfillment( + payload.clone(), + )) + .and_then(|identity| { + let actor_pubkey = identity.public_key_hex(); + let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?; + let request = AppSdkOrderFulfillmentUpdateRequest { + actor_account_id: payload.context.account_id.clone(), + actor_pubkey: actor_pubkey.clone(), + signer_keys: identity.into_keys(), + evidence_events: lifecycle.evidence_events, + root_event: order_lifecycle_sdk_event_ptr( + payload.request_event_id.as_str(), + target_relays.as_slice(), + "order fulfillment requires request event id", + )?, + previous_event: order_lifecycle_sdk_event_ptr( + payload.prev_event_id.as_str(), + target_relays.as_slice(), + "order fulfillment requires previous event id", + )?, + fulfillment: order_fulfillment_publish_payload_to_sdk_fulfillment(payload)?, + relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()), + target_relays, + idempotency_key: Some(sdk_idempotency_key(source_record_id)), + }; + self.enqueue_app_sdk_order_fulfillment_update(request) + .map(|receipt| (actor_pubkey, receipt)) + .map_err(sync_transport_error_from_sdk_runtime_error) + }); + match actor_pubkey { + Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success( + source_kind, + source_record_id, + operation_kind, + actor_pubkey.as_str(), + &receipt, + ), + Err(error) => self.record_app_sdk_migration_failure( + source_kind, + source_record_id, + operation_kind, + None, + sync_transport_error_detail_json(&error), + ), + } + } + + fn enqueue_order_receipt_payload_via_sdk( + &self, + payload: &AppOrderReceiptPublishPayload, + source_kind: AppSdkMigrationReceiptSourceKind, + source_record_id: &str, + ) -> Result<(), AppSqliteError> { + let operation_kind = ORDER_RECEIPT_RECORD_OPERATION_KIND; + let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?; + let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?; + let actor_pubkey = self + .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderReceipt( + payload.clone(), + )) + .and_then(|identity| { + let actor_pubkey = identity.public_key_hex(); + let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?; + let request = AppSdkOrderReceiptRecordRequest { + actor_account_id: payload.context.account_id.clone(), + actor_pubkey: actor_pubkey.clone(), + signer_keys: identity.into_keys(), + evidence_events: lifecycle.evidence_events, + root_event: order_lifecycle_sdk_event_ptr( + payload.request_event_id.as_str(), + target_relays.as_slice(), + "order receipt requires request event id", + )?, + previous_event: order_lifecycle_sdk_event_ptr( + payload.prev_event_id.as_str(), + target_relays.as_slice(), + "order receipt requires previous event id", + )?, + receipt: order_receipt_publish_payload_to_sdk_receipt(payload)?, + relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()), + target_relays, + idempotency_key: Some(sdk_idempotency_key(source_record_id)), + }; + self.enqueue_app_sdk_order_receipt_record(request) + .map(|receipt| (actor_pubkey, receipt)) + .map_err(sync_transport_error_from_sdk_runtime_error) + }); + match actor_pubkey { + Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success( + source_kind, + source_record_id, + operation_kind, + actor_pubkey.as_str(), + &receipt, + ), + Err(error) => self.record_app_sdk_migration_failure( + source_kind, + source_record_id, + operation_kind, + None, + sync_transport_error_detail_json(&error), + ), + } + } + fn local_signing_identity_for_publish_payload( &self, payload: &AppPublishPayload, @@ -5415,6 +5676,41 @@ impl DesktopAppRuntimeState { self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_decision(request)) } + fn enqueue_app_sdk_order_revision_proposal( + &self, + request: AppSdkOrderRevisionProposalRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_revision_proposal(request)) + } + + fn enqueue_app_sdk_order_revision_decision( + &self, + request: AppSdkOrderRevisionDecisionRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_revision_decision(request)) + } + + fn enqueue_app_sdk_order_cancellation( + &self, + request: AppSdkOrderCancellationRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_cancellation(request)) + } + + fn enqueue_app_sdk_order_fulfillment_update( + &self, + request: AppSdkOrderFulfillmentUpdateRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_fulfillment_update(request)) + } + + fn enqueue_app_sdk_order_receipt_record( + &self, + request: AppSdkOrderReceiptRecordRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_receipt_record(request)) + } + fn with_app_sdk_runtime<T>( &self, command: impl FnOnce(&AppSdkRuntime) -> Result<T, AppSdkRuntimeError>, @@ -5915,6 +6211,7 @@ impl DesktopAppRuntimeState { .then_with(|| left.id.cmp(&right.id)) }); + let mut evidence_events = vec![request.request_event.clone()]; let mut buckets = AppActiveOrderEvidenceBuckets::default(); let request_event_id = active_order_event_id(request.request_event_id.as_str(), "request_event_id")?; @@ -5933,6 +6230,7 @@ impl DesktopAppRuntimeState { { continue; } + evidence_events.push(event.clone()); let event_id = active_order_event_id(event.id.as_str(), "event_id")?; let author_pubkey = active_order_pubkey(event.author.as_str(), "author_pubkey")?; match event.kind { @@ -6141,6 +6439,7 @@ impl DesktopAppRuntimeState { .transpose()?; Ok(ResolvedAppOrderLifecycleEvidence { + evidence_events, status: projection.status, payment_state: projection.payment.state, agreement_event_id: projection @@ -7687,6 +7986,40 @@ fn order_decision_sdk_source_record_id(payload: &AppOrderDecisionPublishPayload) format!("app:order_decision:{}", payload.app_order_id) } +fn order_revision_proposal_sdk_source_record_id( + payload: &AppOrderRevisionProposalPublishPayload, +) -> String { + format!( + "app:order_revision_proposal:{}:{}", + payload.app_order_id, payload.revision_id + ) +} + +fn order_revision_decision_sdk_source_record_id( + payload: &AppOrderRevisionDecisionPublishPayload, +) -> String { + format!( + "app:order_revision_decision:{}:{}", + payload.app_order_id, payload.revision_id + ) +} + +fn order_cancellation_sdk_source_record_id(payload: &AppOrderCancellationPublishPayload) -> String { + format!("app:order_cancellation:{}", payload.app_order_id) +} + +fn order_fulfillment_sdk_source_record_id(payload: &AppOrderFulfillmentPublishPayload) -> String { + format!( + "app:order_fulfillment:{}:{}", + payload.app_order_id, + order_fulfillment_status_storage_key(payload.status) + ) +} + +fn order_receipt_sdk_source_record_id(payload: &AppOrderReceiptPublishPayload) -> String { + format!("app:order_receipt:{}", payload.app_order_id) +} + fn sdk_relay_url_policy_for_targets(target_relays: &[String]) -> AppSdkRelayUrlPolicy { if target_relays .iter() @@ -7854,82 +8187,21 @@ async fn publish_app_payload( AppPublishPayload::OrderDecision(_) => Err(AppSyncTransportError::failed( "order decision publish uses AppSdkRuntime", )), - AppPublishPayload::OrderRevisionProposal(payload) => { - let proposal = order_revision_proposal_publish_payload_to_sdk_revision(payload)?; - let request_event_id = publish_event_id(payload.request_event_id.as_str())?; - let prev_event_id = publish_event_id(payload.prev_event_id.as_str())?; - client - .order() - .publish_order_revision_proposal_with_identity( - identity, - &request_event_id, - &prev_event_id, - &proposal, - ) - .await - .map_err(|error| AppSyncTransportError::failed(error.to_string())) - } - AppPublishPayload::OrderRevisionDecision(payload) => { - let decision = - order_revision_decision_publish_payload_to_sdk_revision_decision(payload)?; - let request_event_id = publish_event_id(payload.request_event_id.as_str())?; - let prev_event_id = publish_event_id(payload.prev_event_id.as_str())?; - client - .order() - .publish_order_revision_decision_with_identity( - identity, - &request_event_id, - &prev_event_id, - &decision, - ) - .await - .map_err(|error| AppSyncTransportError::failed(error.to_string())) - } - AppPublishPayload::OrderCancellation(payload) => { - let cancellation = order_cancellation_publish_payload_to_sdk_cancellation(payload)?; - let request_event_id = publish_event_id(payload.request_event_id.as_str())?; - let prev_event_id = publish_event_id(payload.prev_event_id.as_str())?; - client - .order() - .publish_order_cancellation_with_identity( - identity, - &request_event_id, - &prev_event_id, - &cancellation, - ) - .await - .map_err(|error| AppSyncTransportError::failed(error.to_string())) - } - AppPublishPayload::OrderFulfillment(payload) => { - let fulfillment = order_fulfillment_publish_payload_to_sdk_fulfillment(payload)?; - let request_event_id = publish_event_id(payload.request_event_id.as_str())?; - let prev_event_id = publish_event_id(payload.prev_event_id.as_str())?; - client - .order() - .publish_fulfillment_update_with_identity( - identity, - &request_event_id, - &prev_event_id, - &fulfillment, - ) - .await - .map_err(|error| AppSyncTransportError::failed(error.to_string())) - } - AppPublishPayload::OrderReceipt(payload) => { - let receipt = order_receipt_publish_payload_to_sdk_receipt(payload)?; - let request_event_id = publish_event_id(payload.request_event_id.as_str())?; - let prev_event_id = publish_event_id(payload.prev_event_id.as_str())?; - client - .order() - .publish_buyer_receipt_with_identity( - identity, - &request_event_id, - &prev_event_id, - &receipt, - ) - .await - .map_err(|error| AppSyncTransportError::failed(error.to_string())) - } + AppPublishPayload::OrderRevisionProposal(_) => Err(AppSyncTransportError::failed( + "order revision proposal publish uses AppSdkRuntime", + )), + AppPublishPayload::OrderRevisionDecision(_) => Err(AppSyncTransportError::failed( + "order revision decision publish uses AppSdkRuntime", + )), + AppPublishPayload::OrderCancellation(_) => Err(AppSyncTransportError::failed( + "order cancellation publish uses AppSdkRuntime", + )), + AppPublishPayload::OrderFulfillment(_) => Err(AppSyncTransportError::failed( + "order fulfillment publish uses AppSdkRuntime", + )), + AppPublishPayload::OrderReceipt(_) => Err(AppSyncTransportError::failed( + "order receipt publish uses AppSdkRuntime", + )), } } @@ -8179,6 +8451,32 @@ fn order_decision_sdk_request_event_ptr( }) } +fn order_lifecycle_sdk_event_ptr( + event_id: &str, + target_relays: &[String], + missing_message: &'static str, +) -> Result<RadrootsNostrEventPtr, AppSyncTransportError> { + let event_id = event_id.trim(); + if event_id.is_empty() { + return Err(AppSyncTransportError::failed(missing_message)); + } + Ok(RadrootsNostrEventPtr { + id: event_id.to_owned(), + relays: target_relays.first().cloned(), + }) +} + +fn order_fulfillment_status_storage_key(status: RadrootsOrderFulfillmentState) -> &'static str { + match status { + RadrootsOrderFulfillmentState::AcceptedNotFulfilled => "accepted_not_fulfilled", + RadrootsOrderFulfillmentState::Preparing => "preparing", + RadrootsOrderFulfillmentState::ReadyForPickup => "ready_for_pickup", + RadrootsOrderFulfillmentState::OutForDelivery => "out_for_delivery", + RadrootsOrderFulfillmentState::Delivered => "delivered", + RadrootsOrderFulfillmentState::SellerCancelled => "seller_cancelled", + } +} + #[cfg(test)] fn selected_listing_relay( listing_relays: &[String], diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -1311,43 +1311,43 @@ const LEGACY_SDK_BOUNDARY_ALLOWLIST: &[LegacySdkBoundaryAllowlistEntry] = &[ path: "crates/desktop/src/runtime.rs", pattern: "SdkDirectRelayAppSyncTransport", owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still owns deferred direct relay publish transport for unmigrated publish workflows", - removal_condition: "remove when farm, listing, order, fulfillment, and receipt publish workflows enqueue through AppSdkRuntime", + reason: "desktop runtime still owns deferred direct relay publish transport for listing publish", + removal_condition: "remove when listing publish workflow enqueues through AppSdkRuntime", }, LegacySdkBoundaryAllowlistEntry { path: "crates/desktop/src/runtime.rs", pattern: "RadrootsSdkClient", owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still constructs the legacy direct publish client for unmigrated publish workflows", + reason: "desktop runtime still constructs the legacy direct publish client for listing publish", removal_condition: "remove when direct publish workflows no longer construct SDK clients outside AppSdkRuntime", }, LegacySdkBoundaryAllowlistEntry { path: "crates/desktop/src/runtime.rs", pattern: "RadrootsSdkConfig", owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still configures the legacy direct publish client for unmigrated publish workflows", + reason: "desktop runtime still configures the legacy direct publish client for listing publish", removal_condition: "remove when direct publish workflows no longer configure SDK clients outside AppSdkRuntime", }, LegacySdkBoundaryAllowlistEntry { path: "crates/desktop/src/runtime.rs", pattern: "SdkTransportMode::RelayDirect", owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still uses relay direct publish transport for deferred workflow migration", - removal_condition: "remove when all publish workflows route through SDK canonical outbox and sync APIs", + reason: "desktop runtime still uses relay direct publish transport for listing publish", + removal_condition: "remove when listing publish workflow routes through SDK canonical outbox and sync APIs", }, LegacySdkBoundaryAllowlistEntry { path: "crates/desktop/src/runtime.rs", pattern: "SignerConfig::LocalIdentity", owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still configures direct local signing for deferred workflow migration", + reason: "desktop runtime still configures direct local signing for listing publish", removal_condition: "remove when publish signing is mediated by AppSdkRuntime and SDK signer adapters", }, LegacySdkBoundaryAllowlistEntry { path: "crates/desktop/src/runtime.rs", pattern: "PendingSyncOperation::from_publish_payload", owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still creates legacy local outbox publish work for unmigrated listing and order lifecycle workflows", - removal_condition: "remove when listing and remaining order lifecycle publish workflows write SDK canonical outbox requests instead of app local_outbox operations", + reason: "desktop runtime still creates legacy local outbox publish work for listing publish", + removal_condition: "remove when listing publish writes SDK canonical outbox requests instead of app local_outbox operations", }, LegacySdkBoundaryAllowlistEntry { path: "crates/desktop/src/runtime.rs", @@ -1357,41 +1357,6 @@ const LEGACY_SDK_BOUNDARY_ALLOWLIST: &[LegacySdkBoundaryAllowlistEntry] = &[ removal_condition: "remove when listing publish workflow enqueues through AppSdkRuntime", }, LegacySdkBoundaryAllowlistEntry { - path: "crates/desktop/src/runtime.rs", - pattern: "publish_order_revision_proposal_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still calls legacy direct SDK order revision proposal publish APIs", - removal_condition: "remove when seller order revision proposal workflow enqueues through AppSdkRuntime", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/desktop/src/runtime.rs", - pattern: "publish_order_revision_decision_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still calls legacy direct SDK order revision decision publish APIs", - removal_condition: "remove when buyer order revision decision workflow enqueues through AppSdkRuntime", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/desktop/src/runtime.rs", - pattern: "publish_order_cancellation_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still calls legacy direct SDK order cancellation publish APIs", - removal_condition: "remove when buyer order cancellation workflow enqueues through AppSdkRuntime", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/desktop/src/runtime.rs", - pattern: "publish_fulfillment_update_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still calls legacy direct SDK fulfillment publish APIs", - removal_condition: "remove when seller fulfillment workflow enqueues through AppSdkRuntime", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/desktop/src/runtime.rs", - pattern: "publish_buyer_receipt_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "desktop runtime still calls legacy direct SDK receipt publish APIs", - removal_condition: "remove when buyer receipt workflow enqueues through AppSdkRuntime", - }, - LegacySdkBoundaryAllowlistEntry { path: "crates/desktop/src/accounts.rs", pattern: "RadrootsIdentity::from_secret_key_str", owner: "rpv1-app-sdk-hardening.04", @@ -1423,8 +1388,8 @@ const LEGACY_SDK_BOUNDARY_ALLOWLIST: &[LegacySdkBoundaryAllowlistEntry] = &[ path: "crates/sync/src/publish.rs", pattern: "SdkTransportMode::RelayDirect", owner: "rpv1-app-sdk-refactor.07", - reason: "sync payload metadata still marks legacy app local outbox publish work as relay direct", - removal_condition: "remove when app sync publish payloads are replaced by SDK canonical outbox requests", + reason: "sync payload metadata still marks legacy listing local outbox publish work as relay direct", + removal_condition: "remove when listing publish payload metadata is replaced by SDK canonical outbox requests", }, LegacySdkBoundaryAllowlistEntry { path: "crates/sync/src/publish.rs", @@ -1434,41 +1399,6 @@ const LEGACY_SDK_BOUNDARY_ALLOWLIST: &[LegacySdkBoundaryAllowlistEntry] = &[ removal_condition: "remove when listing publish payload metadata is replaced by SDK canonical outbox requests", }, LegacySdkBoundaryAllowlistEntry { - path: "crates/sync/src/publish.rs", - pattern: "publish_order_revision_proposal_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "sync payload metadata still names legacy order revision proposal SDK publish operations", - removal_condition: "remove when order revision proposal payload metadata is replaced by SDK canonical outbox requests", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/sync/src/publish.rs", - pattern: "publish_order_revision_decision_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "sync payload metadata still names legacy order revision decision SDK publish operations", - removal_condition: "remove when order revision decision payload metadata is replaced by SDK canonical outbox requests", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/sync/src/publish.rs", - pattern: "publish_order_cancellation_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "sync payload metadata still names legacy order cancellation SDK publish operations", - removal_condition: "remove when order cancellation payload metadata is replaced by SDK canonical outbox requests", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/sync/src/publish.rs", - pattern: "publish_fulfillment_update_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "sync payload metadata still names legacy fulfillment SDK publish operations", - removal_condition: "remove when fulfillment payload metadata is replaced by SDK canonical outbox requests", - }, - LegacySdkBoundaryAllowlistEntry { - path: "crates/sync/src/publish.rs", - pattern: "publish_buyer_receipt_with_identity", - owner: "rpv1-app-sdk-refactor.07", - reason: "sync payload metadata still names legacy receipt SDK publish operations", - removal_condition: "remove when receipt payload metadata is replaced by SDK canonical outbox requests", - }, - LegacySdkBoundaryAllowlistEntry { path: "crates/store/src/lib.rs", pattern: ".enqueue_pending_operation(", owner: "rpv1-app-sdk-refactor.07", diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs @@ -38,13 +38,15 @@ pub use runtime::{ pub use sdk::{ APP_SDK_DEFAULT_COMMAND_QUEUE_CAPACITY, APP_SDK_STORAGE_DIR_NAME, AppSdkConfig, AppSdkDiagnostics, AppSdkEventStoreDiagnostics, AppSdkFarmPublishRequest, - AppSdkIntegrityDiagnostics, AppSdkLifecycleState, AppSdkOrderDecisionRequest, - AppSdkOrderSubmitRequest, AppSdkOutboxDiagnostics, AppSdkProjectionLifecycleState, - AppSdkProjectionLifecycleStatus, AppSdkRelayUrlPolicy, AppSdkRestorePreflightReceipt, - AppSdkRestorePreflightRequest, AppSdkRuntime, AppSdkRuntimeError, AppSdkRuntimeIssue, - AppSdkRuntimeStatus, AppSdkSqliteStoreDiagnostics, AppSdkStorageDiagnostics, - AppSdkStoragePaths, AppSdkSyncDiagnostics, AppSdkSyncEventStoreDiagnostics, - AppSdkSyncOutboxDiagnostics, AppSdkSyncRelayTargetDiagnostics, AppSdkWorkflowReceipt, - app_sdk_storage_root_from_data_root, + AppSdkIntegrityDiagnostics, AppSdkLifecycleState, AppSdkOrderCancellationRequest, + AppSdkOrderDecisionRequest, AppSdkOrderFulfillmentUpdateRequest, + AppSdkOrderReceiptRecordRequest, AppSdkOrderRevisionDecisionRequest, + AppSdkOrderRevisionProposalRequest, AppSdkOrderSubmitRequest, AppSdkOutboxDiagnostics, + AppSdkProjectionLifecycleState, AppSdkProjectionLifecycleStatus, AppSdkRelayUrlPolicy, + AppSdkRestorePreflightReceipt, AppSdkRestorePreflightRequest, AppSdkRuntime, + AppSdkRuntimeError, AppSdkRuntimeIssue, AppSdkRuntimeStatus, AppSdkSqliteStoreDiagnostics, + AppSdkStorageDiagnostics, AppSdkStoragePaths, AppSdkSyncDiagnostics, + AppSdkSyncEventStoreDiagnostics, AppSdkSyncOutboxDiagnostics, AppSdkSyncRelayTargetDiagnostics, + AppSdkWorkflowReceipt, app_sdk_storage_root_from_data_root, }; pub use startup::{AppStartupEvent, AppStartupEventMetadata, launch_startup_event}; diff --git a/crates/runtime/src/sdk.rs b/crates/runtime/src/sdk.rs @@ -15,17 +15,27 @@ use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, contract::RadrootsActorRole, farm::RadrootsFarm, - order::{RadrootsOrderDecision, RadrootsOrderRequest}, + order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderFulfillmentUpdate, + RadrootsOrderReceipt, RadrootsOrderRequest, RadrootsOrderRevisionDecision, + RadrootsOrderRevisionProposal, + }, }; use radroots_nostr::prelude::RadrootsNostrKeys; use radroots_sdk::{ FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmEnqueueReceipt, IntegrityReceipt, - IntegrityRequest, ORDER_DECISION_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND, - OrderDecisionEnqueueRequest, OrderDecisionReceipt, OrderRequestEvidenceIngestRequest, - OrderSubmitEnqueueRequest, OrderSubmitReceipt, RadrootsSdk, RadrootsSdkError, - RadrootsSdkStoragePaths, RestoreReceipt, RestoreRequest, SdkBackupVerification, - SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, StorageStatusReceipt, StorageStatusRequest, - SyncStatusReceipt, SyncStatusRequest, + IntegrityRequest, ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, + ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND, + ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, + ORDER_SUBMIT_OPERATION_KIND, OrderCancellationEnqueueRequest, OrderCancellationReceipt, + OrderDecisionEnqueueRequest, OrderDecisionReceipt, OrderEvidenceIngestRequest, + OrderFulfillmentUpdateEnqueueRequest, OrderFulfillmentUpdateReceipt, + OrderReceiptRecordEnqueueRequest, OrderReceiptRecordReceipt, OrderRequestEvidenceIngestRequest, + OrderRevisionDecisionEnqueueRequest, OrderRevisionDecisionReceipt, + OrderRevisionProposalEnqueueRequest, OrderRevisionProposalReceipt, OrderSubmitEnqueueRequest, + OrderSubmitReceipt, RadrootsSdk, RadrootsSdkError, RadrootsSdkStoragePaths, RestoreReceipt, + RestoreRequest, SdkBackupVerification, SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, + StorageStatusReceipt, StorageStatusRequest, SyncStatusReceipt, SyncStatusRequest, }; use radroots_sdk::{SdkMutationState, SdkRelayTargetPolicy}; use serde::Serialize; @@ -227,6 +237,71 @@ pub struct AppSdkOrderDecisionRequest { pub idempotency_key: Option<String>, } +pub struct AppSdkOrderRevisionProposalRequest { + pub actor_account_id: String, + pub actor_pubkey: String, + pub signer_keys: RadrootsNostrKeys, + pub evidence_events: Vec<RadrootsNostrEvent>, + pub root_event: RadrootsNostrEventPtr, + pub previous_event: RadrootsNostrEventPtr, + pub proposal: RadrootsOrderRevisionProposal, + pub target_relays: Vec<String>, + pub relay_url_policy: AppSdkRelayUrlPolicy, + pub idempotency_key: Option<String>, +} + +pub struct AppSdkOrderRevisionDecisionRequest { + pub actor_account_id: String, + pub actor_pubkey: String, + pub signer_keys: RadrootsNostrKeys, + pub evidence_events: Vec<RadrootsNostrEvent>, + pub root_event: RadrootsNostrEventPtr, + pub previous_event: RadrootsNostrEventPtr, + pub decision: RadrootsOrderRevisionDecision, + pub target_relays: Vec<String>, + pub relay_url_policy: AppSdkRelayUrlPolicy, + pub idempotency_key: Option<String>, +} + +pub struct AppSdkOrderCancellationRequest { + pub actor_account_id: String, + pub actor_pubkey: String, + pub signer_keys: RadrootsNostrKeys, + pub evidence_events: Vec<RadrootsNostrEvent>, + pub root_event: RadrootsNostrEventPtr, + pub previous_event: RadrootsNostrEventPtr, + pub cancellation: RadrootsOrderCancellation, + pub target_relays: Vec<String>, + pub relay_url_policy: AppSdkRelayUrlPolicy, + pub idempotency_key: Option<String>, +} + +pub struct AppSdkOrderFulfillmentUpdateRequest { + pub actor_account_id: String, + pub actor_pubkey: String, + pub signer_keys: RadrootsNostrKeys, + pub evidence_events: Vec<RadrootsNostrEvent>, + pub root_event: RadrootsNostrEventPtr, + pub previous_event: RadrootsNostrEventPtr, + pub fulfillment: RadrootsOrderFulfillmentUpdate, + pub target_relays: Vec<String>, + pub relay_url_policy: AppSdkRelayUrlPolicy, + pub idempotency_key: Option<String>, +} + +pub struct AppSdkOrderReceiptRecordRequest { + pub actor_account_id: String, + pub actor_pubkey: String, + pub signer_keys: RadrootsNostrKeys, + pub evidence_events: Vec<RadrootsNostrEvent>, + pub root_event: RadrootsNostrEventPtr, + pub previous_event: RadrootsNostrEventPtr, + pub receipt: RadrootsOrderReceipt, + pub target_relays: Vec<String>, + pub relay_url_policy: AppSdkRelayUrlPolicy, + pub idempotency_key: Option<String>, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct AppSdkWorkflowReceipt { pub operation_kind: String, @@ -331,6 +406,26 @@ enum AppSdkWorkerCommand { AppSdkOrderDecisionRequest, mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, ), + EnqueueOrderRevisionProposal( + AppSdkOrderRevisionProposalRequest, + mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, + ), + EnqueueOrderRevisionDecision( + AppSdkOrderRevisionDecisionRequest, + mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, + ), + EnqueueOrderCancellation( + AppSdkOrderCancellationRequest, + mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, + ), + EnqueueOrderFulfillmentUpdate( + AppSdkOrderFulfillmentUpdateRequest, + mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, + ), + EnqueueOrderReceiptRecord( + AppSdkOrderReceiptRecordRequest, + mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, + ), BeginProjectionRebuild( mpsc::Sender<Result<AppSdkProjectionLifecycleStatus, AppSdkRuntimeIssue>>, ), @@ -350,6 +445,19 @@ impl fmt::Debug for AppSdkWorkerCommand { Self::EnqueueFarmPublish(_, _) => formatter.write_str("EnqueueFarmPublish"), Self::EnqueueOrderSubmit(_, _) => formatter.write_str("EnqueueOrderSubmit"), Self::EnqueueOrderDecision(_, _) => formatter.write_str("EnqueueOrderDecision"), + Self::EnqueueOrderRevisionProposal(_, _) => { + formatter.write_str("EnqueueOrderRevisionProposal") + } + Self::EnqueueOrderRevisionDecision(_, _) => { + formatter.write_str("EnqueueOrderRevisionDecision") + } + Self::EnqueueOrderCancellation(_, _) => formatter.write_str("EnqueueOrderCancellation"), + Self::EnqueueOrderFulfillmentUpdate(_, _) => { + formatter.write_str("EnqueueOrderFulfillmentUpdate") + } + Self::EnqueueOrderReceiptRecord(_, _) => { + formatter.write_str("EnqueueOrderReceiptRecord") + } Self::BeginProjectionRebuild(_) => formatter.write_str("BeginProjectionRebuild"), Self::CompleteProjectionRebuild(_) => formatter.write_str("CompleteProjectionRebuild"), } @@ -498,6 +606,51 @@ impl AppSdkRuntime { }) } + pub fn enqueue_order_revision_proposal( + &self, + request: AppSdkOrderRevisionProposalRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.run_command(|response_sender| { + AppSdkWorkerCommand::EnqueueOrderRevisionProposal(request, response_sender) + }) + } + + pub fn enqueue_order_revision_decision( + &self, + request: AppSdkOrderRevisionDecisionRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.run_command(|response_sender| { + AppSdkWorkerCommand::EnqueueOrderRevisionDecision(request, response_sender) + }) + } + + pub fn enqueue_order_cancellation( + &self, + request: AppSdkOrderCancellationRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.run_command(|response_sender| { + AppSdkWorkerCommand::EnqueueOrderCancellation(request, response_sender) + }) + } + + pub fn enqueue_order_fulfillment_update( + &self, + request: AppSdkOrderFulfillmentUpdateRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.run_command(|response_sender| { + AppSdkWorkerCommand::EnqueueOrderFulfillmentUpdate(request, response_sender) + }) + } + + pub fn enqueue_order_receipt_record( + &self, + request: AppSdkOrderReceiptRecordRequest, + ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { + self.run_command(|response_sender| { + AppSdkWorkerCommand::EnqueueOrderReceiptRecord(request, response_sender) + }) + } + pub fn begin_projection_rebuild( &self, ) -> Result<AppSdkProjectionLifecycleStatus, AppSdkRuntimeError> { @@ -996,6 +1149,67 @@ fn run_app_sdk_worker( }; send_worker_result(&shared, response_sender, result); } + AppSdkWorkerCommand::EnqueueOrderRevisionProposal(request, response_sender) => { + let result = if let Some(issue) = lifecycle_busy_issue(&shared) { + Err(issue) + } else { + match sdk.as_ref() { + Some(sdk) => { + enqueue_order_revision_proposal_with_sdk(&runtime, sdk, request) + } + None => Err(runtime_unavailable_issue(&shared)), + } + }; + send_worker_result(&shared, response_sender, result); + } + AppSdkWorkerCommand::EnqueueOrderRevisionDecision(request, response_sender) => { + let result = if let Some(issue) = lifecycle_busy_issue(&shared) { + Err(issue) + } else { + match sdk.as_ref() { + Some(sdk) => { + enqueue_order_revision_decision_with_sdk(&runtime, sdk, request) + } + None => Err(runtime_unavailable_issue(&shared)), + } + }; + send_worker_result(&shared, response_sender, result); + } + AppSdkWorkerCommand::EnqueueOrderCancellation(request, response_sender) => { + let result = if let Some(issue) = lifecycle_busy_issue(&shared) { + Err(issue) + } else { + match sdk.as_ref() { + Some(sdk) => enqueue_order_cancellation_with_sdk(&runtime, sdk, request), + None => Err(runtime_unavailable_issue(&shared)), + } + }; + send_worker_result(&shared, response_sender, result); + } + AppSdkWorkerCommand::EnqueueOrderFulfillmentUpdate(request, response_sender) => { + let result = if let Some(issue) = lifecycle_busy_issue(&shared) { + Err(issue) + } else { + match sdk.as_ref() { + Some(sdk) => { + enqueue_order_fulfillment_update_with_sdk(&runtime, sdk, request) + } + None => Err(runtime_unavailable_issue(&shared)), + } + }; + send_worker_result(&shared, response_sender, result); + } + AppSdkWorkerCommand::EnqueueOrderReceiptRecord(request, response_sender) => { + let result = if let Some(issue) = lifecycle_busy_issue(&shared) { + Err(issue) + } else { + match sdk.as_ref() { + Some(sdk) => enqueue_order_receipt_record_with_sdk(&runtime, sdk, request), + None => Err(runtime_unavailable_issue(&shared)), + } + }; + send_worker_result(&shared, response_sender, result); + } AppSdkWorkerCommand::BeginProjectionRebuild(response_sender) => { let result = match sdk.as_ref() { Some(_) => Ok(begin_projection_rebuild(&shared)), @@ -1084,6 +1298,41 @@ fn run_degraded_worker( Err(runtime_unavailable_issue(&shared)), ); } + AppSdkWorkerCommand::EnqueueOrderRevisionProposal(_, response_sender) => { + send_worker_result( + &shared, + response_sender, + Err(runtime_unavailable_issue(&shared)), + ); + } + AppSdkWorkerCommand::EnqueueOrderRevisionDecision(_, response_sender) => { + send_worker_result( + &shared, + response_sender, + Err(runtime_unavailable_issue(&shared)), + ); + } + AppSdkWorkerCommand::EnqueueOrderCancellation(_, response_sender) => { + send_worker_result( + &shared, + response_sender, + Err(runtime_unavailable_issue(&shared)), + ); + } + AppSdkWorkerCommand::EnqueueOrderFulfillmentUpdate(_, response_sender) => { + send_worker_result( + &shared, + response_sender, + Err(runtime_unavailable_issue(&shared)), + ); + } + AppSdkWorkerCommand::EnqueueOrderReceiptRecord(_, response_sender) => { + send_worker_result( + &shared, + response_sender, + Err(runtime_unavailable_issue(&shared)), + ); + } AppSdkWorkerCommand::BeginProjectionRebuild(response_sender) => { send_worker_result( &shared, @@ -1260,6 +1509,192 @@ fn enqueue_order_decision_with_sdk( )) } +fn enqueue_order_revision_proposal_with_sdk( + runtime: &tokio::runtime::Runtime, + sdk: &RadrootsSdk, + request: AppSdkOrderRevisionProposalRequest, +) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { + let actor = sdk_actor_context( + request.actor_pubkey.as_str(), + request.actor_account_id.as_str(), + RadrootsActorRole::Seller, + )?; + let signer = sdk_local_signer(request.signer_keys)?; + let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; + ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; + let mut enqueue = OrderRevisionProposalEnqueueRequest::new( + actor, + request.root_event, + request.previous_event, + request.proposal, + target_relays, + ); + if let Some(idempotency_key) = request.idempotency_key.as_deref() { + enqueue = enqueue + .try_with_idempotency_key(idempotency_key) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + } + let receipt = runtime + .block_on(sdk.orders().enqueue_revision_proposal(enqueue, &signer)) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + Ok(app_sdk_order_revision_proposal_receipt( + receipt, + request.actor_pubkey, + )) +} + +fn enqueue_order_revision_decision_with_sdk( + runtime: &tokio::runtime::Runtime, + sdk: &RadrootsSdk, + request: AppSdkOrderRevisionDecisionRequest, +) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { + let actor = sdk_actor_context( + request.actor_pubkey.as_str(), + request.actor_account_id.as_str(), + RadrootsActorRole::Buyer, + )?; + let signer = sdk_local_signer(request.signer_keys)?; + let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; + ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; + let mut enqueue = OrderRevisionDecisionEnqueueRequest::new( + actor, + request.root_event, + request.previous_event, + request.decision, + target_relays, + ); + if let Some(idempotency_key) = request.idempotency_key.as_deref() { + enqueue = enqueue + .try_with_idempotency_key(idempotency_key) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + } + let receipt = runtime + .block_on(sdk.orders().enqueue_revision_decision(enqueue, &signer)) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + Ok(app_sdk_order_revision_decision_receipt( + receipt, + request.actor_pubkey, + )) +} + +fn enqueue_order_cancellation_with_sdk( + runtime: &tokio::runtime::Runtime, + sdk: &RadrootsSdk, + request: AppSdkOrderCancellationRequest, +) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { + let actor = sdk_actor_context( + request.actor_pubkey.as_str(), + request.actor_account_id.as_str(), + RadrootsActorRole::Buyer, + )?; + let signer = sdk_local_signer(request.signer_keys)?; + let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; + ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; + let mut enqueue = OrderCancellationEnqueueRequest::new( + actor, + request.root_event, + request.previous_event, + request.cancellation, + target_relays, + ); + if let Some(idempotency_key) = request.idempotency_key.as_deref() { + enqueue = enqueue + .try_with_idempotency_key(idempotency_key) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + } + let receipt = runtime + .block_on(sdk.orders().enqueue_cancellation(enqueue, &signer)) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + Ok(app_sdk_order_cancellation_receipt( + receipt, + request.actor_pubkey, + )) +} + +fn enqueue_order_fulfillment_update_with_sdk( + runtime: &tokio::runtime::Runtime, + sdk: &RadrootsSdk, + request: AppSdkOrderFulfillmentUpdateRequest, +) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { + let actor = sdk_actor_context( + request.actor_pubkey.as_str(), + request.actor_account_id.as_str(), + RadrootsActorRole::Seller, + )?; + let signer = sdk_local_signer(request.signer_keys)?; + let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; + ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; + let mut enqueue = OrderFulfillmentUpdateEnqueueRequest::new( + actor, + request.root_event, + request.previous_event, + request.fulfillment, + target_relays, + ); + if let Some(idempotency_key) = request.idempotency_key.as_deref() { + enqueue = enqueue + .try_with_idempotency_key(idempotency_key) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + } + let receipt = runtime + .block_on(sdk.orders().enqueue_fulfillment_update(enqueue, &signer)) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + Ok(app_sdk_order_fulfillment_update_receipt( + receipt, + request.actor_pubkey, + )) +} + +fn enqueue_order_receipt_record_with_sdk( + runtime: &tokio::runtime::Runtime, + sdk: &RadrootsSdk, + request: AppSdkOrderReceiptRecordRequest, +) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { + let actor = sdk_actor_context( + request.actor_pubkey.as_str(), + request.actor_account_id.as_str(), + RadrootsActorRole::Buyer, + )?; + let signer = sdk_local_signer(request.signer_keys)?; + let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; + ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; + let mut enqueue = OrderReceiptRecordEnqueueRequest::new( + actor, + request.root_event, + request.previous_event, + request.receipt, + target_relays, + ); + if let Some(idempotency_key) = request.idempotency_key.as_deref() { + enqueue = enqueue + .try_with_idempotency_key(idempotency_key) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + } + let receipt = runtime + .block_on(sdk.orders().enqueue_receipt_record(enqueue, &signer)) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + Ok(app_sdk_order_receipt_record_receipt( + receipt, + request.actor_pubkey, + )) +} + +fn ingest_order_evidence_with_sdk( + runtime: &tokio::runtime::Runtime, + sdk: &RadrootsSdk, + evidence_events: Vec<RadrootsNostrEvent>, +) -> Result<(), AppSdkRuntimeIssue> { + for event in evidence_events { + runtime + .block_on( + sdk.orders() + .ingest_evidence(OrderEvidenceIngestRequest::new(event)), + ) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; + } + Ok(()) +} + fn sdk_actor_context( actor_pubkey: &str, actor_account_id: &str, @@ -1334,6 +1769,86 @@ fn app_sdk_order_decision_receipt( } } +fn app_sdk_order_revision_proposal_receipt( + receipt: OrderRevisionProposalReceipt, + actor_pubkey: String, +) -> AppSdkWorkflowReceipt { + AppSdkWorkflowReceipt { + operation_kind: ORDER_REVISION_PROPOSAL_OPERATION_KIND.to_owned(), + expected_event_id: receipt.expected_event_id.as_str().to_owned(), + signed_event_id: receipt.signed_event_id.as_str().to_owned(), + outbox_operation_id: receipt.outbox_operation_id, + outbox_event_id: receipt.outbox_event_id, + state: sdk_mutation_state_key(receipt.state).to_owned(), + idempotency_digest_prefix: receipt.idempotency_digest_prefix, + actor_pubkey, + } +} + +fn app_sdk_order_revision_decision_receipt( + receipt: OrderRevisionDecisionReceipt, + actor_pubkey: String, +) -> AppSdkWorkflowReceipt { + AppSdkWorkflowReceipt { + operation_kind: ORDER_REVISION_DECISION_OPERATION_KIND.to_owned(), + expected_event_id: receipt.expected_event_id.as_str().to_owned(), + signed_event_id: receipt.signed_event_id.as_str().to_owned(), + outbox_operation_id: receipt.outbox_operation_id, + outbox_event_id: receipt.outbox_event_id, + state: sdk_mutation_state_key(receipt.state).to_owned(), + idempotency_digest_prefix: receipt.idempotency_digest_prefix, + actor_pubkey, + } +} + +fn app_sdk_order_cancellation_receipt( + receipt: OrderCancellationReceipt, + actor_pubkey: String, +) -> AppSdkWorkflowReceipt { + AppSdkWorkflowReceipt { + operation_kind: ORDER_CANCELLATION_OPERATION_KIND.to_owned(), + expected_event_id: receipt.expected_event_id.as_str().to_owned(), + signed_event_id: receipt.signed_event_id.as_str().to_owned(), + outbox_operation_id: receipt.outbox_operation_id, + outbox_event_id: receipt.outbox_event_id, + state: sdk_mutation_state_key(receipt.state).to_owned(), + idempotency_digest_prefix: receipt.idempotency_digest_prefix, + actor_pubkey, + } +} + +fn app_sdk_order_fulfillment_update_receipt( + receipt: OrderFulfillmentUpdateReceipt, + actor_pubkey: String, +) -> AppSdkWorkflowReceipt { + AppSdkWorkflowReceipt { + operation_kind: ORDER_FULFILLMENT_UPDATE_OPERATION_KIND.to_owned(), + expected_event_id: receipt.expected_event_id.as_str().to_owned(), + signed_event_id: receipt.signed_event_id.as_str().to_owned(), + outbox_operation_id: receipt.outbox_operation_id, + outbox_event_id: receipt.outbox_event_id, + state: sdk_mutation_state_key(receipt.state).to_owned(), + idempotency_digest_prefix: receipt.idempotency_digest_prefix, + actor_pubkey, + } +} + +fn app_sdk_order_receipt_record_receipt( + receipt: OrderReceiptRecordReceipt, + actor_pubkey: String, +) -> AppSdkWorkflowReceipt { + AppSdkWorkflowReceipt { + operation_kind: ORDER_RECEIPT_RECORD_OPERATION_KIND.to_owned(), + expected_event_id: receipt.expected_event_id.as_str().to_owned(), + signed_event_id: receipt.signed_event_id.as_str().to_owned(), + outbox_operation_id: receipt.outbox_operation_id, + outbox_event_id: receipt.outbox_event_id, + state: sdk_mutation_state_key(receipt.state).to_owned(), + idempotency_digest_prefix: receipt.idempotency_digest_prefix, + actor_pubkey, + } +} + fn sdk_mutation_state_key(state: SdkMutationState) -> &'static str { match state { SdkMutationState::StoredAndQueued => "enqueued", diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs @@ -7,7 +7,10 @@ use radroots_sdk::protocol::order::{ RadrootsOrderRevisionOutcome, }; use radroots_sdk::{ - FARM_PUBLISH_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND, + FARM_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, + ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND, + ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, + ORDER_SUBMIT_OPERATION_KIND, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -49,11 +52,11 @@ impl AppPublishWorkKind { Self::Listing => "listing.publish_draft_with_identity", Self::OrderRequest => ORDER_SUBMIT_OPERATION_KIND, Self::OrderDecision => ORDER_DECISION_OPERATION_KIND, - Self::OrderRevisionProposal => "trade.publish_order_revision_proposal_with_identity", - Self::OrderRevisionDecision => "trade.publish_order_revision_decision_with_identity", - Self::OrderCancellation => "trade.publish_order_cancellation_with_identity", - Self::OrderFulfillment => "trade.publish_fulfillment_update_with_identity", - Self::OrderReceipt => "trade.publish_buyer_receipt_with_identity", + Self::OrderRevisionProposal => ORDER_REVISION_PROPOSAL_OPERATION_KIND, + Self::OrderRevisionDecision => ORDER_REVISION_DECISION_OPERATION_KIND, + Self::OrderCancellation => ORDER_CANCELLATION_OPERATION_KIND, + Self::OrderFulfillment => ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, + Self::OrderReceipt => ORDER_RECEIPT_RECORD_OPERATION_KIND, } } } @@ -320,13 +323,15 @@ impl AppPublishPayload { pub const fn legacy_sdk_transport_mode(&self) -> Option<SdkTransportMode> { match self { - Self::FarmProfile(_) | Self::OrderRequest(_) | Self::OrderDecision(_) => None, - Self::Listing(_) + Self::Listing(_) => Some(SdkTransportMode::RelayDirect), + Self::FarmProfile(_) + | Self::OrderRequest(_) + | Self::OrderDecision(_) | Self::OrderRevisionProposal(_) | Self::OrderRevisionDecision(_) | Self::OrderCancellation(_) | Self::OrderFulfillment(_) - | Self::OrderReceipt(_) => Some(SdkTransportMode::RelayDirect), + | Self::OrderReceipt(_) => None, } } @@ -801,7 +806,9 @@ mod tests { AppOrderRequestPublishPayload, AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload, AppPublishContext, AppPublishPayload, AppPublishValidationFailure, AppPublishWorkKind, FARM_PUBLISH_OPERATION_KIND, - ORDER_DECISION_OPERATION_KIND, + ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, + ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND, + ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, }; use crate::{ PendingSyncOperation, PendingSyncOperationState, SyncAggregateRef, SyncOperationKind, @@ -1067,15 +1074,15 @@ mod tests { assert_eq!( cancellation.work_kind().sdk_operation(), - "trade.publish_order_cancellation_with_identity" + ORDER_CANCELLATION_OPERATION_KIND ); assert_eq!( fulfillment.work_kind().sdk_operation(), - "trade.publish_fulfillment_update_with_identity" + ORDER_FULFILLMENT_UPDATE_OPERATION_KIND ); assert_eq!( receipt.work_kind().sdk_operation(), - "trade.publish_buyer_receipt_with_identity" + ORDER_RECEIPT_RECORD_OPERATION_KIND ); assert_eq!(fulfillment.validation_failures(), Vec::new()); @@ -1224,12 +1231,12 @@ mod tests { assert_eq!( valid_proposal.work_kind().sdk_operation(), - "trade.publish_order_revision_proposal_with_identity" + ORDER_REVISION_PROPOSAL_OPERATION_KIND ); assert_eq!(valid_proposal.validation_failures(), Vec::new()); assert_eq!( invalid_decision.work_kind().sdk_operation(), - "trade.publish_order_revision_decision_with_identity" + ORDER_REVISION_DECISION_OPERATION_KIND ); let proposal_reason_codes: Vec<&str> = invalid_proposal