commit c3f8b0d732d12def73289caa15e0f9704b896ff2
parent 3e1416eb9f62025d7c7b2e53bb5d589252d7beb5
Author: triesap <tyson@radroots.org>
Date: Fri, 19 Jun 2026 03:52:57 -0700
runtime: migrate listing publish to SDK runtime
- enqueue product listing publish through AppSdkRuntime
- remove legacy direct SDK listing publish transport
- record listing SDK migration receipts
- align listing publish metadata with the SDK operation kind
Diffstat:
2 files changed, 277 insertions(+), 587 deletions(-)
diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs
@@ -9,13 +9,14 @@ use chrono::{DateTime, Duration, Utc};
use radroots_app_core::{
AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode,
AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics,
- 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,
+ AppSdkFarmPublishRequest, AppSdkLifecycleState, AppSdkListingPublishRequest,
+ 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::{
@@ -119,11 +120,10 @@ use radroots_sdk::protocol::order::{
RadrootsOrderRevisionProposal,
};
use radroots_sdk::{
- 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,
+ FARM_PUBLISH_OPERATION_KIND, LISTING_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 radroots_sql_core::SqliteExecutor;
use radroots_trade::listing::parse_public_listing_address;
@@ -163,6 +163,7 @@ use crate::remote_signer::{
const APP_DATABASE_FILE_NAME: &str = "app.sqlite3";
const SYNC_TRANSPORT_UNAVAILABLE_MESSAGE: &str = "remote sync transport is not configured";
+const APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE: &str = "app sync publish work uses AppSdkRuntime";
const APP_DIRECT_RELAY_SYNC_TIMEOUT_MS: u64 = 2_000;
const APP_DIRECT_RELAY_CONNECT_TIMEOUT: StdDuration = StdDuration::from_secs(10);
const APP_DIRECT_RELAY_INGEST_LIMIT: usize = 1_000;
@@ -304,30 +305,22 @@ enum AppDirectRelayIngestError {
#[derive(Clone)]
struct SdkDirectRelayAppSyncTransport {
- accounts_manager: RadrootsNostrAccountsManager,
relay_urls: Vec<String>,
- timeout_ms: u64,
}
impl SdkDirectRelayAppSyncTransport {
- fn new(accounts_manager: RadrootsNostrAccountsManager, nostr_relay_urls: Vec<String>) -> Self {
+ fn new(_accounts_manager: RadrootsNostrAccountsManager, nostr_relay_urls: Vec<String>) -> Self {
Self {
- accounts_manager,
relay_urls: nostr_relay_urls,
- timeout_ms: APP_DIRECT_RELAY_SYNC_TIMEOUT_MS,
}
}
#[cfg(test)]
fn with_relay_urls(
- accounts_manager: RadrootsNostrAccountsManager,
+ _accounts_manager: RadrootsNostrAccountsManager,
relay_urls: Vec<String>,
) -> Self {
- Self {
- accounts_manager,
- relay_urls,
- timeout_ms: APP_DIRECT_RELAY_SYNC_TIMEOUT_MS,
- }
+ Self { relay_urls }
}
fn sync_with_sdk(
@@ -335,79 +328,29 @@ impl SdkDirectRelayAppSyncTransport {
request: AppSyncRequest,
) -> Result<AppSyncResult, AppSyncTransportError> {
let run_started_at = current_utc_timestamp();
- let relay_urls = normalized_app_sync_relay_urls(&self.relay_urls)?;
- let client = direct_relay_sdk_client(relay_urls.clone(), self.timeout_ms)?;
- let mut published_receipts = Vec::new();
-
- for operation in &request.pending_operations {
- match publish_pending_sync_operation(
- &client,
- &self.accounts_manager,
- operation,
- &relay_urls,
- ) {
- Ok(receipt) => published_receipts.push(receipt),
- Err(error) => {
- if published_receipts.is_empty() {
- return Err(error);
- }
- return Ok(partial_failed_sync_result(
- &request,
- published_receipts,
- run_started_at,
- error,
- ));
- }
- }
+ let _relay_urls = normalized_app_sync_relay_urls(&self.relay_urls)?;
+
+ if !request.pending_operations.is_empty() {
+ return Err(AppSyncTransportError::failed(
+ APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE,
+ ));
}
Ok(AppSyncResult {
run_status: radroots_app_sync::AppSyncRunStatus::Succeeded,
checkpoint: SyncCheckpointStatus::current(
- request.checkpoint.last_sync_started_at.clone(),
+ Some(run_started_at),
current_utc_timestamp(),
request.checkpoint.last_remote_cursor.clone(),
),
- pushed_operation_count: request.pending_operations.len(),
+ pushed_operation_count: 0,
pulled_record_count: 0,
conflicts: request.known_conflicts,
- published_receipts,
+ published_receipts: Vec::new(),
})
}
}
-fn publish_pending_sync_operation(
- client: &RadrootsSdkClient,
- accounts_manager: &RadrootsNostrAccountsManager,
- operation: &PendingSyncOperation,
- relay_urls: &[String],
-) -> Result<AppPublishedOperationReceipt, AppSyncTransportError> {
- if operation.operation != SyncOperationKind::Upsert {
- return Err(AppSyncTransportError::failed(
- "direct relay app sync supports upsert publish work only",
- ));
- }
- let publish_payload = operation.publish_payload().map_err(|error| {
- AppSyncTransportError::failed(format!(
- "pending app sync operation is not a typed publish payload: {error}"
- ))
- })?;
- publish_payload.validate().map_err(|error| {
- let reason_codes = error
- .reason_codes
- .into_iter()
- .map(|reason| reason.storage_key())
- .collect::<Vec<_>>()
- .join(",");
- AppSyncTransportError::failed(format!(
- "pending app publish work is blocked: {reason_codes}"
- ))
- })?;
- let identity = signing_identity_for_publish_payload(accounts_manager, &publish_payload)?;
- let receipt = publish_app_payload_sync(client, &identity, &publish_payload, relay_urls)?;
- published_operation_receipt(operation.operation_key.as_str(), &publish_payload, receipt)
-}
-
fn signing_identity_for_publish_payload(
accounts_manager: &RadrootsNostrAccountsManager,
publish_payload: &AppPublishPayload,
@@ -458,27 +401,6 @@ fn publish_payload_context(publish_payload: &AppPublishPayload) -> &AppPublishCo
}
}
-fn partial_failed_sync_result(
- request: &AppSyncRequest,
- published_receipts: Vec<AppPublishedOperationReceipt>,
- run_started_at: String,
- error: AppSyncTransportError,
-) -> AppSyncResult {
- AppSyncResult {
- run_status: radroots_app_sync::AppSyncRunStatus::Failed,
- checkpoint: SyncCheckpointStatus::failed(
- Some(run_started_at),
- Some(current_utc_timestamp()),
- request.checkpoint.last_remote_cursor.clone(),
- error.to_string(),
- ),
- pushed_operation_count: published_receipts.len(),
- pulled_record_count: 0,
- conflicts: request.known_conflicts.clone(),
- published_receipts,
- }
-}
-
impl AppSyncTransport for SdkDirectRelayAppSyncTransport {
fn sync(&mut self, request: AppSyncRequest) -> Result<AppSyncResult, AppSyncTransportError> {
self.sync_with_sdk(request)
@@ -5023,13 +4945,19 @@ impl DesktopAppRuntimeState {
source: &str,
source_local_event_id: Option<&str>,
) -> Result<bool, AppSqliteError> {
- let Some(operation) =
- self.product_publish_operation(product_id, source, source_local_event_id)?
+ let Some(payload) =
+ self.product_publish_payload(product_id, source, source_local_event_id)?
else {
return self.refresh_selected_account_sync();
};
- self.enqueue_selected_account_sync_operations(vec![operation])
+ let (source_kind, source_record_id) = listing_publish_source_record(
+ product_id,
+ source,
+ payload.context.source_local_event_id.as_deref(),
+ );
+ self.enqueue_listing_payload_via_sdk(&payload, source_kind, source_record_id.as_str())?;
+ self.refresh_selected_account_sync()
}
fn farm_profile_publish_payload(
@@ -5071,12 +4999,12 @@ impl DesktopAppRuntimeState {
Ok(Some(payload))
}
- fn product_publish_operation(
+ fn product_publish_payload(
&self,
product_id: ProductId,
source: &str,
source_local_event_id: Option<&str>,
- ) -> Result<Option<PendingSyncOperation>, AppSqliteError> {
+ ) -> Result<Option<AppListingPublishPayload>, AppSqliteError> {
let Some(sqlite_store) = self.sqlite_store.as_ref() else {
return Ok(None);
};
@@ -5121,7 +5049,7 @@ impl DesktopAppRuntimeState {
if let Some(source_local_event_id) = source_local_event_id {
context = context.with_source_local_event_id(source_local_event_id.to_owned());
}
- let payload = AppPublishPayload::Listing(AppListingPublishPayload {
+ let payload = AppListingPublishPayload {
context,
product_id,
listing_d_tag: Some(listing_d_tag),
@@ -5141,16 +5069,15 @@ impl DesktopAppRuntimeState {
fulfillment_method: listing_fulfillment_method(&draft, farm_setup, farm_rules),
fulfillment_location: listing_fulfillment_location(&draft, farm_setup, farm_rules),
status: draft.status,
- });
- if payload.validate().is_err() {
+ };
+ if AppPublishPayload::Listing(payload.clone())
+ .validate()
+ .is_err()
+ {
return Ok(None);
}
- PendingSyncOperation::from_publish_payload(payload, current_utc_timestamp())
- .map(Some)
- .map_err(|_| AppSqliteError::InvalidProjection {
- reason: "product publish payload must serialize",
- })
+ Ok(Some(payload))
}
fn order_request_publish_payload(
@@ -5255,6 +5182,50 @@ impl DesktopAppRuntimeState {
}
}
+ fn enqueue_listing_payload_via_sdk(
+ &self,
+ payload: &AppListingPublishPayload,
+ source_kind: AppSdkMigrationReceiptSourceKind,
+ source_record_id: &str,
+ ) -> Result<(), AppSqliteError> {
+ let operation_kind = LISTING_PUBLISH_OPERATION_KIND;
+ let actor_pubkey = self
+ .local_signing_identity_for_publish_payload(&AppPublishPayload::Listing(
+ payload.clone(),
+ ))
+ .and_then(|identity| {
+ let actor_pubkey = identity.public_key_hex();
+ let request = AppSdkListingPublishRequest {
+ actor_account_id: payload.context.account_id.clone(),
+ actor_pubkey: actor_pubkey.clone(),
+ signer_keys: identity.into_keys(),
+ listing: listing_publish_payload_to_sdk_listing(payload)?,
+ target_relays: normalized_app_sync_relay_urls(&self.nostr_relay_urls)?,
+ relay_url_policy: sdk_relay_url_policy_for_targets(&self.nostr_relay_urls),
+ idempotency_key: Some(sdk_idempotency_key(source_record_id)),
+ };
+ self.enqueue_app_sdk_listing_publish(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_request_payload_via_sdk(
&self,
payload: &AppOrderRequestPublishPayload,
@@ -5662,6 +5633,13 @@ impl DesktopAppRuntimeState {
self.with_app_sdk_runtime(|runtime| runtime.enqueue_farm_publish(request))
}
+ fn enqueue_app_sdk_listing_publish(
+ &self,
+ request: AppSdkListingPublishRequest,
+ ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
+ self.with_app_sdk_runtime(|runtime| runtime.enqueue_listing_publish(request))
+ }
+
fn enqueue_app_sdk_order_submit(
&self,
request: AppSdkOrderSubmitRequest,
@@ -7982,6 +7960,26 @@ fn farm_publish_source_record(
})
}
+fn listing_publish_source_record(
+ product_id: ProductId,
+ source: &str,
+ source_local_event_id: Option<&str>,
+) -> (AppSdkMigrationReceiptSourceKind, String) {
+ source_local_event_id
+ .map(|record_id| {
+ (
+ AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
+ record_id.to_owned(),
+ )
+ })
+ .unwrap_or_else(|| {
+ (
+ AppSdkMigrationReceiptSourceKind::LocalOutbox,
+ format!("app:listing_publish:{product_id}:{source}"),
+ )
+ })
+}
+
fn order_decision_sdk_source_record_id(payload: &AppOrderDecisionPublishPayload) -> String {
format!("app:order_decision:{}", payload.app_order_id)
}
@@ -8135,76 +8133,6 @@ fn sync_transport_error_detail_json(error: &AppSyncTransportError) -> serde_json
}
}
-fn direct_relay_sdk_client(
- relay_urls: Vec<String>,
- timeout_ms: u64,
-) -> Result<RadrootsSdkClient, AppSyncTransportError> {
- let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom);
- config.transport = SdkTransportMode::RelayDirect;
- config.signer = SignerConfig::LocalIdentity;
- config.relay = RelayConfig { urls: relay_urls };
- config.network.timeout_ms = timeout_ms;
- RadrootsSdkClient::from_config(config)
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))
-}
-
-fn publish_app_payload_sync(
- client: &RadrootsSdkClient,
- identity: &RadrootsIdentity,
- payload: &AppPublishPayload,
- configured_relay_urls: &[String],
-) -> Result<SdkPublishReceipt, AppSyncTransportError> {
- let runtime = TokioRuntimeBuilder::new_current_thread()
- .enable_all()
- .build()
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))?;
- runtime.block_on(async {
- publish_app_payload(client, identity, payload, configured_relay_urls).await
- })
-}
-
-async fn publish_app_payload(
- client: &RadrootsSdkClient,
- identity: &RadrootsIdentity,
- payload: &AppPublishPayload,
- _configured_relay_urls: &[String],
-) -> Result<SdkPublishReceipt, AppSyncTransportError> {
- match payload {
- AppPublishPayload::FarmProfile(_) => Err(AppSyncTransportError::failed(
- "farm profile publish uses AppSdkRuntime",
- )),
- AppPublishPayload::Listing(payload) => {
- let listing = listing_publish_payload_to_sdk_listing(payload)?;
- client
- .listing()
- .publish_with_identity(identity, &listing)
- .await
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))
- }
- AppPublishPayload::OrderRequest(_) => Err(AppSyncTransportError::failed(
- "order request publish uses AppSdkRuntime",
- )),
- AppPublishPayload::OrderDecision(_) => Err(AppSyncTransportError::failed(
- "order decision publish uses AppSdkRuntime",
- )),
- 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",
- )),
- }
-}
-
fn listing_publish_payload_to_sdk_listing(
payload: &AppListingPublishPayload,
) -> Result<RadrootsListing, AppSyncTransportError> {
@@ -8540,112 +8468,6 @@ fn order_request_publish_payload_to_sdk_order(
.map_err(|error| AppSyncTransportError::failed(error.to_string()))
}
-fn published_operation_receipt(
- operation_key: &str,
- payload: &AppPublishPayload,
- receipt: SdkPublishReceipt,
-) -> Result<AppPublishedOperationReceipt, AppSyncTransportError> {
- let SdkTransportReceipt::RelayDirect(relay_receipt) = receipt.transport_receipt else {
- return Err(AppSyncTransportError::failed(
- "direct relay app sync received non-relay receipt",
- ));
- };
- let (source_account_id, source_local_event_id, listing_addr) = match payload {
- AppPublishPayload::FarmProfile(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- None,
- ),
- AppPublishPayload::Listing(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- None,
- ),
- AppPublishPayload::OrderRequest(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- payload.listing_addr.clone(),
- ),
- AppPublishPayload::OrderDecision(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- Some(payload.listing_addr.clone()),
- ),
- AppPublishPayload::OrderRevisionProposal(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- Some(payload.listing_addr.clone()),
- ),
- AppPublishPayload::OrderRevisionDecision(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- Some(payload.listing_addr.clone()),
- ),
- AppPublishPayload::OrderCancellation(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- Some(payload.listing_addr.clone()),
- ),
- AppPublishPayload::OrderFulfillment(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- Some(payload.listing_addr.clone()),
- ),
- AppPublishPayload::OrderReceipt(payload) => (
- payload.context.account_id.clone(),
- payload.context.source_local_event_id.clone(),
- Some(payload.listing_addr.clone()),
- ),
- };
- let failed_relays = relay_receipt
- .failed_relays
- .iter()
- .map(|failure| {
- RelayDeliveryFailure::new(failure.relay_url.as_str(), failure.error.as_str())
- .map_err(|source| AppSyncTransportError::failed(source.to_string()))
- })
- .collect::<Result<Vec<_>, _>>()?;
- let delivery_evidence = RelayDeliveryEvidence::acknowledged(
- &relay_receipt.target_relays,
- &relay_receipt.connected_relays,
- &relay_receipt.acknowledged_relays,
- failed_relays,
- )
- .map_err(|source| AppSyncTransportError::failed(source.to_string()))?;
- let relay_set_fingerprint = delivery_evidence.relay_set_fingerprint().ok_or_else(|| {
- AppSyncTransportError::failed("direct relay publish requires a non-empty relay set")
- })?;
- let relay_delivery_json = delivery_evidence
- .to_json_value()
- .map_err(|source| AppSyncTransportError::failed(source.to_string()))?;
- let raw_event_json = json!({
- "id": relay_receipt.event.id.clone(),
- "pubkey": relay_receipt.event.author.clone(),
- "created_at": relay_receipt.event.created_at,
- "kind": relay_receipt.event.kind,
- "tags": relay_receipt.event.tags.clone(),
- "content": relay_receipt.event.content.clone(),
- "sig": relay_receipt.event.sig.clone(),
- });
-
- Ok(AppPublishedOperationReceipt {
- operation_key: operation_key.to_owned(),
- source_account_id,
- source_local_event_id,
- listing_addr,
- event_id: relay_receipt.event_id,
- event_kind: relay_receipt.event_kind,
- event_pubkey: relay_receipt.event.author.clone(),
- event_created_at: relay_receipt.event.created_at,
- event_tags_json: json!(relay_receipt.event.tags),
- event_content: relay_receipt.event.content.clone(),
- event_sig: relay_receipt.signature,
- raw_event_json,
- relay_set_fingerprint,
- relay_delivery_json,
- })
-}
-
fn d_tag_from_uuid(uuid: Uuid) -> String {
base64_url_no_pad(uuid.as_bytes())
}
@@ -10853,8 +10675,8 @@ mod tests {
RelayDeliveryEvidence, SourceRuntime,
};
use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp,
- radroots_event_from_nostr, radroots_nostr_build_event,
+ RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrKeys, RadrootsNostrSecretKey,
+ RadrootsNostrTimestamp, radroots_event_from_nostr, radroots_nostr_build_event,
};
use radroots_nostr_accounts::prelude::{
RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore,
@@ -10873,7 +10695,9 @@ mod tests {
RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal,
RadrootsOrderSettlementDecision, RadrootsOrderSettlementOutcome,
};
- use radroots_sdk::{ORDER_DECISION_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND};
+ use radroots_sdk::{
+ LISTING_PUBLISH_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND,
+ };
use radroots_sql_core::{SqlExecutor, SqliteExecutor};
use radroots_trade::order::radroots_order_economics_digest;
use serde_json::json;
@@ -10890,11 +10714,12 @@ mod tests {
"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
use super::{
- APP_DATABASE_FILE_NAME, DesktopAppRuntime, DesktopAppRuntimeActivityContextError,
- DesktopAppRuntimeCommandError, DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeState,
- DesktopAppSdkDiagnosticsState, DesktopAppSyncStatusSummary, DesktopRemoteSignerPaths,
- SYNC_TRANSPORT_UNAVAILABLE_MESSAGE, SdkDirectRelayAppSyncTransport, TokioRuntimeBuilder,
- default_sync_transport, direct_relay_event_source_runtime, farm_sync_payload, is_hex_64,
+ APP_DATABASE_FILE_NAME, APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE, DesktopAppRuntime,
+ DesktopAppRuntimeActivityContextError, DesktopAppRuntimeCommandError,
+ DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeState, DesktopAppSdkDiagnosticsState,
+ DesktopAppSyncStatusSummary, DesktopRemoteSignerPaths, SYNC_TRANSPORT_UNAVAILABLE_MESSAGE,
+ SdkDirectRelayAppSyncTransport, TokioRuntimeBuilder, default_sync_transport,
+ direct_relay_event_source_runtime, farm_sync_payload, is_hex_64,
order_decision_publish_payload_to_sdk_decision, pending_sync_upsert,
signed_event_from_local_record,
};
@@ -11099,12 +10924,11 @@ mod tests {
assert_eq!(value["missing_provenance_relays"], json!([relay_url]));
}
- fn assert_migrated_payload_uses_sdk_runtime(
- error: AppSyncTransportError,
- expected_message: &str,
- ) {
+ fn assert_migrated_payload_uses_sdk_runtime(error: AppSyncTransportError) {
match error {
- AppSyncTransportError::Failed { message } => assert_eq!(message, expected_message),
+ AppSyncTransportError::Failed { message } => {
+ assert_eq!(message, APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE)
+ }
unexpected => panic!("unexpected migrated payload error: {unexpected}"),
}
}
@@ -11139,6 +10963,33 @@ mod tests {
})
}
+ fn publish_signed_test_event_to_relay(relay: &ThreadedAckRelay, event: &RadrootsNostrEvent) {
+ let runtime = TokioRuntimeBuilder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("test relay publish runtime should build");
+ runtime.block_on(async {
+ let client = RadrootsNostrClient::new_signerless();
+ client
+ .add_write_relay(relay.url())
+ .await
+ .expect("test relay should accept write relay");
+ let connection_output = client.try_connect(StdDuration::from_secs(5)).await;
+ assert!(
+ !connection_output.success.is_empty(),
+ "test relay write connection should succeed"
+ );
+ let output = client
+ .send_event_to(vec![relay.url().to_owned()], event)
+ .await
+ .expect("test event should publish to relay");
+ assert!(
+ !output.success.is_empty(),
+ "test event publish should be acknowledged"
+ );
+ });
+ }
+
impl Drop for ThreadedAckRelay {
fn drop(&mut self) {
if let Some(shutdown_tx) = self.shutdown_tx.take() {
@@ -11244,7 +11095,7 @@ mod tests {
}
#[test]
- fn runtime_direct_relay_transport_publishes_typed_farm_work() {
+ fn runtime_direct_relay_transport_rejects_typed_farm_work() {
let relay_a = ThreadedAckRelay::spawn();
let relay_b = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
@@ -11275,13 +11126,13 @@ mod tests {
})
.expect_err("direct relay farm publish should use AppSdkRuntime");
- assert_migrated_payload_uses_sdk_runtime(error, "farm profile publish uses AppSdkRuntime");
+ assert_migrated_payload_uses_sdk_runtime(error);
assert_eq!(relay_a.event_count(), 0);
assert_eq!(relay_b.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_publishes_typed_listing_work() {
+ fn runtime_direct_relay_transport_rejects_typed_listing_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let account_id = manager
@@ -11291,63 +11142,31 @@ mod tests {
.get_signing_identity(&account_id)
.expect("seller signer lookup should succeed")
.expect("seller account should have local signer");
- let farm_id = FarmId::new();
- let product_id = ProductId::new();
- let payload = AppPublishPayload::Listing(AppListingPublishPayload {
- context: AppPublishContext::new(account_id.to_string(), "listing_publish")
- .with_source_local_event_id("app:local_work:listing:direct"),
- product_id,
- listing_d_tag: Some(super::d_tag_from_uuid(product_id.as_uuid())),
- farm_id: Some(farm_id),
- farm_pubkey: Some(identity.public_key_hex()),
- farm_d_tag: Some(super::d_tag_from_uuid(farm_id.as_uuid())),
- title: "North field eggs".to_owned(),
- subtitle: Some("Pasture raised".to_owned()),
- category: Some("eggs".to_owned()),
- unit_label: "each".to_owned(),
- price_minor_units: Some(750),
- price_currency: "USD".to_owned(),
- stock_quantity: Some(12),
- availability_window_id: Some(FulfillmentWindowId::new()),
- availability_starts_at: Some("2099-05-25T14:00:00Z".to_owned()),
- availability_ends_at: Some("2099-05-25T18:00:00Z".to_owned()),
- fulfillment_method: Some("pickup".to_owned()),
- fulfillment_location: Some("farmstand".to_owned()),
- status: ProductStatus::Published,
- });
+ let payload = direct_relay_listing_payload(
+ account_id.to_string().as_str(),
+ identity.public_key_hex(),
+ "listing_publish",
+ );
let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
.expect("typed listing publish work should serialize");
let mut transport =
SdkDirectRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
- let result = transport
+ let error = transport
.sync(AppSyncRequest {
trigger: SyncTrigger::ManualRefresh,
checkpoint: SyncCheckpointStatus::never_synced(),
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect("direct relay listing publish should succeed");
+ .expect_err("direct relay listing publish should use AppSdkRuntime");
- assert_eq!(result.run_status, AppSyncRunStatus::Succeeded);
- assert_eq!(result.pushed_operation_count, 1);
- assert_eq!(result.published_receipts.len(), 1);
- assert_eq!(result.published_receipts[0].event_kind, 30402);
- assert_eq!(
- result.published_receipts[0].event_pubkey,
- identity.public_key_hex()
- );
- assert_eq!(
- result.published_receipts[0]
- .source_local_event_id
- .as_deref(),
- Some("app:local_work:listing:direct")
- );
- assert_eq!(relay.event_count(), 1);
+ assert_migrated_payload_uses_sdk_runtime(error);
+ assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_publishes_typed_order_request_work() {
+ fn runtime_direct_relay_transport_rejects_typed_order_request_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let account_id = manager
@@ -11434,7 +11253,7 @@ mod tests {
})
.expect_err("direct relay order request publish should use AppSdkRuntime");
- assert_migrated_payload_uses_sdk_runtime(error, "order request publish uses AppSdkRuntime");
+ assert_migrated_payload_uses_sdk_runtime(error);
assert_eq!(relay.event_count(), 0);
}
@@ -11481,15 +11300,12 @@ mod tests {
})
.expect_err("direct relay order decision publish should use AppSdkRuntime");
- assert_migrated_payload_uses_sdk_runtime(
- error,
- "order decision publish uses AppSdkRuntime",
- );
+ assert_migrated_payload_uses_sdk_runtime(error);
assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_publishes_typed_order_lifecycle_work() {
+ fn runtime_direct_relay_transport_rejects_typed_order_lifecycle_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let buyer_account_id = manager
@@ -11645,65 +11461,17 @@ mod tests {
let mut transport =
SdkDirectRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
- let result = transport
+ let error = transport
.sync(AppSyncRequest {
trigger: SyncTrigger::ManualRefresh,
checkpoint: SyncCheckpointStatus::never_synced(),
pending_operations: operations,
known_conflicts: Vec::new(),
})
- .expect("direct relay lifecycle publish should succeed");
+ .expect_err("direct relay lifecycle publish should use AppSdkRuntime");
- assert_eq!(result.run_status, AppSyncRunStatus::Succeeded);
- assert_eq!(result.pushed_operation_count, 5);
- assert_eq!(relay.event_count(), 5);
- let kinds = result
- .published_receipts
- .iter()
- .map(|receipt| receipt.event_kind)
- .collect::<Vec<_>>();
- assert_eq!(kinds, vec![3424, 3425, 3432, 3433, 3434]);
- for receipt in &result.published_receipts {
- let event = published_receipt_event(receipt);
- match receipt.event_kind {
- 3424 => {
- let envelope =
- radroots_sdk::protocol::order::parse_order_revision_proposal(&event)
- .expect("order revision proposal should parse");
- assert_eq!(envelope.payload.reason, "harvest count updated");
- assert_eq!(envelope.payload.items[0].bin_count, 3);
- }
- 3425 => {
- let envelope =
- radroots_sdk::protocol::order::parse_order_revision_decision(&event)
- .expect("order revision decision should parse");
- assert_eq!(envelope.payload.revision_id, "revision-1");
- assert_eq!(
- envelope.payload.decision,
- RadrootsOrderRevisionOutcome::Accepted
- );
- }
- 3432 => {
- let envelope = radroots_sdk::protocol::order::parse_order_cancellation(&event)
- .expect("order cancellation should parse");
- assert_eq!(envelope.payload.reason, "buyer cancelled order");
- }
- 3433 => {
- let envelope = radroots_sdk::protocol::order::parse_fulfillment_update(&event)
- .expect("fulfillment update should parse");
- assert_eq!(
- envelope.payload.status,
- RadrootsOrderFulfillmentState::ReadyForPickup
- );
- }
- 3434 => {
- let envelope = radroots_sdk::protocol::order::parse_buyer_receipt(&event)
- .expect("buyer receipt should parse");
- assert!(envelope.payload.received);
- }
- _ => panic!("unexpected lifecycle event kind"),
- }
- }
+ assert_migrated_payload_uses_sdk_runtime(error);
+ assert_eq!(relay.event_count(), 0);
}
#[test]
@@ -11819,7 +11587,7 @@ mod tests {
Some(seller_pubkey.as_str()),
listing_d_tag.as_str(),
);
- let listing_payload = AppPublishPayload::Listing(AppListingPublishPayload {
+ let listing_payload = AppListingPublishPayload {
context: AppPublishContext::new(account_id.to_string(), "relay_ingest_listing"),
product_id,
listing_d_tag: Some(listing_d_tag),
@@ -11839,25 +11607,18 @@ mod tests {
fulfillment_method: Some("pickup".to_owned()),
fulfillment_location: Some("Relay barn".to_owned()),
status: ProductStatus::Published,
- });
- let mut transport =
- SdkDirectRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
- let result = transport
- .sync(AppSyncRequest {
- trigger: SyncTrigger::ManualRefresh,
- checkpoint: SyncCheckpointStatus::never_synced(),
- pending_operations: vec![
- PendingSyncOperation::from_publish_payload(
- listing_payload,
- "2026-05-25T07:00:01Z",
- )
- .expect("listing publish payload should serialize"),
- ],
- known_conflicts: Vec::new(),
- })
- .expect("seller relay publish should succeed");
- assert_eq!(result.run_status, AppSyncRunStatus::Succeeded);
- assert_eq!(result.published_receipts.len(), 1);
+ };
+ let listing = super::listing_publish_payload_to_sdk_listing(&listing_payload)
+ .expect("listing payload should convert to SDK listing");
+ let parts = radroots_sdk::protocol::listing::build_draft(&listing)
+ .expect("listing draft should build")
+ .into_wire_parts();
+ let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
+ .expect("listing event builder should build")
+ .sign_with_keys(identity.keys())
+ .expect("listing event should sign");
+ publish_signed_test_event_to_relay(relay, &event);
+ assert_eq!(relay.event_count(), 1);
projected_product_id
}
@@ -12105,7 +11866,7 @@ mod tests {
}
#[test]
- fn runtime_direct_relay_transport_returns_partial_failure_after_successful_prefix() {
+ fn runtime_direct_relay_transport_rejects_publish_work_before_partial_progress() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let account_id = manager
@@ -12132,26 +11893,17 @@ mod tests {
let mut transport =
SdkDirectRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
- let result = transport
+ let error = transport
.sync(AppSyncRequest {
trigger: SyncTrigger::ManualRefresh,
checkpoint: SyncCheckpointStatus::never_synced(),
pending_operations: vec![successful_operation, unsupported_operation],
known_conflicts: Vec::new(),
})
- .expect("successful prefix should return a partial result");
+ .expect_err("publish work should use AppSdkRuntime before partial progress");
- assert_eq!(result.run_status, AppSyncRunStatus::Failed);
- assert_eq!(result.pushed_operation_count, 1);
- assert_eq!(result.published_receipts.len(), 1);
- assert_eq!(result.checkpoint.state, SyncCheckpointState::Failed);
- assert!(
- result
- .checkpoint
- .last_error_message
- .as_deref()
- .is_some_and(|message| message.contains("supports upsert"))
- );
+ assert_migrated_payload_uses_sdk_runtime(error);
+ assert_eq!(relay.event_count(), 0);
}
#[test]
@@ -12260,28 +12012,21 @@ mod tests {
})
.expect_err("direct relay order request should use AppSdkRuntime");
- assert_migrated_payload_uses_sdk_runtime(error, "order request publish uses AppSdkRuntime");
+ assert_migrated_payload_uses_sdk_runtime(error);
assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_signs_with_payload_account_context() {
+ fn runtime_direct_relay_transport_rejects_payload_account_context_publish_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let first_account_id = manager
.generate_identity(Some("First".to_owned()), true)
.expect("first account");
- let second_account_id = manager
- .generate_identity(Some("Second".to_owned()), true)
- .expect("second account");
let first_identity = manager
.get_signing_identity(&first_account_id)
.expect("first signer")
.expect("first local signer");
- let second_identity = manager
- .get_signing_identity(&second_account_id)
- .expect("second signer")
- .expect("second local signer");
let payload = direct_relay_listing_payload(
first_account_id.to_string().as_str(),
first_identity.public_key_hex(),
@@ -12292,29 +12037,21 @@ mod tests {
let mut transport =
SdkDirectRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
- let result = transport
+ let error = transport
.sync(AppSyncRequest {
trigger: SyncTrigger::ManualRefresh,
checkpoint: SyncCheckpointStatus::never_synced(),
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect("payload account signer should publish");
+ .expect_err("payload account publish work should use AppSdkRuntime");
- assert_eq!(result.run_status, AppSyncRunStatus::Succeeded);
- assert_eq!(result.published_receipts.len(), 1);
- assert_eq!(
- result.published_receipts[0].event_pubkey,
- first_identity.public_key_hex()
- );
- assert_ne!(
- result.published_receipts[0].event_pubkey,
- second_identity.public_key_hex()
- );
+ assert_migrated_payload_uses_sdk_runtime(error);
+ assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_rejects_missing_account_context() {
+ fn runtime_direct_relay_transport_rejects_missing_account_publish_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let farm_id = FarmId::new();
@@ -12337,18 +12074,14 @@ mod tests {
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect_err("watch-only or missing custody should not publish");
+ .expect_err("missing account publish work should use AppSdkRuntime");
- assert!(matches!(error, AppSyncTransportError::Unavailable { .. }));
- assert!(
- error
- .to_string()
- .contains("publish account is not configured locally")
- );
+ assert_migrated_payload_uses_sdk_runtime(error);
+ assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_rejects_watch_only_account_context() {
+ fn runtime_direct_relay_transport_rejects_watch_only_account_publish_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let identity = RadrootsIdentity::generate();
@@ -12373,18 +12106,14 @@ mod tests {
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect_err("watch-only account should not publish");
+ .expect_err("watch-only account publish work should use AppSdkRuntime");
- assert!(matches!(error, AppSyncTransportError::Unavailable { .. }));
- assert!(
- error
- .to_string()
- .contains("publish account is not backed by a local signing key")
- );
+ assert_migrated_payload_uses_sdk_runtime(error);
+ assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_rejects_mismatched_local_signing_custody() {
+ fn runtime_direct_relay_transport_rejects_mismatched_local_signing_publish_work() {
let relay = ThreadedAckRelay::spawn();
let store = Arc::new(RadrootsNostrMemoryAccountStore::new());
let vault = Arc::new(RadrootsNostrSecretVaultMemory::new());
@@ -12423,14 +12152,10 @@ mod tests {
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect_err("mismatched custody should not publish");
+ .expect_err("mismatched custody publish work should use AppSdkRuntime");
- assert!(matches!(error, AppSyncTransportError::Failed { .. }));
- assert!(
- error
- .to_string()
- .contains("public key does not match secret key")
- );
+ assert_migrated_payload_uses_sdk_runtime(error);
+ assert_eq!(relay.event_count(), 0);
}
#[test]
@@ -12811,7 +12536,7 @@ mod tests {
title: "Salad mix".to_owned(),
subtitle: "Cut this morning".to_owned(),
category: "greens".to_owned(),
- unit_label: "bag".to_owned(),
+ unit_label: "each".to_owned(),
price_minor_units: Some(900),
price_currency: "usd".to_owned(),
stock_quantity: Some(11),
@@ -12832,49 +12557,7 @@ mod tests {
.iter()
.filter(|pending| pending.operation.aggregate == SyncAggregateRef::Product(product_id))
.collect::<Vec<_>>();
- assert_eq!(product_pending_operations.len(), 1);
- assert_eq!(
- product_pending_operations[0].operation.operation_key,
- format!("product:{product_id}:upsert")
- );
- assert_eq!(
- product_pending_operations[0].operation.state,
- PendingSyncOperationState::Pending
- );
- let publish_payload = product_pending_operations[0]
- .operation
- .publish_payload()
- .expect("product publish operation should be typed");
- let AppPublishPayload::Listing(payload) = publish_payload else {
- panic!("product publish operation should carry listing payload")
- };
- assert_eq!(payload.product_id, product_id);
- assert_eq!(payload.category.as_deref(), Some("greens"));
- assert_eq!(payload.unit_label, "bag");
- assert_eq!(payload.price_minor_units, Some(900));
- assert_eq!(payload.price_currency, "USD");
- assert_eq!(payload.stock_quantity, Some(11));
- assert_eq!(
- payload.availability_starts_at.as_deref(),
- Some("2099-04-25T14:00:00Z")
- );
- assert_eq!(
- payload.availability_ends_at.as_deref(),
- Some("2099-04-25T18:00:00Z")
- );
- assert_eq!(payload.fulfillment_method.as_deref(), Some("pickup"));
- assert_eq!(
- payload.fulfillment_location.as_deref(),
- Some("14 Orchard Lane")
- );
- assert!(payload.farm_pubkey.as_deref().is_some_and(super::is_hex_64));
- assert!(
- payload
- .context
- .source_local_event_id
- .as_deref()
- .is_some_and(|value| value.starts_with("app:local_work:listing:"))
- );
+ assert!(product_pending_operations.is_empty());
let records = shared_local_event_records(&paths);
let listing_record = records
@@ -12897,18 +12580,41 @@ mod tests {
listing_payload["document"]["primary_bin"]["bin_id"]
.as_str()
.expect("primary bin id should be present"),
- super::listing_primary_bin_id(
- payload
- .listing_d_tag
- .as_deref()
- .expect("listing d tag should exist")
- )
+ super::listing_primary_bin_id(super::d_tag_from_uuid(product_id.as_uuid()).as_str())
);
assert_eq!(listing_payload["document"]["delivery"]["method"], "pickup");
assert_eq!(
listing_payload["document"]["location"]["primary"],
"14 Orchard Lane"
);
+ let receipt = runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .sdk_migration_receipt_repository()
+ .load_receipt(
+ AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
+ listing_record.record_id.as_str(),
+ )
+ .expect("listing SDK migration receipt should load")
+ .expect("listing SDK migration receipt should exist");
+ assert_eq!(receipt.source_record_id, listing_record.record_id);
+ assert_eq!(receipt.sdk_operation_kind, LISTING_PUBLISH_OPERATION_KIND);
+ assert_eq!(receipt.migration_state, AppSdkMigrationState::Enqueued);
+ assert!(receipt.expected_event_id.is_some());
+ assert!(
+ receipt
+ .actor_pubkey
+ .as_deref()
+ .is_some_and(super::is_hex_64)
+ );
+ assert!(!receipt.sdk_outbox_event_ids.is_empty());
+ assert!(receipt.idempotency_digest_prefix.is_some());
+ assert_eq!(
+ receipt.detail_json["operation_kind"],
+ LISTING_PUBLISH_OPERATION_KIND
+ );
cleanup_bootstrapped_runtime_paths(&paths);
}
@@ -21959,7 +21665,7 @@ mod tests {
};
let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload {
- context: AppPublishContext::new(account_id, "seller_order_decision"),
+ context: AppPublishContext::new(account_id.clone(), "seller_order_decision"),
app_order_id: order_id,
farm_id,
trade_order_id: "seller-order-decision-1".to_owned(),
@@ -21977,21 +21683,34 @@ mod tests {
});
let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
.expect("prior order decision publish work should serialize");
- let mut transport = SdkDirectRelayAppSyncTransport::with_relay_urls(
- accounts_manager,
- vec![relay.url().to_owned()],
- );
- let result = transport
- .sync(AppSyncRequest {
- trigger: SyncTrigger::ManualRefresh,
- checkpoint: SyncCheckpointStatus::never_synced(),
- pending_operations: vec![operation],
- known_conflicts: Vec::new(),
- })
- .expect("prior seller decision relay publish should succeed");
-
- assert_eq!(result.run_status, AppSyncRunStatus::Succeeded);
- assert_eq!(result.pushed_operation_count, 1);
+ let payload = operation
+ .publish_payload()
+ .expect("prior order decision operation should carry payload");
+ let AppPublishPayload::OrderDecision(payload) = payload else {
+ panic!("prior order decision operation should carry order decision payload")
+ };
+ let account_id =
+ RadrootsIdentityId::parse(account_id.as_str()).expect("selected account id");
+ let identity = accounts_manager
+ .get_signing_identity(&account_id)
+ .expect("seller signer lookup should succeed")
+ .expect("seller account should have local signer");
+ let request_event_id = test_event_id(payload.request_event_id.as_str());
+ let decision = order_decision_publish_payload_to_sdk_decision(&payload)
+ .expect("order decision payload should convert to SDK decision");
+ let parts = radroots_sdk::protocol::order::build_order_decision_draft(
+ &request_event_id,
+ &request_event_id,
+ &decision,
+ )
+ .expect("order decision draft should build")
+ .into_wire_parts();
+ let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
+ .expect("order decision event builder should build")
+ .sign_with_keys(identity.keys())
+ .expect("order decision event should sign");
+ publish_signed_test_event_to_relay(relay, &event);
+ assert_eq!(relay.event_count(), 1);
}
fn append_app_signed_listing_record(
@@ -23167,19 +22886,6 @@ mod tests {
}
}
- fn published_receipt_event(receipt: &AppPublishedOperationReceipt) -> SdkRadrootsNostrEvent {
- SdkRadrootsNostrEvent {
- id: receipt.event_id.clone(),
- author: receipt.event_pubkey.clone(),
- created_at: receipt.event_created_at,
- kind: receipt.event_kind,
- tags: serde_json::from_value(receipt.event_tags_json.clone())
- .expect("receipt event tags should decode"),
- content: receipt.event_content.clone(),
- sig: receipt.event_sig.clone(),
- }
- }
-
fn shared_local_event_records(paths: &AppDesktopRuntimePaths) -> Vec<LocalEventRecord> {
let database_path = paths
.shared_local_events_database_path()
diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs
@@ -1,16 +1,15 @@
use radroots_app_view::{
FarmId, FarmReadiness, FulfillmentWindowId, OrderId, ProductId, ProductStatus,
};
-use radroots_sdk::SdkTransportMode;
use radroots_sdk::protocol::order::{
RadrootsOrderEconomics, RadrootsOrderFulfillmentState, RadrootsOrderItem,
RadrootsOrderRevisionOutcome,
};
use radroots_sdk::{
- 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,
+ FARM_PUBLISH_OPERATION_KIND, LISTING_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,7 +48,7 @@ impl AppPublishWorkKind {
pub const fn sdk_operation(self) -> &'static str {
match self {
Self::FarmProfile => FARM_PUBLISH_OPERATION_KIND,
- Self::Listing => "listing.publish_draft_with_identity",
+ Self::Listing => LISTING_PUBLISH_OPERATION_KIND,
Self::OrderRequest => ORDER_SUBMIT_OPERATION_KIND,
Self::OrderDecision => ORDER_DECISION_OPERATION_KIND,
Self::OrderRevisionProposal => ORDER_REVISION_PROPOSAL_OPERATION_KIND,
@@ -321,20 +320,6 @@ impl AppPublishPayload {
}
}
- pub const fn legacy_sdk_transport_mode(&self) -> Option<SdkTransportMode> {
- match self {
- Self::Listing(_) => Some(SdkTransportMode::RelayDirect),
- Self::FarmProfile(_)
- | Self::OrderRequest(_)
- | Self::OrderDecision(_)
- | Self::OrderRevisionProposal(_)
- | Self::OrderRevisionDecision(_)
- | Self::OrderCancellation(_)
- | Self::OrderFulfillment(_)
- | Self::OrderReceipt(_) => None,
- }
- }
-
pub const fn operation_kind(&self) -> SyncOperationKind {
SyncOperationKind::Upsert
}
@@ -836,7 +821,6 @@ mod tests {
payload.work_kind().sdk_operation(),
FARM_PUBLISH_OPERATION_KIND
);
- assert_eq!(payload.legacy_sdk_transport_mode(), None);
assert_eq!(payload.validation_failures(), Vec::new());
let operation =