commit f981d6905a74e20414a5102f82c392cffa49a5d8
parent f3be8c83fdb19a29b1077ff8f3d561343940faae
Author: triesap <tyson@radroots.org>
Date: Thu, 16 Jul 2026 16:56:21 +0000
release-product: cut studio to semantic trade envelopes
- route Studio publish and desktop runtime paths through canonical release-product trade mutation envelopes
- remove active-order interop projection and keep validation receipts as passive signed evidence
- migrate the default store away from buyer private plaintext order fields
- expose evidence, conflict, private-terms, and attestation workflow axes in view and i18n surfaces
Diffstat:
19 files changed, 3008 insertions(+), 5425 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -5134,11 +5134,13 @@ dependencies = [
name = "radroots_event_store"
version = "0.1.0-alpha.2"
dependencies = [
+ "hex",
"radroots_event",
"radroots_nostr",
"radroots_transport",
"serde",
"serde_json",
+ "sha2",
"sqlx",
"thiserror 1.0.69",
]
@@ -5330,6 +5332,7 @@ dependencies = [
name = "radroots_sdk"
version = "0.1.0"
dependencies = [
+ "base64 0.22.1",
"hex",
"nostr",
"radroots_authority",
diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs
@@ -11,30 +11,31 @@ use radroots_core::{
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
use radroots_event::{
- RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr,
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts,
farm::{RadrootsFarm, RadrootsFarmPublicLocation, RadrootsFarmRef},
ids::{
- RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress,
- RadrootsOrderId, RadrootsPublicKey,
+ RadrootsAddressableCoordinate, RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId,
+ RadrootsPublicKey, RadrootsTradeId,
},
kinds::{
- KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST,
- KIND_PROFILE,
+ KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION,
+ KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
},
listing::{
RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingPublicLocation,
RadrootsListingStatus,
},
- order::{
- RadrootsOrderDecision, RadrootsOrderEconomics, RadrootsOrderEventType,
- RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderRequest,
+ trade::{
+ RADROOTS_TRADE_CANCELLATION_CONTRACT_ID, RADROOTS_TRADE_DECISION_CONTRACT_ID,
+ RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION,
+ RadrootsFulfillmentProfileV1, RadrootsTradeCancellationProfileV1,
+ RadrootsTradeCandidateLineV1, RadrootsTradeCandidateTermsV1, RadrootsTradeDecisionV1,
+ RadrootsTradeEconomicsProfileV1, RadrootsTradeMutationBodyV1,
+ RadrootsTradeMutationEnvelopeV1, canonical_trade_mutation_content,
},
};
-use radroots_event_codec::order::{
- order_cancellation_from_event, order_decision_from_event, order_event_context_from_tags,
- order_request_from_event,
-};
+use radroots_event_codec::trade::trade_mutation_from_event;
use radroots_identity::{RadrootsIdentity, RadrootsIdentityId};
use radroots_nostr::prelude::{
RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrOutput,
@@ -51,8 +52,8 @@ use radroots_runtime_store::{
};
use radroots_sdk::{
FARM_PUBLISH_OPERATION_KIND, LISTING_PUBLISH_OPERATION_KIND, NostrRelayUrlPolicy,
- TRADE_CANCELLATION_OPERATION_KIND, TRADE_DECISION_OPERATION_KIND, TRADE_SUBMIT_OPERATION_KIND,
- TargetSet,
+ TRADE_CANCEL_OPERATION_KIND, TRADE_DECIDE_CANDIDATE_OPERATION_KIND,
+ TRADE_SUBMIT_PROPOSAL_OPERATION_KIND, TargetSet,
};
use radroots_sql_core::SqlxSqliteExecutor;
use radroots_studio_app_core::{
@@ -63,8 +64,8 @@ use radroots_studio_app_core::{
DesktopRuntimeProjectionLifecycleState, DesktopRuntimePublicFarmLocation,
DesktopRuntimeRelayUrlPolicy, DesktopRuntimeSnapshot, DesktopRuntimeSupervisor,
DesktopRuntimeSupervisorConfig, DesktopRuntimeSupervisorError,
- DesktopRuntimeTradeCancellationRequest, DesktopRuntimeTradeDecision,
- DesktopRuntimeTradeDecisionRequest, DesktopRuntimeTradeProposeRequest, PackDayExportWriteError,
+ DesktopRuntimeTradeCancelRequest, DesktopRuntimeTradeDecideCandidateRequest,
+ DesktopRuntimeTradeSubmitProposalRequest, PackDayExportWriteError,
prepare_pack_day_export_bundle_at_data_root,
shared_runtime_store_database_path_from_shared_accounts, write_prepared_pack_day_export_bundle,
};
@@ -76,9 +77,8 @@ use radroots_studio_app_sqlite::{
AppRelayIngestSuccessInput, AppSqliteError, AppSqliteStore, BuyerOrderRuntimeStoreExport,
BuyerOrderRuntimeStoreLine, BuyerRepeatDemandApplyOutcome, DatabaseTarget,
DesktopRuntimeWorkflowReceiptInput, DesktopRuntimeWorkflowReceiptSourceKind,
- DesktopRuntimeWorkflowReceiptState, SelectedBuyerOrderScope, SellerOrderDecisionExport,
- StoredPendingSyncOperation, StoredRelayIngestCursor, StoredSyncConflict,
- derive_farm_rules_readiness, projected_order_id_from_trade_request,
+ DesktopRuntimeWorkflowReceiptState, SelectedBuyerOrderScope, StoredPendingSyncOperation,
+ StoredRelayIngestCursor, StoredSyncConflict, derive_farm_rules_readiness,
};
use radroots_studio_app_state::{
APP_STATE_FILE_NAME, AppShellProjection, AppStateCommand, AppStatePersistenceRepository,
@@ -91,12 +91,11 @@ use radroots_studio_app_state::{
derive_sync_projection,
};
use radroots_studio_app_sync::{
- AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload,
- AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload, AppOrderDecisionPublishPayload,
- AppOrderRequestItemPayload, AppOrderRequestPublishPayload, AppPublishContext,
- AppPublishPayload, AppPublishedOperationReceipt, AppRelayIngestScopeFreshness,
- AppSyncProjection, AppSyncRequest, AppSyncResult, AppSyncRunStatus, AppSyncTransport,
- AppSyncTransportError, SyncCheckpointStatus, SyncConflictSeverity, SyncTrigger,
+ AppFarmProfilePublishPayload, AppListingPublishPayload, AppPublishContext, AppPublishPayload,
+ AppPublishedOperationReceipt, AppRelayIngestScopeFreshness, AppSyncProjection, AppSyncRequest,
+ AppSyncResult, AppSyncRunStatus, AppSyncTransport, AppSyncTransportError,
+ AppTradeCancellationPublishPayload, AppTradeCandidateDecisionPublishPayload,
+ AppTradeProposalPublishPayload, SyncCheckpointStatus, SyncConflictSeverity, SyncTrigger,
};
#[cfg(test)]
use radroots_studio_app_sync::{PendingSyncOperation, SyncAggregateRef, SyncOperationKind};
@@ -117,13 +116,6 @@ use radroots_studio_app_view::{
ReminderUrgency, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection,
TodayAgendaProjection,
};
-use radroots_trade::identity::RadrootsTradeLocator;
-use radroots_trade::listing::parse_public_listing_address;
-use radroots_trade::order::{
- RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderReductionInputs,
- RadrootsOrderRequestRecord, reduce_order_events,
-};
-use radroots_trade::workflow::RadrootsTradeWorkflowState;
use serde_json::json;
use thiserror::Error;
use tokio::runtime::Builder as TokioRuntimeBuilder;
@@ -166,9 +158,11 @@ const APP_DIRECT_RELAY_INGEST_KINDS: &[u16] = &[
KIND_PROFILE as u16,
KIND_FARM as u16,
KIND_LISTING as u16,
- KIND_ORDER_REQUEST as u16,
- KIND_ORDER_DECISION as u16,
- KIND_ORDER_CANCELLATION as u16,
+ KIND_TRADE_PROPOSAL as u16,
+ KIND_TRADE_DECISION as u16,
+ KIND_TRADE_REVISION_PROPOSAL as u16,
+ KIND_TRADE_REVISION_DECISION as u16,
+ KIND_TRADE_CANCELLATION as u16,
];
#[derive(Debug, Default)]
@@ -209,42 +203,12 @@ struct AppDirectRelayFetchedRelay {
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AppSellerOrderDecisionCommand {
Accept,
- Decline {
- reason: String,
- confirm_public_note: bool,
- },
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct ResolvedAppSellerOrderRequest {
- request_event: RadrootsEventEnvelope,
- request_event_id: String,
- request_author_pubkey: String,
- listing_event_id: Option<String>,
- payload: RadrootsOrderRequest,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct ResolvedAppOrderDecisionEvidence {
- event_id: String,
- payload: RadrootsOrderDecision,
+ Decline { reason: String },
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ResolvedAppOrderLifecycleEvidence {
- request_event_id: String,
- status: RadrootsTradeWorkflowState,
- agreement_event_id: Option<String>,
- last_event_id: Option<String>,
- decision: Option<ResolvedAppOrderDecisionEvidence>,
- cancellation_event_id: Option<String>,
-}
-
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-struct AppActiveOrderEvidenceBuckets {
- requests: Vec<RadrootsOrderRequestRecord>,
- decisions: Vec<RadrootsOrderDecisionRecord>,
- cancellations: Vec<RadrootsOrderCancellationRecord>,
+ root: RadrootsTradeMutationEnvelopeV1,
}
#[derive(Debug, Default)]
@@ -349,9 +313,10 @@ fn publish_payload_context(publish_payload: &AppPublishPayload) -> &AppPublishCo
match publish_payload {
AppPublishPayload::FarmProfile(payload) => &payload.context,
AppPublishPayload::Listing(payload) => &payload.context,
- AppPublishPayload::OrderRequest(payload) => &payload.context,
- AppPublishPayload::OrderDecision(payload) => &payload.context,
- AppPublishPayload::OrderCancellation(payload) => &payload.context,
+ AppPublishPayload::TradeProposal(payload) => &payload.context,
+ AppPublishPayload::TradeRevisionProposal(payload) => &payload.context,
+ AppPublishPayload::TradeCandidateDecision(payload) => &payload.context,
+ AppPublishPayload::TradeCancellation(payload) => &payload.context,
}
}
@@ -778,7 +743,7 @@ impl DesktopAppRuntime {
pub fn prepare_order_accept(
&self,
order_id: OrderId,
- ) -> Result<AppOrderDecisionPublishPayload, AppSqliteError> {
+ ) -> Result<AppTradeCandidateDecisionPublishPayload, AppSqliteError> {
self.lock_state_mut()
.prepare_seller_order_decision(order_id, AppSellerOrderDecisionCommand::Accept)
}
@@ -787,13 +752,12 @@ impl DesktopAppRuntime {
&self,
order_id: OrderId,
reason: &str,
- confirm_public_note: bool,
- ) -> Result<AppOrderDecisionPublishPayload, AppSqliteError> {
+ _confirm_public_note: bool,
+ ) -> Result<AppTradeCandidateDecisionPublishPayload, AppSqliteError> {
self.lock_state_mut().prepare_seller_order_decision(
order_id,
AppSellerOrderDecisionCommand::Decline {
reason: reason.to_owned(),
- confirm_public_note,
},
)
}
@@ -807,13 +771,12 @@ impl DesktopAppRuntime {
&self,
order_id: OrderId,
reason: &str,
- confirm_public_note: bool,
+ _confirm_public_note: bool,
) -> Result<bool, AppSqliteError> {
self.lock_state_mut().publish_seller_order_decision(
order_id,
AppSellerOrderDecisionCommand::Decline {
reason: reason.to_owned(),
- confirm_public_note,
},
)
}
@@ -2458,7 +2421,7 @@ impl DesktopAppRuntimeState {
&mut self,
order_id: OrderId,
command: AppSellerOrderDecisionCommand,
- ) -> Result<AppOrderDecisionPublishPayload, AppSqliteError> {
+ ) -> Result<AppTradeCandidateDecisionPublishPayload, AppSqliteError> {
let _ = self.import_shared_runtime_store()?;
let relay_urls = normalized_app_sync_relay_urls(&self.nostr_relay_urls).map_err(|_| {
AppSqliteError::InvalidProjection {
@@ -2497,30 +2460,12 @@ impl DesktopAppRuntimeState {
reason: "seller order decision requires a selected seller public key",
},
)?;
- let request = self.resolve_seller_order_request_evidence(order_id)?;
- if request.payload.seller_pubkey.trim() != seller_pubkey.as_str() {
+ let lifecycle = self.resolve_order_lifecycle_evidence(order_id)?;
+ if lifecycle.root.seller_pubkey.as_str() != seller_pubkey.as_str() {
return Err(AppSqliteError::InvalidProjection {
reason: "seller order decision seller account does not match order seller",
});
}
- let listing_address = parse_public_listing_address(request.payload.listing_addr.as_str())
- .map_err(|_| AppSqliteError::InvalidProjection {
- reason: "seller order decision listing address is invalid",
- })?;
- if listing_address.seller_pubkey.as_str() != seller_pubkey.as_str() {
- return Err(AppSqliteError::InvalidProjection {
- reason: "seller order decision listing address is outside seller authority",
- });
- }
- let lifecycle = self.resolve_order_lifecycle_evidence(&request)?;
- if lifecycle.decision.is_some()
- || lifecycle.status != RadrootsTradeWorkflowState::Requested
- || lifecycle.cancellation_event_id.is_some()
- {
- return Err(AppSqliteError::InvalidProjection {
- reason: "seller order decision requires an undecided order",
- });
- }
let Some(order_export) =
sqlite_store.load_seller_order_decision_export(farm_id, order_id)?
else {
@@ -2534,43 +2479,21 @@ impl DesktopAppRuntimeState {
});
}
- let confirm_public_note = match &command {
- AppSellerOrderDecisionCommand::Accept => false,
- AppSellerOrderDecisionCommand::Decline {
- confirm_public_note,
- ..
- } => *confirm_public_note,
- };
- let decision = match command {
- AppSellerOrderDecisionCommand::Accept => AppOrderDecisionPayload::Accepted {
- inventory_commitments: seller_order_inventory_commitments(&order_export)?,
- },
- AppSellerOrderDecisionCommand::Decline { reason, .. } => {
- let reason = reason.trim();
- if reason.is_empty() {
- return Err(AppSqliteError::InvalidProjection {
- reason: "seller order decline requires a non-empty reason",
- });
- }
- AppOrderDecisionPayload::Declined {
- reason: reason.to_owned(),
- }
- }
- };
- let payload = AppOrderDecisionPublishPayload {
+ let acknowledge_private_terms =
+ seller_decision_acknowledges_private_terms(&lifecycle.root, &command);
+ let envelope = trade_decision_envelope_from_proposal(
+ &lifecycle.root,
+ &command,
+ current_runtime_time_seconds()?,
+ )?;
+ let payload = AppTradeCandidateDecisionPublishPayload {
context: AppPublishContext::new(account_id, "seller_order_decision"),
- app_order_id: order_id,
+ order_id,
farm_id,
- trade_order_id: request.payload.order_id.to_string(),
- request_event_id: request.request_event_id,
- listing_event_id: request.listing_event_id,
- listing_addr: request.payload.listing_addr.to_string(),
- buyer_pubkey: request.payload.buyer_pubkey.to_string(),
- seller_pubkey: request.payload.seller_pubkey.to_string(),
- decision,
- confirm_public_note,
+ envelope,
+ acknowledge_private_terms,
};
- AppPublishPayload::OrderDecision(payload.clone())
+ AppPublishPayload::TradeCandidateDecision(payload.clone())
.validate()
.map_err(|_| AppSqliteError::InvalidProjection {
reason: "seller order decision publish payload is invalid",
@@ -2607,8 +2530,8 @@ impl DesktopAppRuntimeState {
command: AppSellerOrderDecisionCommand,
) -> Result<bool, AppSqliteError> {
let payload = self.prepare_seller_order_decision(order_id, command)?;
- let source_record_id = order_decision_sdk_source_record_id(&payload);
- self.enqueue_order_decision_payload_via_sdk(
+ let source_record_id = trade_candidate_decision_sdk_source_record_id(&payload);
+ self.enqueue_trade_candidate_decision_payload_via_sdk(
&payload,
DesktopRuntimeWorkflowReceiptSourceKind::AppWorkflow,
source_record_id.as_str(),
@@ -2620,8 +2543,8 @@ impl DesktopAppRuntimeState {
fn prepare_buyer_order_cancellation(
&mut self,
order_id: OrderId,
- confirm_public_note: bool,
- ) -> Result<AppOrderCancellationPublishPayload, AppSqliteError> {
+ _confirm_public_note: bool,
+ ) -> Result<AppTradeCancellationPublishPayload, AppSqliteError> {
let _ = self.import_shared_runtime_store()?;
let relay_urls = normalized_app_sync_relay_urls(&self.nostr_relay_urls).map_err(|_| {
AppSqliteError::InvalidProjection {
@@ -2668,49 +2591,24 @@ impl DesktopAppRuntimeState {
reason: "buyer order cancellation requires an open pre-agreement order",
});
}
- let request = self.resolve_seller_order_request_evidence(order_id)?;
- if request.payload.buyer_pubkey.trim() != buyer_pubkey.as_str() {
+ let lifecycle = self.resolve_order_lifecycle_evidence(order_id)?;
+ if lifecycle.root.buyer_pubkey.as_str() != buyer_pubkey.as_str() {
return Err(AppSqliteError::InvalidProjection {
reason: "buyer order cancellation buyer account does not match order buyer",
});
}
- let lifecycle = self.resolve_order_lifecycle_evidence(&request)?;
- if lifecycle.cancellation_event_id.is_some() {
- return Err(AppSqliteError::InvalidProjection {
- reason: "buyer order cancellation requires an open pre-agreement order",
- });
- }
- match lifecycle.status {
- RadrootsTradeWorkflowState::Requested => {}
- RadrootsTradeWorkflowState::AgreedPendingValidation
- | RadrootsTradeWorkflowState::Committed => {
- return Err(AppSqliteError::InvalidProjection {
- reason: "buyer order cancellation requires an open pre-agreement order",
- });
- }
- RadrootsTradeWorkflowState::Missing
- | RadrootsTradeWorkflowState::Declined
- | RadrootsTradeWorkflowState::Cancelled
- | RadrootsTradeWorkflowState::ValidationExpired
- | RadrootsTradeWorkflowState::Invalid => {
- return Err(AppSqliteError::InvalidProjection {
- reason: "buyer order cancellation requires an open pre-agreement order",
- });
- }
- };
- let payload = AppOrderCancellationPublishPayload {
+ let envelope = trade_cancellation_envelope_from_proposal(
+ &lifecycle.root,
+ "buyer cancelled order",
+ current_runtime_time_seconds()?,
+ )?;
+ let payload = AppTradeCancellationPublishPayload {
context: AppPublishContext::new(account_id.clone(), "buyer_order_cancellation"),
- app_order_id: order_id,
+ order_id,
farm_id: detail.farm_id,
- trade_order_id: request.payload.order_id.to_string(),
- request_event_id: request.request_event_id,
- listing_addr: request.payload.listing_addr.to_string(),
- buyer_pubkey: request.payload.buyer_pubkey.to_string(),
- seller_pubkey: request.payload.seller_pubkey.to_string(),
- reason: "buyer cancelled order".to_owned(),
- confirm_public_note,
+ envelope,
};
- AppPublishPayload::OrderCancellation(payload.clone())
+ AppPublishPayload::TradeCancellation(payload.clone())
.validate()
.map_err(|_| AppSqliteError::InvalidProjection {
reason: "buyer order cancellation publish payload is invalid",
@@ -2724,8 +2622,8 @@ impl DesktopAppRuntimeState {
confirm_public_note: bool,
) -> Result<bool, AppSqliteError> {
let payload = self.prepare_buyer_order_cancellation(order_id, confirm_public_note)?;
- let source_record_id = order_cancellation_sdk_source_record_id(&payload);
- self.enqueue_order_cancellation_payload_via_sdk(
+ let source_record_id = trade_cancellation_sdk_source_record_id(&payload);
+ self.enqueue_trade_cancellation_payload_via_sdk(
&payload,
DesktopRuntimeWorkflowReceiptSourceKind::AppWorkflow,
source_record_id.as_str(),
@@ -4088,8 +3986,8 @@ impl DesktopAppRuntimeState {
.context
.source_local_event_id
.clone()
- .unwrap_or_else(|| format!("app:order_request:{}", payload.order_id));
- self.enqueue_order_request_payload_via_sdk(
+ .unwrap_or_else(|| format!("app:trade_proposal:{}", payload.order_id));
+ self.enqueue_trade_proposal_payload_via_sdk(
&payload,
DesktopRuntimeWorkflowReceiptSourceKind::SharedRuntimeStore,
source_record_id.as_str(),
@@ -4278,7 +4176,7 @@ impl DesktopAppRuntimeState {
buyer_context: &BuyerContext,
order: &BuyerOrderRuntimeStoreExport,
local_work: Option<&AppOrderLocalWorkPublishSource>,
- ) -> Result<Option<AppOrderRequestPublishPayload>, AppSqliteError> {
+ ) -> Result<Option<AppTradeProposalPublishPayload>, AppSqliteError> {
let Some(local_work) = local_work else {
return Ok(None);
};
@@ -4286,43 +4184,23 @@ impl DesktopAppRuntimeState {
return Ok(None);
};
let buyer_pubkey = self.runtime_store_owner_pubkey(buyer_account);
- let export = AppBuyerOrderRequestExport::from_order(order, buyer_pubkey.as_deref())?;
- if !export.is_supported() {
- return Ok(None);
- }
- let Some((currency_code, total_minor_units)) = order_currency_and_total(order)? else {
+ let Some(buyer_pubkey) = buyer_pubkey.as_deref() else {
return Ok(None);
};
- let context = AppPublishContext::new(
- buyer_account.account.account_id.clone(),
- "place_personal_order",
- )
- .with_source_local_event_id(local_work.record_id.clone());
- let payload = AppOrderRequestPublishPayload {
+ let context =
+ AppPublishContext::new(buyer_account.account.account_id.clone(), "trade_proposal")
+ .with_source_local_event_id(local_work.record_id.clone());
+ let payload = AppTradeProposalPublishPayload {
context,
order_id: order.order_id,
farm_id: order.farm_id,
- status: Some(order.status.clone()),
- order_document_json: Some(local_work.payload.clone()),
- listing_addr: export.listing_addr,
- listing_event_id: export.listing_event_id,
- listing_relays: export.listing_relays,
- buyer_pubkey: export.buyer_pubkey,
- seller_pubkey: export.seller_pubkey,
- items: order
- .lines
- .iter()
- .map(|line| AppOrderRequestItemPayload {
- product_id: line.product_id,
- quantity: line.quantity,
- })
- .collect(),
- currency_code: Some(currency_code),
- total_minor_units: Some(total_minor_units),
- note: non_empty_string(order.buyer_order_note.as_str()),
- confirm_public_note: order.buyer_order_note_public_confirmed,
+ envelope: trade_proposal_envelope_from_order(
+ order,
+ buyer_pubkey,
+ current_runtime_time_seconds()?,
+ )?,
};
- if AppPublishPayload::OrderRequest(payload.clone())
+ if AppPublishPayload::TradeProposal(payload.clone())
.validate()
.is_err()
{
@@ -4429,37 +4307,29 @@ impl DesktopAppRuntimeState {
}
}
- fn enqueue_order_request_payload_via_sdk(
+ fn enqueue_trade_proposal_payload_via_sdk(
&self,
- payload: &AppOrderRequestPublishPayload,
+ payload: &AppTradeProposalPublishPayload,
source_kind: DesktopRuntimeWorkflowReceiptSourceKind,
source_record_id: &str,
) -> Result<(), AppSqliteError> {
- let operation_kind = TRADE_SUBMIT_OPERATION_KIND;
+ let operation_kind = TRADE_SUBMIT_PROPOSAL_OPERATION_KIND;
let actor_pubkey = self
- .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderRequest(
+ .local_signing_identity_for_publish_payload(&AppPublishPayload::TradeProposal(
payload.clone(),
))
.and_then(|identity| {
let actor_pubkey = identity.public_key_hex();
- let order_parts = order_request_publish_payload_to_sdk_product_parts(payload)?;
- let request = DesktopRuntimeTradeProposeRequest {
+ let request = DesktopRuntimeTradeSubmitProposalRequest {
actor_account_id: payload.context.account_id.clone(),
actor_pubkey: actor_pubkey.clone(),
signer: DesktopRuntimeLocalSigner::from_local_identity_keys(
identity.into_keys(),
),
- listing_event: order_request_sdk_listing_event_ptr(payload)?,
- order_id: order_parts.order_id,
- listing_addr: order_parts.listing_addr,
- seller_pubkey: order_parts.seller_pubkey,
- items: order_parts.items,
- economics: order_parts.economics,
- public_note: payload.note.clone(),
- confirm_public_note: payload.confirm_public_note,
+ envelope: payload.envelope.clone(),
idempotency_key: Some(sdk_idempotency_key(source_record_id)),
};
- self.enqueue_desktop_runtime_trade_propose(request)
+ self.enqueue_desktop_runtime_trade_submit_proposal(request)
.map(|receipt| (actor_pubkey, receipt))
.map_err(sync_transport_error_from_sdk_runtime_error)
});
@@ -4481,31 +4351,30 @@ impl DesktopAppRuntimeState {
}
}
- fn enqueue_order_decision_payload_via_sdk(
+ fn enqueue_trade_candidate_decision_payload_via_sdk(
&self,
- payload: &AppOrderDecisionPublishPayload,
+ payload: &AppTradeCandidateDecisionPublishPayload,
source_kind: DesktopRuntimeWorkflowReceiptSourceKind,
source_record_id: &str,
) -> Result<(), AppSqliteError> {
- let operation_kind = TRADE_DECISION_OPERATION_KIND;
+ let operation_kind = TRADE_DECIDE_CANDIDATE_OPERATION_KIND;
let actor_pubkey = self
- .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderDecision(
+ .local_signing_identity_for_publish_payload(&AppPublishPayload::TradeCandidateDecision(
payload.clone(),
))
.and_then(|identity| {
let actor_pubkey = identity.public_key_hex();
- let request = DesktopRuntimeTradeDecisionRequest {
+ let request = DesktopRuntimeTradeDecideCandidateRequest {
actor_account_id: payload.context.account_id.clone(),
actor_pubkey: actor_pubkey.clone(),
signer: DesktopRuntimeLocalSigner::from_local_identity_keys(
identity.into_keys(),
),
- locator: trade_locator_from_decision_payload(payload)?,
- decision: trade_decision_from_publish_payload(payload)?,
- confirm_public_note: payload.confirm_public_note,
+ envelope: payload.envelope.clone(),
+ acknowledge_private_terms: payload.acknowledge_private_terms,
idempotency_key: Some(sdk_idempotency_key(source_record_id)),
};
- self.enqueue_desktop_runtime_trade_decision(request)
+ self.enqueue_desktop_runtime_trade_decide_candidate(request)
.map(|receipt| (actor_pubkey, receipt))
.map_err(sync_transport_error_from_sdk_runtime_error)
});
@@ -4527,28 +4396,26 @@ impl DesktopAppRuntimeState {
}
}
- fn enqueue_order_cancellation_payload_via_sdk(
+ fn enqueue_trade_cancellation_payload_via_sdk(
&self,
- payload: &AppOrderCancellationPublishPayload,
+ payload: &AppTradeCancellationPublishPayload,
source_kind: DesktopRuntimeWorkflowReceiptSourceKind,
source_record_id: &str,
) -> Result<(), AppSqliteError> {
- let operation_kind = TRADE_CANCELLATION_OPERATION_KIND;
+ let operation_kind = TRADE_CANCEL_OPERATION_KIND;
let actor_pubkey = self
- .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderCancellation(
+ .local_signing_identity_for_publish_payload(&AppPublishPayload::TradeCancellation(
payload.clone(),
))
.and_then(|identity| {
let actor_pubkey = identity.public_key_hex();
- let request = DesktopRuntimeTradeCancellationRequest {
+ let request = DesktopRuntimeTradeCancelRequest {
actor_account_id: payload.context.account_id.clone(),
actor_pubkey: actor_pubkey.clone(),
signer: DesktopRuntimeLocalSigner::from_local_identity_keys(
identity.into_keys(),
),
- locator: trade_locator_from_cancellation_payload(payload)?,
- reason: payload.reason.clone(),
- confirm_public_note: payload.confirm_public_note,
+ envelope: payload.envelope.clone(),
idempotency_key: Some(sdk_idempotency_key(source_record_id)),
};
self.enqueue_desktop_runtime_trade_cancellation(request)
@@ -4597,23 +4464,23 @@ impl DesktopAppRuntimeState {
self.with_desktop_runtime_supervisor(|runtime| runtime.enqueue_listing_publish(request))
}
- fn enqueue_desktop_runtime_trade_propose(
+ fn enqueue_desktop_runtime_trade_submit_proposal(
&self,
- request: DesktopRuntimeTradeProposeRequest,
+ request: DesktopRuntimeTradeSubmitProposalRequest,
) -> Result<DesktopRuntimeEffectReceipt, DesktopRuntimeSupervisorError> {
- self.with_desktop_runtime_supervisor(|runtime| runtime.trade_propose(request))
+ self.with_desktop_runtime_supervisor(|runtime| runtime.trade_submit_proposal(request))
}
- fn enqueue_desktop_runtime_trade_decision(
+ fn enqueue_desktop_runtime_trade_decide_candidate(
&self,
- request: DesktopRuntimeTradeDecisionRequest,
+ request: DesktopRuntimeTradeDecideCandidateRequest,
) -> Result<DesktopRuntimeEffectReceipt, DesktopRuntimeSupervisorError> {
- self.with_desktop_runtime_supervisor(|runtime| runtime.trade_decide(request))
+ self.with_desktop_runtime_supervisor(|runtime| runtime.trade_decide_candidate(request))
}
fn enqueue_desktop_runtime_trade_cancellation(
&self,
- request: DesktopRuntimeTradeCancellationRequest,
+ request: DesktopRuntimeTradeCancelRequest,
) -> Result<DesktopRuntimeEffectReceipt, DesktopRuntimeSupervisorError> {
self.with_desktop_runtime_supervisor(|runtime| runtime.trade_cancel(request))
}
@@ -4982,270 +4849,54 @@ impl DesktopAppRuntimeState {
sqlite_store.import_shared_runtime_store_from_path(database_path.as_path())
}
- fn resolve_seller_order_request_evidence(
- &self,
- order_id: OrderId,
- ) -> Result<ResolvedAppSellerOrderRequest, AppSqliteError> {
- let mut matched_requests = BTreeMap::new();
- self.collect_seller_order_request_evidence_from_shared_events(
- &order_id,
- &mut matched_requests,
- )?;
- self.collect_seller_order_request_evidence_from_local_interop(
- &order_id,
- &mut matched_requests,
- )?;
-
- if matched_requests.len() > 1 {
- return Err(AppSqliteError::InvalidProjection {
- reason: "seller order decision found multiple signed order requests",
- });
- }
-
- matched_requests
- .into_values()
- .next()
- .ok_or(AppSqliteError::InvalidProjection {
- reason: "seller order decision requires signed order request evidence",
- })
- }
-
- fn collect_seller_order_request_evidence_from_shared_events(
- &self,
- order_id: &OrderId,
- matched_requests: &mut BTreeMap<String, ResolvedAppSellerOrderRequest>,
- ) -> Result<(), AppSqliteError> {
- let store = self.open_shared_runtime_store_store()?;
- let Some(store) = store else {
- return Ok(());
- };
- let mut before = None;
-
- loop {
- let records = match before {
- Some((before_change_seq, before_seq)) => store
- .list_records_changed_before(
- before_change_seq,
- before_seq,
- APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE,
- )
- .map_err(|source| AppSqliteError::RuntimeStore {
- operation: "load shared order request evidence",
- source,
- })?,
- None => store
- .list_records_changed_latest(APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE)
- .map_err(|source| AppSqliteError::RuntimeStore {
- operation: "load shared order request evidence",
- source,
- })?,
- };
- if records.is_empty() {
- break;
- }
- let is_last_page =
- records.len() < APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE as usize;
- before = records.last().map(|record| (record.change_seq, record.seq));
-
- for record in records {
- if record.family != RuntimeStoreRecordFamily::SignedEvent
- || record.event_kind
- != Some(i64::from(RadrootsOrderEventType::OrderRequested.kind()))
- || !signed_order_request_evidence_record_is_usable(&record)
- {
- continue;
- }
- let Some(event) = signed_event_from_local_record(&record)? else {
- continue;
- };
- let Ok(envelope) = order_request_from_event(&event) else {
- continue;
- };
- insert_seller_order_request_evidence(
- order_id,
- &event,
- envelope.payload,
- matched_requests,
- );
- }
-
- if before.is_none() || is_last_page {
- break;
- }
- }
-
- Ok(())
- }
-
- fn collect_seller_order_request_evidence_from_local_interop(
- &self,
- order_id: &OrderId,
- matched_requests: &mut BTreeMap<String, ResolvedAppSellerOrderRequest>,
- ) -> Result<(), AppSqliteError> {
- let Some(sqlite_store) = self.sqlite_store.as_ref() else {
- return Ok(());
- };
- let events = sqlite_store.load_local_interop_signed_events_by_kind(i64::from(
- RadrootsOrderEventType::OrderRequested.kind(),
- ))?;
-
- for event in events {
- let Ok(envelope) = order_request_from_event(&event) else {
- continue;
- };
- insert_seller_order_request_evidence(
- order_id,
- &event,
- envelope.payload,
- matched_requests,
- );
- }
-
- Ok(())
- }
-
fn resolve_order_lifecycle_evidence(
&self,
- request: &ResolvedAppSellerOrderRequest,
+ order_id: OrderId,
) -> Result<ResolvedAppOrderLifecycleEvidence, AppSqliteError> {
- let mut events = self.collect_order_lifecycle_signed_events()?;
- events.sort_by(|left, right| {
- left.created_at_u64()
- .cmp(&right.created_at_u64())
- .then_with(|| left.id_str().cmp(right.id_str()))
- });
-
- let mut buckets = AppActiveOrderEvidenceBuckets::default();
- let request_event_id =
- active_order_event_id(request.request_event_id.as_str(), "request_event_id")?;
- let request_author_pubkey = active_order_pubkey(
- request.request_author_pubkey.as_str(),
- "request_author_pubkey",
- )?;
- buckets.requests.push(RadrootsOrderRequestRecord {
- event_id: request_event_id.clone(),
- author_pubkey: request_author_pubkey,
- payload: request.payload.clone(),
+ let target_trade_id = trade_id_from_order_id(order_id)?;
+ let mut matches = self
+ .collect_trade_proposal_envelopes()?
+ .into_iter()
+ .filter(|envelope| envelope.trade_id == target_trade_id)
+ .collect::<Vec<_>>();
+ matches.sort_by(|left, right| {
+ left.authored_at_unix_s
+ .cmp(&right.authored_at_unix_s)
+ .then_with(|| left.mutation_id.cmp(&right.mutation_id))
});
- for event in events {
- if trade_chain_tag_value(&event, "e_root").as_deref()
- != Some(request.request_event_id.as_str())
- {
- continue;
- }
- let event_id = active_order_event_id(event.id_str(), "event_id")?;
- let author_pubkey = active_order_pubkey(event.author_str(), "author_pubkey")?;
- match event.kind_u32() {
- KIND_ORDER_DECISION => {
- let envelope = order_decision_from_event(&event).map_err(|_| {
- AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
- }
- })?;
- let context = active_order_event_record_context(&event, envelope.message_type)?;
- buckets.decisions.push(RadrootsOrderDecisionRecord {
- event_id,
- author_pubkey,
- counterparty_pubkey: context.0,
- root_event_id: context.1,
- prev_event_id: context.2,
- payload: envelope.payload,
- });
- }
- KIND_ORDER_CANCELLATION => {
- let Ok(envelope) = order_cancellation_from_event(&event) else {
- return Err(AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
- });
- };
- let context = active_order_event_record_context(&event, envelope.message_type)?;
- buckets.cancellations.push(RadrootsOrderCancellationRecord {
- event_id,
- author_pubkey,
- counterparty_pubkey: context.0,
- root_event_id: context.1,
- prev_event_id: context.2,
- payload: envelope.payload,
- });
- }
- _ => {}
- }
- }
-
- let projection = reduce_order_events(
- &request.payload.order_id,
- RadrootsOrderReductionInputs {
- requests: buckets.requests.clone(),
- decisions: buckets.decisions.clone(),
- cancellations: buckets.cancellations.clone(),
- },
- );
- if !projection.issues.is_empty() || projection.status == RadrootsTradeWorkflowState::Invalid
- {
- return Err(AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
- });
- }
- if projection.request_event_id.as_ref() != Some(&request_event_id) {
+ matches.dedup_by(|left, right| left.mutation_id == right.mutation_id);
+ if matches.len() > 1 {
return Err(AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
+ reason: "trade lifecycle evidence has multiple root proposals",
});
}
-
- let decision = projection
- .decision_event_id
- .as_ref()
- .map(|event_id| {
- buckets
- .decisions
- .iter()
- .find(|decision| decision.event_id == *event_id)
- .map(|decision| ResolvedAppOrderDecisionEvidence {
- event_id: decision.event_id.to_string(),
- payload: decision.payload.clone(),
- })
- .ok_or(AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
- })
- })
- .transpose()?;
Ok(ResolvedAppOrderLifecycleEvidence {
- request_event_id: request.request_event_id.clone(),
- status: projection.status,
- agreement_event_id: projection
- .agreement_event_id
- .map(|event_id| event_id.to_string()),
- last_event_id: projection
- .last_event_id
- .map(|event_id| event_id.to_string()),
- decision,
- cancellation_event_id: projection
- .cancellation_event_id
- .map(|event_id| event_id.to_string()),
+ root: matches.pop().ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade lifecycle requires signed proposal evidence",
+ })?,
})
}
- fn collect_order_lifecycle_signed_events(
+ fn collect_trade_proposal_envelopes(
&self,
- ) -> Result<Vec<RadrootsEventEnvelope>, AppSqliteError> {
- let mut events = Vec::new();
+ ) -> Result<Vec<RadrootsTradeMutationEnvelopeV1>, AppSqliteError> {
+ let mut envelopes = Vec::new();
let mut seen_event_ids = BTreeSet::new();
- let kinds = [KIND_ORDER_DECISION, KIND_ORDER_CANCELLATION];
if let Some(sqlite_store) = self.sqlite_store.as_ref() {
- for kind in kinds {
- for event in
- sqlite_store.load_local_interop_signed_events_by_kind(i64::from(kind))?
+ for event in sqlite_store
+ .load_local_interop_signed_events_by_kind(i64::from(KIND_TRADE_PROPOSAL))?
+ {
+ if seen_event_ids.insert(event.id_str().to_owned())
+ && let Ok(envelope) = trade_mutation_from_event(&event)
{
- if seen_event_ids.insert(event.id_str().to_owned()) {
- events.push(event);
- }
+ envelopes.push(envelope);
}
}
}
let Some(store) = self.open_shared_runtime_store_store()? else {
- return Ok(events);
+ return Ok(envelopes);
};
let mut before = None;
loop {
@@ -5257,13 +4908,13 @@ impl DesktopAppRuntimeState {
APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE,
)
.map_err(|source| AppSqliteError::RuntimeStore {
- operation: "load shared order lifecycle evidence",
+ operation: "load shared trade proposal evidence",
source,
})?,
None => store
.list_records_changed_latest(APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE)
.map_err(|source| AppSqliteError::RuntimeStore {
- operation: "load shared order lifecycle evidence",
+ operation: "load shared trade proposal evidence",
source,
})?,
};
@@ -5279,16 +4930,18 @@ impl DesktopAppRuntimeState {
continue;
};
if record.family != RuntimeStoreRecordFamily::SignedEvent
- || !kinds.contains(&u32::try_from(kind).unwrap_or_default())
- || !signed_order_request_evidence_record_is_usable(&record)
+ || u32::try_from(kind).ok() != Some(KIND_TRADE_PROPOSAL)
+ || !signed_event_evidence_record_is_usable(&record)
{
continue;
}
let Some(event) = signed_event_from_local_record(&record)? else {
continue;
};
- if seen_event_ids.insert(event.id_str().to_owned()) {
- events.push(event);
+ if seen_event_ids.insert(event.id_str().to_owned())
+ && let Ok(envelope) = trade_mutation_from_event(&event)
+ {
+ envelopes.push(envelope);
}
}
@@ -5297,7 +4950,7 @@ impl DesktopAppRuntimeState {
}
}
- Ok(events)
+ Ok(envelopes)
}
fn open_shared_runtime_store_store(
@@ -5566,7 +5219,7 @@ impl DesktopAppRuntimeState {
timestamp,
);
if sqlite_store.buyer_order_coordination_is_synced(buyer_context, order.order_id)? {
- return Ok(Some(AppOrderLocalWorkPublishSource { record_id, payload }));
+ return Ok(Some(AppOrderLocalWorkPublishSource { record_id }));
}
validate_buyer_order_request_local_work_payload(&payload).map_err(|source| {
AppSqliteError::RuntimeStore {
@@ -5619,7 +5272,7 @@ impl DesktopAppRuntimeState {
return Err(error);
}
sqlite_store.mark_buyer_order_coordination_synced(buyer_context, order.order_id)?;
- Ok(Some(AppOrderLocalWorkPublishSource { record_id, payload }))
+ Ok(Some(AppOrderLocalWorkPublishSource { record_id }))
}
fn append_app_local_work_record(
@@ -6834,12 +6487,14 @@ fn listing_publish_source_record(
})
}
-fn order_decision_sdk_source_record_id(payload: &AppOrderDecisionPublishPayload) -> String {
- format!("app:order_decision:{}", payload.app_order_id)
+fn trade_candidate_decision_sdk_source_record_id(
+ payload: &AppTradeCandidateDecisionPublishPayload,
+) -> String {
+ format!("app:trade_candidate_decision:{}", payload.order_id)
}
-fn order_cancellation_sdk_source_record_id(payload: &AppOrderCancellationPublishPayload) -> String {
- format!("app:order_cancellation:{}", payload.app_order_id)
+fn trade_cancellation_sdk_source_record_id(payload: &AppTradeCancellationPublishPayload) -> String {
+ format!("app:trade_cancellation:{}", payload.order_id)
}
fn sdk_relay_url_policy_for_targets(target_relays: &[String]) -> DesktopRuntimeRelayUrlPolicy {
@@ -7137,27 +6792,6 @@ fn parse_app_listing_delivery_method(
}
}
-fn order_request_sdk_listing_event_ptr(
- payload: &AppOrderRequestPublishPayload,
-) -> Result<RadrootsEventPtr, AppSyncTransportError> {
- let listing_event_id = payload
- .listing_event_id
- .as_deref()
- .ok_or_else(|| {
- AppSyncTransportError::failed("order request publish requires listing event id")
- })?
- .trim()
- .to_owned();
- let listing_relay = normalized_listing_relays(payload.listing_relays.as_slice())?
- .into_iter()
- .next();
-
- Ok(RadrootsEventPtr {
- id: listing_event_id,
- relays: listing_relay,
- })
-}
-
#[cfg(test)]
fn selected_listing_relay(
listing_relays: &[String],
@@ -7175,6 +6809,7 @@ fn selected_listing_relay(
Err(missing_listing_provenance_relay_error(&known_relays))
}
+#[cfg(test)]
fn normalized_listing_relays(
listing_relays: &[String],
) -> Result<Vec<String>, AppSyncTransportError> {
@@ -7205,49 +6840,6 @@ fn missing_listing_provenance_relay_error(known_relays: &[String]) -> AppSyncTra
)
}
-struct SdkOrderRequestProductParts {
- order_id: RadrootsOrderId,
- listing_addr: RadrootsListingAddress,
- seller_pubkey: RadrootsPublicKey,
- items: Vec<RadrootsOrderItem>,
- economics: RadrootsOrderEconomics,
-}
-
-fn order_request_publish_payload_to_sdk_product_parts(
- payload: &AppOrderRequestPublishPayload,
-) -> Result<SdkOrderRequestProductParts, AppSyncTransportError> {
- let Some(document_json) = payload.order_document_json.as_ref() else {
- return Err(AppSyncTransportError::failed(
- "order request publish requires order document",
- ));
- };
- let order_json = document_json.pointer("/document/order").ok_or_else(|| {
- AppSyncTransportError::failed("order request publish document is missing order")
- })?;
- let items_json = order_json.get("items").ok_or_else(|| {
- AppSyncTransportError::failed("order request publish document is missing order items")
- })?;
- let economics_json = order_json.get("economics").ok_or_else(|| {
- AppSyncTransportError::failed("order request publish document is missing order economics")
- })?;
- let listing_addr = payload.listing_addr.as_deref().ok_or_else(|| {
- AppSyncTransportError::failed("order request publish requires listing address")
- })?;
- let seller_pubkey = payload.seller_pubkey.as_deref().ok_or_else(|| {
- AppSyncTransportError::failed("order request publish requires seller pubkey")
- })?;
-
- Ok(SdkOrderRequestProductParts {
- order_id: publish_order_id(payload.order_id.to_string().as_str())?,
- listing_addr: publish_listing_addr(listing_addr)?,
- seller_pubkey: publish_pubkey(seller_pubkey)?,
- items: serde_json::from_value::<Vec<RadrootsOrderItem>>(items_json.clone())
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))?,
- economics: serde_json::from_value::<RadrootsOrderEconomics>(economics_json.clone())
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))?,
- })
-}
-
fn d_tag_from_uuid(uuid: Uuid) -> String {
base64_url_no_pad(uuid.as_bytes())
}
@@ -7370,7 +6962,6 @@ struct AppBuyerOrderRequestExport {
#[derive(Clone, Debug)]
struct AppOrderLocalWorkPublishSource {
record_id: String,
- payload: serde_json::Value,
}
impl AppBuyerOrderRequestExport {
@@ -7523,10 +7114,6 @@ fn buyer_order_request_local_work_payload(
"status": order.status,
"buyer_context_key": order.buyer_context_key,
"buyer_name": order.buyer_name,
- "buyer_email": order.buyer_email,
- "buyer_phone": order.buyer_phone,
- "buyer_order_note": order.buyer_order_note,
- "buyer_order_note_public_confirmed": order.buyer_order_note_public_confirmed,
"fulfillment": {
"window_id": order.fulfillment_window_id.map(|id| id.to_string()),
"label": order.fulfillment_window_label,
@@ -7636,42 +7223,398 @@ fn order_economics_json(
})))
}
-fn order_currency_and_total(
+fn trade_proposal_envelope_from_order(
order: &BuyerOrderRuntimeStoreExport,
-) -> Result<Option<(String, u32)>, AppSqliteError> {
- let mut currency = None::<String>;
- let mut total_minor_units = 0_u32;
+ buyer_pubkey: &str,
+ authored_at_unix_s: i64,
+) -> Result<RadrootsTradeMutationEnvelopeV1, AppSqliteError> {
+ let buyer_pubkey = parse_trade_pubkey(buyer_pubkey, "trade proposal requires buyer pubkey")?;
+ let seller_pubkey = parse_trade_pubkey(
+ shared_optional_line_value(&order.lines, |line| line.seller_pubkey.as_deref())
+ .as_deref()
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires seller pubkey",
+ })?,
+ "trade proposal requires seller pubkey",
+ )?;
+ let farm_id = RadrootsDTag::parse(
+ shared_optional_line_value(&order.lines, |line| line.farm_key.as_deref())
+ .unwrap_or_else(|| d_tag_from_uuid(order.farm_id.as_uuid()))
+ .as_str(),
+ )
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires valid farm id",
+ })?;
+ let candidate =
+ trade_candidate_terms_from_order(order, &buyer_pubkey, &seller_pubkey, &farm_id)?;
+ let authored_at_unix_s =
+ u64::try_from(authored_at_unix_s).map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal timestamp must be non-negative",
+ })?;
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: trade_id_from_order_id(order.order_id)?,
+ root_mutation_id: None,
+ buyer_pubkey: buyer_pubkey.clone(),
+ seller_pubkey: seller_pubkey.clone(),
+ farm_id,
+ parent_mutation_ids: Vec::new(),
+ author_pubkey: buyer_pubkey,
+ counterparty_pubkey: seller_pubkey,
+ authored_at_unix_s,
+ body: RadrootsTradeMutationBodyV1::Proposal { candidate },
+ })
+ .map(|canonical| canonical.envelope)
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal envelope is invalid",
+ })
+}
- for line in &order.lines {
- let Some(unit_price_minor_units) = line.unit_price_minor_units else {
- return Ok(None);
- };
+fn trade_candidate_terms_from_order(
+ order: &BuyerOrderRuntimeStoreExport,
+ buyer_pubkey: &RadrootsPublicKey,
+ seller_pubkey: &RadrootsPublicKey,
+ farm_id: &RadrootsDTag,
+) -> Result<RadrootsTradeCandidateTermsV1, AppSqliteError> {
+ if order.lines.is_empty() {
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires order lines",
+ });
+ }
+ let mut lines = Vec::with_capacity(order.lines.len());
+ let mut currency = None::<String>;
+ let mut subtotal_minor_units = 0_u32;
+ for (index, line) in order.lines.iter().enumerate() {
+ let listing_addr = RadrootsAddressableCoordinate::parse(
+ line.listing_addr
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires listing address",
+ })?,
+ )
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires valid listing address",
+ })?;
+ let listing_event_id = RadrootsEventId::parse(
+ line.listing_event_id
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires listing event id",
+ })?,
+ )
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires valid listing event id",
+ })?;
+ let bin_id = RadrootsInventoryBinId::parse(
+ line.listing_bin_id
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires listing bin id",
+ })?,
+ )
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires valid listing bin id",
+ })?;
+ if line.quantity == 0 {
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires positive quantities",
+ });
+ }
+ let unit_code = canonical_quantity_unit(line.quantity_unit_label.as_str()).ok_or(
+ AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires canonical quantity units",
+ },
+ )?;
+ let unit_price_minor_units =
+ line.unit_price_minor_units
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires unit prices",
+ })?;
+ if unit_price_minor_units == 0 {
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires positive unit prices",
+ });
+ }
let line_currency = normalize_currency_code(line.price_currency.as_str());
if line_currency.len() != 3 || !line_currency.bytes().all(|byte| byte.is_ascii_uppercase())
{
- return Ok(None);
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires canonical currency",
+ });
}
if let Some(existing_currency) = currency.as_deref() {
if existing_currency != line_currency {
- return Ok(None);
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires a single currency",
+ });
}
} else {
currency = Some(line_currency.clone());
}
- let line_total = unit_price_minor_units.checked_mul(line.quantity).ok_or(
+ let line_subtotal_minor_units = unit_price_minor_units.checked_mul(line.quantity).ok_or(
AppSqliteError::InvalidProjection {
- reason: "buyer order publish line total overflowed",
+ reason: "trade proposal line subtotal overflowed",
},
)?;
- total_minor_units =
- total_minor_units
- .checked_add(line_total)
- .ok_or(AppSqliteError::InvalidProjection {
- reason: "buyer order publish total overflowed",
- })?;
+ subtotal_minor_units = subtotal_minor_units
+ .checked_add(line_subtotal_minor_units)
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal subtotal overflowed",
+ })?;
+ lines.push(RadrootsTradeCandidateLineV1 {
+ line_id: RadrootsDTag::parse(format!("line-{index}-{}", line.product_id).as_str())
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires valid line ids",
+ })?,
+ listing_addr,
+ listing_event_id: listing_event_id.clone(),
+ listing_snapshot_sha256: listing_event_id.to_string(),
+ product_id: line.product_id.to_string(),
+ option_id: None,
+ bin_id,
+ quantity_mantissa: line.quantity.to_string(),
+ quantity_scale: 0,
+ unit_code: unit_code.to_owned(),
+ unit_profile: "app-cart".to_owned(),
+ unit_price_mantissa: unit_price_minor_units.to_string(),
+ currency_code: line_currency.clone(),
+ line_subtotal_mantissa: line_subtotal_minor_units.to_string(),
+ replaces_line_id: None,
+ });
+ }
+ let currency = currency.ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires currency",
+ })?;
+ let starts_at_unix_s = parse_order_fulfillment_time(order.fulfillment_starts_at.as_deref())?;
+ let ends_at_unix_s = parse_order_fulfillment_time(order.fulfillment_ends_at.as_deref())?;
+ if starts_at_unix_s >= ends_at_unix_s {
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires valid fulfillment window",
+ });
}
+ Ok(RadrootsTradeCandidateTermsV1 {
+ candidate_id: None,
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ base_candidate_id: None,
+ supersession_intent: None,
+ buyer_pubkey: buyer_pubkey.clone(),
+ seller_pubkey: seller_pubkey.clone(),
+ farm_id: farm_id.clone(),
+ lines,
+ line_tombstones: Vec::new(),
+ economics: RadrootsTradeEconomicsProfileV1 {
+ profile_id: "app-cart".to_owned(),
+ currency_code: currency,
+ currency_exponent: 2,
+ rounding_profile: "minor-units".to_owned(),
+ subtotal_mantissa: subtotal_minor_units.to_string(),
+ discount_total_mantissa: "0".to_owned(),
+ adjustment_total_mantissa: "0".to_owned(),
+ total_mantissa: subtotal_minor_units.to_string(),
+ adjustments: Vec::new(),
+ },
+ fulfillment: RadrootsFulfillmentProfileV1 {
+ profile_id: order
+ .fulfillment_window_id
+ .map(|id| id.to_string())
+ .unwrap_or_else(|| "app-fulfillment-window".to_owned()),
+ method: "pickup".to_owned(),
+ starts_at_unix_s,
+ ends_at_unix_s,
+ timezone: "UTC".to_owned(),
+ utc_offset_seconds: 0,
+ fold: 0,
+ location_class: "farm".to_owned(),
+ requires_private_terms: false,
+ },
+ cancellation: RadrootsTradeCancellationProfileV1 {
+ profile_id: "buyer-pre-agreement".to_owned(),
+ buyer_pre_agreement: true,
+ post_agreement_cutoff_unix_s: None,
+ },
+ private_terms: None,
+ proposal_expires_at_unix_s: starts_at_unix_s.saturating_sub(1),
+ })
+}
- Ok(currency.map(|currency| (currency, total_minor_units)))
+fn trade_decision_envelope_from_proposal(
+ proposal: &RadrootsTradeMutationEnvelopeV1,
+ command: &AppSellerOrderDecisionCommand,
+ authored_at_unix_s: i64,
+) -> Result<RadrootsTradeMutationEnvelopeV1, AppSqliteError> {
+ let proposal_mutation_id =
+ proposal
+ .mutation_id
+ .clone()
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade decision requires proposal mutation id",
+ })?;
+ let candidate = proposal_candidate_terms(proposal)?;
+ let candidate_id = candidate
+ .candidate_id
+ .clone()
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade decision requires candidate id",
+ })?;
+ let decision = match command {
+ AppSellerOrderDecisionCommand::Accept => RadrootsTradeDecisionV1::Accepted {
+ reservation_assertion: None,
+ },
+ AppSellerOrderDecisionCommand::Decline { reason } => {
+ let reason = reason.trim();
+ if reason.is_empty() {
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "seller order decline requires a non-empty reason",
+ });
+ }
+ RadrootsTradeDecisionV1::Declined {
+ reason: reason.to_owned(),
+ }
+ }
+ };
+ let authored_at_unix_s =
+ u64::try_from(authored_at_unix_s).map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade decision timestamp must be non-negative",
+ })?;
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_DECISION_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: proposal.trade_id.clone(),
+ root_mutation_id: proposal.mutation_id.clone(),
+ buyer_pubkey: proposal.buyer_pubkey.clone(),
+ seller_pubkey: proposal.seller_pubkey.clone(),
+ farm_id: proposal.farm_id.clone(),
+ parent_mutation_ids: vec![proposal_mutation_id.clone()],
+ author_pubkey: proposal.seller_pubkey.clone(),
+ counterparty_pubkey: proposal.buyer_pubkey.clone(),
+ authored_at_unix_s,
+ body: RadrootsTradeMutationBodyV1::Decision {
+ proposal_mutation_id,
+ candidate_id,
+ decision,
+ },
+ })
+ .map(|canonical| canonical.envelope)
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade decision envelope is invalid",
+ })
+}
+
+fn trade_cancellation_envelope_from_proposal(
+ proposal: &RadrootsTradeMutationEnvelopeV1,
+ reason: &str,
+ authored_at_unix_s: i64,
+) -> Result<RadrootsTradeMutationEnvelopeV1, AppSqliteError> {
+ let root_mutation_id =
+ proposal
+ .mutation_id
+ .clone()
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade cancellation requires proposal mutation id",
+ })?;
+ let candidate_id = proposal_candidate_terms(proposal)?
+ .candidate_id
+ .clone()
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade cancellation requires candidate id",
+ })?;
+ let reason = reason.trim();
+ if reason.is_empty() {
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "trade cancellation requires reason",
+ });
+ }
+ let authored_at_unix_s =
+ u64::try_from(authored_at_unix_s).map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade cancellation timestamp must be non-negative",
+ })?;
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_CANCELLATION_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: proposal.trade_id.clone(),
+ root_mutation_id: Some(root_mutation_id.clone()),
+ buyer_pubkey: proposal.buyer_pubkey.clone(),
+ seller_pubkey: proposal.seller_pubkey.clone(),
+ farm_id: proposal.farm_id.clone(),
+ parent_mutation_ids: vec![root_mutation_id],
+ author_pubkey: proposal.buyer_pubkey.clone(),
+ counterparty_pubkey: proposal.seller_pubkey.clone(),
+ authored_at_unix_s,
+ body: RadrootsTradeMutationBodyV1::Cancellation {
+ target_candidate_id: Some(candidate_id),
+ target_claim_mutation_id: None,
+ reason: reason.to_owned(),
+ },
+ })
+ .map(|canonical| canonical.envelope)
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade cancellation envelope is invalid",
+ })
+}
+
+fn proposal_candidate_terms(
+ proposal: &RadrootsTradeMutationEnvelopeV1,
+) -> Result<&RadrootsTradeCandidateTermsV1, AppSqliteError> {
+ match &proposal.body {
+ RadrootsTradeMutationBodyV1::Proposal { candidate } => Ok(candidate),
+ _ => Err(AppSqliteError::InvalidProjection {
+ reason: "trade lifecycle root must be a proposal",
+ }),
+ }
+}
+
+fn seller_decision_acknowledges_private_terms(
+ proposal: &RadrootsTradeMutationEnvelopeV1,
+ command: &AppSellerOrderDecisionCommand,
+) -> bool {
+ if !matches!(command, AppSellerOrderDecisionCommand::Accept) {
+ return false;
+ }
+ proposal_candidate_terms(proposal)
+ .ok()
+ .and_then(|candidate| candidate.private_terms.as_ref())
+ .is_some_and(|private_terms| private_terms.required_acknowledgement)
+}
+
+fn trade_id_from_order_id(order_id: OrderId) -> Result<RadrootsTradeId, AppSqliteError> {
+ let raw = order_id.to_string().replace('-', "");
+ RadrootsTradeId::parse(raw.as_str()).map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "app order id must map to trade id",
+ })
+}
+
+fn parse_trade_pubkey(
+ value: &str,
+ reason: &'static str,
+) -> Result<RadrootsPublicKey, AppSqliteError> {
+ RadrootsPublicKey::parse(value.trim()).map_err(|_| AppSqliteError::InvalidProjection { reason })
+}
+
+fn parse_order_fulfillment_time(value: Option<&str>) -> Result<u64, AppSqliteError> {
+ let value = value
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires fulfillment timestamps",
+ })?;
+ let timestamp = DateTime::parse_from_rfc3339(value)
+ .map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal requires valid fulfillment timestamps",
+ })?
+ .timestamp();
+ u64::try_from(timestamp).map_err(|_| AppSqliteError::InvalidProjection {
+ reason: "trade proposal fulfillment timestamp must be non-negative",
+ })
}
fn shared_optional_line_value(
@@ -8819,256 +8762,33 @@ fn event_tags_from_value(
) -> Result<Vec<Vec<String>>, AppSqliteError> {
let Some(value) = value else {
return Ok(Vec::new());
- };
- let Some(tags) = value.as_array() else {
- return Err(AppSqliteError::InvalidProjection {
- reason: "signed local event tags must be an array",
- });
- };
-
- tags.iter()
- .map(|tag| {
- let Some(values) = tag.as_array() else {
- return Err(AppSqliteError::InvalidProjection {
- reason: "signed local event tag must be an array",
- });
- };
- values
- .iter()
- .map(|value| {
- value
- .as_str()
- .map(str::to_owned)
- .ok_or(AppSqliteError::InvalidProjection {
- reason: "signed local event tag values must be strings",
- })
- })
- .collect()
- })
- .collect()
-}
-
-fn trade_chain_tag_value(event: &RadrootsEventEnvelope, key: &str) -> Option<String> {
- event.tags_as_vec().iter().find_map(|tag| {
- if tag.first().map(String::as_str) == Some(key) {
- tag.get(1)
- .map(String::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_owned)
- } else {
- None
- }
- })
-}
-
-fn active_order_event_id(
- value: &str,
- field: &'static str,
-) -> Result<RadrootsEventId, AppSqliteError> {
- value.parse().map_err(|_| AppSqliteError::DecodeId {
- field,
- value: value.to_owned(),
- })
-}
-
-fn active_order_pubkey(
- value: &str,
- field: &'static str,
-) -> Result<RadrootsPublicKey, AppSqliteError> {
- value.parse().map_err(|_| AppSqliteError::DecodeId {
- field,
- value: value.to_owned(),
- })
-}
-
-fn active_order_event_record_context(
- event: &RadrootsEventEnvelope,
- message_type: RadrootsOrderEventType,
-) -> Result<(RadrootsPublicKey, RadrootsEventId, RadrootsEventId), AppSqliteError> {
- let tags = event.tags_as_vec();
- let context = order_event_context_from_tags(message_type, &tags).map_err(|_| {
- AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
- }
- })?;
- let root_event_id = context
- .root_event_id
- .ok_or(AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
- })?;
- let prev_event_id = context
- .prev_event_id
- .ok_or(AppSqliteError::InvalidProjection {
- reason: "order lifecycle evidence is invalid",
- })?;
- Ok((context.counterparty_pubkey, root_event_id, prev_event_id))
-}
-
-fn insert_seller_order_request_evidence(
- order_id: &OrderId,
- event: &RadrootsEventEnvelope,
- payload: RadrootsOrderRequest,
- matched_requests: &mut BTreeMap<String, ResolvedAppSellerOrderRequest>,
-) {
- let app_order_id = projected_order_id_from_trade_request(
- payload.order_id.as_str(),
- payload.buyer_pubkey.as_str(),
- event.id_str(),
- );
- if app_order_id != *order_id {
- return;
- }
- matched_requests
- .entry(event.id_str().to_owned())
- .or_insert_with(|| ResolvedAppSellerOrderRequest {
- request_event: event.clone(),
- request_event_id: event.id_str().to_owned(),
- request_author_pubkey: event.author_str().to_owned(),
- listing_event_id: listing_event_id_from_tags(&event.tags_as_vec()),
- payload,
- });
-}
-
-fn listing_event_id_from_tags(tags: &[Vec<String>]) -> Option<String> {
- tags.iter().find_map(|tag| {
- if tag.first().map(String::as_str) == Some("listing_event") {
- tag.get(1)
- .map(String::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_owned)
- } else {
- None
- }
- })
-}
-
-fn seller_order_inventory_commitments(
- order: &SellerOrderDecisionExport,
-) -> Result<Vec<AppOrderDecisionInventoryCommitment>, AppSqliteError> {
- if order.lines.is_empty() {
+ };
+ let Some(tags) = value.as_array() else {
return Err(AppSqliteError::InvalidProjection {
- reason: "seller order decision requires order lines",
+ reason: "signed local event tags must be an array",
});
- }
+ };
- order
- .lines
- .iter()
- .map(|line| {
- let bin_id =
- line.listing_bin_id
- .as_deref()
- .ok_or(AppSqliteError::InvalidProjection {
- reason: "seller order decision requires listing bin evidence",
- })?;
- let stock_count = line.stock_count.ok_or(AppSqliteError::InvalidProjection {
- reason: "seller order decision requires current product stock",
- })?;
- let available_quantity = stock_count.checked_sub(line.reserved_quantity).ok_or(
- AppSqliteError::InvalidProjection {
- reason: "seller order decision inventory is over-reserved",
- },
- )?;
- if line.quantity > available_quantity {
+ tags.iter()
+ .map(|tag| {
+ let Some(values) = tag.as_array() else {
return Err(AppSqliteError::InvalidProjection {
- reason: "seller order decision would over-reserve inventory",
+ reason: "signed local event tag must be an array",
});
- }
-
- Ok(AppOrderDecisionInventoryCommitment {
- bin_id: bin_id.to_owned(),
- bin_count: line.quantity,
- })
- })
- .collect()
-}
-
-fn publish_order_id(value: &str) -> Result<RadrootsOrderId, AppSyncTransportError> {
- RadrootsOrderId::parse(value).map_err(|error| AppSyncTransportError::failed(error.to_string()))
-}
-
-fn publish_event_id(value: &str) -> Result<RadrootsEventId, AppSyncTransportError> {
- RadrootsEventId::parse(value).map_err(|error| AppSyncTransportError::failed(error.to_string()))
-}
-
-fn publish_pubkey(value: &str) -> Result<RadrootsPublicKey, AppSyncTransportError> {
- RadrootsPublicKey::parse(value)
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))
-}
-
-fn publish_listing_addr(value: &str) -> Result<RadrootsListingAddress, AppSyncTransportError> {
- RadrootsListingAddress::parse(value)
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))
-}
-
-fn publish_bin_id(value: &str) -> Result<RadrootsInventoryBinId, AppSyncTransportError> {
- RadrootsInventoryBinId::parse(value)
- .map_err(|error| AppSyncTransportError::failed(error.to_string()))
-}
-
-fn trade_locator_from_parts(
- trade_order_id: &str,
- request_event_id: &str,
- listing_addr: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
-) -> Result<RadrootsTradeLocator, AppSyncTransportError> {
- Ok(
- RadrootsTradeLocator::from_order_id(publish_order_id(trade_order_id)?)
- .with_root_event_id(publish_event_id(request_event_id)?)
- .with_listing_addr(publish_listing_addr(listing_addr)?)
- .with_buyer_pubkey(publish_pubkey(buyer_pubkey)?)
- .with_seller_pubkey(publish_pubkey(seller_pubkey)?),
- )
-}
-
-fn trade_locator_from_decision_payload(
- payload: &AppOrderDecisionPublishPayload,
-) -> Result<RadrootsTradeLocator, AppSyncTransportError> {
- trade_locator_from_parts(
- payload.trade_order_id.as_str(),
- payload.request_event_id.as_str(),
- payload.listing_addr.as_str(),
- payload.buyer_pubkey.as_str(),
- payload.seller_pubkey.as_str(),
- )
-}
-
-fn trade_locator_from_cancellation_payload(
- payload: &AppOrderCancellationPublishPayload,
-) -> Result<RadrootsTradeLocator, AppSyncTransportError> {
- trade_locator_from_parts(
- payload.trade_order_id.as_str(),
- payload.request_event_id.as_str(),
- payload.listing_addr.as_str(),
- payload.buyer_pubkey.as_str(),
- payload.seller_pubkey.as_str(),
- )
-}
-
-fn trade_decision_from_publish_payload(
- payload: &AppOrderDecisionPublishPayload,
-) -> Result<DesktopRuntimeTradeDecision, AppSyncTransportError> {
- match &payload.decision {
- AppOrderDecisionPayload::Accepted {
- inventory_commitments,
- } => Ok(DesktopRuntimeTradeDecision::Accept {
- inventory_commitments: inventory_commitments
+ };
+ values
.iter()
- .map(|commitment| {
- Ok(RadrootsOrderInventoryCommitment {
- bin_id: publish_bin_id(commitment.bin_id.as_str())?,
- bin_count: commitment.bin_count,
- })
+ .map(|value| {
+ value
+ .as_str()
+ .map(str::to_owned)
+ .ok_or(AppSqliteError::InvalidProjection {
+ reason: "signed local event tag values must be strings",
+ })
})
- .collect::<Result<Vec<_>, AppSyncTransportError>>()?,
- }),
- AppOrderDecisionPayload::Declined { reason } => Ok(DesktopRuntimeTradeDecision::Decline {
- reason: reason.clone(),
- }),
- }
+ .collect()
+ })
+ .collect()
}
#[cfg(test)]
@@ -9103,7 +8823,7 @@ fn farm_sync_payload(
.to_string()
}
-fn signed_order_request_evidence_record_is_usable(record: &RuntimeStoreRecord) -> bool {
+fn signed_event_evidence_record_is_usable(record: &RuntimeStoreRecord) -> bool {
if record.status != RuntimeStoreRecordStatus::Published
|| matches!(
record.outbox_status,
@@ -9135,25 +8855,22 @@ mod tests {
use chrono::{Duration, Utc};
use futures_util::{SinkExt, StreamExt};
- use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
- };
use radroots_event::ids::{
- RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
- RadrootsOrderQuoteId, RadrootsPublicKey,
- };
- use radroots_event::order::{
- RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
- RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment,
- RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
+ RadrootsAddressableCoordinate, RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId,
+ RadrootsPublicKey, RadrootsTradeId,
};
use radroots_event::{
- RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr,
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts,
+ trade::{
+ RADROOTS_TRADE_CANCELLATION_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION,
+ RadrootsFulfillmentProfileV1, RadrootsTradeCancellationProfileV1,
+ RadrootsTradeCandidateLineV1, RadrootsTradeCandidateTermsV1, RadrootsTradeDecisionV1,
+ RadrootsTradeEconomicsProfileV1, RadrootsTradeMutationBodyV1,
+ RadrootsTradeMutationEnvelopeV1, canonical_trade_mutation_content,
+ },
wire::RadrootsNip01EventWireParts as WireEventParts,
};
- use radroots_event_codec::order::{
- order_cancellation_event_build, order_decision_event_build, order_request_event_build,
- };
+ use radroots_event_codec::trade::{trade_mutation_event_build, trade_mutation_from_event};
use radroots_identity::{RadrootsIdentity, RadrootsIdentityId};
use radroots_nostr::prelude::{
RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrKeys, RadrootsNostrSecretKey,
@@ -9170,16 +8887,15 @@ mod tests {
RuntimeStoreRecordStatus, SourceRuntime,
};
use radroots_sdk::{
- LISTING_PUBLISH_OPERATION_KIND, TRADE_CANCELLATION_OPERATION_KIND,
- TRADE_DECISION_OPERATION_KIND, TRADE_SUBMIT_OPERATION_KIND,
+ LISTING_PUBLISH_OPERATION_KIND, TRADE_CANCEL_OPERATION_KIND,
+ TRADE_DECIDE_CANDIDATE_OPERATION_KIND, TRADE_SUBMIT_PROPOSAL_OPERATION_KIND,
};
use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor};
use radroots_studio_app_core::{
AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform,
AppSharedAccountsPaths, DesktopRuntimeLifecycleState,
DesktopRuntimeProjectionLifecycleState, DesktopRuntimePublicFarmLocation,
- DesktopRuntimeSnapshot, DesktopRuntimeTradeDecision, SHARED_ACCOUNTS_STORE_FILE_NAME,
- SHARED_IDENTITY_FILE_NAME,
+ DesktopRuntimeSnapshot, SHARED_ACCOUNTS_STORE_FILE_NAME, SHARED_IDENTITY_FILE_NAME,
};
use radroots_studio_app_remote_signer::{
RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord,
@@ -9187,7 +8903,7 @@ mod tests {
use radroots_studio_app_sqlite::{
AppSqliteError, AppSqliteStore, BuyerOrderCoordinationState, DatabaseTarget,
DesktopRuntimeWorkflowReceiptSourceKind, DesktopRuntimeWorkflowReceiptState,
- latest_schema_version, projected_order_id_from_trade_request,
+ latest_schema_version,
};
use radroots_studio_app_state::{
APP_STATE_FILE_NAME, AppStateCommand, AppStatePersistenceRepository, AppStateRepository,
@@ -9195,15 +8911,14 @@ mod tests {
HomeRoute,
};
use radroots_studio_app_sync::{
- AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload,
- AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload,
- AppOrderDecisionPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload,
- AppPublishContext, AppPublishPayload, AppPublishedOperationReceipt,
- AppRelayIngestScopeFreshness, AppRelayIngestScopeStatus, AppSyncRequest, AppSyncResult,
- AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, PendingSyncOperation,
- PendingSyncOperationState, RecordedAppSyncTransport, SyncAggregateRef, SyncCheckpointState,
- SyncCheckpointStatus, SyncConflict, SyncConflictKind, SyncConflictResolutionStatus,
- SyncConflictSeverity, SyncOperationKind, SyncTrigger,
+ AppFarmProfilePublishPayload, AppListingPublishPayload, AppPublishContext,
+ AppPublishPayload, AppPublishedOperationReceipt, AppRelayIngestScopeFreshness,
+ AppRelayIngestScopeStatus, AppSyncRequest, AppSyncResult, AppSyncRunStatus,
+ AppSyncTransport, AppSyncTransportError, AppTradeCancellationPublishPayload,
+ PendingSyncOperation, PendingSyncOperationState, RecordedAppSyncTransport,
+ SyncAggregateRef, SyncCheckpointState, SyncCheckpointStatus, SyncConflict,
+ SyncConflictKind, SyncConflictResolutionStatus, SyncConflictSeverity, SyncOperationKind,
+ SyncTrigger,
};
use radroots_studio_app_view::{
AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface,
@@ -9249,7 +8964,6 @@ mod tests {
DesktopRuntimeSupervisorDiagnosticsState, SYNC_TRANSPORT_UNAVAILABLE_MESSAGE,
TokioRuntimeBuilder, default_sync_transport, direct_relay_event_source_runtime,
farm_sync_payload, is_hex_64, pending_sync_upsert, signed_event_from_local_record,
- trade_decision_from_publish_payload,
};
use crate::pack_day_host_handoff::PackDayHostHandoffError;
use crate::pack_day_print::{
@@ -9527,17 +9241,20 @@ mod tests {
}));
}
- fn seed_relay_with_order_request(
+ fn seed_relay_with_trade_proposal(
runtime: &DesktopAppRuntime,
relay: &ThreadedAckRelay,
order_id: OrderId,
) {
- let request_event = runtime
+ let proposal_event = runtime
.lock_state()
- .resolve_seller_order_request_evidence(order_id)
- .expect("order request evidence should resolve")
- .request_event;
- seed_relay_with_sdk_event(relay, &request_event);
+ .resolve_order_lifecycle_evidence(order_id)
+ .expect("trade proposal evidence should resolve")
+ .root;
+ let parts =
+ trade_mutation_event_build(proposal_event).expect("trade proposal event should build");
+ let event = signed_test_event(SDK_TEST_BUYER_SECRET_KEY_HEX, 1_774_000_010, parts);
+ seed_relay_with_sdk_event(relay, &event);
}
fn seed_relay_with_shared_event_id(
@@ -9667,14 +9384,6 @@ mod tests {
signed_event_id(format!("app:signed_event:listing:{label}").as_str())
}
- fn test_order_id(value: &str) -> RadrootsOrderId {
- RadrootsOrderId::parse(value).expect("order id")
- }
-
- fn test_quote_id(value: &str) -> RadrootsOrderQuoteId {
- RadrootsOrderQuoteId::parse(value).expect("quote id")
- }
-
fn test_bin_id(value: &str) -> RadrootsInventoryBinId {
RadrootsInventoryBinId::parse(value).expect("bin id")
}
@@ -9683,37 +9392,134 @@ mod tests {
RadrootsPublicKey::parse(value).expect("pubkey")
}
- fn test_listing_addr(value: &str) -> RadrootsListingAddress {
- RadrootsListingAddress::parse(value).expect("listing address")
+ fn test_trade_order_id(order_id: OrderId) -> String {
+ order_id.to_string().replace('-', "")
}
- fn test_order_decision_publish_payload_to_event_payload(
- payload: &AppOrderDecisionPublishPayload,
- ) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: test_order_id(payload.trade_order_id.as_str()),
- listing_addr: test_listing_addr(payload.listing_addr.as_str()),
- buyer_pubkey: test_pubkey(payload.buyer_pubkey.as_str()),
- seller_pubkey: test_pubkey(payload.seller_pubkey.as_str()),
- decision: match &payload.decision {
- AppOrderDecisionPayload::Accepted {
- inventory_commitments,
- } => RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: inventory_commitments
- .iter()
- .map(|commitment| RadrootsOrderInventoryCommitment {
- bin_id: test_bin_id(commitment.bin_id.as_str()),
- bin_count: commitment.bin_count,
- })
- .collect(),
+ fn test_trade_proposal_envelope(
+ trade_order_id: &str,
+ listing_addr: &str,
+ listing_event_id: &str,
+ buyer_pubkey: &str,
+ seller_pubkey: &str,
+ order_quantity: u32,
+ ) -> RadrootsTradeMutationEnvelopeV1 {
+ let quantity = order_quantity.to_string();
+ let total_minor_units = 800_u32
+ .checked_mul(order_quantity)
+ .expect("test trade total should fit")
+ .to_string();
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: super::RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: RadrootsTradeId::parse(trade_order_id).expect("trade id"),
+ root_mutation_id: None,
+ buyer_pubkey: test_pubkey(buyer_pubkey),
+ seller_pubkey: test_pubkey(seller_pubkey),
+ farm_id: RadrootsDTag::parse("seller-decision-farm").expect("farm d tag"),
+ parent_mutation_ids: Vec::new(),
+ author_pubkey: test_pubkey(buyer_pubkey),
+ counterparty_pubkey: test_pubkey(seller_pubkey),
+ authored_at_unix_s: 1_774_000_010,
+ body: RadrootsTradeMutationBodyV1::Proposal {
+ candidate: RadrootsTradeCandidateTermsV1 {
+ candidate_id: None,
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ base_candidate_id: None,
+ supersession_intent: None,
+ buyer_pubkey: test_pubkey(buyer_pubkey),
+ seller_pubkey: test_pubkey(seller_pubkey),
+ farm_id: RadrootsDTag::parse("seller-decision-farm").expect("farm d tag"),
+ lines: vec![RadrootsTradeCandidateLineV1 {
+ line_id: RadrootsDTag::parse("seller-order-primary-line")
+ .expect("line d tag"),
+ listing_addr: RadrootsAddressableCoordinate::parse(listing_addr)
+ .expect("listing address"),
+ listing_event_id: test_event_id(listing_event_id),
+ listing_snapshot_sha256: test_event_id_seed(listing_event_id),
+ product_id: "seller-order-product".to_owned(),
+ option_id: None,
+ bin_id: test_bin_id("seller-order-primary-bin"),
+ quantity_mantissa: quantity,
+ quantity_scale: 0,
+ unit_code: "each".to_owned(),
+ unit_profile: "app-each".to_owned(),
+ unit_price_mantissa: "800".to_owned(),
+ currency_code: "USD".to_owned(),
+ line_subtotal_mantissa: total_minor_units.clone(),
+ replaces_line_id: None,
+ }],
+ line_tombstones: Vec::new(),
+ economics: RadrootsTradeEconomicsProfileV1 {
+ profile_id: "seller-decision-fixed".to_owned(),
+ currency_code: "USD".to_owned(),
+ currency_exponent: 2,
+ rounding_profile: "minor-units".to_owned(),
+ subtotal_mantissa: total_minor_units.clone(),
+ discount_total_mantissa: "0".to_owned(),
+ adjustment_total_mantissa: "0".to_owned(),
+ total_mantissa: total_minor_units,
+ adjustments: Vec::new(),
+ },
+ fulfillment: RadrootsFulfillmentProfileV1 {
+ profile_id: "seller-decision-pickup".to_owned(),
+ method: "pickup".to_owned(),
+ starts_at_unix_s: 4_102_444_800,
+ ends_at_unix_s: 4_102_531_200,
+ timezone: "UTC".to_owned(),
+ utc_offset_seconds: 0,
+ fold: 0,
+ location_class: "farm".to_owned(),
+ requires_private_terms: false,
+ },
+ cancellation: RadrootsTradeCancellationProfileV1 {
+ profile_id: "buyer-pre-agreement".to_owned(),
+ buyer_pre_agreement: true,
+ post_agreement_cutoff_unix_s: None,
+ },
+ private_terms: None,
+ proposal_expires_at_unix_s: 4_102_444_799,
},
- AppOrderDecisionPayload::Declined { reason } => {
- RadrootsOrderDecisionOutcome::Declined {
- reason: reason.clone(),
- }
- }
},
- }
+ })
+ .expect("canonical trade proposal")
+ .envelope
+ }
+
+ fn direct_relay_trade_work_payload(
+ account_id: &str,
+ buyer_pubkey: &str,
+ seller_pubkey: &str,
+ source: &str,
+ ) -> AppPublishPayload {
+ let order_id = OrderId::generate();
+ let trade_id = RadrootsTradeId::parse(order_id.to_string().replace('-', "").as_str())
+ .expect("trade id");
+ AppPublishPayload::TradeCancellation(AppTradeCancellationPublishPayload {
+ context: AppPublishContext::new(account_id.to_owned(), source),
+ order_id,
+ farm_id: FarmId::generate(),
+ envelope: RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_CANCELLATION_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id,
+ root_mutation_id: None,
+ buyer_pubkey: test_pubkey(buyer_pubkey),
+ seller_pubkey: test_pubkey(seller_pubkey),
+ farm_id: RadrootsDTag::parse("direct-relay-test").expect("farm d tag"),
+ parent_mutation_ids: Vec::new(),
+ author_pubkey: test_pubkey(buyer_pubkey),
+ counterparty_pubkey: test_pubkey(seller_pubkey),
+ authored_at_unix_s: 1_774_000_020,
+ body: RadrootsTradeMutationBodyV1::Cancellation {
+ target_candidate_id: None,
+ target_claim_mutation_id: None,
+ reason: "direct relay transport guard".to_owned(),
+ },
+ },
+ })
}
fn install_recorded_sync_transport(
@@ -9817,7 +9623,7 @@ mod tests {
}
#[test]
- fn runtime_direct_relay_transport_rejects_typed_order_request_work() {
+ fn runtime_direct_relay_transport_rejects_typed_trade_proposal_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let account_id = manager
@@ -9828,71 +9634,14 @@ mod tests {
.expect("buyer signer lookup should succeed")
.expect("buyer account should have local signer");
let seller_identity = RadrootsIdentity::generate();
- let product_id = ProductId::generate();
- let order_id = OrderId::generate();
- let listing_event_id = "1".repeat(64);
- let listing_addr = format!(
- "30402:{}:{}",
- seller_identity.public_key_hex(),
- super::d_tag_from_uuid(ProductId::generate().as_uuid())
- );
- let order_document = RadrootsOrderRequest {
- order_id: test_order_id(order_id.to_string().as_str()),
- listing_addr: test_listing_addr(listing_addr.as_str()),
- buyer_pubkey: test_pubkey(buyer_identity.public_key_hex().as_str()),
- seller_pubkey: test_pubkey(seller_identity.public_key_hex().as_str()),
- items: vec![RadrootsOrderItem {
- bin_id: test_bin_id("bin-1"),
- bin_count: 1,
- }],
- economics: RadrootsOrderEconomics {
- quote_id: test_quote_id(format!("quote-{order_id}").as_str()),
- quote_version: 1,
- pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
- currency: RadrootsCoreCurrency::USD,
- items: vec![RadrootsOrderEconomicItem {
- bin_id: test_bin_id("bin-1"),
- bin_count: 1,
- quantity_amount: RadrootsCoreDecimal::from(1u32),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: RadrootsCoreDecimal::from(5u32),
- unit_price_currency: RadrootsCoreCurrency::USD,
- line_subtotal: RadrootsCoreMoney::from_minor_units_u32(
- 500,
- RadrootsCoreCurrency::USD,
- ),
- }],
- discounts: Vec::new(),
- adjustments: Vec::new(),
- subtotal: RadrootsCoreMoney::from_minor_units_u32(500, RadrootsCoreCurrency::USD),
- discount_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
- adjustment_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
- total: RadrootsCoreMoney::from_minor_units_u32(500, RadrootsCoreCurrency::USD),
- },
- };
- let payload = AppPublishPayload::OrderRequest(AppOrderRequestPublishPayload {
- context: AppPublishContext::new(account_id.to_string(), "place_personal_order")
- .with_source_local_event_id("app:local_work:order_request:direct"),
- order_id,
- farm_id: FarmId::generate(),
- status: Some("needs_action".to_owned()),
- order_document_json: Some(json!({"document": {"order": order_document}})),
- listing_addr: Some(listing_addr),
- listing_event_id: Some(listing_event_id),
- listing_relays: vec![relay.url().to_owned()],
- buyer_pubkey: Some(buyer_identity.public_key_hex()),
- seller_pubkey: Some(seller_identity.public_key_hex()),
- items: vec![AppOrderRequestItemPayload {
- product_id,
- quantity: 1,
- }],
- currency_code: Some("USD".to_owned()),
- total_minor_units: Some(500),
- note: Some("coordinate pickup".to_owned()),
- confirm_public_note: true,
- });
+ let payload = direct_relay_trade_work_payload(
+ account_id.to_string().as_str(),
+ buyer_identity.public_key_hex().as_str(),
+ seller_identity.public_key_hex().as_str(),
+ "trade_proposal",
+ );
let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
- .expect("typed order request publish work should serialize");
+ .expect("typed trade proposal publish work should serialize");
let mut transport =
ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
@@ -9903,14 +9652,14 @@ mod tests {
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect_err("direct relay order request publish should use DesktopRuntimeSupervisor");
+ .expect_err("direct relay trade proposal publish should use DesktopRuntimeSupervisor");
assert_publish_payload_uses_desktop_runtime_supervisor(error);
assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_rejects_typed_order_decision_work() {
+ fn runtime_direct_relay_transport_rejects_typed_trade_decision_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let account_id = manager
@@ -9921,26 +9670,14 @@ mod tests {
.expect("seller signer lookup should succeed")
.expect("seller account should have local signer");
let buyer_pubkey = "1111111111111111111111111111111111111111111111111111111111111111";
- let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload {
- context: AppPublishContext::new(account_id.to_string(), "seller_order_decision"),
- app_order_id: OrderId::generate(),
- farm_id: FarmId::generate(),
- trade_order_id: "order-1".to_owned(),
- request_event_id: test_event_id_seed("order-request-event-1"),
- listing_event_id: Some(test_event_id_seed("listing-event-1")),
- listing_addr: format!("30402:{}:listing-key", identity.public_key_hex()),
- buyer_pubkey: buyer_pubkey.to_owned(),
- seller_pubkey: identity.public_key_hex(),
- decision: AppOrderDecisionPayload::Accepted {
- inventory_commitments: vec![AppOrderDecisionInventoryCommitment {
- bin_id: "bin-1".to_owned(),
- bin_count: 2,
- }],
- },
- confirm_public_note: false,
- });
+ let payload = direct_relay_trade_work_payload(
+ account_id.to_string().as_str(),
+ buyer_pubkey,
+ identity.public_key_hex().as_str(),
+ "trade_candidate_decision",
+ );
let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
- .expect("typed order decision publish work should serialize");
+ .expect("typed trade decision publish work should serialize");
let mut transport =
ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
@@ -9951,14 +9688,14 @@ mod tests {
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect_err("direct relay order decision publish should use DesktopRuntimeSupervisor");
+ .expect_err("direct relay trade decision publish should use DesktopRuntimeSupervisor");
assert_publish_payload_uses_desktop_runtime_supervisor(error);
assert_eq!(relay.event_count(), 0);
}
#[test]
- fn runtime_direct_relay_transport_rejects_typed_order_lifecycle_work() {
+ fn runtime_direct_relay_transport_rejects_typed_trade_lifecycle_work() {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let buyer_account_id = manager
@@ -9975,42 +9712,17 @@ mod tests {
.get_signing_identity(&seller_account_id)
.expect("seller signer lookup should succeed")
.expect("seller account should have local signer");
- let app_order_id = OrderId::generate();
- let farm_id = FarmId::generate();
- let listing_addr = format!(
- "30402:{}:AAAAAAAAAAAAAAAAAAAAAg",
- seller_identity.public_key_hex()
+ let cancellation = direct_relay_trade_work_payload(
+ buyer_account_id.to_string().as_str(),
+ buyer_identity.public_key_hex().as_str(),
+ seller_identity.public_key_hex().as_str(),
+ "trade_cancellation",
);
- let common = (
- app_order_id,
- farm_id,
- "order-1".to_owned(),
- test_event_id_seed("order-request-event-1"),
- listing_addr,
- buyer_identity.public_key_hex(),
- seller_identity.public_key_hex(),
- );
- let cancellation =
- AppPublishPayload::OrderCancellation(AppOrderCancellationPublishPayload {
- context: AppPublishContext::new(
- buyer_account_id.to_string(),
- "buyer_order_cancellation",
- ),
- app_order_id: common.0,
- farm_id: common.1,
- trade_order_id: common.2.clone(),
- request_event_id: common.3.clone(),
- listing_addr: common.4.clone(),
- buyer_pubkey: common.5.clone(),
- seller_pubkey: common.6.clone(),
- reason: "buyer cancelled order".to_owned(),
- confirm_public_note: false,
- });
let operations = [cancellation]
.into_iter()
.map(|payload| {
PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
- .expect("typed lifecycle publish work should serialize")
+ .expect("typed trade lifecycle publish work should serialize")
})
.collect::<Vec<_>>();
let mut transport =
@@ -10023,7 +9735,7 @@ mod tests {
pending_operations: operations,
known_conflicts: Vec::new(),
})
- .expect_err("direct relay lifecycle publish should use DesktopRuntimeSupervisor");
+ .expect_err("direct relay trade lifecycle publish should use DesktopRuntimeSupervisor");
assert_publish_payload_uses_desktop_runtime_supervisor(error);
assert_eq!(relay.event_count(), 0);
@@ -10530,7 +10242,8 @@ mod tests {
}
#[test]
- fn runtime_direct_relay_transport_rejects_order_request_missing_listing_provenance_target() {
+ fn runtime_direct_relay_transport_rejects_trade_work_before_transport_level_provenance_lookup()
+ {
let relay = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let account_id = manager
@@ -10541,28 +10254,14 @@ mod tests {
.expect("buyer signer lookup should succeed")
.expect("buyer account should have local signer");
let seller_pubkey = "2222222222222222222222222222222222222222222222222222222222222222";
- let payload = AppPublishPayload::OrderRequest(AppOrderRequestPublishPayload {
- context: AppPublishContext::new(account_id.to_string(), "order_missing_listing_relay"),
- order_id: OrderId::generate(),
- farm_id: FarmId::generate(),
- status: Some("needs_action".to_owned()),
- order_document_json: Some(json!({"document": {"order": {}}})),
- listing_addr: Some(format!("30402:{seller_pubkey}:listing-key")),
- listing_event_id: Some("listing-event-id".to_owned()),
- listing_relays: vec!["wss://listing.example".to_owned()],
- buyer_pubkey: Some(identity.public_key_hex()),
- seller_pubkey: Some(seller_pubkey.to_owned()),
- items: vec![AppOrderRequestItemPayload {
- product_id: ProductId::generate(),
- quantity: 1,
- }],
- currency_code: Some("USD".to_owned()),
- total_minor_units: Some(450),
- note: None,
- confirm_public_note: false,
- });
+ let payload = direct_relay_trade_work_payload(
+ account_id.to_string().as_str(),
+ identity.public_key_hex().as_str(),
+ seller_pubkey,
+ "trade_proposal",
+ );
let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-25T07:00:00Z")
- .expect("order publish payload should serialize");
+ .expect("trade publish payload should serialize");
let mut transport =
ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
@@ -10573,7 +10272,7 @@ mod tests {
pending_operations: vec![operation],
known_conflicts: Vec::new(),
})
- .expect_err("direct relay order request should use DesktopRuntimeSupervisor");
+ .expect_err("direct relay trade work should use DesktopRuntimeSupervisor");
assert_publish_payload_uses_desktop_runtime_supervisor(error);
assert_eq!(relay.event_count(), 0);
@@ -14517,7 +14216,7 @@ mod tests {
.expect("buyer order detail")
.order_note
.as_deref(),
- Some("Leave by the cooler")
+ None
);
}
@@ -14656,30 +14355,22 @@ mod tests {
let payload = runtime
.prepare_order_accept(order_id)
.expect("seller order accept payload should prepare");
- let decision = trade_decision_from_publish_payload(&payload)
- .expect("order accept payload should convert to SDK trade decision");
- assert_eq!(payload.app_order_id, order_id);
- assert_eq!(payload.trade_order_id, "seller-order-decision-1");
- assert_eq!(
- payload.request_event_id,
- shared_order_request_event_id(&paths, "seller-order-decision-1")
- );
+ assert_eq!(payload.order_id, order_id);
+ assert_eq!(payload.envelope.buyer_pubkey.as_str(), buyer_pubkey);
+ assert_eq!(payload.envelope.seller_pubkey.as_str(), seller_pubkey);
+ assert_eq!(payload.envelope.author_pubkey.as_str(), seller_pubkey);
+ assert_eq!(payload.envelope.counterparty_pubkey.as_str(), buyer_pubkey);
+ assert!(!payload.acknowledge_private_terms);
+ let RadrootsTradeMutationBodyV1::Decision { decision, .. } = &payload.envelope.body else {
+ panic!("expected trade candidate decision body");
+ };
assert_eq!(
- payload.listing_event_id,
- Some(signed_listing_event_id("seller-order-decision"))
+ decision,
+ &RadrootsTradeDecisionV1::Accepted {
+ reservation_assertion: None
+ }
);
- assert_eq!(payload.buyer_pubkey, buyer_pubkey);
- assert_eq!(payload.seller_pubkey, seller_pubkey);
- let DesktopRuntimeTradeDecision::Accept {
- inventory_commitments,
- } = decision
- else {
- panic!("expected accepted decision");
- };
- assert_eq!(inventory_commitments.len(), 1);
- assert_eq!(inventory_commitments[0].bin_id, "seller-order-primary-bin");
- assert_eq!(inventory_commitments[0].bin_count, 2);
cleanup_bootstrapped_runtime_paths(&paths);
}
@@ -14694,22 +14385,20 @@ mod tests {
let payload = runtime
.prepare_order_decline(order_id, " out of stock ", true)
.expect("seller order decline payload should prepare");
- let decision = trade_decision_from_publish_payload(&payload)
- .expect("order decline payload should convert to SDK trade decision");
- assert_eq!(payload.buyer_pubkey, buyer_pubkey);
- assert_eq!(payload.seller_pubkey, seller_pubkey);
+ assert_eq!(payload.order_id, order_id);
+ assert_eq!(payload.envelope.buyer_pubkey.as_str(), buyer_pubkey);
+ assert_eq!(payload.envelope.seller_pubkey.as_str(), seller_pubkey);
+ assert!(!payload.acknowledge_private_terms);
+ let RadrootsTradeMutationBodyV1::Decision { decision, .. } = &payload.envelope.body else {
+ panic!("expected trade candidate decision body");
+ };
assert_eq!(
- payload.decision,
- AppOrderDecisionPayload::Declined {
+ decision,
+ &RadrootsTradeDecisionV1::Declined {
reason: "out of stock".to_owned()
}
);
- assert!(payload.confirm_public_note);
- let DesktopRuntimeTradeDecision::Decline { reason } = decision else {
- panic!("expected declined decision");
- };
- assert_eq!(reason, "out of stock");
cleanup_bootstrapped_runtime_paths(&paths);
}
@@ -14726,11 +14415,11 @@ mod tests {
.prepare_order_accept(order_id)
.expect("seller order accept payload should prepare from older evidence");
- assert_eq!(payload.trade_order_id, "seller-order-decision-1");
- assert_eq!(
- payload.request_event_id,
- shared_order_request_event_id(&paths, "seller-order-decision-1")
- );
+ assert_eq!(payload.order_id, order_id);
+ assert!(matches!(
+ payload.envelope.body,
+ RadrootsTradeMutationBodyV1::Decision { .. }
+ ));
cleanup_bootstrapped_runtime_paths(&paths);
}
@@ -14750,7 +14439,7 @@ mod tests {
assert!(matches!(
error,
AppSqliteError::InvalidProjection {
- reason: "seller order decision requires signed order request evidence"
+ reason: "trade lifecycle requires signed proposal evidence"
}
));
@@ -14785,7 +14474,7 @@ mod tests {
assert_eq!(persisted_order_status(&runtime, order_id), "needs_action");
assert_eq!(
persisted_order_workflow_agreement(&runtime, order_id),
- "agreed_pending_validation"
+ "contested"
);
assert_eq!(relay.event_count(), 1);
@@ -14858,7 +14547,7 @@ mod tests {
seller_order_decision_sdk_runtime("seller_order_accept_publish", 6, 2);
install_direct_relay_sync_transport(&runtime, &relay);
restore_sdk_runtime_with_relays(&runtime, &paths, vec![relay.url().to_owned()]);
- seed_relay_with_order_request(&runtime, &relay, order_id);
+ seed_relay_with_trade_proposal(&runtime, &relay, order_id);
assert!(
runtime
@@ -14889,7 +14578,7 @@ mod tests {
seller_order_decision_sdk_runtime("seller_order_decline_publish", 6, 2);
install_direct_relay_sync_transport(&runtime, &relay);
restore_sdk_runtime_with_relays(&runtime, &paths, vec![relay.url().to_owned()]);
- seed_relay_with_order_request(&runtime, &relay, order_id);
+ seed_relay_with_trade_proposal(&runtime, &relay, order_id);
assert!(
runtime
@@ -15109,13 +14798,13 @@ mod tests {
payload["document"]["order"]["economics"]["total"]["amount"],
"16.00"
);
- assert_eq!(
- payload["app_order"]["buyer_order_note"],
- "Leave by the cooler"
- );
- assert_eq!(
- payload["app_order"]["buyer_order_note_public_confirmed"],
- true
+ assert!(payload["app_order"].get("buyer_email").is_none());
+ assert!(payload["app_order"].get("buyer_phone").is_none());
+ assert!(payload["app_order"].get("buyer_order_note").is_none());
+ assert!(
+ payload["app_order"]
+ .get("buyer_order_note_public_confirmed")
+ .is_none()
);
assert_eq!(
payload["app_order"]["lines"][0]["listing_bin_id"],
@@ -15476,10 +15165,7 @@ mod tests {
assert_eq!(row.status, BuyerOrderStatus::Placed);
assert_eq!(detail.order_id, fixture.order_id);
assert_eq!(detail.status, BuyerOrderStatus::Placed);
- assert_eq!(
- detail.workflow.agreement,
- TradeAgreementStatus::AgreedPendingValidation
- );
+ assert_eq!(detail.workflow.agreement, TradeAgreementStatus::Contested);
assert_eq!(
detail.workflow.provenance.last_event_id.as_deref(),
Some(fixture.decision_event_id.as_str())
@@ -18975,7 +18661,8 @@ mod tests {
let listing_key = super::d_tag_from_uuid(ProductId::generate().as_uuid());
let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
let listing_event_id = signed_listing_event_id(label);
- let trade_order_id = format!("{label}-trade-order");
+ let order_id = OrderId::generate();
+ let trade_order_id = test_trade_order_id(order_id);
append_app_signed_listing_record(
&paths,
"linked-seller-account",
@@ -18994,11 +18681,6 @@ mod tests {
seller_pubkey,
2,
);
- let order_id = projected_order_id_from_trade_request(
- trade_order_id.as_str(),
- buyer_pubkey.as_str(),
- request_event_id.as_str(),
- );
let decision_event_id = if append_decision {
append_signed_order_decision_record(
&paths,
@@ -19052,7 +18734,8 @@ mod tests {
.public_key_hex;
let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned();
let product_id = ProductId::generate();
- let trade_order_id = "seller-order-decision-1";
+ let order_id = OrderId::generate();
+ let trade_order_id = test_trade_order_id(order_id);
let farm_key = super::d_tag_from_uuid(farm_id.as_uuid());
let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
@@ -19066,20 +18749,15 @@ mod tests {
listing_event_id.as_str(),
stock_count,
);
- let request_event_id = append_signed_order_request_record(
+ let _request_event_id = append_signed_order_request_record(
&paths,
- trade_order_id,
+ trade_order_id.as_str(),
listing_addr.as_str(),
listing_event_id.as_str(),
buyer_pubkey.as_str(),
seller_pubkey.as_str(),
order_quantity,
);
- let order_id = projected_order_id_from_trade_request(
- trade_order_id,
- buyer_pubkey.as_str(),
- request_event_id.as_str(),
- );
(
runtime,
@@ -19118,7 +18796,8 @@ mod tests {
.public_key_hex;
let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned();
let product_id = ProductId::generate();
- let trade_order_id = "seller-order-decision-1";
+ let order_id = OrderId::generate();
+ let trade_order_id = test_trade_order_id(order_id);
let farm_key = super::d_tag_from_uuid(farm_id.as_uuid());
let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
@@ -19132,20 +18811,15 @@ mod tests {
listing_event_id.as_str(),
stock_count,
);
- let request_event_id = append_verified_signed_order_request_record(
+ let _request_event_id = append_verified_signed_order_request_record(
&paths,
- trade_order_id,
+ trade_order_id.as_str(),
listing_addr.as_str(),
listing_event_id.as_str(),
buyer_pubkey.as_str(),
seller_pubkey.as_str(),
order_quantity,
);
- let order_id = projected_order_id_from_trade_request(
- trade_order_id,
- buyer_pubkey.as_str(),
- request_event_id.as_str(),
- );
(
runtime,
@@ -19161,11 +18835,11 @@ mod tests {
runtime: &DesktopAppRuntime,
relay: &ThreadedAckRelay,
order_id: OrderId,
- product_id: ProductId,
- seller_pubkey: &str,
- buyer_pubkey: &str,
+ _product_id: ProductId,
+ _seller_pubkey: &str,
+ _buyer_pubkey: &str,
) {
- let (account_id, farm_id, accounts_manager) = {
+ let (account_id, accounts_manager, proposal) = {
let state = runtime.lock_state();
let selected_account = state
.state_store
@@ -19175,62 +18849,35 @@ mod tests {
.expect("selected seller account");
(
selected_account.account.account_id.clone(),
- state.selected_farm_id().expect("selected farm"),
state
.accounts_manager
.as_ref()
.expect("accounts manager")
.clone(),
+ state
+ .resolve_order_lifecycle_evidence(order_id)
+ .expect("seller trade proposal evidence should resolve")
+ .root,
)
};
- let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
- let request_event_id = runtime
- .lock_state()
- .resolve_seller_order_request_evidence(order_id)
- .expect("seller request evidence should resolve")
- .request_event
- .id_str()
- .to_owned();
- let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload {
- 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(),
- request_event_id,
- listing_event_id: Some(signed_listing_event_id("seller-order-decision")),
- listing_addr: format!("30402:{seller_pubkey}:{listing_key}"),
- buyer_pubkey: buyer_pubkey.to_owned(),
- seller_pubkey: seller_pubkey.to_owned(),
- decision: AppOrderDecisionPayload::Accepted {
- inventory_commitments: vec![AppOrderDecisionInventoryCommitment {
- bin_id: "seller-order-primary-bin".to_owned(),
- bin_count: 2,
- }],
- },
- confirm_public_note: false,
- });
- let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
- .expect("prior order decision publish work should serialize");
- 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 = test_order_decision_publish_payload_to_event_payload(&payload);
- let parts = order_decision_event_build(&request_event_id, &request_event_id, &decision)
- .expect("order decision draft should build");
+ let envelope = super::trade_decision_envelope_from_proposal(
+ &proposal,
+ &super::AppSellerOrderDecisionCommand::Accept,
+ 1_774_000_030,
+ )
+ .expect("trade decision envelope should build");
+ let parts = trade_mutation_event_build(envelope)
+ .expect("trade decision event builder should build");
let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .expect("order decision event builder should build")
+ .expect("trade decision event builder should build")
.sign_with_keys(identity.keys())
- .expect("order decision event should sign");
+ .expect("trade decision event should sign");
publish_signed_test_event_to_relay(relay, &event);
assert_eq!(relay.event_count(), 1);
}
@@ -19360,25 +19007,16 @@ mod tests {
.expect("open shared runtime store db");
let store = RuntimeStore::new(executor);
store.migrate_up().expect("migrate shared runtime store");
- let order = RadrootsOrderRequest {
- order_id: test_order_id(trade_order_id),
- listing_addr: test_listing_addr(listing_addr),
- buyer_pubkey: test_pubkey(buyer_pubkey),
- seller_pubkey: test_pubkey(seller_pubkey),
- items: vec![RadrootsOrderItem {
- bin_id: test_bin_id("seller-order-primary-bin"),
- bin_count: order_quantity,
- }],
- economics: signed_order_request_economics(trade_order_id, order_quantity),
- };
- let parts = order_request_event_build(
- &RadrootsEventPtr {
- id: test_event_id_seed(listing_event_id),
- relays: Some("wss://relay.example".to_owned()),
- },
- &order,
- )
- .expect("order request draft should build");
+ let envelope = test_trade_proposal_envelope(
+ trade_order_id,
+ listing_addr,
+ listing_event_id,
+ buyer_pubkey,
+ seller_pubkey,
+ order_quantity,
+ );
+ let parts =
+ trade_mutation_event_build(envelope).expect("trade proposal event draft should build");
let record_id = format!("app:signed_event:order-request:{trade_order_id}");
let event_id = signed_event_id(record_id.as_str());
let event = test_event_from_parts(
@@ -19433,7 +19071,7 @@ mod tests {
relay_set_fingerprint: Some("relay-set".to_owned()),
relay_delivery_json: Some(relay_delivery_json),
})
- .expect("append signed order request");
+ .expect("append signed trade proposal");
stored_event_id
}
@@ -19457,33 +19095,24 @@ mod tests {
.expect("open shared runtime store db");
let store = RuntimeStore::new(executor);
store.migrate_up().expect("migrate shared runtime store");
- let order = RadrootsOrderRequest {
- order_id: test_order_id(trade_order_id),
- listing_addr: test_listing_addr(listing_addr),
- buyer_pubkey: test_pubkey(buyer_pubkey),
- seller_pubkey: test_pubkey(seller_pubkey),
- items: vec![RadrootsOrderItem {
- bin_id: test_bin_id("seller-order-primary-bin"),
- bin_count: order_quantity,
- }],
- economics: signed_order_request_economics(trade_order_id, order_quantity),
- };
- let parts = order_request_event_build(
- &RadrootsEventPtr {
- id: test_event_id_seed(listing_event_id),
- relays: Some("wss://relay.example".to_owned()),
- },
- &order,
- )
- .expect("order request draft should build");
+ let envelope = test_trade_proposal_envelope(
+ trade_order_id,
+ listing_addr,
+ listing_event_id,
+ buyer_pubkey,
+ seller_pubkey,
+ order_quantity,
+ );
+ let parts =
+ trade_mutation_event_build(envelope).expect("trade proposal event draft should build");
let secret_key = RadrootsNostrSecretKey::from_hex(SDK_TEST_BUYER_SECRET_KEY_HEX)
.expect("SDK test buyer secret key should parse");
let keys = RadrootsNostrKeys::new(secret_key);
let signed_event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .expect("order request event should build")
+ .expect("trade proposal event should build")
.custom_created_at(RadrootsNostrTimestamp::from_secs(1_774_000_010))
.sign_with_keys(&keys)
- .expect("order request event should sign");
+ .expect("trade proposal event should sign");
let event = radroots_event_from_nostr(&signed_event);
let stored_event_id = event.id_str().to_owned();
let event_created_at = i64::try_from(event.created_at_u64()).expect("event timestamp");
@@ -19531,68 +19160,29 @@ mod tests {
relay_set_fingerprint: Some("relay-set".to_owned()),
relay_delivery_json: Some(relay_delivery_json),
})
- .expect("append verified signed order request");
+ .expect("append verified signed trade proposal");
stored_event_id
}
- fn signed_order_request_economics(
- trade_order_id: &str,
- order_quantity: u32,
- ) -> RadrootsOrderEconomics {
- let currency = RadrootsCoreCurrency::USD;
- let unit_price_minor_units = 800_u32;
- let total_minor_units = unit_price_minor_units
- .checked_mul(order_quantity)
- .expect("order total should fit");
-
- RadrootsOrderEconomics {
- quote_id: test_quote_id(format!("{trade_order_id}-quote").as_str()),
- quote_version: 1,
- pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
- currency,
- items: vec![RadrootsOrderEconomicItem {
- bin_id: test_bin_id("seller-order-primary-bin"),
- bin_count: order_quantity,
- quantity_amount: RadrootsCoreDecimal::from(1u32),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: RadrootsCoreDecimal::from(8u32),
- unit_price_currency: currency,
- line_subtotal: RadrootsCoreMoney::from_minor_units_u32(total_minor_units, currency),
- }],
- discounts: Vec::new(),
- adjustments: Vec::new(),
- subtotal: RadrootsCoreMoney::from_minor_units_u32(total_minor_units, currency),
- discount_total: RadrootsCoreMoney::zero(currency),
- adjustment_total: RadrootsCoreMoney::zero(currency),
- total: RadrootsCoreMoney::from_minor_units_u32(total_minor_units, currency),
- }
- }
-
fn append_signed_order_decision_record(
paths: &AppDesktopRuntimePaths,
- trade_order_id: &str,
+ _trade_order_id: &str,
request_event_id: &str,
listing_addr: &str,
- buyer_pubkey: &str,
+ _buyer_pubkey: &str,
seller_pubkey: &str,
- order_quantity: u32,
+ _order_quantity: u32,
) -> String {
- let payload = RadrootsOrderDecision {
- order_id: test_order_id(trade_order_id),
- listing_addr: test_listing_addr(listing_addr),
- buyer_pubkey: test_pubkey(buyer_pubkey),
- seller_pubkey: test_pubkey(seller_pubkey),
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: test_bin_id("seller-order-primary-bin"),
- bin_count: order_quantity,
- }],
- },
- };
- let request_event_id = test_event_id(request_event_id);
- let parts = order_decision_event_build(&request_event_id, &request_event_id, &payload)
- .expect("order decision draft should build");
- let record_id = format!("app:signed_event:order-decision:{trade_order_id}");
+ let proposal = shared_trade_proposal_envelope_by_event_id(paths, request_event_id);
+ let envelope = super::trade_decision_envelope_from_proposal(
+ &proposal,
+ &super::AppSellerOrderDecisionCommand::Accept,
+ 1_774_000_030,
+ )
+ .expect("trade decision envelope should build");
+ let parts =
+ trade_mutation_event_build(envelope).expect("trade decision draft should build");
+ let record_id = format!("app:signed_event:trade-decision:{request_event_id}");
append_trade_signed_event_record(
paths,
record_id.as_str(),
@@ -19605,26 +19195,24 @@ mod tests {
#[allow(clippy::too_many_arguments)]
fn append_signed_order_cancellation_record_with_prev(
paths: &AppDesktopRuntimePaths,
- trade_order_id: &str,
+ _trade_order_id: &str,
event_key: &str,
request_event_id: &str,
- prev_event_id: &str,
+ _prev_event_id: &str,
listing_addr: &str,
buyer_pubkey: &str,
- seller_pubkey: &str,
+ _seller_pubkey: &str,
) -> String {
- let request_event_id = test_event_id(request_event_id);
- let prev_event_id = test_event_id(prev_event_id);
- let payload = RadrootsOrderCancellation {
- order_id: test_order_id(trade_order_id),
- listing_addr: test_listing_addr(listing_addr),
- buyer_pubkey: test_pubkey(buyer_pubkey),
- seller_pubkey: test_pubkey(seller_pubkey),
- reason: "buyer cancelled order".to_owned(),
- };
- let parts = order_cancellation_event_build(&request_event_id, &prev_event_id, &payload)
- .expect("order cancellation draft should build");
- let record_id = format!("app:signed_event:cancellation:{event_key}");
+ let proposal = shared_trade_proposal_envelope_by_event_id(paths, request_event_id);
+ let envelope = super::trade_cancellation_envelope_from_proposal(
+ &proposal,
+ "buyer cancelled order",
+ 1_774_000_040,
+ )
+ .expect("trade cancellation envelope should build");
+ let parts =
+ trade_mutation_event_build(envelope).expect("trade cancellation draft should build");
+ let record_id = format!("app:signed_event:trade-cancellation:{event_key}");
append_trade_signed_event_record(
paths,
record_id.as_str(),
@@ -19634,6 +19222,18 @@ mod tests {
)
}
+ fn shared_trade_proposal_envelope_by_event_id(
+ paths: &AppDesktopRuntimePaths,
+ event_id: &str,
+ ) -> RadrootsTradeMutationEnvelopeV1 {
+ shared_runtime_store_records(paths)
+ .into_iter()
+ .find(|record| record.event_id.as_deref() == Some(event_id))
+ .and_then(|record| signed_event_from_local_record(&record).ok().flatten())
+ .and_then(|event| trade_mutation_from_event(&event).ok())
+ .expect("shared trade proposal envelope should exist")
+ }
+
fn assert_order_lifecycle_evidence_invalid(error: AppSqliteError) {
assert!(
matches!(
@@ -19743,10 +19343,10 @@ mod tests {
outbox_status = 'pending',
relay_set_fingerprint = NULL,
relay_delivery_json = NULL
- WHERE record_id = 'app:signed_event:order-request:seller-order-decision-1'",
+ WHERE record_id LIKE 'app:signed_event:order-request:%'",
"[]",
)
- .expect("mark shared order request evidence pending");
+ .expect("mark shared trade proposal evidence pending");
}
fn append_unrelated_signed_event_records(paths: &AppDesktopRuntimePaths, count: usize) {
@@ -19945,18 +19545,6 @@ mod tests {
.expect("shared local records should list")
}
- fn shared_order_request_event_id(
- paths: &AppDesktopRuntimePaths,
- trade_order_id: &str,
- ) -> String {
- let record_id = format!("app:signed_event:order-request:{trade_order_id}");
- shared_runtime_store_records(paths)
- .into_iter()
- .find(|record| record.record_id == record_id)
- .and_then(|record| record.event_id)
- .expect("signed order request event id should exist")
- }
-
fn shared_order_events_by_kind(
paths: &AppDesktopRuntimePaths,
kind: i64,
@@ -20051,7 +19639,10 @@ mod tests {
.expect("SDK workflow receipt should load")
.expect("SDK workflow receipt should exist");
assert_eq!(receipt.source_record_id, source_record_id);
- assert_eq!(receipt.sdk_operation_kind, TRADE_SUBMIT_OPERATION_KIND);
+ assert_eq!(
+ receipt.sdk_operation_kind,
+ TRADE_SUBMIT_PROPOSAL_OPERATION_KIND
+ );
assert_eq!(
receipt.workflow_state, expected_state,
"receipt detail: {}",
@@ -20069,7 +19660,7 @@ mod tests {
order_id: OrderId,
expected_state: DesktopRuntimeWorkflowReceiptState,
) {
- let source_record_id = format!("app:order_decision:{order_id}");
+ let source_record_id = format!("app:trade_candidate_decision:{order_id}");
let receipt = runtime
.lock_state()
.sqlite_store
@@ -20083,7 +19674,10 @@ mod tests {
.expect("SDK workflow receipt should load")
.expect("SDK workflow receipt should exist");
assert_eq!(receipt.source_record_id, source_record_id);
- assert_eq!(receipt.sdk_operation_kind, TRADE_DECISION_OPERATION_KIND);
+ assert_eq!(
+ receipt.sdk_operation_kind,
+ TRADE_DECIDE_CANDIDATE_OPERATION_KIND
+ );
assert_eq!(
receipt.workflow_state, expected_state,
"receipt detail: {}",
@@ -20103,8 +19697,8 @@ mod tests {
) {
assert_order_runtime_workflow_receipt(
runtime,
- format!("app:order_cancellation:{order_id}").as_str(),
- TRADE_CANCELLATION_OPERATION_KIND,
+ format!("app:trade_cancellation:{order_id}").as_str(),
+ TRADE_CANCEL_OPERATION_KIND,
expected_state,
);
}
diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs
@@ -708,13 +708,16 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::TradeWorkflowAxisAgreement",
"AppTextKey::TradeWorkflowAxisRevision",
"AppTextKey::TradeWorkflowAxisInventory",
+ "AppTextKey::TradeWorkflowAxisEvidence",
+ "AppTextKey::TradeWorkflowAxisConflict",
+ "AppTextKey::TradeWorkflowAxisPrivateTerms",
+ "AppTextKey::TradeWorkflowAxisAttestation",
"AppTextKey::TradeWorkflowAxisSource",
"AppTextKey::TradeWorkflowAgreementRequested",
- "AppTextKey::TradeWorkflowAgreementAgreedPendingValidation",
"AppTextKey::TradeWorkflowAgreementCommitted",
+ "AppTextKey::TradeWorkflowAgreementContested",
"AppTextKey::TradeWorkflowAgreementDeclined",
"AppTextKey::TradeWorkflowAgreementCancelled",
- "AppTextKey::TradeWorkflowAgreementValidationExpired",
"AppTextKey::TradeWorkflowAgreementInvalid",
"AppTextKey::TradeWorkflowRevisionNone",
"AppTextKey::TradeWorkflowRevisionChangeProposed",
@@ -724,6 +727,26 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::TradeWorkflowInventoryReserved",
"AppTextKey::TradeWorkflowInventorySoldOut",
"AppTextKey::TradeWorkflowInventoryNeedsReview",
+ "AppTextKey::TradeWorkflowEvidenceComplete",
+ "AppTextKey::TradeWorkflowEvidenceMissing",
+ "AppTextKey::TradeWorkflowEvidenceQueryPartial",
+ "AppTextKey::TradeWorkflowEvidenceUnsupportedVersion",
+ "AppTextKey::TradeWorkflowConflictNone",
+ "AppTextKey::TradeWorkflowConflictConcurrentCandidates",
+ "AppTextKey::TradeWorkflowConflictDoubleAcceptance",
+ "AppTextKey::TradeWorkflowConflictDecisionConflict",
+ "AppTextKey::TradeWorkflowConflictCancellationConflict",
+ "AppTextKey::TradeWorkflowConflictInvalidCausalChain",
+ "AppTextKey::TradeWorkflowConflictInventoryAuthorityConflict",
+ "AppTextKey::TradeWorkflowPrivateTermsNotRequired",
+ "AppTextKey::TradeWorkflowPrivateTermsAvailableVerified",
+ "AppTextKey::TradeWorkflowPrivateTermsMissing",
+ "AppTextKey::TradeWorkflowPrivateTermsUndecryptable",
+ "AppTextKey::TradeWorkflowPrivateTermsCommitmentMismatch",
+ "AppTextKey::TradeWorkflowAttestationNone",
+ "AppTextKey::TradeWorkflowAttestationPresentValid",
+ "AppTextKey::TradeWorkflowAttestationPresentInvalid",
+ "AppTextKey::TradeWorkflowAttestationConflicting",
"AppTextKey::TradeWorkflowProvenanceApp",
"AppTextKey::TradeWorkflowProvenanceCli",
"AppTextKey::TradeWorkflowProvenanceRelay",
@@ -1083,20 +1106,25 @@ const FORBIDDEN_PAYMENT_ACTION_COPY_TERMS: &[&str] = &[
const FORBIDDEN_PRODUCTION_EVENT_KIND_LITERALS: &[(&str, &str)] = &[
("30340", "KIND_FARM"),
("30402", "KIND_LISTING"),
- ("3422", "KIND_ORDER_REQUEST"),
- ("3423", "KIND_ORDER_DECISION"),
+ ("3422", "RETIRED_COMMERCIAL_KIND_3422"),
+ ("3423", "RETIRED_COMMERCIAL_KIND_3423"),
("3426", "KIND_TRADE_QUESTION"),
("3427", "KIND_TRADE_ANSWER"),
("3428", "KIND_TRADE_DISCOUNT_REQUEST"),
("3429", "KIND_TRADE_DISCOUNT_OFFER"),
("3430", "KIND_TRADE_DISCOUNT_ACCEPT"),
("3431", "RESERVED_ORDER_KIND_3431"),
- ("3432", "KIND_ORDER_CANCELLATION"),
+ ("3432", "RETIRED_COMMERCIAL_KIND_3432"),
("3433", "KIND_ORDER_FULFILLMENT_UPDATE"),
("3434", "KIND_ORDER_RECEIPT"),
("3435", "KIND_ORDER_PAYMENT_RECORD"),
("3436", "KIND_ORDER_SETTLEMENT_DECISION"),
("3440", "KIND_TRADE_VALIDATION_RECEIPT"),
+ ("3470", "KIND_TRADE_PROPOSAL"),
+ ("3471", "KIND_TRADE_DECISION"),
+ ("3472", "KIND_TRADE_REVISION_PROPOSAL"),
+ ("3473", "KIND_TRADE_REVISION_DECISION"),
+ ("3474", "KIND_TRADE_CANCELLATION"),
];
struct SdkBoundaryForbiddenPattern {
@@ -1508,21 +1536,18 @@ fn app_production_trade_event_kinds_use_shared_constants() {
#[test]
fn app_store_validation_receipts_do_not_commit_without_trust_policy() {
let source = read_source_path(app_root().join("crates/store/src/interop.rs").as_path());
- let valid_result_branch = "\"valid\" => (";
- let start = source
- .find(valid_result_branch)
- .expect("store interop should project valid validation receipt results");
- let end = source[start..]
- .find("\"needs_review\" => (")
- .map(|offset| start + offset)
- .expect("store interop should project invalid validation receipt results");
- let branch = &source[start..end];
+ let projection = source_segment(
+ source.as_str(),
+ "fn upsert_validation_receipt_projection(",
+ "fn upsert_farm_summary(",
+ );
- assert!(branch.contains("OrderStatus::NeedsAction.storage_key()"));
- assert!(branch.contains("TradeAgreementStatus::AgreedPendingValidation.storage_key()"));
- assert!(branch.contains("TradeInventoryStatus::Reserved.storage_key()"));
- assert!(!branch.contains("OrderStatus::Scheduled.storage_key()"));
- assert!(!branch.contains("TradeAgreementStatus::Committed.storage_key()"));
+ assert!(projection.contains("let order_id: Option<String> = None;"));
+ assert!(projection.contains("INSERT INTO order_validation_receipts"));
+ assert!(!projection.contains("UPDATE orders"));
+ assert!(!projection.contains("workflow_agreement"));
+ assert!(!projection.contains("OrderStatus::Scheduled"));
+ assert!(!projection.contains("TradeAgreementStatus::Committed"));
}
#[test]
@@ -1556,151 +1581,131 @@ fn app_production_sdk_boundary_usage_is_exception_scoped() {
}
#[test]
-fn desktop_runtime_trade_propose_request_stays_product_shaped() {
+fn desktop_runtime_trade_requests_stay_semantic_envelope_shaped() {
let source = read_source_path(app_root().join("crates/runtime/src/sdk.rs").as_path());
- let request = struct_block(source.as_str(), "DesktopRuntimeTradeProposeRequest");
- assert!(
- !request.contains("RadrootsOrderRequest"),
- "DesktopRuntimeTradeProposeRequest must not expose protocol-shaped order requests"
- );
- for required_field in [
- "pub order_id: RadrootsOrderId",
- "pub listing_addr: RadrootsListingAddress",
- "pub seller_pubkey: RadrootsPublicKey",
- "pub items: Vec<RadrootsOrderItem>",
- "pub economics: RadrootsOrderEconomics",
- "pub public_note: Option<String>",
- "pub confirm_public_note: bool",
+ for request_struct in [
+ "DesktopRuntimeTradeSubmitProposalRequest",
+ "DesktopRuntimeTradeProposeRevisionRequest",
+ "DesktopRuntimeTradeDecideCandidateRequest",
+ "DesktopRuntimeTradeCancelRequest",
] {
+ let request = struct_block(source.as_str(), request_struct);
+ for required_field in [
+ "pub actor_account_id: String",
+ "pub actor_pubkey: String",
+ "pub signer: DesktopRuntimeLocalSigner",
+ "pub envelope: RadrootsTradeMutationEnvelopeV1",
+ "pub idempotency_key: Option<String>",
+ ] {
+ assert!(
+ request.contains(required_field),
+ "{request_struct} is missing semantic SDK field `{required_field}`"
+ );
+ }
assert!(
- request.contains(required_field),
- "DesktopRuntimeTradeProposeRequest is missing product field `{required_field}`"
+ !request.contains("RadrootsOrder"),
+ "{request_struct} must not expose order-era protocol types"
);
}
- assert!(source.contains("fn app_trade_privacy_confirmation(confirm_public_note: bool)"));
- assert!(source.contains("if confirm_public_note"));
- assert!(source.contains(".with_optional_public_note(request.public_note)"));
+
+ assert!(source.contains("pub acknowledge_private_terms: bool",));
assert!(source.contains(
- ".with_privacy_confirmation(app_trade_privacy_confirmation(request.confirm_public_note))"
+ "sdk.trades()\n .commands()\n .submit_proposal_with_explicit_signer"
+ ));
+ assert!(source.contains(
+ "sdk.trades()\n .commands()\n .propose_revision_with_explicit_signer"
+ ));
+ assert!(source.contains(
+ "sdk.trades()\n .commands()\n .decide_candidate_with_explicit_signer"
+ ));
+ assert!(source.contains(
+ "sdk.trades()\n .commands()\n .cancel_trade_with_explicit_signer"
));
- assert!(
- !source.contains("fn app_trade_privacy_confirmation()"),
- "public-sensitive note confirmation must not become ambient"
- );
}
#[test]
-fn desktop_runtime_trade_mutation_requests_stay_locator_and_resync_owned() {
+fn desktop_runtime_trade_mutation_requests_validate_body_and_policy() {
let source = read_source_path(app_root().join("crates/runtime/src/sdk.rs").as_path());
- for request_struct in [
- "DesktopRuntimeTradeProposeRequest",
- "DesktopRuntimeTradeDecisionRequest",
- "DesktopRuntimeTradeCancellationRequest",
- ] {
- let request = struct_block(source.as_str(), request_struct);
- assert!(
- !request.contains("evidence_events"),
- "{request_struct} must not expose caller-owned evidence events"
- );
- }
-
- for request_struct in [
- "DesktopRuntimeTradeDecisionRequest",
- "DesktopRuntimeTradeCancellationRequest",
- ] {
- let request = struct_block(source.as_str(), request_struct);
- assert!(
- request.contains("pub locator: RadrootsTradeLocator"),
- "{request_struct} must require a root-capable SDK trade locator"
- );
- }
-
- assert!(source.contains("sdk.trades().buyer().propose_trade(sdk_request)"));
- for (label, start, end, required_count) in [
+ for (label, start, end, required_body) in [
(
"trade proposal",
- "fn trade_propose_with_sdk(",
- "fn trade_decision_with_sdk(",
- 0usize,
+ "fn trade_submit_proposal_with_sdk(",
+ "fn trade_propose_revision_with_sdk(",
+ "RadrootsTradeMutationBodyV1::Proposal",
+ ),
+ (
+ "trade revision proposal",
+ "fn trade_propose_revision_with_sdk(",
+ "fn trade_decide_candidate_with_sdk(",
+ "RadrootsTradeMutationBodyV1::RevisionProposal",
),
(
- "trade decision",
- "fn trade_decision_with_sdk(",
+ "trade candidate decision",
+ "fn trade_decide_candidate_with_sdk(",
"fn trade_cancel_with_sdk(",
- 2usize,
+ "RadrootsTradeMutationBodyV1::Decision",
),
(
"trade cancellation",
"fn trade_cancel_with_sdk(",
"fn sdk_actor_context(",
- 1usize,
+ "RadrootsTradeMutationBodyV1::Cancellation",
),
] {
let segment = source_segment(source.as_str(), start, end);
+ assert!(segment.contains("validate_trade_envelope_body"));
assert!(
- segment
- .matches("TradeEvidenceMode::ResyncBeforeMutation")
- .count()
- >= required_count,
- "{label} must use SDK resync evidence mode before mutation"
+ segment.contains(required_body),
+ "{label} must validate semantic mutation body `{required_body}`"
);
+ assert!(segment.contains("app_trade_target_policy()"));
assert!(
- !segment.contains("TradeEvidenceMode::LocalOnly"),
- "{label} must not use local-only evidence for app mutation authority"
+ segment.contains("app_trade_satisfaction_policy()"),
+ "{label} must use SDK satisfaction policy"
+ );
+ assert!(
+ !segment.contains("TradeEvidenceMode::"),
+ "{label} must not revive caller-owned evidence mode selection"
);
}
}
#[test]
-fn app_store_active_order_projection_is_root_aware_and_non_authoritative() {
- let source = read_source_path(app_root().join("crates/store/src/interop.rs").as_path());
- let project_active_order = source_segment(
- source.as_str(),
- "fn project_active_order(",
- "fn upsert_order_request(",
- );
- assert!(project_active_order.contains("current_evidence.root_event_id()"));
- assert!(project_active_order.contains("root_event_id.as_str()"));
- assert!(!project_active_order.contains("current_evidence.order_id()"));
-
- let load_active_order_evidence = source_segment(
- source.as_str(),
- "fn load_active_order_evidence(",
- "fn replace_order_request_lines(",
- );
- assert!(load_active_order_evidence.contains("record.root_event_id() == root_event_id"));
- assert!(!load_active_order_evidence.contains("record.order_id()"));
-
- let active_evidence_identity = source_segment(
- source.as_str(),
- "fn root_event_id(&self)",
- "struct ActiveOrderEvidenceBuckets",
- );
- assert!(active_evidence_identity.contains("Self::Request(record) => record.event_id.as_str()"));
- assert!(
- active_evidence_identity
- .contains("Self::Decision(record) => record.root_event_id.as_str()")
- );
+fn app_store_has_no_active_order_projection_authority() {
+ for relative_path in [
+ "crates/store/src/interop.rs",
+ "crates/store/src/lib.rs",
+ "crates/store/src/repo/workflow.rs",
+ ] {
+ let source = read_source_path(app_root().join(relative_path).as_path());
+ assert!(
+ !source.contains("project_active_order")
+ && !source.contains("load_active_order_evidence")
+ && !source.contains("ActiveOrderEvidenceBuckets")
+ && !source.contains("projected_order_id_from_trade_request")
+ && !source.contains("TradeEvidenceMode::"),
+ "{relative_path} must not retain old active-order projection authority"
+ );
+ }
+}
- let projected_order_id = source_segment(
- source.as_str(),
- "pub fn projected_order_id_from_trade_request(",
- "fn active_order_change_status(",
- );
- assert!(projected_order_id.contains("root_event_id: &str"));
- assert!(projected_order_id.contains("radroots-cli-order-root"));
-
- for forbidden in [
- "TradeAcceptRequest::new",
- "TradeDeclineRequest::new",
- "TradeCancelRequest::new",
- "TradeEvidenceMode::",
+#[test]
+fn app_runtime_ingests_every_canonical_trade_mutation_kind() {
+ let source = read_source_path(app_root().join("crates/desktop/src/runtime.rs").as_path());
+ let ingest_kinds = source_segment(source.as_str(), "const APP_DIRECT_RELAY_INGEST_KINDS", "];");
+
+ for required_kind in [
+ "KIND_TRADE_PROPOSAL",
+ "KIND_TRADE_DECISION",
+ "KIND_TRADE_REVISION_PROPOSAL",
+ "KIND_TRADE_REVISION_DECISION",
+ "KIND_TRADE_CANCELLATION",
] {
assert!(
- !source.contains(forbidden),
- "store projection must not become SDK mutation authority through `{forbidden}`"
+ ingest_kinds.contains(required_kind),
+ "direct relay ingest must include canonical mutation kind `{required_kind}`"
);
}
}
@@ -1761,6 +1766,48 @@ fn app_store_current_schema_surfaces_reject_retired_terms() {
);
}
+#[test]
+fn app_store_current_default_schema_omits_buyer_private_plaintext() {
+ let app_root = app_root();
+ let latest_schema = read_source_path(
+ app_root
+ .join("crates/store/migrations/0031_drop_default_buyer_private_fields.sql")
+ .as_path(),
+ );
+ let buyer_repo = read_source_path(app_root.join("crates/store/src/repo/buyer.rs").as_path());
+ let buyer_repo_production =
+ production_source_without_tests("crates/store/src/repo/buyer.rs", buyer_repo.as_str())
+ .expect("buyer repo production source should classify");
+ let runtime_source = read_source_path(app_root.join("crates/desktop/src/runtime.rs").as_path());
+ let runtime_production =
+ production_source_without_tests("crates/desktop/src/runtime.rs", runtime_source.as_str())
+ .expect("runtime production source should classify");
+
+ for (label, source) in [
+ ("latest default schema", latest_schema.as_str()),
+ (
+ "buyer repository production source",
+ buyer_repo_production.as_str(),
+ ),
+ (
+ "desktop runtime production source",
+ runtime_production.as_str(),
+ ),
+ ] {
+ for private_field in [
+ "buyer_email",
+ "buyer_phone",
+ "buyer_order_note",
+ "buyer_order_note_public_confirmed",
+ ] {
+ assert!(
+ !source.contains(private_field),
+ "{label} must not contain default-store private field `{private_field}`"
+ );
+ }
+ }
+}
+
const APP_STORE_RETIRED_SCHEMA_TERMS: &[&str] = &[
"legacy",
"compat",
diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs
@@ -82,7 +82,8 @@ use radroots_studio_app_view::{
ReminderDeliveryState, ReminderId, ReminderLogEntryProjection, ReminderLogProjection,
ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection,
SettingsAccountProjection, ShellSection, TodayAgendaProjection, TodaySetupTaskKind,
- TradeAgreementStatus, TradeEconomicsProjection, TradeInventoryStatus, TradeRevisionStatus,
+ TradeAgreementStatus, TradeAttestationStatus, TradeConflictStatus, TradeEconomicsProjection,
+ TradeEvidenceStatus, TradeInventoryStatus, TradePrivateTermsStatus, TradeRevisionStatus,
TradeValidationReceiptProjection, TradeValidationReceiptResult, TradeValidationReceiptType,
TradeWorkflowProjection, TradeWorkflowSource,
};
@@ -11588,6 +11589,22 @@ fn trade_workflow_detail_badge_strip(workflow: &TradeWorkflowProjection) -> AnyE
AppTextKey::TradeWorkflowAxisInventory,
trade_inventory_status_key(workflow.inventory),
));
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisEvidence,
+ trade_evidence_status_key(workflow.evidence),
+ ));
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisConflict,
+ trade_conflict_status_key(workflow.conflict),
+ ));
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisPrivateTerms,
+ trade_private_terms_status_key(workflow.private_terms),
+ ));
+ badges.push(trade_workflow_labeled_key_badge(
+ AppTextKey::TradeWorkflowAxisAttestation,
+ trade_attestation_status_key(workflow.attestation),
+ ));
if workflow.provenance.primary_source != TradeWorkflowSource::Unknown {
badges.push(trade_workflow_labeled_key_badge(
AppTextKey::TradeWorkflowAxisSource,
@@ -11615,6 +11632,26 @@ fn trade_workflow_list_badge_strip(workflow: &TradeWorkflowProjection) -> AnyEle
badges.push(trade_workflow_value_badge(trade_inventory_status_key(
workflow.inventory,
)));
+ if workflow.evidence != TradeEvidenceStatus::Complete {
+ badges.push(trade_workflow_value_badge(trade_evidence_status_key(
+ workflow.evidence,
+ )));
+ }
+ if workflow.conflict != TradeConflictStatus::None {
+ badges.push(trade_workflow_value_badge(trade_conflict_status_key(
+ workflow.conflict,
+ )));
+ }
+ if workflow.private_terms != TradePrivateTermsStatus::NotRequired {
+ badges.push(trade_workflow_value_badge(trade_private_terms_status_key(
+ workflow.private_terms,
+ )));
+ }
+ if workflow.attestation != TradeAttestationStatus::None {
+ badges.push(trade_workflow_value_badge(trade_attestation_status_key(
+ workflow.attestation,
+ )));
+ }
app_cluster(APP_UI_THEME.foundation.spacing.tight_px)
.w_full()
@@ -11631,6 +11668,12 @@ fn trade_workflow_status_stack(workflow: &TradeWorkflowProjection) -> AnyElement
.child(trade_workflow_value_badge(trade_inventory_status_key(
workflow.inventory,
)))
+ .child(trade_workflow_value_badge(trade_evidence_status_key(
+ workflow.evidence,
+ )))
+ .child(trade_workflow_value_badge(trade_conflict_status_key(
+ workflow.conflict,
+ )))
.into_any_element()
}
@@ -11646,15 +11689,10 @@ fn trade_workflow_value_badge(value_key: AppTextKey) -> AnyElement {
fn trade_agreement_status_key(status: TradeAgreementStatus) -> AppTextKey {
match status {
TradeAgreementStatus::Requested => AppTextKey::TradeWorkflowAgreementRequested,
- TradeAgreementStatus::AgreedPendingValidation => {
- AppTextKey::TradeWorkflowAgreementAgreedPendingValidation
- }
TradeAgreementStatus::Committed => AppTextKey::TradeWorkflowAgreementCommitted,
+ TradeAgreementStatus::Contested => AppTextKey::TradeWorkflowAgreementContested,
TradeAgreementStatus::Declined => AppTextKey::TradeWorkflowAgreementDeclined,
TradeAgreementStatus::Cancelled => AppTextKey::TradeWorkflowAgreementCancelled,
- TradeAgreementStatus::ValidationExpired => {
- AppTextKey::TradeWorkflowAgreementValidationExpired
- }
TradeAgreementStatus::Invalid => AppTextKey::TradeWorkflowAgreementInvalid,
}
}
@@ -11677,6 +11715,64 @@ fn trade_inventory_status_key(status: TradeInventoryStatus) -> AppTextKey {
}
}
+fn trade_evidence_status_key(status: TradeEvidenceStatus) -> AppTextKey {
+ match status {
+ TradeEvidenceStatus::Complete => AppTextKey::TradeWorkflowEvidenceComplete,
+ TradeEvidenceStatus::Missing => AppTextKey::TradeWorkflowEvidenceMissing,
+ TradeEvidenceStatus::QueryPartial => AppTextKey::TradeWorkflowEvidenceQueryPartial,
+ TradeEvidenceStatus::UnsupportedVersion => {
+ AppTextKey::TradeWorkflowEvidenceUnsupportedVersion
+ }
+ }
+}
+
+fn trade_conflict_status_key(status: TradeConflictStatus) -> AppTextKey {
+ match status {
+ TradeConflictStatus::None => AppTextKey::TradeWorkflowConflictNone,
+ TradeConflictStatus::ConcurrentCandidates => {
+ AppTextKey::TradeWorkflowConflictConcurrentCandidates
+ }
+ TradeConflictStatus::DoubleAcceptance => AppTextKey::TradeWorkflowConflictDoubleAcceptance,
+ TradeConflictStatus::DecisionConflict => AppTextKey::TradeWorkflowConflictDecisionConflict,
+ TradeConflictStatus::CancellationConflict => {
+ AppTextKey::TradeWorkflowConflictCancellationConflict
+ }
+ TradeConflictStatus::InvalidCausalChain => {
+ AppTextKey::TradeWorkflowConflictInvalidCausalChain
+ }
+ TradeConflictStatus::InventoryAuthorityConflict => {
+ AppTextKey::TradeWorkflowConflictInventoryAuthorityConflict
+ }
+ }
+}
+
+fn trade_private_terms_status_key(status: TradePrivateTermsStatus) -> AppTextKey {
+ match status {
+ TradePrivateTermsStatus::NotRequired => AppTextKey::TradeWorkflowPrivateTermsNotRequired,
+ TradePrivateTermsStatus::AvailableVerified => {
+ AppTextKey::TradeWorkflowPrivateTermsAvailableVerified
+ }
+ TradePrivateTermsStatus::Missing => AppTextKey::TradeWorkflowPrivateTermsMissing,
+ TradePrivateTermsStatus::Undecryptable => {
+ AppTextKey::TradeWorkflowPrivateTermsUndecryptable
+ }
+ TradePrivateTermsStatus::CommitmentMismatch => {
+ AppTextKey::TradeWorkflowPrivateTermsCommitmentMismatch
+ }
+ }
+}
+
+fn trade_attestation_status_key(status: TradeAttestationStatus) -> AppTextKey {
+ match status {
+ TradeAttestationStatus::None => AppTextKey::TradeWorkflowAttestationNone,
+ TradeAttestationStatus::PresentValid => AppTextKey::TradeWorkflowAttestationPresentValid,
+ TradeAttestationStatus::PresentInvalid => {
+ AppTextKey::TradeWorkflowAttestationPresentInvalid
+ }
+ TradeAttestationStatus::Conflicting => AppTextKey::TradeWorkflowAttestationConflicting,
+ }
+}
+
fn trade_workflow_source_key(source: TradeWorkflowSource) -> AppTextKey {
match source {
TradeWorkflowSource::App => AppTextKey::TradeWorkflowProvenanceApp,
@@ -16279,8 +16375,9 @@ mod tests {
startup_issue_summary_text, startup_notice_text, startup_signer_preview_summary,
startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable,
startup_signer_status_spec, startup_signer_transport_failure_requires_notice,
- trade_agreement_status_key, trade_inventory_status_key, trade_revision_status_key,
- trade_workflow_source_key,
+ trade_agreement_status_key, trade_attestation_status_key, trade_conflict_status_key,
+ trade_evidence_status_key, trade_inventory_status_key, trade_private_terms_status_key,
+ trade_revision_status_key, trade_workflow_source_key,
};
use crate::runtime::{
DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeSummary, DesktopAppSyncConflictSummary,
@@ -16322,7 +16419,8 @@ mod tests {
ReminderDeliveryState, ReminderId, ReminderKind, ReminderSurface, ReminderUrgency,
RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection,
TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TradeAgreementStatus,
- TradeEconomicsProjection, TradeInventoryStatus, TradeRevisionStatus,
+ TradeAttestationStatus, TradeConflictStatus, TradeEconomicsProjection, TradeEvidenceStatus,
+ TradeInventoryStatus, TradePrivateTermsStatus, TradeRevisionStatus,
TradeWorkflowProjection, TradeWorkflowSource,
};
use std::{
@@ -16739,14 +16837,14 @@ mod tests {
AppTextKey::TradeWorkflowAgreementRequested,
),
(
- TradeAgreementStatus::AgreedPendingValidation,
- AppTextKey::TradeWorkflowAgreementAgreedPendingValidation,
- ),
- (
TradeAgreementStatus::Committed,
AppTextKey::TradeWorkflowAgreementCommitted,
),
(
+ TradeAgreementStatus::Contested,
+ AppTextKey::TradeWorkflowAgreementContested,
+ ),
+ (
TradeAgreementStatus::Declined,
AppTextKey::TradeWorkflowAgreementDeclined,
),
@@ -16755,10 +16853,6 @@ mod tests {
AppTextKey::TradeWorkflowAgreementCancelled,
),
(
- TradeAgreementStatus::ValidationExpired,
- AppTextKey::TradeWorkflowAgreementValidationExpired,
- ),
- (
TradeAgreementStatus::Invalid,
AppTextKey::TradeWorkflowAgreementInvalid,
),
@@ -16811,6 +16905,110 @@ mod tests {
assert!(!app_text(key).is_empty());
}
+ for (status, key) in [
+ (
+ TradeEvidenceStatus::Complete,
+ AppTextKey::TradeWorkflowEvidenceComplete,
+ ),
+ (
+ TradeEvidenceStatus::Missing,
+ AppTextKey::TradeWorkflowEvidenceMissing,
+ ),
+ (
+ TradeEvidenceStatus::QueryPartial,
+ AppTextKey::TradeWorkflowEvidenceQueryPartial,
+ ),
+ (
+ TradeEvidenceStatus::UnsupportedVersion,
+ AppTextKey::TradeWorkflowEvidenceUnsupportedVersion,
+ ),
+ ] {
+ assert_eq!(trade_evidence_status_key(status), key);
+ assert!(!app_text(key).is_empty());
+ }
+
+ for (status, key) in [
+ (
+ TradeConflictStatus::None,
+ AppTextKey::TradeWorkflowConflictNone,
+ ),
+ (
+ TradeConflictStatus::ConcurrentCandidates,
+ AppTextKey::TradeWorkflowConflictConcurrentCandidates,
+ ),
+ (
+ TradeConflictStatus::DoubleAcceptance,
+ AppTextKey::TradeWorkflowConflictDoubleAcceptance,
+ ),
+ (
+ TradeConflictStatus::DecisionConflict,
+ AppTextKey::TradeWorkflowConflictDecisionConflict,
+ ),
+ (
+ TradeConflictStatus::CancellationConflict,
+ AppTextKey::TradeWorkflowConflictCancellationConflict,
+ ),
+ (
+ TradeConflictStatus::InvalidCausalChain,
+ AppTextKey::TradeWorkflowConflictInvalidCausalChain,
+ ),
+ (
+ TradeConflictStatus::InventoryAuthorityConflict,
+ AppTextKey::TradeWorkflowConflictInventoryAuthorityConflict,
+ ),
+ ] {
+ assert_eq!(trade_conflict_status_key(status), key);
+ assert!(!app_text(key).is_empty());
+ }
+
+ for (status, key) in [
+ (
+ TradePrivateTermsStatus::NotRequired,
+ AppTextKey::TradeWorkflowPrivateTermsNotRequired,
+ ),
+ (
+ TradePrivateTermsStatus::AvailableVerified,
+ AppTextKey::TradeWorkflowPrivateTermsAvailableVerified,
+ ),
+ (
+ TradePrivateTermsStatus::Missing,
+ AppTextKey::TradeWorkflowPrivateTermsMissing,
+ ),
+ (
+ TradePrivateTermsStatus::Undecryptable,
+ AppTextKey::TradeWorkflowPrivateTermsUndecryptable,
+ ),
+ (
+ TradePrivateTermsStatus::CommitmentMismatch,
+ AppTextKey::TradeWorkflowPrivateTermsCommitmentMismatch,
+ ),
+ ] {
+ assert_eq!(trade_private_terms_status_key(status), key);
+ assert!(!app_text(key).is_empty());
+ }
+
+ for (status, key) in [
+ (
+ TradeAttestationStatus::None,
+ AppTextKey::TradeWorkflowAttestationNone,
+ ),
+ (
+ TradeAttestationStatus::PresentValid,
+ AppTextKey::TradeWorkflowAttestationPresentValid,
+ ),
+ (
+ TradeAttestationStatus::PresentInvalid,
+ AppTextKey::TradeWorkflowAttestationPresentInvalid,
+ ),
+ (
+ TradeAttestationStatus::Conflicting,
+ AppTextKey::TradeWorkflowAttestationConflicting,
+ ),
+ ] {
+ assert_eq!(trade_attestation_status_key(status), key);
+ assert!(!app_text(key).is_empty());
+ }
+
for (source, key) in [
(
TradeWorkflowSource::App,
diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs
@@ -431,13 +431,16 @@ define_app_text_keys! {
TradeWorkflowAxisAgreement => "trade.workflow.axis.agreement",
TradeWorkflowAxisRevision => "trade.workflow.axis.revision",
TradeWorkflowAxisInventory => "trade.workflow.axis.inventory",
+ TradeWorkflowAxisEvidence => "trade.workflow.axis.evidence",
+ TradeWorkflowAxisConflict => "trade.workflow.axis.conflict",
+ TradeWorkflowAxisPrivateTerms => "trade.workflow.axis.private_terms",
+ TradeWorkflowAxisAttestation => "trade.workflow.axis.attestation",
TradeWorkflowAxisSource => "trade.workflow.axis.source",
TradeWorkflowAgreementRequested => "trade.workflow.agreement.requested",
- TradeWorkflowAgreementAgreedPendingValidation => "trade.workflow.agreement.agreed_pending_validation",
TradeWorkflowAgreementCommitted => "trade.workflow.agreement.committed",
+ TradeWorkflowAgreementContested => "trade.workflow.agreement.contested",
TradeWorkflowAgreementDeclined => "trade.workflow.agreement.declined",
TradeWorkflowAgreementCancelled => "trade.workflow.agreement.cancelled",
- TradeWorkflowAgreementValidationExpired => "trade.workflow.agreement.validation_expired",
TradeWorkflowAgreementInvalid => "trade.workflow.agreement.invalid",
TradeWorkflowRevisionNone => "trade.workflow.revision.none",
TradeWorkflowRevisionChangeProposed => "trade.workflow.revision.change_proposed",
@@ -447,6 +450,26 @@ define_app_text_keys! {
TradeWorkflowInventoryReserved => "trade.workflow.inventory.reserved",
TradeWorkflowInventorySoldOut => "trade.workflow.inventory.sold_out",
TradeWorkflowInventoryNeedsReview => "trade.workflow.inventory.needs_review",
+ TradeWorkflowEvidenceComplete => "trade.workflow.evidence.complete",
+ TradeWorkflowEvidenceMissing => "trade.workflow.evidence.missing",
+ TradeWorkflowEvidenceQueryPartial => "trade.workflow.evidence.query_partial",
+ TradeWorkflowEvidenceUnsupportedVersion => "trade.workflow.evidence.unsupported_version",
+ TradeWorkflowConflictNone => "trade.workflow.conflict.none",
+ TradeWorkflowConflictConcurrentCandidates => "trade.workflow.conflict.concurrent_candidates",
+ TradeWorkflowConflictDoubleAcceptance => "trade.workflow.conflict.double_acceptance",
+ TradeWorkflowConflictDecisionConflict => "trade.workflow.conflict.decision_conflict",
+ TradeWorkflowConflictCancellationConflict => "trade.workflow.conflict.cancellation_conflict",
+ TradeWorkflowConflictInvalidCausalChain => "trade.workflow.conflict.invalid_causal_chain",
+ TradeWorkflowConflictInventoryAuthorityConflict => "trade.workflow.conflict.inventory_authority_conflict",
+ TradeWorkflowPrivateTermsNotRequired => "trade.workflow.private_terms.not_required",
+ TradeWorkflowPrivateTermsAvailableVerified => "trade.workflow.private_terms.available_verified",
+ TradeWorkflowPrivateTermsMissing => "trade.workflow.private_terms.missing",
+ TradeWorkflowPrivateTermsUndecryptable => "trade.workflow.private_terms.undecryptable",
+ TradeWorkflowPrivateTermsCommitmentMismatch => "trade.workflow.private_terms.commitment_mismatch",
+ TradeWorkflowAttestationNone => "trade.workflow.attestation.none",
+ TradeWorkflowAttestationPresentValid => "trade.workflow.attestation.present_valid",
+ TradeWorkflowAttestationPresentInvalid => "trade.workflow.attestation.present_invalid",
+ TradeWorkflowAttestationConflicting => "trade.workflow.attestation.conflicting",
TradeWorkflowProvenanceApp => "trade.workflow.provenance.app",
TradeWorkflowProvenanceCli => "trade.workflow.provenance.cli",
TradeWorkflowProvenanceRelay => "trade.workflow.provenance.relay",
diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs
@@ -633,24 +633,30 @@ mod tests {
app_text(AppTextKey::TradeWorkflowAxisAgreement),
"Agreement"
);
+ assert_eq!(app_text(AppTextKey::TradeWorkflowAxisEvidence), "Evidence");
+ assert_eq!(app_text(AppTextKey::TradeWorkflowAxisConflict), "Conflict");
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAxisPrivateTerms),
+ "Private terms"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAxisAttestation),
+ "Attestation"
+ );
assert_eq!(app_text(AppTextKey::TradeWorkflowAxisSource), "Source");
assert_eq!(
app_text(AppTextKey::TradeWorkflowAgreementRequested),
"Requested"
);
assert_eq!(
- app_text(AppTextKey::TradeWorkflowAgreementAgreedPendingValidation),
- "Pending validation"
+ app_text(AppTextKey::TradeWorkflowAgreementContested),
+ "Contested"
);
assert_eq!(
app_text(AppTextKey::TradeWorkflowAgreementCommitted),
"Committed"
);
assert_eq!(
- app_text(AppTextKey::TradeWorkflowAgreementValidationExpired),
- "Validation expired"
- );
- assert_eq!(
app_text(AppTextKey::TradeWorkflowAgreementInvalid),
"Invalid"
);
@@ -666,6 +672,22 @@ mod tests {
app_text(AppTextKey::TradeWorkflowInventoryReserved),
"Reserved"
);
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowEvidenceMissing),
+ "Missing"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowConflictDoubleAcceptance),
+ "Double acceptance"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowPrivateTermsCommitmentMismatch),
+ "Commitment mismatch"
+ );
+ assert_eq!(
+ app_text(AppTextKey::TradeWorkflowAttestationPresentInvalid),
+ "Invalid"
+ );
assert_eq!(app_text(AppTextKey::TradeWorkflowProvenanceCli), "CLI");
assert_eq!(
app_text(AppTextKey::TradeWorkflowProvenanceRuntimeStore),
diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs
@@ -51,9 +51,9 @@ pub use sdk::{
DesktopRuntimeStorageDiagnostics, DesktopRuntimeStoragePaths, DesktopRuntimeSupervisor,
DesktopRuntimeSupervisorConfig, DesktopRuntimeSupervisorError, DesktopRuntimeSyncDiagnostics,
DesktopRuntimeSyncEventStoreDiagnostics, DesktopRuntimeSyncOutboxDiagnostics,
- DesktopRuntimeSyncTransportTargetDiagnostics, DesktopRuntimeTradeCancellationRequest,
- DesktopRuntimeTradeDecision, DesktopRuntimeTradeDecisionRequest,
- DesktopRuntimeTradeProposeRequest, DesktopRuntimeWorkflowReceipt,
+ DesktopRuntimeSyncTransportTargetDiagnostics, DesktopRuntimeTradeCancelRequest,
+ DesktopRuntimeTradeDecideCandidateRequest, DesktopRuntimeTradeProposeRevisionRequest,
+ DesktopRuntimeTradeSubmitProposalRequest, DesktopRuntimeWorkflowReceipt,
desktop_runtime_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
@@ -14,36 +14,27 @@ use std::{
use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner};
use radroots_event::{
- RadrootsEventPtr,
contract::RadrootsActorRole,
farm::{RadrootsFarm, RadrootsFarmPublicLocation},
- ids::{
- RadrootsAddressableCoordinate, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey,
- },
+ ids::{RadrootsAddressableCoordinate, RadrootsPublicKey},
kinds::KIND_FARM,
listing::{RadrootsListing, RadrootsListingPublicLocation},
- order::{
- RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem,
- RadrootsOrderRequest,
- },
+ trade::{RadrootsTradeMutationBodyV1, RadrootsTradeMutationEnvelopeV1},
};
use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_sdk::SdkMutationState;
use radroots_sdk::{
- FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmEnqueueReceipt, IntegrityReceipt,
- IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest,
- ListingEnqueueReceipt, NostrProfile, NostrRelayUrlPolicy, PrivacyPreflightConfirmation,
- ProductSensitivityField, PublishMode, RadrootsClient, RadrootsSdkError,
- RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkStoragePaths, RestoreReceipt,
- RestoreRequest, SatisfactionPolicy, SdkBackupVerification, SdkPublicLocality,
- StorageStatusReceipt, StorageStatusRequest, SyncStatusReceipt, SyncStatusRequest,
- TRADE_CANCELLATION_OPERATION_KIND, TRADE_DECISION_OPERATION_KIND, TRADE_SUBMIT_OPERATION_KIND,
- TargetPolicy, TradeAcceptRequest, TradeCancelRequest, TradeCancellationPlan,
- TradeCancellationReceipt, TradeDecisionPlan, TradeDecisionReceipt, TradeDeclineRequest,
- TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest, TradeSubmitPlan,
- TradeSubmitReceipt, TransportProfile,
+ CancelTradeRequest, DecideCandidateRequest, FARM_PUBLISH_OPERATION_KIND,
+ FarmEnqueuePublishRequest, FarmEnqueueReceipt, IntegrityReceipt, IntegrityRequest,
+ LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest, ListingEnqueueReceipt,
+ NostrProfile, NostrRelayUrlPolicy, ProposeRevisionRequest, RadrootsClient, RadrootsSdkError,
+ RadrootsSdkStoragePaths, RestoreReceipt, RestoreRequest, SatisfactionPolicy,
+ SdkBackupVerification, SdkIdempotencyKey, SdkPublicLocality, StorageStatusReceipt,
+ StorageStatusRequest, SubmitProposalRequest, SyncStatusReceipt, SyncStatusRequest,
+ TRADE_CANCEL_OPERATION_KIND, TRADE_DECIDE_CANDIDATE_OPERATION_KIND,
+ TRADE_PROPOSE_REVISION_OPERATION_KIND, TRADE_SUBMIT_PROPOSAL_OPERATION_KIND, TargetPolicy,
+ TradeCommandReceipt, TransportProfile,
};
-use radroots_trade::identity::RadrootsTradeLocator;
use serde::Serialize;
use serde_json::{Value, json};
use thiserror::Error;
@@ -276,47 +267,36 @@ pub struct DesktopRuntimeListingPublishRequest {
pub idempotency_key: Option<String>,
}
-pub struct DesktopRuntimeTradeProposeRequest {
+pub struct DesktopRuntimeTradeSubmitProposalRequest {
pub actor_account_id: String,
pub actor_pubkey: String,
pub signer: DesktopRuntimeLocalSigner,
- pub listing_event: RadrootsEventPtr,
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub seller_pubkey: RadrootsPublicKey,
- pub items: Vec<RadrootsOrderItem>,
- pub economics: RadrootsOrderEconomics,
- pub public_note: Option<String>,
- pub confirm_public_note: bool,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
pub idempotency_key: Option<String>,
}
-pub enum DesktopRuntimeTradeDecision {
- Accept {
- inventory_commitments: Vec<RadrootsOrderInventoryCommitment>,
- },
- Decline {
- reason: String,
- },
+pub struct DesktopRuntimeTradeProposeRevisionRequest {
+ pub actor_account_id: String,
+ pub actor_pubkey: String,
+ pub signer: DesktopRuntimeLocalSigner,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
+ pub idempotency_key: Option<String>,
}
-pub struct DesktopRuntimeTradeDecisionRequest {
+pub struct DesktopRuntimeTradeDecideCandidateRequest {
pub actor_account_id: String,
pub actor_pubkey: String,
pub signer: DesktopRuntimeLocalSigner,
- pub locator: RadrootsTradeLocator,
- pub decision: DesktopRuntimeTradeDecision,
- pub confirm_public_note: bool,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
+ pub acknowledge_private_terms: bool,
pub idempotency_key: Option<String>,
}
-pub struct DesktopRuntimeTradeCancellationRequest {
+pub struct DesktopRuntimeTradeCancelRequest {
pub actor_account_id: String,
pub actor_pubkey: String,
pub signer: DesktopRuntimeLocalSigner,
- pub locator: RadrootsTradeLocator,
- pub reason: String,
- pub confirm_public_note: bool,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
pub idempotency_key: Option<String>,
}
@@ -376,9 +356,10 @@ pub enum DesktopRuntimeEffectKind {
RestorePreflight,
FarmPublish,
ListingPublish,
- TradePropose,
- TradeDecision,
- TradeCancellation,
+ TradeSubmitProposal,
+ TradeProposeRevision,
+ TradeDecideCandidate,
+ TradeCancel,
BeginProjectionRebuild,
CompleteProjectionRebuild,
}
@@ -448,17 +429,21 @@ enum DesktopRuntimeEffect {
DesktopRuntimeEffectReceipt,
Box<DesktopRuntimeListingPublishRequest>,
),
- TradePropose(
+ TradeSubmitProposal(
+ DesktopRuntimeEffectReceipt,
+ Box<DesktopRuntimeTradeSubmitProposalRequest>,
+ ),
+ TradeProposeRevision(
DesktopRuntimeEffectReceipt,
- Box<DesktopRuntimeTradeProposeRequest>,
+ Box<DesktopRuntimeTradeProposeRevisionRequest>,
),
- TradeDecision(
+ TradeDecideCandidate(
DesktopRuntimeEffectReceipt,
- Box<DesktopRuntimeTradeDecisionRequest>,
+ Box<DesktopRuntimeTradeDecideCandidateRequest>,
),
- TradeCancellation(
+ TradeCancel(
DesktopRuntimeEffectReceipt,
- Box<DesktopRuntimeTradeCancellationRequest>,
+ Box<DesktopRuntimeTradeCancelRequest>,
),
BeginProjectionRebuild(DesktopRuntimeEffectReceipt),
CompleteProjectionRebuild(DesktopRuntimeEffectReceipt),
@@ -471,9 +456,10 @@ impl fmt::Debug for DesktopRuntimeEffect {
Self::RestorePreflight(_, _) => formatter.write_str("RestorePreflight"),
Self::EnqueueFarmPublish(_, _) => formatter.write_str("EnqueueFarmPublish"),
Self::EnqueueListingPublish(_, _) => formatter.write_str("EnqueueListingPublish"),
- Self::TradePropose(_, _) => formatter.write_str("TradePropose"),
- Self::TradeDecision(_, _) => formatter.write_str("TradeDecision"),
- Self::TradeCancellation(_, _) => formatter.write_str("TradeCancellation"),
+ Self::TradeSubmitProposal(_, _) => formatter.write_str("TradeSubmitProposal"),
+ Self::TradeProposeRevision(_, _) => formatter.write_str("TradeProposeRevision"),
+ Self::TradeDecideCandidate(_, _) => formatter.write_str("TradeDecideCandidate"),
+ Self::TradeCancel(_, _) => formatter.write_str("TradeCancel"),
Self::BeginProjectionRebuild(_) => formatter.write_str("BeginProjectionRebuild"),
Self::CompleteProjectionRebuild(_) => formatter.write_str("CompleteProjectionRebuild"),
}
@@ -625,42 +611,55 @@ impl DesktopRuntimeSupervisor {
)
}
- pub fn trade_propose(
+ pub fn trade_submit_proposal(
&self,
- request: DesktopRuntimeTradeProposeRequest,
+ request: DesktopRuntimeTradeSubmitProposalRequest,
) -> Result<DesktopRuntimeEffectReceipt, DesktopRuntimeSupervisorError> {
let actor_pubkey = Some(request.actor_pubkey.clone());
self.submit_effect(
- DesktopRuntimeEffectKind::TradePropose,
- Some(TRADE_SUBMIT_OPERATION_KIND.to_owned()),
+ DesktopRuntimeEffectKind::TradeSubmitProposal,
+ Some(TRADE_SUBMIT_PROPOSAL_OPERATION_KIND.to_owned()),
actor_pubkey,
- |receipt| DesktopRuntimeEffect::TradePropose(receipt, Box::new(request)),
+ |receipt| DesktopRuntimeEffect::TradeSubmitProposal(receipt, Box::new(request)),
)
}
- pub fn trade_decide(
+ pub fn trade_propose_revision(
&self,
- request: DesktopRuntimeTradeDecisionRequest,
+ request: DesktopRuntimeTradeProposeRevisionRequest,
) -> Result<DesktopRuntimeEffectReceipt, DesktopRuntimeSupervisorError> {
let actor_pubkey = Some(request.actor_pubkey.clone());
self.submit_effect(
- DesktopRuntimeEffectKind::TradeDecision,
- Some(TRADE_DECISION_OPERATION_KIND.to_owned()),
+ DesktopRuntimeEffectKind::TradeProposeRevision,
+ Some(TRADE_PROPOSE_REVISION_OPERATION_KIND.to_owned()),
actor_pubkey,
- |receipt| DesktopRuntimeEffect::TradeDecision(receipt, Box::new(request)),
+ |receipt| DesktopRuntimeEffect::TradeProposeRevision(receipt, Box::new(request)),
+ )
+ }
+
+ pub fn trade_decide_candidate(
+ &self,
+ request: DesktopRuntimeTradeDecideCandidateRequest,
+ ) -> Result<DesktopRuntimeEffectReceipt, DesktopRuntimeSupervisorError> {
+ let actor_pubkey = Some(request.actor_pubkey.clone());
+ self.submit_effect(
+ DesktopRuntimeEffectKind::TradeDecideCandidate,
+ Some(TRADE_DECIDE_CANDIDATE_OPERATION_KIND.to_owned()),
+ actor_pubkey,
+ |receipt| DesktopRuntimeEffect::TradeDecideCandidate(receipt, Box::new(request)),
)
}
pub fn trade_cancel(
&self,
- request: DesktopRuntimeTradeCancellationRequest,
+ request: DesktopRuntimeTradeCancelRequest,
) -> Result<DesktopRuntimeEffectReceipt, DesktopRuntimeSupervisorError> {
let actor_pubkey = Some(request.actor_pubkey.clone());
self.submit_effect(
- DesktopRuntimeEffectKind::TradeCancellation,
- Some(TRADE_CANCELLATION_OPERATION_KIND.to_owned()),
+ DesktopRuntimeEffectKind::TradeCancel,
+ Some(TRADE_CANCEL_OPERATION_KIND.to_owned()),
actor_pubkey,
- |receipt| DesktopRuntimeEffect::TradeCancellation(receipt, Box::new(request)),
+ |receipt| DesktopRuntimeEffect::TradeCancel(receipt, Box::new(request)),
)
}
@@ -1189,29 +1188,40 @@ fn run_desktop_runtime_worker(
};
finish_workflow_result(&shared, receipt, result);
}
- DesktopRuntimeEffect::TradePropose(receipt, request) => {
+ DesktopRuntimeEffect::TradeSubmitProposal(receipt, request) => {
+ let result = if let Some(issue) = lifecycle_busy_issue(&shared) {
+ Err(issue)
+ } else {
+ match sdk.as_ref() {
+ Some(_) => trade_submit_proposal_with_sdk(&runtime, &config, *request),
+ None => Err(runtime_unavailable_issue(&shared)),
+ }
+ };
+ finish_workflow_result(&shared, receipt, result);
+ }
+ DesktopRuntimeEffect::TradeProposeRevision(receipt, request) => {
let result = if let Some(issue) = lifecycle_busy_issue(&shared) {
Err(issue)
} else {
match sdk.as_ref() {
- Some(_) => trade_propose_with_sdk(&runtime, &config, *request),
+ Some(_) => trade_propose_revision_with_sdk(&runtime, &config, *request),
None => Err(runtime_unavailable_issue(&shared)),
}
};
finish_workflow_result(&shared, receipt, result);
}
- DesktopRuntimeEffect::TradeDecision(receipt, request) => {
+ DesktopRuntimeEffect::TradeDecideCandidate(receipt, request) => {
let result = if let Some(issue) = lifecycle_busy_issue(&shared) {
Err(issue)
} else {
match sdk.as_ref() {
- Some(_) => trade_decision_with_sdk(&runtime, &config, *request),
+ Some(_) => trade_decide_candidate_with_sdk(&runtime, &config, *request),
None => Err(runtime_unavailable_issue(&shared)),
}
};
finish_workflow_result(&shared, receipt, result);
}
- DesktopRuntimeEffect::TradeCancellation(receipt, request) => {
+ DesktopRuntimeEffect::TradeCancel(receipt, request) => {
let result = if let Some(issue) = lifecycle_busy_issue(&shared) {
Err(issue)
} else {
@@ -1291,26 +1301,6 @@ async fn build_sdk_runtime(
.await
}
-async fn build_sdk_runtime_with_signer(
- config: &DesktopRuntimeSupervisorConfig,
- signer: DesktopRuntimeLocalSigner,
-) -> Result<RadrootsClient, DesktopRuntimeIssue> {
- let local_signer = RadrootsLocalEventSigner::new(signer.into_keys()).map_err(|error| {
- DesktopRuntimeIssue::runtime_error("sdk_signer_init_failed", error.to_string())
- })?;
- let signer = RadrootsSdkLocalKeySigner::from_event_signer(local_signer)
- .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?;
- let transport_profile = app_transport_profile(config)
- .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?;
- RadrootsClient::builder()
- .directory_storage(config.storage_root.clone())
- .transport_profile(transport_profile)
- .signer_provider(RadrootsSdkSignerProvider::LocalKey(signer))
- .build()
- .await
- .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))
-}
-
fn app_transport_profile(
config: &DesktopRuntimeSupervisorConfig,
) -> Result<TransportProfile, RadrootsSdkError> {
@@ -1323,10 +1313,6 @@ fn app_transport_profile(
)?))
}
-fn app_trade_publish_mode() -> PublishMode {
- PublishMode::EnqueueOnly
-}
-
fn app_trade_satisfaction_policy() -> SatisfactionPolicy {
SatisfactionPolicy::NoWait
}
@@ -1335,15 +1321,6 @@ fn app_trade_target_policy() -> TargetPolicy {
TargetPolicy::default_profile()
}
-fn app_trade_privacy_confirmation(confirm_public_note: bool) -> PrivacyPreflightConfirmation {
- if confirm_public_note {
- PrivacyPreflightConfirmation::new()
- .confirm(ProductSensitivityField::PublicButSensitiveNotes)
- } else {
- PrivacyPreflightConfirmation::new()
- }
-}
-
fn block_on_sdk_result<T>(
runtime: &tokio::runtime::Runtime,
operation: &'static str,
@@ -1362,23 +1339,6 @@ fn block_on_sdk_result<T>(
}
}
-fn block_on_desktop_result<T>(
- runtime: &tokio::runtime::Runtime,
- operation: &'static str,
- future: impl Future<Output = Result<T, DesktopRuntimeIssue>>,
-) -> Result<T, DesktopRuntimeIssue> {
- match runtime.block_on(async {
- tokio::time::timeout(
- Duration::from_millis(DESKTOP_RUNTIME_SDK_EFFECT_TIMEOUT_MS),
- future,
- )
- .await
- }) {
- Ok(result) => result,
- Err(_) => Err(sdk_effect_timeout_issue(operation)),
- }
-}
-
fn sdk_effect_timeout_issue(operation: &'static str) -> DesktopRuntimeIssue {
DesktopRuntimeIssue::runtime_error(
"desktop_runtime_sdk_effect_timeout",
@@ -1544,162 +1504,162 @@ fn enqueue_listing_publish_with_sdk(
))
}
-fn trade_propose_with_sdk(
+fn trade_submit_proposal_with_sdk(
runtime: &tokio::runtime::Runtime,
config: &DesktopRuntimeSupervisorConfig,
- request: DesktopRuntimeTradeProposeRequest,
+ request: DesktopRuntimeTradeSubmitProposalRequest,
) -> Result<DesktopRuntimeWorkflowReceipt, DesktopRuntimeIssue> {
- let actor = sdk_actor_context(
+ validate_trade_envelope_body(&request.envelope, "proposal", |body| {
+ matches!(body, RadrootsTradeMutationBodyV1::Proposal { .. })
+ })?;
+ let actor = sdk_trade_actor_context(
request.actor_pubkey.as_str(),
request.actor_account_id.as_str(),
- RadrootsActorRole::Buyer,
+ &request.envelope,
)?;
- let sdk = block_on_desktop_result(
+ let signer = sdk_local_signer(request.signer)?;
+ let sdk = block_on_sdk_result(
runtime,
- "desktop_runtime_trade_propose_build",
- build_sdk_runtime_with_signer(config, request.signer),
+ "desktop_runtime_trade_submit_build",
+ build_sdk_runtime(config),
)?;
- let buyer_pubkey =
- RadrootsPublicKey::parse(request.actor_pubkey.as_str()).map_err(|error| {
- DesktopRuntimeIssue::from_sdk_error(&RadrootsSdkError::InvalidRequest {
- message: format!("trade proposal buyer public key is invalid: {error}"),
- })
- })?;
- let order = RadrootsOrderRequest {
- order_id: request.order_id,
- listing_addr: request.listing_addr,
- buyer_pubkey,
- seller_pubkey: request.seller_pubkey,
- items: request.items,
- economics: request.economics,
- };
- let mut sdk_request = TradeProposeRequest::new(
- actor,
- request.listing_event,
- order,
- app_trade_target_policy(),
- app_trade_publish_mode(),
- app_trade_satisfaction_policy(),
- )
- .with_optional_public_note(request.public_note)
- .with_privacy_confirmation(app_trade_privacy_confirmation(request.confirm_public_note));
+ let mut sdk_request =
+ SubmitProposalRequest::new(actor, request.envelope, app_trade_target_policy())
+ .with_satisfaction_policy(app_trade_satisfaction_policy());
if let Some(idempotency_key) = request.idempotency_key.as_deref() {
sdk_request = sdk_request
.try_with_idempotency_key(idempotency_key)
.map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?;
}
- let outcome = block_on_sdk_result(
+ let receipt = block_on_sdk_result(
runtime,
- "desktop_runtime_trade_propose",
- sdk.trades().buyer().propose_trade(sdk_request),
+ "desktop_runtime_trade_submit_proposal",
+ sdk.trades()
+ .commands()
+ .submit_proposal_with_explicit_signer(sdk_request, &signer),
)?;
- desktop_runtime_trade_propose_receipt(outcome, request.actor_pubkey)
+ Ok(desktop_runtime_trade_receipt(receipt, request.actor_pubkey))
}
-fn trade_decision_with_sdk(
+fn trade_propose_revision_with_sdk(
runtime: &tokio::runtime::Runtime,
config: &DesktopRuntimeSupervisorConfig,
- request: DesktopRuntimeTradeDecisionRequest,
+ request: DesktopRuntimeTradeProposeRevisionRequest,
) -> Result<DesktopRuntimeWorkflowReceipt, DesktopRuntimeIssue> {
- let actor = sdk_actor_context(
+ validate_trade_envelope_body(&request.envelope, "revision proposal", |body| {
+ matches!(body, RadrootsTradeMutationBodyV1::RevisionProposal { .. })
+ })?;
+ let actor = sdk_trade_actor_context(
request.actor_pubkey.as_str(),
request.actor_account_id.as_str(),
- RadrootsActorRole::Seller,
+ &request.envelope,
)?;
- let sdk = block_on_desktop_result(
+ let signer = sdk_local_signer(request.signer)?;
+ let sdk = block_on_sdk_result(
+ runtime,
+ "desktop_runtime_trade_revision_build",
+ build_sdk_runtime(config),
+ )?;
+ let mut sdk_request =
+ ProposeRevisionRequest::new(actor, request.envelope, app_trade_target_policy())
+ .with_satisfaction_policy(app_trade_satisfaction_policy());
+ if let Some(idempotency_key) = request.idempotency_key.as_deref() {
+ sdk_request = sdk_request.with_idempotency_key(
+ SdkIdempotencyKey::new(idempotency_key)
+ .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?,
+ );
+ }
+ let receipt = block_on_sdk_result(
+ runtime,
+ "desktop_runtime_trade_propose_revision",
+ sdk.trades()
+ .commands()
+ .propose_revision_with_explicit_signer(sdk_request, &signer),
+ )?;
+ Ok(desktop_runtime_trade_receipt(receipt, request.actor_pubkey))
+}
+
+fn trade_decide_candidate_with_sdk(
+ runtime: &tokio::runtime::Runtime,
+ config: &DesktopRuntimeSupervisorConfig,
+ request: DesktopRuntimeTradeDecideCandidateRequest,
+) -> Result<DesktopRuntimeWorkflowReceipt, DesktopRuntimeIssue> {
+ validate_trade_envelope_body(&request.envelope, "candidate decision", |body| {
+ matches!(
+ body,
+ RadrootsTradeMutationBodyV1::Decision { .. }
+ | RadrootsTradeMutationBodyV1::RevisionDecision { .. }
+ )
+ })?;
+ let actor = sdk_trade_actor_context(
+ request.actor_pubkey.as_str(),
+ request.actor_account_id.as_str(),
+ &request.envelope,
+ )?;
+ let signer = sdk_local_signer(request.signer)?;
+ let sdk = block_on_sdk_result(
runtime,
"desktop_runtime_trade_decision_build",
- build_sdk_runtime_with_signer(config, request.signer),
+ build_sdk_runtime(config),
)?;
- let publish_mode = app_trade_publish_mode();
- let satisfaction_policy = app_trade_satisfaction_policy();
- let outcome = match request.decision {
- DesktopRuntimeTradeDecision::Accept {
- inventory_commitments,
- } => {
- let mut sdk_request = TradeAcceptRequest::new(
- actor,
- request.locator,
- inventory_commitments,
- app_trade_target_policy(),
- publish_mode,
- satisfaction_policy,
- TradeEvidenceMode::ResyncBeforeMutation,
- )
- .with_privacy_confirmation(app_trade_privacy_confirmation(false));
- if let Some(idempotency_key) = request.idempotency_key.as_deref() {
- sdk_request = sdk_request
- .try_with_idempotency_key(idempotency_key)
- .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?;
- }
- block_on_sdk_result(
- runtime,
- "desktop_runtime_trade_accept",
- sdk.trades().seller().accept_trade(sdk_request),
- )?
- }
- DesktopRuntimeTradeDecision::Decline { reason } => {
- let mut sdk_request = TradeDeclineRequest::new(
- actor,
- request.locator,
- reason,
- app_trade_target_policy(),
- publish_mode,
- satisfaction_policy,
- TradeEvidenceMode::ResyncBeforeMutation,
- )
- .with_privacy_confirmation(app_trade_privacy_confirmation(request.confirm_public_note));
- if let Some(idempotency_key) = request.idempotency_key.as_deref() {
- sdk_request = sdk_request
- .try_with_idempotency_key(idempotency_key)
- .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?;
- }
- block_on_sdk_result(
- runtime,
- "desktop_runtime_trade_decline",
- sdk.trades().seller().decline_trade(sdk_request),
- )?
- }
- };
- desktop_runtime_trade_decision_receipt(outcome, request.actor_pubkey)
+ let mut sdk_request =
+ DecideCandidateRequest::new(actor, request.envelope, app_trade_target_policy())
+ .with_satisfaction_policy(app_trade_satisfaction_policy());
+ if request.acknowledge_private_terms {
+ sdk_request = sdk_request.acknowledge_private_terms();
+ }
+ if let Some(idempotency_key) = request.idempotency_key.as_deref() {
+ sdk_request = sdk_request.with_idempotency_key(
+ SdkIdempotencyKey::new(idempotency_key)
+ .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?,
+ );
+ }
+ let receipt = block_on_sdk_result(
+ runtime,
+ "desktop_runtime_trade_decide_candidate",
+ sdk.trades()
+ .commands()
+ .decide_candidate_with_explicit_signer(sdk_request, &signer),
+ )?;
+ Ok(desktop_runtime_trade_receipt(receipt, request.actor_pubkey))
}
fn trade_cancel_with_sdk(
runtime: &tokio::runtime::Runtime,
config: &DesktopRuntimeSupervisorConfig,
- request: DesktopRuntimeTradeCancellationRequest,
+ request: DesktopRuntimeTradeCancelRequest,
) -> Result<DesktopRuntimeWorkflowReceipt, DesktopRuntimeIssue> {
- let actor = sdk_actor_context(
+ validate_trade_envelope_body(&request.envelope, "cancellation", |body| {
+ matches!(body, RadrootsTradeMutationBodyV1::Cancellation { .. })
+ })?;
+ let actor = sdk_trade_actor_context(
request.actor_pubkey.as_str(),
request.actor_account_id.as_str(),
- RadrootsActorRole::Buyer,
+ &request.envelope,
)?;
- let sdk = block_on_desktop_result(
+ let signer = sdk_local_signer(request.signer)?;
+ let sdk = block_on_sdk_result(
runtime,
"desktop_runtime_trade_cancel_build",
- build_sdk_runtime_with_signer(config, request.signer),
+ build_sdk_runtime(config),
)?;
- let mut sdk_request = TradeCancelRequest::new(
- actor,
- request.locator,
- request.reason,
- app_trade_target_policy(),
- app_trade_publish_mode(),
- app_trade_satisfaction_policy(),
- TradeEvidenceMode::ResyncBeforeMutation,
- )
- .with_privacy_confirmation(app_trade_privacy_confirmation(request.confirm_public_note));
+ let mut sdk_request =
+ CancelTradeRequest::new(actor, request.envelope, app_trade_target_policy());
+ sdk_request.satisfaction_policy = app_trade_satisfaction_policy();
if let Some(idempotency_key) = request.idempotency_key.as_deref() {
- sdk_request = sdk_request
- .try_with_idempotency_key(idempotency_key)
- .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?;
+ sdk_request = sdk_request.with_idempotency_key(
+ SdkIdempotencyKey::new(idempotency_key)
+ .map_err(|error| DesktopRuntimeIssue::from_sdk_error(&error))?,
+ );
}
- let outcome = block_on_sdk_result(
+ let receipt = block_on_sdk_result(
runtime,
"desktop_runtime_trade_cancel",
- sdk.trades().buyer().cancel_trade(sdk_request),
+ sdk.trades()
+ .commands()
+ .cancel_trade_with_explicit_signer(sdk_request, &signer),
)?;
- desktop_runtime_trade_cancellation_receipt(outcome, request.actor_pubkey)
+ Ok(desktop_runtime_trade_receipt(receipt, request.actor_pubkey))
}
fn sdk_actor_context(
@@ -1712,6 +1672,53 @@ fn sdk_actor_context(
)
}
+fn sdk_trade_actor_context(
+ actor_pubkey: &str,
+ actor_account_id: &str,
+ envelope: &RadrootsTradeMutationEnvelopeV1,
+) -> Result<RadrootsActorContext, DesktopRuntimeIssue> {
+ let parsed_actor = RadrootsPublicKey::parse(actor_pubkey).map_err(|error| {
+ DesktopRuntimeIssue::from_sdk_error(&RadrootsSdkError::InvalidRequest {
+ message: format!("trade command actor public key is invalid: {error}"),
+ })
+ })?;
+ if parsed_actor != envelope.author_pubkey {
+ return Err(DesktopRuntimeIssue::from_sdk_error(
+ &RadrootsSdkError::InvalidRequest {
+ message: "trade command actor must match the mutation envelope author".to_owned(),
+ },
+ ));
+ }
+ let role = if envelope.author_pubkey == envelope.buyer_pubkey {
+ RadrootsActorRole::Buyer
+ } else if envelope.author_pubkey == envelope.seller_pubkey {
+ RadrootsActorRole::Seller
+ } else {
+ return Err(DesktopRuntimeIssue::from_sdk_error(
+ &RadrootsSdkError::InvalidRequest {
+ message: "trade command author must be the buyer or seller".to_owned(),
+ },
+ ));
+ };
+ sdk_actor_context(actor_pubkey, actor_account_id, role)
+}
+
+fn validate_trade_envelope_body(
+ envelope: &RadrootsTradeMutationEnvelopeV1,
+ expected: &'static str,
+ accepts: impl FnOnce(&RadrootsTradeMutationBodyV1) -> bool,
+) -> Result<(), DesktopRuntimeIssue> {
+ if accepts(&envelope.body) {
+ Ok(())
+ } else {
+ Err(DesktopRuntimeIssue::from_sdk_error(
+ &RadrootsSdkError::InvalidRequest {
+ message: format!("trade command requires a {expected} mutation envelope"),
+ },
+ ))
+ }
+}
+
fn sdk_local_signer(
signer: DesktopRuntimeLocalSigner,
) -> Result<RadrootsLocalEventSigner, DesktopRuntimeIssue> {
@@ -1784,73 +1791,22 @@ fn desktop_runtime_listing_receipt(
}
}
-fn desktop_runtime_trade_propose_receipt(
- outcome: TradeMutationOutcome<TradeSubmitPlan, TradeSubmitReceipt>,
- actor_pubkey: String,
-) -> Result<DesktopRuntimeWorkflowReceipt, DesktopRuntimeIssue> {
- match outcome {
- TradeMutationOutcome::Enqueued { receipt }
- | TradeMutationOutcome::Published { receipt, .. } => Ok(DesktopRuntimeWorkflowReceipt {
- operation_kind: TRADE_SUBMIT_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,
- }),
- TradeMutationOutcome::DryRun { .. } => Err(unexpected_trade_dry_run_issue("trade.propose")),
- }
-}
-
-fn desktop_runtime_trade_decision_receipt(
- outcome: TradeMutationOutcome<TradeDecisionPlan, TradeDecisionReceipt>,
+fn desktop_runtime_trade_receipt(
+ receipt: TradeCommandReceipt,
actor_pubkey: String,
-) -> Result<DesktopRuntimeWorkflowReceipt, DesktopRuntimeIssue> {
- match outcome {
- TradeMutationOutcome::Enqueued { receipt }
- | TradeMutationOutcome::Published { receipt, .. } => Ok(DesktopRuntimeWorkflowReceipt {
- operation_kind: TRADE_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,
- }),
- TradeMutationOutcome::DryRun { .. } => Err(unexpected_trade_dry_run_issue("trade.decide")),
- }
-}
-
-fn desktop_runtime_trade_cancellation_receipt(
- outcome: TradeMutationOutcome<TradeCancellationPlan, TradeCancellationReceipt>,
- actor_pubkey: String,
-) -> Result<DesktopRuntimeWorkflowReceipt, DesktopRuntimeIssue> {
- match outcome {
- TradeMutationOutcome::Enqueued { receipt }
- | TradeMutationOutcome::Published { receipt, .. } => Ok(DesktopRuntimeWorkflowReceipt {
- operation_kind: TRADE_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,
- }),
- TradeMutationOutcome::DryRun { .. } => Err(unexpected_trade_dry_run_issue("trade.cancel")),
+) -> DesktopRuntimeWorkflowReceipt {
+ DesktopRuntimeWorkflowReceipt {
+ operation_kind: receipt.operation_kind,
+ 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.delivery_state).to_owned(),
+ idempotency_digest_prefix: Some(receipt.idempotency_digest_prefix),
+ actor_pubkey,
}
}
-fn unexpected_trade_dry_run_issue(operation: &'static str) -> DesktopRuntimeIssue {
- DesktopRuntimeIssue::runtime_error(
- "sdk_trade_unexpected_dry_run",
- format!("{operation} returned a dry-run plan for an enqueue-only Studio command"),
- )
-}
-
fn sdk_mutation_state_key(state: SdkMutationState) -> &'static str {
match state {
SdkMutationState::StoredAndQueued => "enqueued",
@@ -2002,9 +1958,10 @@ fn effect_receipt(effect: &DesktopRuntimeEffect) -> DesktopRuntimeEffectReceipt
| DesktopRuntimeEffect::RestorePreflight(receipt, _)
| DesktopRuntimeEffect::EnqueueFarmPublish(receipt, _)
| DesktopRuntimeEffect::EnqueueListingPublish(receipt, _)
- | DesktopRuntimeEffect::TradePropose(receipt, _)
- | DesktopRuntimeEffect::TradeDecision(receipt, _)
- | DesktopRuntimeEffect::TradeCancellation(receipt, _)
+ | DesktopRuntimeEffect::TradeSubmitProposal(receipt, _)
+ | DesktopRuntimeEffect::TradeProposeRevision(receipt, _)
+ | DesktopRuntimeEffect::TradeDecideCandidate(receipt, _)
+ | DesktopRuntimeEffect::TradeCancel(receipt, _)
| DesktopRuntimeEffect::BeginProjectionRebuild(receipt)
| DesktopRuntimeEffect::CompleteProjectionRebuild(receipt) => receipt.clone(),
}
diff --git a/crates/store/migrations/0031_drop_default_buyer_private_fields.sql b/crates/store/migrations/0031_drop_default_buyer_private_fields.sql
@@ -0,0 +1,320 @@
+DROP INDEX IF EXISTS idx_order_validation_receipts_order_time;
+DROP INDEX IF EXISTS idx_order_validation_receipts_root_order;
+DROP INDEX IF EXISTS idx_order_lines_order_sort;
+DROP INDEX IF EXISTS idx_buyer_order_coordination_context_state_updated_at;
+DROP INDEX IF EXISTS idx_buyer_order_coordination_state_updated_at;
+DROP INDEX IF EXISTS idx_orders_farm_status;
+DROP INDEX IF EXISTS idx_orders_farm_window_status_updated_at;
+DROP INDEX IF EXISTS idx_orders_buyer_context_updated_at;
+DROP INDEX IF EXISTS idx_buyer_cart_lines_context_updated_at;
+
+ALTER TABLE order_validation_receipts RENAME TO order_validation_receipts_public_private_previous;
+ALTER TABLE order_lines RENAME TO order_lines_public_private_previous;
+ALTER TABLE buyer_order_coordination_records RENAME TO buyer_order_coordination_records_public_private_previous;
+ALTER TABLE orders RENAME TO orders_public_private_previous;
+ALTER TABLE buyer_cart_lines RENAME TO buyer_cart_lines_public_private_previous;
+ALTER TABLE buyer_carts RENAME TO buyer_carts_public_private_previous;
+
+CREATE TABLE buyer_carts (
+ buyer_context_key TEXT PRIMARY KEY NOT NULL,
+ farm_id TEXT REFERENCES farms(id) ON DELETE SET NULL,
+ buyer_name TEXT NOT NULL DEFAULT '',
+ updated_at TEXT NOT NULL
+);
+
+INSERT INTO buyer_carts (
+ buyer_context_key,
+ farm_id,
+ buyer_name,
+ updated_at
+)
+SELECT
+ buyer_context_key,
+ farm_id,
+ buyer_name,
+ updated_at
+FROM buyer_carts_public_private_previous;
+
+CREATE TABLE buyer_cart_lines (
+ buyer_context_key TEXT NOT NULL REFERENCES buyer_carts(buyer_context_key) ON DELETE CASCADE,
+ product_id TEXT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
+ quantity INTEGER NOT NULL CHECK (quantity > 0),
+ updated_at TEXT NOT NULL,
+ listing_bin_id TEXT,
+ quantity_unit_label TEXT,
+ unit_price_minor_units INTEGER CHECK (
+ unit_price_minor_units IS NULL OR unit_price_minor_units >= 0
+ ),
+ price_currency TEXT,
+ farm_key TEXT,
+ listing_addr TEXT,
+ listing_event_id TEXT,
+ seller_pubkey TEXT,
+ listing_relays_json TEXT,
+ PRIMARY KEY (buyer_context_key, product_id)
+);
+
+INSERT INTO buyer_cart_lines (
+ buyer_context_key,
+ product_id,
+ quantity,
+ updated_at,
+ listing_bin_id,
+ quantity_unit_label,
+ unit_price_minor_units,
+ price_currency,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
+ listing_relays_json
+)
+SELECT
+ buyer_context_key,
+ product_id,
+ quantity,
+ updated_at,
+ listing_bin_id,
+ quantity_unit_label,
+ unit_price_minor_units,
+ price_currency,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
+ listing_relays_json
+FROM buyer_cart_lines_public_private_previous;
+
+CREATE TABLE orders (
+ id TEXT PRIMARY KEY NOT NULL,
+ farm_id TEXT NOT NULL REFERENCES farms(id) ON DELETE CASCADE,
+ fulfillment_window_id TEXT REFERENCES fulfillment_windows(id) ON DELETE SET NULL,
+ order_number TEXT NOT NULL,
+ customer_display_name TEXT NOT NULL,
+ status TEXT NOT NULL CHECK (
+ status IN ('needs_action', 'scheduled', 'packed', 'completed', 'declined', 'needs_review')
+ ),
+ updated_at TEXT NOT NULL,
+ buyer_context_key TEXT,
+ workflow_revision TEXT NOT NULL DEFAULT 'none' CHECK (
+ workflow_revision IN ('none', 'change_proposed', 'updated', 'kept_as_placed')
+ ),
+ workflow_agreement TEXT NOT NULL DEFAULT 'requested' CHECK (
+ workflow_agreement IN ('requested', 'committed', 'contested', 'declined', 'cancelled', 'invalid')
+ ),
+ workflow_inventory TEXT NOT NULL DEFAULT 'needs_review' CHECK (
+ workflow_inventory IN ('available', 'reserved', 'sold_out', 'needs_review')
+ ),
+ workflow_provenance_source TEXT NOT NULL DEFAULT 'unknown' CHECK (
+ workflow_provenance_source IN ('app', 'cli', 'relay', 'runtime_store', 'unknown')
+ ),
+ workflow_provenance_last_event_id TEXT
+);
+
+INSERT INTO orders (
+ id,
+ farm_id,
+ fulfillment_window_id,
+ order_number,
+ customer_display_name,
+ status,
+ updated_at,
+ buyer_context_key,
+ workflow_revision,
+ workflow_agreement,
+ workflow_inventory,
+ workflow_provenance_source,
+ workflow_provenance_last_event_id
+)
+SELECT
+ id,
+ farm_id,
+ fulfillment_window_id,
+ order_number,
+ customer_display_name,
+ status,
+ updated_at,
+ buyer_context_key,
+ workflow_revision,
+ CASE workflow_agreement
+ WHEN 'agreed_pending_validation' THEN 'contested'
+ WHEN 'validation_expired' THEN 'invalid'
+ ELSE workflow_agreement
+ END,
+ workflow_inventory,
+ workflow_provenance_source,
+ workflow_provenance_last_event_id
+FROM orders_public_private_previous;
+
+CREATE TABLE order_lines (
+ id TEXT PRIMARY KEY NOT NULL,
+ order_id TEXT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ quantity_value INTEGER NOT NULL CHECK (quantity_value >= 0),
+ quantity_unit_label TEXT NOT NULL DEFAULT '',
+ quantity_display TEXT NOT NULL,
+ sort_index INTEGER NOT NULL DEFAULT 0,
+ listing_bin_id TEXT,
+ unit_price_minor_units INTEGER CHECK (
+ unit_price_minor_units IS NULL OR unit_price_minor_units >= 0
+ ),
+ price_currency TEXT NOT NULL DEFAULT 'USD',
+ farm_key TEXT,
+ listing_addr TEXT,
+ listing_event_id TEXT,
+ seller_pubkey TEXT,
+ listing_relays_json TEXT
+);
+
+INSERT INTO order_lines (
+ id,
+ order_id,
+ title,
+ quantity_value,
+ quantity_unit_label,
+ quantity_display,
+ sort_index,
+ listing_bin_id,
+ unit_price_minor_units,
+ price_currency,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
+ listing_relays_json
+)
+SELECT
+ id,
+ order_id,
+ title,
+ quantity_value,
+ quantity_unit_label,
+ quantity_display,
+ sort_index,
+ listing_bin_id,
+ unit_price_minor_units,
+ price_currency,
+ farm_key,
+ listing_addr,
+ listing_event_id,
+ seller_pubkey,
+ listing_relays_json
+FROM order_lines_public_private_previous;
+
+CREATE TABLE buyer_order_coordination_records (
+ order_id TEXT PRIMARY KEY NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
+ buyer_context_key TEXT NOT NULL,
+ record_id TEXT,
+ state TEXT NOT NULL CHECK (state IN ('pending', 'synced', 'failed')),
+ payload_json TEXT,
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+ last_error_message TEXT,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ synced_at TEXT
+);
+
+INSERT INTO buyer_order_coordination_records (
+ order_id,
+ buyer_context_key,
+ record_id,
+ state,
+ payload_json,
+ attempt_count,
+ last_error_message,
+ created_at,
+ updated_at,
+ synced_at
+)
+SELECT
+ order_id,
+ buyer_context_key,
+ record_id,
+ state,
+ payload_json,
+ attempt_count,
+ last_error_message,
+ created_at,
+ updated_at,
+ synced_at
+FROM buyer_order_coordination_records_public_private_previous;
+
+CREATE TABLE order_validation_receipts (
+ event_id TEXT PRIMARY KEY NOT NULL,
+ order_id TEXT REFERENCES orders(id) ON DELETE CASCADE,
+ raw_order_id TEXT NOT NULL,
+ root_event_id TEXT NOT NULL,
+ listing_event_id TEXT NOT NULL,
+ target_event_id TEXT NOT NULL,
+ receipt_type TEXT NOT NULL CHECK (
+ receipt_type IN ('listing_validation', 'trade_transition', 'inventory_state', 'state_checkpoint')
+ ),
+ result TEXT NOT NULL CHECK (
+ result IN ('valid', 'needs_review')
+ ),
+ proof_system TEXT NOT NULL CHECK (
+ proof_system IN ('none', 'sp1_core', 'sp1_compressed', 'sp1_groth16', 'sp1_plonk')
+ ),
+ event_set_root TEXT NOT NULL,
+ reducer_output_root TEXT NOT NULL,
+ public_values_hash TEXT NOT NULL,
+ event_created_at INTEGER NOT NULL CHECK (event_created_at >= 0)
+);
+
+INSERT INTO order_validation_receipts (
+ event_id,
+ order_id,
+ raw_order_id,
+ root_event_id,
+ listing_event_id,
+ target_event_id,
+ receipt_type,
+ result,
+ proof_system,
+ event_set_root,
+ reducer_output_root,
+ public_values_hash,
+ event_created_at
+)
+SELECT
+ event_id,
+ order_id,
+ raw_order_id,
+ root_event_id,
+ listing_event_id,
+ target_event_id,
+ receipt_type,
+ result,
+ proof_system,
+ event_set_root,
+ reducer_output_root,
+ public_values_hash,
+ event_created_at
+FROM order_validation_receipts_public_private_previous;
+
+CREATE INDEX idx_buyer_cart_lines_context_updated_at
+ ON buyer_cart_lines(buyer_context_key, updated_at DESC, product_id DESC);
+CREATE INDEX idx_orders_farm_status ON orders(farm_id, status);
+CREATE INDEX idx_orders_farm_window_status_updated_at
+ ON orders(farm_id, fulfillment_window_id, status, updated_at DESC, id DESC);
+CREATE INDEX idx_orders_buyer_context_updated_at
+ ON orders(buyer_context_key, updated_at DESC, id DESC)
+ WHERE buyer_context_key IS NOT NULL AND trim(buyer_context_key) <> '';
+CREATE INDEX idx_order_lines_order_sort
+ ON order_lines(order_id, sort_index, id);
+CREATE INDEX idx_buyer_order_coordination_context_state_updated_at
+ ON buyer_order_coordination_records(buyer_context_key, state, updated_at);
+CREATE INDEX idx_buyer_order_coordination_state_updated_at
+ ON buyer_order_coordination_records(state, updated_at);
+CREATE INDEX idx_order_validation_receipts_order_time
+ ON order_validation_receipts(order_id, event_created_at DESC, event_id DESC)
+ WHERE order_id IS NOT NULL;
+CREATE INDEX idx_order_validation_receipts_root_order
+ ON order_validation_receipts(root_event_id, raw_order_id);
+
+DROP TABLE order_validation_receipts_public_private_previous;
+DROP TABLE order_lines_public_private_previous;
+DROP TABLE buyer_order_coordination_records_public_private_previous;
+DROP TABLE orders_public_private_previous;
+DROP TABLE buyer_cart_lines_public_private_previous;
+DROP TABLE buyer_carts_public_private_previous;
diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs
@@ -2,18 +2,10 @@ use std::{fs, path::Path};
use radroots_event::{
RadrootsEventEnvelope, RadrootsEventEnvelopeParts,
- ids::{RadrootsEventId, RadrootsOrderId, RadrootsPublicKey},
kinds::{
KIND_FARM as RADROOTS_KIND_FARM, KIND_LISTING as RADROOTS_KIND_LISTING,
- KIND_ORDER_CANCELLATION as RADROOTS_KIND_ORDER_CANCELLATION,
- KIND_ORDER_DECISION as RADROOTS_KIND_ORDER_DECISION,
- KIND_ORDER_REQUEST as RADROOTS_KIND_ORDER_REQUEST, KIND_TRADE_VALIDATION_RECEIPT,
+ KIND_TRADE_VALIDATION_RECEIPT,
},
- order::{RadrootsOrderEconomics, RadrootsOrderItem, RadrootsOrderRequest},
-};
-use radroots_event_codec::order::{
- order_cancellation_from_event, order_decision_from_event, order_event_context_from_tags,
- order_request_from_event,
};
use radroots_runtime_store::{
PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryState, RuntimeStore,
@@ -22,14 +14,8 @@ use radroots_runtime_store::{
use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor};
use radroots_studio_app_view::{
FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary,
- FulfillmentWindowId, OrderId, OrderStatus, PickupLocationId, ProductId, ProductStatus,
- TradeAgreementStatus, TradeInventoryStatus, TradeProvenanceProjection, TradeRevisionStatus,
+ FulfillmentWindowId, PickupLocationId, ProductId, ProductStatus,
TradeValidationReceiptProofSystem, TradeValidationReceiptResult, TradeValidationReceiptType,
- TradeWorkflowProjection, TradeWorkflowSource, order_status_from_active_order_projection,
-};
-use radroots_trade::order::{
- RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderProjection,
- RadrootsOrderReductionInputs, RadrootsOrderRequestRecord, reduce_order_events,
};
use radroots_trade::validation_receipt::{
RadrootsTradeValidationReceipt, RadrootsValidationReceiptTags, validation_receipt_from_event,
@@ -47,12 +33,7 @@ const RUNTIME_STORE_BATCH_LIMIT: u32 = 500;
const APP_LOCAL_INTEROP_CURSOR_ID: &str = "radroots_studio_app_sqlite_projection_v1";
const KIND_FARM: i64 = RADROOTS_KIND_FARM as i64;
const KIND_LISTING: i64 = RADROOTS_KIND_LISTING as i64;
-const KIND_ORDER_REQUEST: i64 = RADROOTS_KIND_ORDER_REQUEST as i64;
-const KIND_ORDER_DECISION: i64 = RADROOTS_KIND_ORDER_DECISION as i64;
-const KIND_ORDER_CANCEL: i64 = RADROOTS_KIND_ORDER_CANCELLATION as i64;
const KIND_VALIDATION_RECEIPT: i64 = KIND_TRADE_VALIDATION_RECEIPT as i64;
-const ACTIVE_ORDER_EVENT_KINDS: [i64; 3] =
- [KIND_ORDER_REQUEST, KIND_ORDER_DECISION, KIND_ORDER_CANCEL];
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AppLocalInteropImportReport {
@@ -292,69 +273,6 @@ impl<'a> AppLocalInteropRepository<'a> {
Ok(events)
}
- fn load_signed_event_by_event_id(
- &self,
- event_id: &str,
- ) -> Result<Option<RadrootsEventEnvelope>, AppSqliteError> {
- let mut statement = self
- .connection
- .prepare(
- "SELECT
- event_id,
- event_kind,
- local_status,
- outbox_status,
- relay_delivery_json,
- event_pubkey,
- event_created_at,
- event_tags_json,
- event_content,
- event_sig
- FROM local_interop_imports
- WHERE record_family = 'signed_event'
- AND event_id = ?1
- ORDER BY local_seq DESC, record_id DESC",
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "prepare local interop signed event id evidence query",
- source,
- })?;
- let rows = statement
- .query_map(crate::app_sqlite_params![event_id], |row| {
- Ok(StoredLocalInteropSignedEventEvidence {
- event_id: row.try_get(0)?,
- event_kind: row.try_get(1)?,
- local_status: row.try_get(2)?,
- outbox_status: row.try_get(3)?,
- relay_delivery_json: row.try_get(4)?,
- event_pubkey: row.try_get(5)?,
- event_created_at: row.try_get(6)?,
- event_tags_json: row.try_get(7)?,
- event_content: row.try_get(8)?,
- event_sig: row.try_get(9)?,
- })
- })
- .map_err(|source| AppSqliteError::Query {
- operation: "query local interop signed event id evidence",
- source,
- })?;
-
- for row in rows {
- let evidence = row.map_err(|source| AppSqliteError::Query {
- operation: "read local interop signed event id evidence row",
- source,
- })?;
- if !signed_event_local_interop_evidence_is_usable(&evidence) {
- continue;
- }
- if let Some(event) = signed_event_from_local_interop_evidence(&evidence)? {
- return Ok(Some(event));
- }
- }
-
- Ok(None)
- }
-
fn last_imported_change_seq(&self) -> Result<i64, AppSqliteError> {
match self.connection.query_row(
"SELECT last_change_seq
@@ -747,7 +665,6 @@ impl<'a> AppLocalInteropRepository<'a> {
Some(KIND_FARM) => self.import_signed_farm(record),
Some(KIND_LISTING) => self.import_signed_listing(record),
Some(KIND_VALIDATION_RECEIPT) => self.import_signed_validation_receipt(record),
- Some(kind) if active_order_event_kind(kind) => self.import_signed_active_order(record),
_ => Ok(Some(ProjectionRecord {
kind: "signed_event",
projected_id: record.event_id.clone(),
@@ -1098,23 +1015,6 @@ impl<'a> AppLocalInteropRepository<'a> {
Ok(Some(projection_record))
}
- fn import_signed_active_order(
- &self,
- record: &RuntimeStoreRecord,
- ) -> Result<Option<ProjectionRecord>, AppSqliteError> {
- if !signed_event_record_is_usable(record) {
- return Ok(Some(signed_event_projection(record)));
- }
- let Some(event) = signed_event_from_record(record)? else {
- return Ok(Some(signed_event_projection(record)));
- };
- let Some(current_evidence) = active_order_evidence_from_event(&event) else {
- return Ok(Some(signed_event_projection(record)));
- };
- self.project_active_order(record, current_evidence)?;
- Ok(Some(signed_event_projection(record)))
- }
-
fn import_signed_validation_receipt(
&self,
record: &RuntimeStoreRecord,
@@ -1135,194 +1035,13 @@ impl<'a> AppLocalInteropRepository<'a> {
}))
}
- fn project_active_order(
- &self,
- record: &RuntimeStoreRecord,
- current_evidence: ActiveOrderEvidence,
- ) -> Result<(), AppSqliteError> {
- if let ActiveOrderEvidence::Request(request) = ¤t_evidence {
- let order_id =
- self.upsert_order_request(record, &request.payload, request.event_id.as_str())?;
- self.attach_validation_receipts_for_request(
- request.event_id.as_str(),
- request.payload.order_id.as_str(),
- order_id,
- )?;
- }
- let mut evidence = self.load_active_order_evidence(current_evidence.root_event_id())?;
- evidence.push(current_evidence);
- dedupe_active_order_evidence(&mut evidence);
- let Some((raw_order_id, buyer_pubkey, root_event_id)) = evidence
- .first()
- .map(ActiveOrderEvidence::order_projection_identity)
- else {
- return Ok(());
- };
- let raw_order_id = raw_order_id.to_owned();
- let buyer_pubkey = buyer_pubkey.to_owned();
- let root_event_id = root_event_id.to_owned();
- let order_id = projected_order_id(
- raw_order_id.as_str(),
- buyer_pubkey.as_str(),
- root_event_id.as_str(),
- );
- let buckets = ActiveOrderEvidenceBuckets::from_evidence(evidence);
- let requests = buckets.requests.clone();
- let reducer_order_id =
- raw_order_id
- .parse::<RadrootsOrderId>()
- .map_err(|_| AppSqliteError::DecodeId {
- field: "order_id",
- value: raw_order_id.clone(),
- })?;
- let projection = reduce_order_events(
- &reducer_order_id,
- RadrootsOrderReductionInputs {
- requests: buckets.requests,
- decisions: buckets.decisions,
- cancellations: buckets.cancellations,
- },
- );
- let request_payload = projection.request_event_id.as_deref().and_then(|event_id| {
- requests
- .iter()
- .find(|request| request.event_id == event_id)
- .map(|request| &request.payload)
- });
- let revision = active_order_change_status(&projection);
- let agreement_source =
- request_payload.map(|request| active_order_agreement_source(request, &projection));
- self.apply_active_order_projection(
- order_id,
- &projection,
- revision,
- agreement_source.as_ref(),
- )
- }
-
- fn upsert_order_request(
- &self,
- record: &RuntimeStoreRecord,
- payload: &RadrootsOrderRequest,
- root_event_id: &str,
- ) -> Result<OrderId, AppSqliteError> {
- let existing_listing =
- self.existing_listing_projection(Some(payload.listing_addr.as_str()))?;
- let farm_id = if let Some(existing_listing) = existing_listing.as_ref() {
- existing_listing.farm_id
- } else {
- deterministic_farm_id(
- Some(payload.seller_pubkey.as_str()),
- payload.listing_addr.as_str(),
- )
- };
- self.ensure_farm_exists(farm_id)?;
- let order_id = projected_order_id(
- payload.order_id.as_str(),
- payload.buyer_pubkey.as_str(),
- root_event_id,
- );
- let order_number = existing_order_number(self.connection, order_id)?
- .unwrap_or_else(|| deterministic_order_number(payload.order_id.as_str()));
- self.connection
- .execute(
- "INSERT INTO orders (
- id,
- farm_id,
- fulfillment_window_id,
- order_number,
- customer_display_name,
- status,
- updated_at,
- buyer_context_key,
- buyer_email,
- buyer_phone,
- buyer_order_note
- ) VALUES (?1, ?2, null, ?3, ?4, 'needs_action', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), ?5, '', '', '')
- ON CONFLICT(id) DO UPDATE SET
- farm_id = excluded.farm_id,
- order_number = excluded.order_number,
- customer_display_name = excluded.customer_display_name,
- status = excluded.status,
- buyer_context_key = coalesce(orders.buyer_context_key, excluded.buyer_context_key),
- updated_at = excluded.updated_at",
- crate::app_sqlite_params![
- order_id.to_string(),
- farm_id.to_string(),
- order_number.as_str(),
- order_customer_display_name(payload.buyer_pubkey.as_str()),
- order_buyer_context_key(record, payload.buyer_pubkey.as_str()),
- ],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "upsert local interop order request",
- source,
- })?;
- self.replace_order_request_lines(order_id, payload, existing_listing.as_ref(), record)?;
- Ok(order_id)
- }
-
- fn apply_active_order_projection(
- &self,
- order_id: OrderId,
- projection: &RadrootsOrderProjection,
- revision: TradeRevisionStatus,
- agreement_source: Option<&ActiveOrderAgreementSource>,
- ) -> Result<(), AppSqliteError> {
- let workflow = TradeWorkflowProjection::from_active_order_projection(
- order_id,
- projection,
- revision,
- TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::RuntimeStore),
- );
- let Some(status) = order_status_from_active_order_projection(projection) else {
- return Ok(());
- };
- self.connection
- .execute(
- "UPDATE orders
- SET status = ?2,
- workflow_revision = ?3,
- workflow_agreement = ?4,
- workflow_inventory = ?5,
- workflow_provenance_source = ?6,
- workflow_provenance_last_event_id = ?7,
- updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
- WHERE id = ?1",
- crate::app_sqlite_params![
- workflow.order_id.to_string(),
- status.storage_key(),
- workflow.revision.storage_key(),
- workflow.agreement.storage_key(),
- workflow.inventory.storage_key(),
- workflow.provenance.primary_source.storage_key(),
- workflow.provenance.last_event_id.as_deref()
- ],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "apply local interop active order projection",
- source,
- })?;
- if projection.economics.is_some()
- && let Some(agreement_source) = agreement_source
- {
- self.replace_active_order_agreement_lines(workflow.order_id, agreement_source)?;
- }
- self.apply_validation_receipt_workflow_projection(workflow.order_id)?;
- Ok(())
- }
-
fn upsert_validation_receipt_projection(
&self,
event: &RadrootsEventEnvelope,
receipt: &RadrootsTradeValidationReceipt,
tags: &RadrootsValidationReceiptTags,
) -> Result<(), AppSqliteError> {
- let order_id = match self.validation_receipt_order_attachment(tags)? {
- ValidationReceiptOrderAttachment::Pending => None,
- ValidationReceiptOrderAttachment::Attached(order_id) => Some(order_id),
- ValidationReceiptOrderAttachment::Rejected => return Ok(()),
- };
+ let order_id: Option<String> = None;
let result = TradeValidationReceiptResult::from_validation_receipt_result(receipt.result);
let receipt_type =
TradeValidationReceiptType::from_validation_receipt_type(receipt.receipt_type);
@@ -1367,7 +1086,7 @@ impl<'a> AppLocalInteropRepository<'a> {
event_created_at = excluded.event_created_at",
crate::app_sqlite_params![
event.id_str(),
- order_id.map(|order_id| order_id.to_string()),
+ order_id,
tags.order_id.as_str(),
tags.root_event_id.as_str(),
tags.listing_event_id.as_str(),
@@ -1385,459 +1104,73 @@ impl<'a> AppLocalInteropRepository<'a> {
operation: "upsert local interop validation receipt projection",
source,
})?;
- if let Some(order_id) = order_id {
- self.apply_validation_receipt_workflow_projection(order_id)?;
- }
Ok(())
}
- fn validation_receipt_order_attachment(
- &self,
- tags: &RadrootsValidationReceiptTags,
- ) -> Result<ValidationReceiptOrderAttachment, AppSqliteError> {
- let Some(root_event) = self.load_signed_event_by_event_id(tags.root_event_id.as_str())?
- else {
- return Ok(ValidationReceiptOrderAttachment::Pending);
- };
- let Ok(envelope) = order_request_from_event(&root_event) else {
- return Ok(ValidationReceiptOrderAttachment::Rejected);
- };
- if envelope.payload.order_id != tags.order_id {
- return Ok(ValidationReceiptOrderAttachment::Rejected);
- }
-
- Ok(ValidationReceiptOrderAttachment::Attached(
- projected_order_id(
- envelope.payload.order_id.as_str(),
- envelope.payload.buyer_pubkey.as_str(),
- tags.root_event_id.as_str(),
- ),
- ))
- }
-
- fn attach_validation_receipts_for_request(
- &self,
- root_event_id: &str,
- raw_order_id: &str,
- order_id: OrderId,
- ) -> Result<(), AppSqliteError> {
+ fn upsert_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
self.connection
.execute(
- "UPDATE order_validation_receipts
- SET order_id = ?3
- WHERE root_event_id = ?1
- AND raw_order_id = ?2
- AND order_id IS NULL",
- crate::app_sqlite_params![root_event_id, raw_order_id, order_id.to_string()],
+ "INSERT INTO farms (id, display_name, readiness, created_at, updated_at)
+ VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+ ON CONFLICT(id) DO UPDATE SET
+ display_name = excluded.display_name,
+ readiness = excluded.readiness,
+ updated_at = excluded.updated_at",
+ crate::app_sqlite_params![
+ farm.farm_id.to_string(),
+ farm.display_name.as_str(),
+ farm_readiness_storage_key(farm.readiness),
+ ],
)
.map_err(|source| AppSqliteError::Query {
- operation: "attach local interop validation receipts to request",
+ operation: "upsert local interop farm summary",
source,
})?;
- self.apply_validation_receipt_workflow_projection(order_id)?;
Ok(())
}
- fn apply_validation_receipt_workflow_projection(
- &self,
- order_id: OrderId,
- ) -> Result<(), AppSqliteError> {
- let current: Option<(String, String, Option<String>)> = self
- .connection
- .query_row(
- "SELECT status, workflow_agreement, workflow_provenance_last_event_id
- FROM orders
- WHERE id = ?1",
- crate::app_sqlite_params![order_id.to_string()],
- |row| Ok((row.try_get(0)?, row.try_get(1)?, row.try_get(2)?)),
- )
- .optional()
- .map_err(|source| AppSqliteError::Query {
- operation: "load order workflow before validation receipt projection",
- source,
- })?;
- let Some((status, agreement, Some(target_event_id))) = current else {
- return Ok(());
- };
- if !matches!(
- agreement.as_str(),
- value if value == TradeAgreementStatus::AgreedPendingValidation.storage_key()
- || value == TradeAgreementStatus::Committed.storage_key()
- || value == TradeAgreementStatus::Invalid.storage_key()
- ) {
- return Ok(());
- }
- if !matches!(
- status.as_str(),
- value if value == OrderStatus::NeedsAction.storage_key()
- || value == OrderStatus::Scheduled.storage_key()
- || value == OrderStatus::NeedsReview.storage_key()
- ) {
- return Ok(());
- }
- let latest_receipt: Option<String> = self
- .connection
- .query_row(
- "SELECT result
- FROM order_validation_receipts
- WHERE order_id = ?1
- AND receipt_type = 'trade_transition'
- AND target_event_id = ?2
- ORDER BY event_created_at DESC, event_id DESC
- LIMIT 1",
- crate::app_sqlite_params![order_id.to_string(), target_event_id.as_str()],
- |row| row.try_get(0),
- )
- .optional()
- .map_err(|source| AppSqliteError::Query {
- operation: "load latest validation receipt workflow projection",
- source,
- })?;
- let Some(result) = latest_receipt else {
- return Ok(());
- };
- let (status, agreement, inventory) = match result.as_str() {
- "valid" => (
- OrderStatus::NeedsAction.storage_key(),
- TradeAgreementStatus::AgreedPendingValidation.storage_key(),
- TradeInventoryStatus::Reserved.storage_key(),
- ),
- "needs_review" => (
- OrderStatus::NeedsReview.storage_key(),
- TradeAgreementStatus::Invalid.storage_key(),
- TradeInventoryStatus::NeedsReview.storage_key(),
- ),
- _ => {
- return Err(AppSqliteError::DecodeEnum {
- field: "order_validation_receipts.result",
- value: result,
- });
- }
- };
+ fn upsert_local_work_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
self.connection
.execute(
- "UPDATE orders
- SET status = ?2,
- workflow_agreement = ?3,
- workflow_inventory = ?4,
- workflow_provenance_source = ?5,
- workflow_provenance_last_event_id = ?6,
- updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
- WHERE id = ?1",
+ "INSERT INTO farms (id, display_name, readiness, created_at, updated_at)
+ VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+ ON CONFLICT(id) DO UPDATE SET
+ display_name = excluded.display_name,
+ readiness = CASE
+ WHEN farms.readiness = 'ready' AND excluded.readiness = 'incomplete'
+ THEN farms.readiness
+ ELSE excluded.readiness
+ END,
+ updated_at = excluded.updated_at",
crate::app_sqlite_params![
- order_id.to_string(),
- status,
- agreement,
- inventory,
- TradeWorkflowSource::RuntimeStore.storage_key(),
- target_event_id,
+ farm.farm_id.to_string(),
+ farm.display_name.as_str(),
+ farm_readiness_storage_key(farm.readiness),
],
)
.map_err(|source| AppSqliteError::Query {
- operation: "apply validation receipt workflow projection",
+ operation: "upsert local interop local work farm summary",
source,
})?;
Ok(())
}
- fn load_active_order_evidence(
- &self,
- root_event_id: &str,
- ) -> Result<Vec<ActiveOrderEvidence>, AppSqliteError> {
- let mut evidence = Vec::new();
- for kind in ACTIVE_ORDER_EVENT_KINDS {
- for event in self.load_signed_events_by_kind(kind)? {
- let Some(record) = active_order_evidence_from_event(&event) else {
- continue;
- };
- if record.root_event_id() == root_event_id {
- evidence.push(record);
- }
- }
- }
- Ok(evidence)
- }
-
- fn replace_order_request_lines(
+ fn mark_farm_buyer_visible(
&self,
- order_id: OrderId,
- payload: &RadrootsOrderRequest,
- existing_listing: Option<&ExistingListingProjection>,
+ farm_id: FarmId,
record: &RuntimeStoreRecord,
+ method: FarmOrderMethod,
) -> Result<(), AppSqliteError> {
self.connection
.execute(
- "DELETE FROM order_lines WHERE order_id = ?1",
- crate::app_sqlite_params![order_id.to_string()],
+ "UPDATE farms
+ SET readiness = 'ready',
+ updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
+ WHERE id = ?1",
+ [farm_id.to_string()],
)
.map_err(|source| AppSqliteError::Query {
- operation: "replace local interop order lines",
- source,
- })?;
- for (index, item) in payload.items.iter().enumerate() {
- let economics_item = payload
- .economics
- .items
- .iter()
- .find(|candidate| candidate.bin_id == item.bin_id);
- let unit_label = economics_item
- .map(|item| item.quantity_unit.to_string())
- .or_else(|| existing_listing.map(|listing| listing.unit_label.clone()))
- .unwrap_or_else(|| "item".to_owned());
- let unit_price_minor_units = economics_item.and_then(|item| {
- parse_decimal_minor_units(item.unit_price_amount.to_string().as_str())
- });
- let price_currency = economics_item
- .map(|item| item.unit_price_currency.to_string())
- .unwrap_or_else(|| payload.economics.currency.to_string());
- let title = existing_listing
- .map(|listing| listing.title.clone())
- .unwrap_or_else(|| item.bin_id.to_string());
- self.connection
- .execute(
- "INSERT INTO order_lines (
- id,
- order_id,
- title,
- quantity_value,
- quantity_unit_label,
- quantity_display,
- listing_bin_id,
- unit_price_minor_units,
- price_currency,
- farm_key,
- listing_addr,
- listing_event_id,
- listing_relays_json,
- seller_pubkey,
- sort_index
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, null, ?13, ?14)",
- crate::app_sqlite_params![
- format!(
- "{}:{}",
- order_id,
- order_line_product_id(payload, existing_listing, item)
- ),
- order_id.to_string(),
- title.as_str(),
- i64::from(item.bin_count),
- unit_label.as_str(),
- format_quantity_display(item.bin_count, unit_label.as_str()),
- item.bin_id.as_str(),
- unit_price_minor_units,
- price_currency.as_str(),
- existing_listing.and_then(|listing| listing.farm_key.as_deref()),
- payload.listing_addr.as_str(),
- listing_event_id_from_order_record(record).as_deref(),
- payload.seller_pubkey.as_str(),
- index as i64,
- ],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "insert local interop order line",
- source,
- })?;
- }
- Ok(())
- }
-
- fn replace_active_order_agreement_lines(
- &self,
- order_id: OrderId,
- source: &ActiveOrderAgreementSource,
- ) -> Result<(), AppSqliteError> {
- let existing_listing =
- self.existing_listing_projection(Some(source.listing_addr.as_str()))?;
- let metadata = self.existing_order_line_metadata(order_id)?;
- self.connection
- .execute(
- "DELETE FROM order_lines WHERE order_id = ?1",
- crate::app_sqlite_params![order_id.to_string()],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "replace local interop active order agreement lines",
- source,
- })?;
- for (index, item) in source.items.iter().enumerate() {
- let economics_item = source
- .economics
- .items
- .iter()
- .find(|candidate| candidate.bin_id == item.bin_id);
- let unit_label = economics_item
- .map(|item| item.quantity_unit.to_string())
- .or_else(|| {
- existing_listing
- .as_ref()
- .map(|listing| listing.unit_label.clone())
- })
- .unwrap_or_else(|| "item".to_owned());
- let unit_price_minor_units = economics_item.and_then(|item| {
- parse_decimal_minor_units(item.unit_price_amount.to_string().as_str())
- });
- let price_currency = economics_item
- .map(|item| item.unit_price_currency.to_string())
- .unwrap_or_else(|| source.economics.currency.to_string());
- let title = existing_listing
- .as_ref()
- .filter(|listing| {
- listing
- .listing_bin_id
- .as_deref()
- .is_none_or(|listing_bin_id| listing_bin_id == item.bin_id)
- })
- .map(|listing| listing.title.clone())
- .unwrap_or_else(|| item.bin_id.to_string());
- self.connection
- .execute(
- "INSERT INTO order_lines (
- id,
- order_id,
- title,
- quantity_value,
- quantity_unit_label,
- quantity_display,
- listing_bin_id,
- unit_price_minor_units,
- price_currency,
- farm_key,
- listing_addr,
- listing_event_id,
- listing_relays_json,
- seller_pubkey,
- sort_index
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
- crate::app_sqlite_params![
- format!(
- "{}:{}",
- order_id,
- order_agreement_line_product_id(
- source.listing_addr.as_str(),
- source.seller_pubkey.as_str(),
- existing_listing.as_ref(),
- item,
- )
- ),
- order_id.to_string(),
- title.as_str(),
- i64::from(item.bin_count),
- unit_label.as_str(),
- format_quantity_display(item.bin_count, unit_label.as_str()),
- item.bin_id.as_str(),
- unit_price_minor_units,
- price_currency.as_str(),
- existing_listing
- .as_ref()
- .and_then(|listing| listing.farm_key.as_deref()),
- source.listing_addr.as_str(),
- metadata
- .as_ref()
- .and_then(|metadata| metadata.listing_event_id.as_deref()),
- metadata
- .as_ref()
- .and_then(|metadata| metadata.listing_relays_json.as_deref()),
- source.seller_pubkey.as_str(),
- index as i64,
- ],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "insert local interop active order agreement line",
- source,
- })?;
- }
- Ok(())
- }
-
- fn existing_order_line_metadata(
- &self,
- order_id: OrderId,
- ) -> Result<Option<ExistingOrderLineMetadata>, AppSqliteError> {
- self.connection
- .query_row(
- "SELECT listing_event_id, listing_relays_json
- FROM order_lines
- WHERE order_id = ?1
- ORDER BY sort_index ASC, id ASC
- LIMIT 1",
- crate::app_sqlite_params![order_id.to_string()],
- |row| {
- Ok(ExistingOrderLineMetadata {
- listing_event_id: row.try_get::<Option<String>, _>(0)?,
- listing_relays_json: row.try_get::<Option<String>, _>(1)?,
- })
- },
- )
- .optional()
- .map_err(|source| AppSqliteError::Query {
- operation: "load existing local interop order line metadata",
- source,
- })
- }
-
- fn upsert_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
- self.connection
- .execute(
- "INSERT INTO farms (id, display_name, readiness, created_at, updated_at)
- VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
- ON CONFLICT(id) DO UPDATE SET
- display_name = excluded.display_name,
- readiness = excluded.readiness,
- updated_at = excluded.updated_at",
- crate::app_sqlite_params![
- farm.farm_id.to_string(),
- farm.display_name.as_str(),
- farm_readiness_storage_key(farm.readiness),
- ],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "upsert local interop farm summary",
- source,
- })?;
- Ok(())
- }
-
- fn upsert_local_work_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
- self.connection
- .execute(
- "INSERT INTO farms (id, display_name, readiness, created_at, updated_at)
- VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
- ON CONFLICT(id) DO UPDATE SET
- display_name = excluded.display_name,
- readiness = CASE
- WHEN farms.readiness = 'ready' AND excluded.readiness = 'incomplete'
- THEN farms.readiness
- ELSE excluded.readiness
- END,
- updated_at = excluded.updated_at",
- crate::app_sqlite_params![
- farm.farm_id.to_string(),
- farm.display_name.as_str(),
- farm_readiness_storage_key(farm.readiness),
- ],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "upsert local interop local work farm summary",
- source,
- })?;
- Ok(())
- }
-
- fn mark_farm_buyer_visible(
- &self,
- farm_id: FarmId,
- record: &RuntimeStoreRecord,
- method: FarmOrderMethod,
- ) -> Result<(), AppSqliteError> {
- self.connection
- .execute(
- "UPDATE farms
- SET readiness = 'ready',
- updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
- WHERE id = ?1",
- [farm_id.to_string()],
- )
- .map_err(|source| AppSqliteError::Query {
- operation: "mark local interop farm buyer visible",
+ operation: "mark local interop farm buyer visible",
source,
})?;
let Some(account_id) = record
@@ -2609,13 +1942,6 @@ enum DuplicateSignedEventAction {
Skip,
}
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum ValidationReceiptOrderAttachment {
- Pending,
- Attached(OrderId),
- Rejected,
-}
-
#[derive(Clone, Debug, Eq, PartialEq)]
struct ProjectionRecord {
kind: &'static str,
@@ -2767,86 +2093,6 @@ struct ExistingListingProjection {
farm_key: Option<String>,
}
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct ActiveOrderAgreementSource {
- listing_addr: String,
- seller_pubkey: String,
- items: Vec<RadrootsOrderItem>,
- economics: RadrootsOrderEconomics,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct ExistingOrderLineMetadata {
- listing_event_id: Option<String>,
- listing_relays_json: Option<String>,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-enum ActiveOrderEvidence {
- Request(RadrootsOrderRequestRecord),
- Decision(RadrootsOrderDecisionRecord),
- Cancellation(RadrootsOrderCancellationRecord),
-}
-
-impl ActiveOrderEvidence {
- fn event_id(&self) -> &str {
- match self {
- Self::Request(record) => record.event_id.as_str(),
- Self::Decision(record) => record.event_id.as_str(),
- Self::Cancellation(record) => record.event_id.as_str(),
- }
- }
-
- fn root_event_id(&self) -> &str {
- match self {
- Self::Request(record) => record.event_id.as_str(),
- Self::Decision(record) => record.root_event_id.as_str(),
- Self::Cancellation(record) => record.root_event_id.as_str(),
- }
- }
-
- fn order_projection_identity(&self) -> (&str, &str, &str) {
- match self {
- Self::Request(record) => (
- record.payload.order_id.as_str(),
- record.payload.buyer_pubkey.as_str(),
- record.event_id.as_str(),
- ),
- Self::Decision(record) => (
- record.payload.order_id.as_str(),
- record.payload.buyer_pubkey.as_str(),
- record.root_event_id.as_str(),
- ),
- Self::Cancellation(record) => (
- record.payload.order_id.as_str(),
- record.payload.buyer_pubkey.as_str(),
- record.root_event_id.as_str(),
- ),
- }
- }
-}
-
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-struct ActiveOrderEvidenceBuckets {
- requests: Vec<RadrootsOrderRequestRecord>,
- decisions: Vec<RadrootsOrderDecisionRecord>,
- cancellations: Vec<RadrootsOrderCancellationRecord>,
-}
-
-impl ActiveOrderEvidenceBuckets {
- fn from_evidence(evidence: Vec<ActiveOrderEvidence>) -> Self {
- let mut buckets = Self::default();
- for record in evidence {
- match record {
- ActiveOrderEvidence::Request(record) => buckets.requests.push(record),
- ActiveOrderEvidence::Decision(record) => buckets.decisions.push(record),
- ActiveOrderEvidence::Cancellation(record) => buckets.cancellations.push(record),
- }
- }
- buckets
- }
-}
-
fn listing_currentness_row(
row: &sqlx::sqlite::SqliteRow,
) -> Result<StoredListingCurrentnessEvidence, sqlx::Error> {
@@ -2991,65 +2237,6 @@ fn parse_app_d_tag_uuid(value: &str) -> Option<Uuid> {
}
}
-fn active_order_event_kind(kind: i64) -> bool {
- ACTIVE_ORDER_EVENT_KINDS.contains(&kind)
-}
-
-fn active_event_id(event: &RadrootsEventEnvelope) -> Option<RadrootsEventId> {
- Some(event.id().clone())
-}
-
-fn active_author_pubkey(event: &RadrootsEventEnvelope) -> Option<RadrootsPublicKey> {
- Some(event.author().clone())
-}
-
-fn active_order_evidence_from_event(event: &RadrootsEventEnvelope) -> Option<ActiveOrderEvidence> {
- match i64::from(event.kind_u32()) {
- KIND_ORDER_REQUEST => {
- let envelope = order_request_from_event(event).ok()?;
- Some(ActiveOrderEvidence::Request(RadrootsOrderRequestRecord {
- event_id: active_event_id(event)?,
- author_pubkey: active_author_pubkey(event)?,
- payload: envelope.payload,
- }))
- }
- KIND_ORDER_DECISION => {
- let envelope = order_decision_from_event(event).ok()?;
- let tags = event.tags_as_vec();
- let context = order_event_context_from_tags(envelope.message_type, &tags).ok()?;
- Some(ActiveOrderEvidence::Decision(RadrootsOrderDecisionRecord {
- event_id: active_event_id(event)?,
- author_pubkey: active_author_pubkey(event)?,
- counterparty_pubkey: context.counterparty_pubkey,
- root_event_id: context.root_event_id?,
- prev_event_id: context.prev_event_id?,
- payload: envelope.payload,
- }))
- }
- KIND_ORDER_CANCEL => {
- let envelope = order_cancellation_from_event(event).ok()?;
- let tags = event.tags_as_vec();
- let context = order_event_context_from_tags(envelope.message_type, &tags).ok()?;
- Some(ActiveOrderEvidence::Cancellation(
- RadrootsOrderCancellationRecord {
- event_id: active_event_id(event)?,
- author_pubkey: active_author_pubkey(event)?,
- counterparty_pubkey: context.counterparty_pubkey,
- root_event_id: context.root_event_id?,
- prev_event_id: context.prev_event_id?,
- payload: envelope.payload,
- },
- ))
- }
- _ => None,
- }
-}
-
-fn dedupe_active_order_evidence(evidence: &mut Vec<ActiveOrderEvidence>) {
- evidence.sort_by(|left, right| left.event_id().cmp(right.event_id()));
- evidence.dedup_by(|left, right| left.event_id() == right.event_id());
-}
-
fn signed_event_projection(record: &RuntimeStoreRecord) -> ProjectionRecord {
ProjectionRecord {
kind: "signed_event",
@@ -3227,143 +2414,6 @@ fn tags_from_json(value: &Value) -> Option<Vec<Vec<String>>> {
})
}
-pub fn projected_order_id_from_trade_request(
- order_id: &str,
- buyer_pubkey: &str,
- root_event_id: &str,
-) -> OrderId {
- OrderId::from(deterministic_uuid(
- "radroots-cli-order-root",
- Some(buyer_pubkey),
- format!("{}:{}", root_event_id.trim(), order_id.trim()).as_str(),
- ))
-}
-
-fn projected_order_id(order_id: &str, buyer_pubkey: &str, root_event_id: &str) -> OrderId {
- projected_order_id_from_trade_request(order_id, buyer_pubkey, root_event_id)
-}
-
-fn active_order_change_status(_projection: &RadrootsOrderProjection) -> TradeRevisionStatus {
- TradeRevisionStatus::None
-}
-
-fn active_order_agreement_source(
- request: &RadrootsOrderRequest,
- _projection: &RadrootsOrderProjection,
-) -> ActiveOrderAgreementSource {
- ActiveOrderAgreementSource {
- listing_addr: request.listing_addr.to_string(),
- seller_pubkey: request.seller_pubkey.to_string(),
- items: request.items.clone(),
- economics: request.economics.clone(),
- }
-}
-
-fn order_line_product_id(
- payload: &RadrootsOrderRequest,
- existing_listing: Option<&ExistingListingProjection>,
- item: &radroots_event::order::RadrootsOrderItem,
-) -> ProductId {
- order_agreement_line_product_id(
- payload.listing_addr.as_str(),
- payload.seller_pubkey.as_str(),
- existing_listing,
- item,
- )
-}
-
-fn order_agreement_line_product_id(
- listing_addr: &str,
- seller_pubkey: &str,
- existing_listing: Option<&ExistingListingProjection>,
- item: &RadrootsOrderItem,
-) -> ProductId {
- if let Some(existing_listing) = existing_listing
- && existing_listing
- .listing_bin_id
- .as_deref()
- .is_none_or(|listing_bin_id| listing_bin_id == item.bin_id)
- {
- return existing_listing.product_id;
- }
- let product_key = format!("{listing_addr}:{}", item.bin_id);
- deterministic_product_id(Some(seller_pubkey), product_key.as_str())
-}
-
-fn deterministic_order_number(order_id: &str) -> String {
- let trimmed = order_id.trim();
- let suffix = trimmed
- .chars()
- .filter(|ch| ch.is_ascii_alphanumeric())
- .take(8)
- .collect::<String>();
- if suffix.is_empty() {
- "R-RELAY".to_owned()
- } else {
- format!("R-{suffix}")
- }
-}
-
-fn existing_order_number(
- connection: &AppSqliteDatabase,
- order_id: OrderId,
-) -> Result<Option<String>, AppSqliteError> {
- connection
- .query_row(
- "SELECT order_number FROM orders WHERE id = ?1 LIMIT 1",
- crate::app_sqlite_params![order_id.to_string()],
- |row| row.try_get::<String, _>(0),
- )
- .optional()
- .map_err(|source| AppSqliteError::Query {
- operation: "load existing local interop order number",
- source,
- })
-}
-
-fn order_customer_display_name(buyer_pubkey: &str) -> String {
- let prefix = buyer_pubkey.trim().chars().take(12).collect::<String>();
- if prefix.is_empty() {
- "Relay buyer".to_owned()
- } else {
- format!("Relay buyer {prefix}")
- }
-}
-
-fn order_buyer_context_key(record: &RuntimeStoreRecord, buyer_pubkey: &str) -> String {
- if record.source_runtime == SourceRuntime::App
- && record
- .event_pubkey
- .as_deref()
- .map(str::trim)
- .is_some_and(|event_pubkey| event_pubkey == buyer_pubkey.trim())
- && let Some(owner_account_id) = record
- .owner_account_id
- .as_deref()
- .map(str::trim)
- .filter(|owner_account_id| !owner_account_id.is_empty())
- {
- return format!("account:{owner_account_id}");
- }
- format!("nostr:{}", buyer_pubkey.trim())
-}
-
-fn format_quantity_display(quantity: u32, unit_label: &str) -> String {
- let unit_label = unit_label.trim();
- if unit_label.is_empty() {
- quantity.to_string()
- } else {
- format!("{quantity} {unit_label}")
- }
-}
-
-fn listing_event_id_from_order_record(record: &RuntimeStoreRecord) -> Option<String> {
- record
- .event_tags_json
- .as_ref()
- .and_then(|tags| tag_index_value(Some(tags), "listing_event", 1))
-}
-
fn base64_url_digit(byte: u8) -> Option<u8> {
match byte {
b'A'..=b'Z' => Some(byte - b'A'),
@@ -3733,36 +2783,18 @@ fn farm_readiness_from_storage_key(readiness: &str) -> Result<FarmReadiness, App
mod tests {
use std::collections::BTreeSet;
- use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
- };
use radroots_event::{
- RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr,
- ids::{
- RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
- RadrootsOrderQuoteId, RadrootsPublicKey,
- },
- order::{
- RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
- RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics,
- RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis,
- RadrootsOrderRequest,
- },
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts, ids::RadrootsEventId,
wire::RadrootsNip01EventWireParts as WireEventParts,
};
- use radroots_event_codec::order::{
- order_cancellation_event_build, order_decision_event_build, order_request_event_build,
- };
use radroots_runtime_store::{
PublishOutboxStatus, RelayDeliveryEvidence, RuntimeStore, RuntimeStoreRecordFamily,
RuntimeStoreRecordInput, RuntimeStoreRecordStatus, RuntimeStoreRecordUpdate, SourceRuntime,
};
use radroots_sql_core::SqlxSqliteExecutor;
use radroots_studio_app_view::{
- BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderId, OrderStatus,
- OrdersFilter, OrdersScreenQueryState, ProductAvailabilityState, ProductId,
- TradeAgreementStatus, TradeInventoryStatus, TradeValidationReceiptProofSystem,
- TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowSource,
+ BuyerContext, FarmId, FarmOrderMethod, ProductAvailabilityState, ProductId,
+ TradeValidationReceiptResult,
};
use radroots_trade::validation_receipt::{
RadrootsTradeValidationReceipt, RadrootsValidationReceiptProof,
@@ -3775,9 +2807,8 @@ mod tests {
use uuid::Uuid;
use super::{
- KIND_FARM, KIND_LISTING, KIND_ORDER_REQUEST, KIND_VALIDATION_RECEIPT,
- deterministic_farm_id, deterministic_product_id, projected_farm_id, projected_order_id,
- projected_product_id,
+ KIND_FARM, KIND_LISTING, KIND_VALIDATION_RECEIPT, deterministic_farm_id,
+ deterministic_product_id, projected_farm_id, projected_product_id,
};
use crate::{AppSqliteStore, BuyerRepeatDemandApplyOutcome, DatabaseTarget};
@@ -4231,14 +3262,6 @@ mod tests {
.expect("seed origin product");
}
- fn decimal(raw: &str) -> RadrootsCoreDecimal {
- raw.parse().expect("valid decimal")
- }
-
- fn usd(raw: &str) -> RadrootsCoreMoney {
- RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD)
- }
-
fn test_pubkey(seed: &str) -> String {
let left = Uuid::new_v5(&Uuid::NAMESPACE_URL, seed.as_bytes());
let right_seed = format!("{seed}:right");
@@ -4250,332 +3273,23 @@ mod tests {
test_pubkey(seed)
}
- fn typed_order_id(raw: &str) -> RadrootsOrderId {
- raw.parse().expect("valid order id")
- }
-
- fn typed_quote_id(raw: &str) -> RadrootsOrderQuoteId {
- raw.parse().expect("valid quote id")
- }
-
- fn typed_bin_id(raw: &str) -> RadrootsInventoryBinId {
- raw.parse().expect("valid bin id")
- }
-
- fn typed_event_id(raw: &str) -> RadrootsEventId {
- raw.parse().expect("valid event id")
- }
-
- fn typed_pubkey(raw: &str) -> RadrootsPublicKey {
- raw.parse().expect("valid pubkey")
- }
-
- fn typed_listing_addr(raw: &str) -> RadrootsListingAddress {
- raw.parse().expect("valid listing address")
- }
-
- fn listing_event_ptr(event_id: &str) -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: test_event_id_seed(event_id),
- relays: Some("ws://127.0.0.1:1234/".to_owned()),
- }
+ fn hex_event_id(seed: u8) -> String {
+ format!("{seed:064x}")
}
- fn order_request_payload(
- order_id: &str,
- listing_addr: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
- ) -> RadrootsOrderRequest {
- RadrootsOrderRequest {
- order_id: typed_order_id(order_id),
- listing_addr: typed_listing_addr(listing_addr),
- buyer_pubkey: typed_pubkey(buyer_pubkey),
- seller_pubkey: typed_pubkey(seller_pubkey),
- items: vec![RadrootsOrderItem {
- bin_id: typed_bin_id("bin-1"),
- bin_count: 2,
- }],
- economics: RadrootsOrderEconomics {
- quote_id: typed_quote_id(format!("quote-{order_id}").as_str()),
- quote_version: 1,
- pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
- currency: RadrootsCoreCurrency::USD,
- items: vec![RadrootsOrderEconomicItem {
- bin_id: typed_bin_id("bin-1"),
- bin_count: 2,
- quantity_amount: decimal("1"),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: decimal("8"),
- unit_price_currency: RadrootsCoreCurrency::USD,
- line_subtotal: usd("16"),
- }],
- discounts: Vec::<RadrootsOrderEconomicLine>::new(),
- adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
- subtotal: usd("16"),
- discount_total: usd("0"),
- adjustment_total: usd("0"),
- total: usd("16"),
- },
- }
+ fn test_event_signature_seed(seed: &str) -> String {
+ let base = test_event_id_seed(seed);
+ format!("{base}{base}")
}
- fn accepted_order_decision_payload(
- order_id: &str,
- listing_addr: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
- ) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: typed_order_id(order_id),
- listing_addr: typed_listing_addr(listing_addr),
- buyer_pubkey: typed_pubkey(buyer_pubkey),
- seller_pubkey: typed_pubkey(seller_pubkey),
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: typed_bin_id("bin-1"),
- bin_count: 2,
- }],
- },
- }
+ fn hash32(seed: u8) -> String {
+ format!("0x{seed:064x}")
}
- fn declined_order_decision_payload(
- order_id: &str,
- listing_addr: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
- ) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: typed_order_id(order_id),
- listing_addr: typed_listing_addr(listing_addr),
- buyer_pubkey: typed_pubkey(buyer_pubkey),
- seller_pubkey: typed_pubkey(seller_pubkey),
- decision: RadrootsOrderDecisionOutcome::Declined {
- reason: "not available for this pickup".to_owned(),
- },
- }
- }
-
- fn order_cancel_payload(
- order_id: &str,
- listing_addr: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
- ) -> RadrootsOrderCancellation {
- RadrootsOrderCancellation {
- order_id: typed_order_id(order_id),
- listing_addr: typed_listing_addr(listing_addr),
- buyer_pubkey: typed_pubkey(buyer_pubkey),
- seller_pubkey: typed_pubkey(seller_pubkey),
- reason: "buyer changed pickup plan".to_owned(),
- }
- }
-
- struct ValidationReceiptOrderFixture {
- app_store: AppSqliteStore,
- events: RuntimeStore<SqlxSqliteExecutor>,
- buyer_context: BuyerContext,
- seller_farm_id: FarmId,
- order_id: OrderId,
- order_id_raw: String,
- listing_addr: String,
- buyer_pubkey: String,
- seller_pubkey: String,
- listing_event_id: String,
- request_event_id: String,
- decision_event_id: String,
- }
-
- fn validation_receipt_order_fixture(label: &str) -> ValidationReceiptOrderFixture {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "DDDDDDDDDDDDDDDDDDDDDD";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAw";
- let seller_pubkey = test_pubkey(format!("{label}-seller").as_str());
- let buyer_pubkey = test_pubkey(format!("{label}-buyer").as_str());
- let order_id_raw = format!("{label}-order");
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- let listing_event_id = hex_event_id(21);
- let request_event_id = hex_event_id(22);
- let decision_event_id = hex_event_id(23);
- events
- .append_record(&signed_market_listing_record(
- format!("{label}-listing-record").as_str(),
- seller_pubkey.as_str(),
- farm_key,
- listing_key,
- "Validation Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append signed listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import signed listing");
-
- let request_payload = order_request_payload(
- order_id_raw.as_str(),
- listing_addr.as_str(),
- buyer_pubkey.as_str(),
- seller_pubkey.as_str(),
- );
- let request_parts =
- order_request_event_build(&listing_event_ptr(&listing_event_id), &request_payload)
- .expect("build validation order request");
- let request_event = event_from_parts_at(
- request_event_id.as_str(),
- buyer_pubkey.as_str(),
- request_parts,
- 1_777_665_601,
- );
- events
- .append_record(&signed_order_event_record(
- format!("app:signed_event:{label}:request").as_str(),
- &request_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_validation"),
- ))
- .expect("append validation order request");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import validation order request");
-
- let decision_payload = accepted_order_decision_payload(
- order_id_raw.as_str(),
- listing_addr.as_str(),
- buyer_pubkey.as_str(),
- seller_pubkey.as_str(),
- );
- let decision_parts = order_decision_event_build(
- &typed_event_id(request_event_id.as_str()),
- &typed_event_id(request_event_id.as_str()),
- &decision_payload,
- )
- .expect("build validation order decision");
- let decision_event = event_from_parts_at(
- decision_event_id.as_str(),
- seller_pubkey.as_str(),
- decision_parts,
- 1_777_665_602,
- );
- events
- .append_record(&signed_order_event_record(
- format!("cli:signed_event:{label}:decision").as_str(),
- &decision_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append validation order decision");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import validation order decision");
-
- ValidationReceiptOrderFixture {
- app_store,
- events,
- buyer_context: BuyerContext::account("acct_validation"),
- seller_farm_id: deterministic_farm_id(Some(seller_pubkey.as_str()), farm_key),
- order_id: projected_order_id(
- order_id_raw.as_str(),
- buyer_pubkey.as_str(),
- request_event_id.as_str(),
- ),
- order_id_raw,
- listing_addr,
- buyer_pubkey,
- seller_pubkey,
- listing_event_id,
- request_event_id,
- decision_event_id,
- }
- }
-
- fn event_from_parts(
- event_id: &str,
- author: &str,
- parts: WireEventParts,
- ) -> RadrootsEventEnvelope {
- event_from_parts_at(event_id, author, parts, 1_777_665_600)
- }
-
- fn event_from_parts_at(
- event_id: &str,
- author: &str,
- parts: WireEventParts,
- created_at: u32,
- ) -> RadrootsEventEnvelope {
- let event_id = event_id
- .parse::<RadrootsEventId>()
- .map(|event_id| event_id.to_string())
- .unwrap_or_else(|_| test_event_id_seed(event_id));
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- sig: test_event_signature_seed(event_id.as_str()),
- id: event_id,
- author: author.to_owned(),
- created_at: u64::from(created_at),
- kind: parts.kind,
- tags: parts.tags,
- content: parts.content,
- })
- .expect("test event envelope")
- }
-
- fn event_with_tags(
- event: &RadrootsEventEnvelope,
- tags: Vec<Vec<String>>,
- ) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- sig: event.sig_str().to_owned(),
- id: event.id_str().to_owned(),
- author: event.author_str().to_owned(),
- created_at: event.created_at_u64(),
- kind: event.kind_u32(),
- tags,
- content: event.content().to_owned(),
- })
- .expect("test event envelope with tags")
- }
-
- fn event_with_kind(event: &RadrootsEventEnvelope, kind: u32) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- sig: event.sig_str().to_owned(),
- id: event.id_str().to_owned(),
- author: event.author_str().to_owned(),
- created_at: event.created_at_u64(),
- kind,
- tags: event.tags_as_vec(),
- content: event.content().to_owned(),
- })
- .expect("test event envelope with kind")
- }
-
- fn hex_event_id(seed: u8) -> String {
- format!("{seed:064x}")
- }
-
- fn test_event_signature_seed(seed: &str) -> String {
- let base = test_event_id_seed(seed);
- format!("{base}{base}")
- }
-
- fn hash32(seed: u8) -> String {
- format!("0x{seed:064x}")
- }
-
- fn validation_error_bitmap(result: RadrootsValidationReceiptResult) -> String {
- match result {
- RadrootsValidationReceiptResult::Valid => format!("0x{:032x}", 0),
- RadrootsValidationReceiptResult::Invalid => format!("0x{:032x}", 1),
+ fn validation_error_bitmap(result: RadrootsValidationReceiptResult) -> String {
+ match result {
+ RadrootsValidationReceiptResult::Valid => format!("0x{:032x}", 0),
+ RadrootsValidationReceiptResult::Invalid => format!("0x{:032x}", 1),
}
}
@@ -4620,6 +3334,28 @@ mod tests {
}
}
+ fn event_from_parts_at(
+ event_id: &str,
+ author: &str,
+ parts: WireEventParts,
+ created_at: u32,
+ ) -> RadrootsEventEnvelope {
+ let event_id = event_id
+ .parse::<RadrootsEventId>()
+ .map(|event_id| event_id.to_string())
+ .unwrap_or_else(|_| test_event_id_seed(event_id));
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
+ sig: test_event_signature_seed(event_id.as_str()),
+ id: event_id,
+ author: author.to_owned(),
+ created_at: u64::from(created_at),
+ kind: parts.kind,
+ tags: parts.tags,
+ content: parts.content,
+ })
+ .expect("test event envelope")
+ }
+
#[expect(clippy::too_many_arguments)]
fn validation_receipt_event(
event_id: &str,
@@ -4638,10 +3374,9 @@ mod tests {
event_from_parts_at(event_id, author, parts, created_at)
}
- fn signed_order_event_record(
+ fn signed_receipt_event_record(
record_id: &str,
event: &RadrootsEventEnvelope,
- listing_addr: &str,
source_runtime: SourceRuntime,
owner_account_id: Option<&str>,
) -> RuntimeStoreRecordInput {
@@ -4666,7 +3401,7 @@ mod tests {
owner_account_id: owner_account_id.map(str::to_owned),
owner_pubkey: Some(event.author_str().to_owned()),
farm_id: None,
- listing_addr: Some(listing_addr.to_owned()),
+ listing_addr: None,
local_work_json: None,
event_id: Some(event.id_str().to_owned()),
event_kind: Some(i64::from(event.kind_u32())),
@@ -4685,1518 +3420,81 @@ mod tests {
"sig": event.sig_str(),
})),
outbox_status: PublishOutboxStatus::Acknowledged,
- relay_set_fingerprint: Some("relay-set".to_owned()),
- relay_delivery_json: Some(relay_delivery_json),
- }
- }
-
- #[test]
- fn imports_signed_order_request_into_seller_order_projection() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "AAAAAAAAAAAAAAAAAAAAAA";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAg";
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = "relay-order-1";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- events
- .append_record(&signed_market_listing_record(
- "order-visible-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Order Visible Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append signed listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import signed listing");
- let payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let parts = order_request_event_build(&listing_event_ptr("listing-event-1"), &payload)
- .expect("build order request event");
- let event = event_from_parts("order-request-event-1", buyer_pubkey, parts);
- events
- .append_record(&signed_order_event_record(
- "cli:signed_event:order-request:1",
- &event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append order request");
-
- let report = app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import signed order request");
- let farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
- let order_id = projected_order_id(order_id_raw, buyer_pubkey, event.id_str());
- let orders = app_store
- .load_orders_list(
- farm_id,
- &OrdersScreenQueryState {
- filter: OrdersFilter::All,
- fulfillment_window_id: None,
- },
- )
- .expect("load seller orders");
- let detail = app_store
- .load_order_detail(farm_id, order_id)
- .expect("load order detail")
- .expect("order detail");
- let imported = app_store
- .load_local_interop_records()
- .expect("load imported records");
- let signed_evidence = app_store
- .load_local_interop_signed_events_by_kind(KIND_ORDER_REQUEST)
- .expect("load signed event evidence");
- let buyer_context_key: String = app_store
- .connection()
- .query_row(
- "SELECT buyer_context_key FROM orders WHERE id = ?1",
- [order_id.to_string()],
- |row| row.try_get(0),
- )
- .expect("load buyer context key");
-
- assert_eq!(report.imported_records, 1);
- assert!(
- imported
- .iter()
- .any(|record| record.projected_kind == "signed_event"
- && record.event_kind == Some(KIND_ORDER_REQUEST)
- && record.event_id.as_deref() == Some(event.id_str()))
- );
- assert_eq!(signed_evidence, vec![event.clone()]);
- assert_eq!(orders.rows.len(), 1);
- assert_eq!(orders.rows[0].order_id, order_id);
- assert_eq!(orders.rows[0].status, OrderStatus::NeedsAction);
- assert_eq!(
- orders.rows[0].customer_display_name,
- format!("Relay buyer {}", &buyer_pubkey[..12])
- );
- assert_eq!(detail.items.len(), 1);
- assert_eq!(detail.items[0].title, "Order Visible Eggs");
- assert_eq!(detail.items[0].quantity_display, "2 each");
- assert_eq!(buyer_context_key, format!("nostr:{buyer_pubkey}"));
- }
-
- #[test]
- fn local_interop_order_request_evidence_requires_usable_delivery_state() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let buyer_pubkey = test_pubkey("buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg");
- let relay_url = "ws://127.0.0.1:1234";
- let build_event = |event_id: &str, order_id_raw: &str| {
- let payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let parts = order_request_event_build(&listing_event_ptr("listing-event-1"), &payload)
- .expect("build order request event");
- event_from_parts(event_id, buyer_pubkey, parts)
- };
- let acknowledged_event = build_event("order-request-evidence-ack", "usable-ack");
- events
- .append_record(&signed_order_event_record(
- "cli:signed_event:order-request:evidence-ack",
- &acknowledged_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append acknowledged order request evidence");
-
- let observed_event = build_event("order-request-evidence-observed", "usable-observed");
- let mut observed_record = signed_order_event_record(
- "cli:signed_event:order-request:evidence-observed",
- &observed_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- observed_record.outbox_status = PublishOutboxStatus::None;
- observed_record.relay_delivery_json = Some(
- RelayDeliveryEvidence::observed([relay_url], [relay_url], [relay_url], Vec::new())
- .expect("observed relay evidence")
- .to_json_value()
- .expect("observed relay evidence json"),
- );
- events
- .append_record(&observed_record)
- .expect("append observed order request evidence");
-
- let pending_event = build_event("order-request-evidence-pending", "pending");
- let mut pending_record = signed_order_event_record(
- "cli:signed_event:order-request:evidence-pending",
- &pending_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- pending_record.status = RuntimeStoreRecordStatus::PendingPublish;
- pending_record.outbox_status = PublishOutboxStatus::Pending;
- pending_record.relay_delivery_json = Some(
- RelayDeliveryEvidence::pending([relay_url])
- .expect("pending relay evidence")
- .to_json_value()
- .expect("pending relay evidence json"),
- );
- events
- .append_record(&pending_record)
- .expect("append pending order request evidence");
-
- let failed_event = build_event("order-request-evidence-failed", "failed");
- let mut failed_record = signed_order_event_record(
- "cli:signed_event:order-request:evidence-failed",
- &failed_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- failed_record.outbox_status = PublishOutboxStatus::Failed;
- failed_record.relay_delivery_json = Some(json!({
- "state": "failed",
- "target_relays": [relay_url],
- "connected_relays": [relay_url],
- "acknowledged_relays": [],
- "failed_relays": [{"relay_url": relay_url, "error": "relay rejected event"}]
- }));
- events
- .append_record(&failed_record)
- .expect("append failed order request evidence");
-
- let local_only_event = build_event("order-request-evidence-local-only", "local-only");
- let mut local_only_record = signed_order_event_record(
- "cli:signed_event:order-request:evidence-local-only",
- &local_only_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- local_only_record.outbox_status = PublishOutboxStatus::None;
- local_only_record.relay_set_fingerprint = None;
- local_only_record.relay_delivery_json = None;
- events
- .append_record(&local_only_record)
- .expect("append local-only order request evidence");
-
- let invalid_delivery_event = build_event(
- "order-request-evidence-invalid-delivery",
- "invalid-delivery",
- );
- let mut invalid_delivery_record = signed_order_event_record(
- "cli:signed_event:order-request:evidence-malformed-delivery",
- &invalid_delivery_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- invalid_delivery_record.relay_delivery_json = Some(json!({
- "state": "acknowledged"
- }));
- let invalid_delivery_error = events
- .append_record(&invalid_delivery_record)
- .expect_err("reject invalid delivery order request evidence");
- assert!(
- invalid_delivery_error
- .to_string()
- .contains("acknowledged relay delivery evidence requires acknowledged_relays")
- );
-
- let malformed_event =
- build_event("order-request-evidence-malformed-event", "malformed-event");
- let mut malformed_record = signed_order_event_record(
- "cli:signed_event:order-request:evidence-malformed-event",
- &malformed_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- malformed_record.event_tags_json = Some(json!({"invalid": "tags"}));
- events
- .append_record(&malformed_record)
- .expect("append malformed order request evidence");
-
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import signed evidence records");
- let signed_evidence = app_store
- .load_local_interop_signed_events_by_kind(KIND_ORDER_REQUEST)
- .expect("load filtered signed event evidence");
-
- assert_eq!(signed_evidence, vec![acknowledged_event, observed_event]);
- }
-
- #[test]
- fn app_origin_signed_order_request_and_decision_project_to_buyer_orders() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "CCCCCCCCCCCCCCCCCCCCCC";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAg";
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("app-buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = "app-relay-order-1";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- events
- .append_record(&signed_market_listing_record(
- "buyer-order-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Buyer Order Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append signed listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import signed listing");
- let request_payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let request_parts = order_request_event_build(
- &listing_event_ptr("buyer-order-listing-event"),
- &request_payload,
- )
- .expect("build order request event");
- let request_event =
- event_from_parts("buyer-order-request-event", buyer_pubkey, request_parts);
- events
- .append_record(&signed_order_event_record(
- "app:signed_event:order-request:buyer",
- &request_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_buyer"),
- ))
- .expect("append app order request");
-
- let request_report = app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import app order request");
- let buyer_context = BuyerContext::account("acct_buyer");
- let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id_str());
- let farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
- let buyer_orders = app_store
- .load_buyer_orders(&buyer_context)
- .expect("load buyer orders after request");
-
- assert_eq!(request_report.imported_records, 1);
- assert_eq!(buyer_orders.rows.len(), 1);
- assert_eq!(buyer_orders.rows[0].order_id, order_id);
- assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Placed);
-
- let decision_payload = accepted_order_decision_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let decision_parts = order_decision_event_build(
- &typed_event_id(request_event.id_str()),
- &typed_event_id(request_event.id_str()),
- &decision_payload,
- )
- .expect("build order decision event");
- let decision_event =
- event_from_parts("buyer-order-decision-event", seller_pubkey, decision_parts);
- events
- .append_record(&signed_order_event_record(
- "cli:signed_event:order-decision:buyer",
- &decision_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append order decision");
-
- let decision_report = app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import order decision");
- let buyer_orders = app_store
- .load_buyer_orders(&buyer_context)
- .expect("load buyer orders after decision");
- let buyer_detail = app_store
- .load_buyer_order_detail(&buyer_context, order_id)
- .expect("load buyer order detail")
- .expect("buyer order detail");
- let seller_orders = app_store
- .load_orders_list(
- farm_id,
- &OrdersScreenQueryState {
- filter: OrdersFilter::All,
- fulfillment_window_id: None,
- },
- )
- .expect("load seller orders after decision");
-
- assert_eq!(decision_report.imported_records, 1);
- assert_eq!(buyer_orders.rows.len(), 1);
- assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Placed);
- assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed);
- assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction);
- assert_eq!(buyer_detail.workflow, buyer_orders.rows[0].workflow);
- assert_eq!(
- seller_orders.rows[0].workflow,
- buyer_orders.rows[0].workflow
- );
- assert_eq!(
- buyer_orders.rows[0].workflow.agreement,
- TradeAgreementStatus::AgreedPendingValidation
- );
- assert_eq!(
- buyer_orders.rows[0].workflow.inventory,
- TradeInventoryStatus::Reserved
- );
- assert_eq!(
- buyer_orders.rows[0].workflow.provenance.primary_source,
- TradeWorkflowSource::RuntimeStore
- );
- assert_eq!(
- buyer_orders.rows[0]
- .workflow
- .provenance
- .last_event_id
- .as_deref(),
- Some(decision_event.id_str())
- );
- assert_eq!(
- buyer_orders.rows[0].workflow.economics.total_minor_units,
- Some(1600)
- );
- }
-
- #[test]
- fn duplicate_app_order_request_roots_project_to_distinct_buyer_orders() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "CCCCCCCCCCCCCCCCCCCCCC";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAy";
- let seller_pubkey = test_pubkey("duplicate-root-seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("duplicate-root-buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = "duplicate-root-order";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- events
- .append_record(&signed_market_listing_record(
- "duplicate-root-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Duplicate Root Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append duplicate-root listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import duplicate-root listing");
-
- let request_payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let first_request_parts = order_request_event_build(
- &listing_event_ptr("duplicate-root-listing-event"),
- &request_payload,
- )
- .expect("build duplicate-root order request");
- let first_request_event = event_from_parts(
- "duplicate-root-request-first",
- buyer_pubkey,
- first_request_parts,
- );
- let second_request_parts = order_request_event_build(
- &listing_event_ptr("duplicate-root-listing-event"),
- &request_payload,
- )
- .expect("build second duplicate-root order request");
- let second_request_event = event_from_parts(
- "duplicate-root-request-second",
- buyer_pubkey,
- second_request_parts,
- );
- for (record_id, event) in [
- (
- "app:signed_event:duplicate-root:first",
- &first_request_event,
- ),
- (
- "app:signed_event:duplicate-root:second",
- &second_request_event,
- ),
- ] {
- events
- .append_record(&signed_order_event_record(
- record_id,
- event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_duplicate_root"),
- ))
- .expect("append duplicate-root request");
- }
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import duplicate-root requests");
-
- let first_order_id =
- projected_order_id(order_id_raw, buyer_pubkey, first_request_event.id_str());
- let second_order_id =
- projected_order_id(order_id_raw, buyer_pubkey, second_request_event.id_str());
- let buyer_context = BuyerContext::account("acct_duplicate_root");
- let buyer_orders = app_store
- .load_buyer_orders(&buyer_context)
- .expect("load duplicate-root buyer orders");
- let projected_ids = buyer_orders
- .rows
- .iter()
- .map(|row| row.order_id)
- .collect::<Vec<_>>();
-
- assert_ne!(first_order_id, second_order_id);
- assert_eq!(buyer_orders.rows.len(), 2);
- assert!(projected_ids.contains(&first_order_id));
- assert!(projected_ids.contains(&second_order_id));
- }
-
- #[test]
- fn app_origin_signed_order_request_and_decline_project_to_buyer_orders() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "CCCCCCCCCCCCCCCCCCCCCC";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAg";
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("app-buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = "app-relay-order-declined-1";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- events
- .append_record(&signed_market_listing_record(
- "buyer-order-decline-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Buyer Order Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append signed listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import signed listing");
- let request_payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let request_parts = order_request_event_build(
- &listing_event_ptr("buyer-order-decline-listing-event"),
- &request_payload,
- )
- .expect("build order request event");
- let request_event = event_from_parts(
- "buyer-order-decline-request-event",
- buyer_pubkey,
- request_parts,
- );
- events
- .append_record(&signed_order_event_record(
- "app:signed_event:order-request:buyer-declined",
- &request_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_buyer"),
- ))
- .expect("append app order request");
-
- let request_report = app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import app order request");
- let buyer_context = BuyerContext::account("acct_buyer");
- let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id_str());
- let buyer_orders = app_store
- .load_buyer_orders(&buyer_context)
- .expect("load buyer orders after request");
-
- assert_eq!(request_report.imported_records, 1);
- assert_eq!(buyer_orders.rows.len(), 1);
- assert_eq!(buyer_orders.rows[0].order_id, order_id);
- assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Placed);
-
- let decision_payload = declined_order_decision_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let decision_parts = order_decision_event_build(
- &typed_event_id(request_event.id_str()),
- &typed_event_id(request_event.id_str()),
- &decision_payload,
- )
- .expect("build declined order decision event");
- let decision_event = event_from_parts(
- "buyer-order-decline-decision-event",
- seller_pubkey,
- decision_parts,
- );
- events
- .append_record(&signed_order_event_record(
- "cli:signed_event:order-decision:buyer-declined",
- &decision_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append declined order decision");
-
- let decision_report = app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import declined order decision");
- let buyer_orders = app_store
- .load_buyer_orders(&buyer_context)
- .expect("load buyer orders after declined decision");
- let buyer_detail = app_store
- .load_buyer_order_detail(&buyer_context, order_id)
- .expect("load buyer order detail")
- .expect("buyer order detail");
- let seller_orders = app_store
- .load_orders_list(
- deterministic_farm_id(Some(seller_pubkey), farm_key),
- &OrdersScreenQueryState {
- filter: OrdersFilter::All,
- fulfillment_window_id: None,
- },
- )
- .expect("load seller orders after declined decision");
-
- assert_eq!(decision_report.imported_records, 1);
- assert_eq!(buyer_orders.rows.len(), 1);
- assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Declined);
- assert_eq!(buyer_detail.status, BuyerOrderStatus::Declined);
- assert_eq!(seller_orders.rows[0].status, OrderStatus::Declined);
- assert_eq!(seller_orders.summary.needs_action_orders, 0);
- assert_eq!(seller_orders.summary.scheduled_orders, 0);
- assert_eq!(seller_orders.summary.packed_orders, 0);
- assert!(seller_orders.rows[0].primary_action.is_none());
- }
-
- #[test]
- fn active_order_decision_projects_agreement_state_through_cli_reducer() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "DDDDDDDDDDDDDDDDDDDDDD";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAw";
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("app-buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = "active-lifecycle-order-1";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- events
- .append_record(&signed_market_listing_record(
- "active-lifecycle-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Lifecycle Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append signed listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import signed listing");
-
- let request_payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let request_parts = order_request_event_build(
- &listing_event_ptr("active-lifecycle-listing-event"),
- &request_payload,
- )
- .expect("build lifecycle order request");
- let request_event = event_from_parts(
- "active-lifecycle-request-event",
- buyer_pubkey,
- request_parts,
- );
- events
- .append_record(&signed_order_event_record(
- "app:signed_event:active-lifecycle:request",
- &request_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_lifecycle"),
- ))
- .expect("append lifecycle order request");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import lifecycle order request");
-
- let seller_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
- let decision_payload = accepted_order_decision_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let decision_parts = order_decision_event_build(
- &typed_event_id(request_event.id_str()),
- &typed_event_id(request_event.id_str()),
- &decision_payload,
- )
- .expect("build lifecycle order decision");
- let decision_event = event_from_parts(
- "active-lifecycle-decision-event",
- seller_pubkey,
- decision_parts,
- );
- events
- .append_record(&signed_order_event_record(
- "cli:signed_event:active-lifecycle:decision",
- &decision_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append lifecycle order decision");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import lifecycle order decision");
- let seller_orders = app_store
- .load_orders_list(
- seller_farm_id,
- &OrdersScreenQueryState {
- filter: OrdersFilter::All,
- fulfillment_window_id: None,
- },
- )
- .expect("load lifecycle seller orders after decision");
- assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction);
- assert_eq!(
- seller_orders.rows[0].workflow.agreement,
- TradeAgreementStatus::AgreedPendingValidation
- );
- assert_eq!(
- seller_orders.rows[0].workflow.inventory,
- TradeInventoryStatus::Reserved
- );
- assert_eq!(seller_orders.rows[0].primary_action, None);
- }
-
- #[test]
- fn valid_validation_receipt_keeps_buyer_and_seller_order_pending_validation() {
- let fixture =
- validation_receipt_order_fixture("validation-receipt-valid-pending-validation");
- let valid_event = validation_receipt_event(
- hex_event_id(29).as_str(),
- fixture.seller_pubkey.as_str(),
- fixture.order_id_raw.as_str(),
- fixture.listing_event_id.as_str(),
- fixture.request_event_id.as_str(),
- fixture.decision_event_id.as_str(),
- RadrootsValidationReceiptResult::Valid,
- 1_777_665_603,
- );
- fixture
- .events
- .append_record(&signed_order_event_record(
- "cli:signed_event:validation-receipt:valid-pending-validation",
- &valid_event,
- fixture.listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append valid validation receipt");
- fixture
- .app_store
- .import_shared_runtime_store_from_store(&fixture.events)
- .expect("import valid validation receipt");
-
- let buyer_detail = fixture
- .app_store
- .load_buyer_order_detail(&fixture.buyer_context, fixture.order_id)
- .expect("load buyer pending-validation validation receipt detail")
- .expect("buyer pending-validation validation receipt detail");
- let seller_detail = fixture
- .app_store
- .load_order_detail(fixture.seller_farm_id, fixture.order_id)
- .expect("load seller pending-validation validation receipt detail")
- .expect("seller pending-validation validation receipt detail");
-
- assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed);
- assert_eq!(seller_detail.status, OrderStatus::NeedsAction);
- assert_eq!(
- buyer_detail.workflow.agreement,
- TradeAgreementStatus::AgreedPendingValidation
- );
- assert_eq!(
- seller_detail.workflow.agreement,
- TradeAgreementStatus::AgreedPendingValidation
- );
- assert_eq!(
- buyer_detail.workflow.inventory,
- TradeInventoryStatus::Reserved
- );
- assert_eq!(
- buyer_detail.workflow.provenance.last_event_id.as_deref(),
- Some(fixture.decision_event_id.as_str())
- );
- assert_eq!(buyer_detail.validation_receipts.len(), 1);
- assert_eq!(
- buyer_detail.validation_receipts[0].result,
- TradeValidationReceiptResult::Valid
- );
- }
-
- #[test]
- fn validation_receipts_drive_buyer_and_seller_order_details() {
- let fixture = validation_receipt_order_fixture("validation-receipt-driven");
- let valid_event = validation_receipt_event(
- hex_event_id(31).as_str(),
- fixture.seller_pubkey.as_str(),
- fixture.order_id_raw.as_str(),
- fixture.listing_event_id.as_str(),
- fixture.request_event_id.as_str(),
- fixture.decision_event_id.as_str(),
- RadrootsValidationReceiptResult::Valid,
- 1_777_665_603,
- );
- let invalid_event = validation_receipt_event(
- hex_event_id(32).as_str(),
- fixture.seller_pubkey.as_str(),
- fixture.order_id_raw.as_str(),
- fixture.listing_event_id.as_str(),
- fixture.request_event_id.as_str(),
- fixture.decision_event_id.as_str(),
- RadrootsValidationReceiptResult::Invalid,
- 1_777_665_604,
- );
- let duplicate_valid_event = valid_event.clone();
- fixture
- .events
- .append_record(&signed_order_event_record(
- "cli:signed_event:validation-receipt:valid",
- &valid_event,
- fixture.listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append valid validation receipt");
- fixture
- .events
- .append_record(&signed_order_event_record(
- "cli:signed_event:validation-receipt:invalid",
- &invalid_event,
- fixture.listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append invalid validation receipt");
- fixture
- .events
- .append_record(&signed_order_event_record(
- "cli:signed_event:validation-receipt:valid-duplicate",
- &duplicate_valid_event,
- fixture.listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append duplicate validation receipt");
- fixture
- .app_store
- .import_shared_runtime_store_from_store(&fixture.events)
- .expect("import validation receipts");
-
- let buyer_detail = fixture
- .app_store
- .load_buyer_order_detail(&fixture.buyer_context, fixture.order_id)
- .expect("load buyer validation receipt detail")
- .expect("buyer validation receipt detail");
- let seller_detail = fixture
- .app_store
- .load_order_detail(fixture.seller_farm_id, fixture.order_id)
- .expect("load seller validation receipt detail")
- .expect("seller validation receipt detail");
- let imports = fixture
- .app_store
- .load_local_interop_records()
- .expect("load validation receipt imports");
-
- assert_eq!(buyer_detail.status, BuyerOrderStatus::NeedsReview);
- assert_eq!(seller_detail.status, OrderStatus::NeedsReview);
- assert_eq!(
- buyer_detail.workflow.agreement,
- TradeAgreementStatus::Invalid
- );
- assert_eq!(
- seller_detail.workflow.agreement,
- TradeAgreementStatus::Invalid
- );
- assert_eq!(
- buyer_detail.workflow.inventory,
- TradeInventoryStatus::NeedsReview
- );
- assert_eq!(
- buyer_detail.workflow.provenance.last_event_id.as_deref(),
- Some(fixture.decision_event_id.as_str())
- );
- assert_eq!(seller_detail.primary_action, None);
- assert_eq!(buyer_detail.validation_receipts.len(), 2);
- assert_eq!(
- buyer_detail.validation_receipts,
- seller_detail.validation_receipts
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].event_id,
- invalid_event.id_str()
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].result,
- TradeValidationReceiptResult::NeedsReview
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].receipt_type,
- TradeValidationReceiptType::TradeTransition
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].proof_system,
- TradeValidationReceiptProofSystem::None
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].target_event_id,
- fixture.decision_event_id
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].event_set_root,
- hash32(42)
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].reducer_output_root,
- hash32(43)
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].public_values_hash,
- hash32(45)
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].recorded_at,
- 1_777_665_604
- );
- assert_eq!(
- buyer_detail.validation_receipts[1].event_id,
- valid_event.id_str()
- );
- assert_eq!(
- buyer_detail.validation_receipts[1].result,
- TradeValidationReceiptResult::Valid
- );
- assert!(
- imports
- .iter()
- .any(|record| record.projected_kind == "validation_receipt"
- && record.event_kind == Some(KIND_VALIDATION_RECEIPT)
- && record.event_id.as_deref() == Some(invalid_event.id_str()))
- );
+ relay_set_fingerprint: Some("relay-set".to_owned()),
+ relay_delivery_json: Some(relay_delivery_json),
+ }
}
#[test]
- fn validation_receipt_import_before_order_request_attaches_when_request_arrives() {
+ fn validation_receipt_import_is_passive_signed_receipt_evidence() {
let app_store =
AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
let events = runtime_store_store();
- let farm_key = "DDDDDDDDDDDDDDDDDDDDDD";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAw";
- let seller_pubkey = test_pubkey("validation-out-of-order-seller");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("validation-out-of-order-buyer");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = "validation-out-of-order";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- let listing_event_id = hex_event_id(51);
- let request_event_id = hex_event_id(52);
- let target_event_id = hex_event_id(53);
- events
- .append_record(&signed_market_listing_record(
- "validation-out-of-order-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Validation Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append out-of-order listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import out-of-order listing");
-
- let receipt_event = validation_receipt_event(
- hex_event_id(54).as_str(),
- seller_pubkey,
- order_id_raw,
- listing_event_id.as_str(),
- request_event_id.as_str(),
- target_event_id.as_str(),
+ let event = validation_receipt_event(
+ hex_event_id(29).as_str(),
+ test_pubkey("receipt-seller").as_str(),
+ "passive-receipt-order",
+ hex_event_id(30).as_str(),
+ hex_event_id(31).as_str(),
+ hex_event_id(32).as_str(),
RadrootsValidationReceiptResult::Valid,
1_777_665_603,
);
events
- .append_record(&signed_order_event_record(
- "cli:signed_event:validation-receipt:before-request",
- &receipt_event,
- listing_addr.as_str(),
+ .append_record(&signed_receipt_event_record(
+ "cli:signed_event:validation-receipt:passive",
+ &event,
SourceRuntime::Cli,
None,
))
- .expect("append receipt before request");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import receipt before request");
+ .expect("append validation receipt");
- let pending_count: i64 = app_store
+ let report = app_store
+ .import_shared_runtime_store_from_store(&events)
+ .expect("import validation receipt");
+ let imports = app_store
+ .load_local_interop_records()
+ .expect("load validation receipt imports");
+ let receipt_row = app_store
.connection()
.query_row(
- "SELECT count(*) FROM order_validation_receipts WHERE order_id IS NULL",
- crate::empty_params(),
- |row| row.try_get(0),
- )
- .expect("count pending validation receipts");
- assert_eq!(pending_count, 1);
-
- let request_payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let request_parts = order_request_event_build(
- &listing_event_ptr(listing_event_id.as_str()),
- &request_payload,
- )
- .expect("build request after validation receipt");
- let request_event = event_from_parts_at(
- request_event_id.as_str(),
- buyer_pubkey,
- request_parts,
- 1_777_665_604,
- );
- events
- .append_record(&signed_order_event_record(
- "app:signed_event:validation-receipt:request-after",
- &request_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_validation_out_of_order"),
- ))
- .expect("append request after receipt");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import request after receipt");
-
- let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id_str());
- let buyer_context = BuyerContext::account("acct_validation_out_of_order");
- let buyer_detail = app_store
- .load_buyer_order_detail(&buyer_context, order_id)
- .expect("load attached buyer validation receipt detail")
- .expect("attached buyer validation receipt detail");
- let seller_detail = app_store
- .load_order_detail(
- deterministic_farm_id(Some(seller_pubkey), farm_key),
- order_id,
- )
- .expect("load attached seller validation receipt detail")
- .expect("attached seller validation receipt detail");
-
- assert_eq!(buyer_detail.validation_receipts.len(), 1);
- assert_eq!(
- buyer_detail.validation_receipts,
- seller_detail.validation_receipts
- );
- assert_eq!(
- buyer_detail.validation_receipts[0].event_id,
- receipt_event.id_str()
- );
- assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed);
- assert_eq!(seller_detail.status, OrderStatus::NeedsAction);
- }
-
- #[test]
- fn validation_receipt_invalid_candidates_do_not_surface_as_order_evidence() {
- let fixture = validation_receipt_order_fixture("validation-receipt-invalid-candidates");
- let mismatched_tag_event = validation_receipt_event(
- hex_event_id(61).as_str(),
- fixture.seller_pubkey.as_str(),
- fixture.order_id_raw.as_str(),
- fixture.listing_event_id.as_str(),
- fixture.request_event_id.as_str(),
- fixture.decision_event_id.as_str(),
- RadrootsValidationReceiptResult::Valid,
- 1_777_665_603,
- );
- let mut mismatched_tags = mismatched_tag_event.tags_as_vec();
- if let Some(tag) = mismatched_tags
- .iter_mut()
- .find(|tag| tag.first().map(String::as_str) == Some("event_set_root"))
- {
- tag[1] = hash32(99);
- }
- let mismatched_tag_event = event_with_tags(&mismatched_tag_event, mismatched_tags);
- let wrong_order_event = validation_receipt_event(
- hex_event_id(62).as_str(),
- fixture.seller_pubkey.as_str(),
- "wrong-order-id",
- fixture.listing_event_id.as_str(),
- fixture.request_event_id.as_str(),
- fixture.decision_event_id.as_str(),
- RadrootsValidationReceiptResult::Valid,
- 1_777_665_604,
- );
- let buyer_kind_candidate = validation_receipt_event(
- hex_event_id(63).as_str(),
- fixture.buyer_pubkey.as_str(),
- fixture.order_id_raw.as_str(),
- fixture.listing_event_id.as_str(),
- fixture.request_event_id.as_str(),
- fixture.decision_event_id.as_str(),
- RadrootsValidationReceiptResult::Valid,
- 1_777_665_605,
- );
- let buyer_kind_candidate =
- event_with_kind(&buyer_kind_candidate, KIND_ORDER_REQUEST as u32);
-
- for (record_id, event) in [
- (
- "cli:signed_event:validation-receipt:mismatched-tag",
- &mismatched_tag_event,
- ),
- (
- "cli:signed_event:validation-receipt:wrong-order",
- &wrong_order_event,
- ),
- (
- "cli:signed_event:validation-receipt:buyer-kind",
- &buyer_kind_candidate,
- ),
- ] {
- fixture
- .events
- .append_record(&signed_order_event_record(
- record_id,
- event,
- fixture.listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append invalid validation receipt candidate");
- }
- fixture
- .app_store
- .import_shared_runtime_store_from_store(&fixture.events)
- .expect("import invalid validation receipt candidates");
-
- let buyer_detail = fixture
- .app_store
- .load_buyer_order_detail(&fixture.buyer_context, fixture.order_id)
- .expect("load buyer detail after invalid validation receipt candidates")
- .expect("buyer detail after invalid validation receipt candidates");
- let seller_detail = fixture
- .app_store
- .load_order_detail(fixture.seller_farm_id, fixture.order_id)
- .expect("load seller detail after invalid validation receipt candidates")
- .expect("seller detail after invalid validation receipt candidates");
-
- assert!(buyer_detail.validation_receipts.is_empty());
- assert!(seller_detail.validation_receipts.is_empty());
- assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed);
- assert_eq!(seller_detail.status, OrderStatus::NeedsAction);
- assert_eq!(
- buyer_detail.workflow.agreement,
- TradeAgreementStatus::AgreedPendingValidation
- );
- assert_eq!(seller_detail.primary_action, None);
- }
-
- #[test]
- fn active_order_pre_agreement_cancellation_projects_through_cli_reducer_state() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "EEEEEEEEEEEEEEEEEEEEEE";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAx";
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("app-buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = "active-cancel-order-1";
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- events
- .append_record(&signed_market_listing_record(
- "active-cancel-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Cancellation Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append cancellation listing");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import cancellation listing");
-
- let request_payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let request_parts = order_request_event_build(
- &listing_event_ptr("active-cancel-listing-event"),
- &request_payload,
- )
- .expect("build cancellation order request");
- let request_event =
- event_from_parts("active-cancel-request-event", buyer_pubkey, request_parts);
- events
- .append_record(&signed_order_event_record(
- "app:signed_event:active-cancel:request",
- &request_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_cancel"),
- ))
- .expect("append cancellation order request");
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import cancellation order request");
-
- let cancel_payload = order_cancel_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let cancel_parts = order_cancellation_event_build(
- &typed_event_id(request_event.id_str()),
- &typed_event_id(request_event.id_str()),
- &cancel_payload,
- )
- .expect("build cancellation");
- let cancel_event = event_from_parts("active-cancel-event", buyer_pubkey, cancel_parts);
- events
- .append_record(&signed_order_event_record(
- "app:signed_event:active-cancel:cancel",
- &cancel_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_cancel"),
- ))
- .expect("append cancellation");
- let cancel_report = app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import cancellation");
-
- let seller_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key);
- let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id_str());
- let buyer_context = BuyerContext::account("acct_cancel");
- let buyer_detail = app_store
- .load_buyer_order_detail(&buyer_context, order_id)
- .expect("load cancellation buyer detail")
- .expect("cancellation buyer detail");
- let seller_orders = app_store
- .load_orders_list(
- seller_farm_id,
- &OrdersScreenQueryState {
- filter: OrdersFilter::All,
- fulfillment_window_id: None,
+ "SELECT order_id, raw_order_id, result, target_event_id
+ FROM order_validation_receipts
+ WHERE event_id = ?1",
+ crate::app_sqlite_params![event.id_str()],
+ |row| {
+ Ok((
+ row.try_get::<Option<String>, _>(0)?,
+ row.try_get::<String, _>(1)?,
+ row.try_get::<String, _>(2)?,
+ row.try_get::<String, _>(3)?,
+ ))
},
)
- .expect("load cancellation seller orders");
-
- assert_eq!(cancel_report.imported_records, 1);
- assert_eq!(buyer_detail.status, BuyerOrderStatus::Declined);
- assert_eq!(
- buyer_detail.workflow.agreement,
- TradeAgreementStatus::Cancelled
- );
- assert_eq!(seller_orders.rows[0].status, OrderStatus::Declined);
- assert_eq!(
- seller_orders.rows[0].workflow.agreement,
- TradeAgreementStatus::Cancelled
- );
- assert_eq!(seller_orders.rows[0].primary_action, None);
- }
-
- #[test]
- fn conflicting_order_decisions_project_to_needs_action_deterministically() {
- let run_case = |accepted_first: bool| {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let farm_key = "FFFFFFFFFFFFFFFFFFFFFF";
- let listing_key = "AAAAAAAAAAAAAAAAAAAAAw";
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("app-buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let order_id_raw = if accepted_first {
- "active-conflict-order-accepted-first"
- } else {
- "active-conflict-order-declined-first"
- };
- let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
- events
- .append_record(&signed_market_listing_record(
- "active-conflict-listing",
- seller_pubkey,
- farm_key,
- listing_key,
- "Conflict Eggs",
- "9",
- "active",
- "pickup",
- "North barn pickup",
- 4_102_444_800,
- 4_102_531_200,
- RuntimeStoreRecordStatus::Published,
- PublishOutboxStatus::Acknowledged,
- ))
- .expect("append conflict listing");
- let request_payload = order_request_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let request_parts = order_request_event_build(
- &listing_event_ptr("active-conflict-listing-event"),
- &request_payload,
- )
- .expect("build conflict request");
- let request_event =
- event_from_parts("active-conflict-request-event", buyer_pubkey, request_parts);
- events
- .append_record(&signed_order_event_record(
- "app:signed_event:active-conflict:request",
- &request_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_conflict"),
- ))
- .expect("append conflict request");
- let accepted_payload = accepted_order_decision_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let accepted_parts = order_decision_event_build(
- &typed_event_id(request_event.id_str()),
- &typed_event_id(request_event.id_str()),
- &accepted_payload,
- )
- .expect("build accepted conflict decision");
- let accepted_event = event_from_parts(
- "active-conflict-accepted-event",
- seller_pubkey,
- accepted_parts,
- );
- let declined_payload = declined_order_decision_payload(
- order_id_raw,
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let declined_parts = order_decision_event_build(
- &typed_event_id(request_event.id_str()),
- &typed_event_id(request_event.id_str()),
- &declined_payload,
- )
- .expect("build declined conflict decision");
- let declined_event = event_from_parts(
- "active-conflict-declined-event",
- seller_pubkey,
- declined_parts,
- );
- let ordered_events = if accepted_first {
- [accepted_event, declined_event]
- } else {
- [declined_event, accepted_event]
- };
- for (index, event) in ordered_events.iter().enumerate() {
- events
- .append_record(&signed_order_event_record(
- &format!("cli:signed_event:active-conflict:decision:{index}"),
- event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- ))
- .expect("append conflict decision");
- }
-
- app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import conflicting decisions");
- let order_id = projected_order_id(order_id_raw, buyer_pubkey, request_event.id_str());
- let detail = app_store
- .load_order_detail(
- deterministic_farm_id(Some(seller_pubkey), farm_key),
- order_id,
- )
- .expect("load conflict order detail")
- .expect("conflict order detail");
- detail.status
- };
-
- assert_eq!(run_case(true), OrderStatus::NeedsAction);
- assert_eq!(run_case(false), OrderStatus::NeedsAction);
- }
-
- #[test]
- fn malformed_order_event_remains_signed_event_evidence_without_projection() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- events
- .append_record(&RuntimeStoreRecordInput {
- record_id: "cli:signed_event:order-request:malformed".to_owned(),
- family: RuntimeStoreRecordFamily::SignedEvent,
- status: RuntimeStoreRecordStatus::Published,
- source_runtime: SourceRuntime::Cli,
- created_at_ms: 1100,
- inserted_at_ms: 1101,
- owner_account_id: None,
- owner_pubkey: Some("buyer-pubkey".to_owned()),
- farm_id: None,
- listing_addr: Some("30402:seller-pubkey:listing-key".to_owned()),
- local_work_json: None,
- event_id: Some("malformed-order-event".to_owned()),
- event_kind: Some(KIND_ORDER_REQUEST),
- event_pubkey: Some("buyer-pubkey".to_owned()),
- event_created_at: Some(1100),
- event_tags_json: Some(json!([["d", "bad-order"]])),
- event_content: Some("not-json".to_owned()),
- event_sig: Some("signature".to_owned()),
- raw_event_json: Some(json!({
- "id": "malformed-order-event",
- "kind": KIND_ORDER_REQUEST,
- "pubkey": "buyer-pubkey",
- "content": "not-json"
- })),
- outbox_status: PublishOutboxStatus::Acknowledged,
- relay_set_fingerprint: Some("relay-set".to_owned()),
- relay_delivery_json: Some(json!({
- "state": "acknowledged",
- "acknowledged_relays": ["ws://127.0.0.1:1234/"]
- })),
- })
- .expect("append malformed order event");
-
- let report = app_store
- .import_shared_runtime_store_from_store(&events)
- .expect("import malformed order event");
- let imported = app_store
- .load_local_interop_records()
- .expect("load imported records");
+ .expect("load passive validation receipt row");
let order_count: i64 = app_store
.connection()
.query_row(
- "SELECT COUNT(*) FROM orders",
+ "SELECT count(*) FROM orders",
crate::empty_params(),
|row| row.try_get(0),
)
.expect("load order count");
assert_eq!(report.imported_records, 1);
- assert_eq!(report.skipped_records, 0);
- assert_eq!(imported.len(), 1);
- assert_eq!(imported[0].projected_kind, "signed_event");
+ assert_eq!(receipt_row.0, None);
+ assert_eq!(receipt_row.1, "passive-receipt-order");
assert_eq!(
- imported[0].event_id.as_deref(),
- Some("malformed-order-event")
+ receipt_row.2,
+ TradeValidationReceiptResult::Valid.storage_key()
);
+ assert_eq!(receipt_row.3, hex_event_id(32));
assert_eq!(order_count, 0);
+ assert!(imports.iter().any(|record| {
+ record.projected_kind == "validation_receipt"
+ && record.event_kind == Some(KIND_VALIDATION_RECEIPT)
+ && record.event_id.as_deref() == Some(event.id_str())
+ }));
}
#[test]
@@ -7980,173 +5278,6 @@ mod tests {
}
#[test]
- fn app_order_request_receipt_replaces_prior_relay_duplicate() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let listing_addr = format!("30402:{seller_pubkey}:app-order-listing");
- let payload = order_request_payload(
- "app-order-receipt-replaces-relay",
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let parts = order_request_event_build(&listing_event_ptr("listing-event"), &payload)
- .expect("build order request event");
- let event = event_from_parts("app-order-request-event", buyer_pubkey, parts);
- let mut relay_record = signed_order_event_record(
- "app:relay_event:order-request:duplicate",
- &event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- relay_record.outbox_status = PublishOutboxStatus::None;
- relay_record.relay_delivery_json = Some(json!({
- "state": "observed",
- "observed_relays": ["ws://127.0.0.1:1234/"]
- }));
- let relay_record = events
- .append_record(&relay_record)
- .expect("append relay order request");
- let app_record = events
- .append_record(&signed_order_event_record(
- "app:signed_event:order-request:duplicate",
- &event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_buyer"),
- ))
- .expect("append app order request receipt");
-
- let report = app_store
- .import_local_event_records(&[relay_record, app_record])
- .expect("import duplicate order request records");
- let imported = app_store
- .load_local_interop_records()
- .expect("load imported records");
- let stored = imported
- .iter()
- .find(|record| record.event_id.as_deref() == Some(event.id_str()))
- .expect("app order request evidence");
-
- assert_eq!(report.imported_records, 2);
- assert_eq!(report.skipped_records, 0);
- assert_eq!(
- imported
- .iter()
- .filter(|record| record.event_id.as_deref() == Some(event.id_str()))
- .count(),
- 1
- );
- assert_eq!(stored.record_id, "app:signed_event:order-request:duplicate");
- assert_eq!(stored.source_runtime, SourceRuntime::App.as_str());
- assert_eq!(stored.owner_account_id.as_deref(), Some("acct_buyer"));
- assert_eq!(
- stored.outbox_status,
- PublishOutboxStatus::Acknowledged.as_str()
- );
- assert_eq!(stored.listing_addr.as_deref(), Some(listing_addr.as_str()));
- }
-
- #[test]
- fn relay_order_decision_duplicate_does_not_downgrade_app_receipt() {
- let app_store =
- AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
- let events = runtime_store_store();
- let seller_pubkey = test_pubkey("seller-pubkey");
- let seller_pubkey = seller_pubkey.as_str();
- let buyer_pubkey = test_pubkey("buyer-pubkey");
- let buyer_pubkey = buyer_pubkey.as_str();
- let listing_addr = format!("30402:{seller_pubkey}:app-decision-listing");
- let request_payload = order_request_payload(
- "app-decision-receipt",
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let request_parts =
- order_request_event_build(&listing_event_ptr("listing-event"), &request_payload)
- .expect("build order request event");
- let request_event =
- event_from_parts("app-decision-request-event", buyer_pubkey, request_parts);
- let decision_payload = accepted_order_decision_payload(
- "app-decision-receipt",
- listing_addr.as_str(),
- buyer_pubkey,
- seller_pubkey,
- );
- let decision_parts = order_decision_event_build(
- &typed_event_id(request_event.id_str()),
- &typed_event_id(request_event.id_str()),
- &decision_payload,
- )
- .expect("build order decision event");
- let decision_event =
- event_from_parts("app-order-decision-event", seller_pubkey, decision_parts);
- let app_record = events
- .append_record(&signed_order_event_record(
- "app:signed_event:order-decision:duplicate",
- &decision_event,
- listing_addr.as_str(),
- SourceRuntime::App,
- Some("acct_seller"),
- ))
- .expect("append app order decision receipt");
- let mut relay_record = signed_order_event_record(
- "app:relay_event:order-decision:duplicate",
- &decision_event,
- listing_addr.as_str(),
- SourceRuntime::Cli,
- None,
- );
- relay_record.outbox_status = PublishOutboxStatus::None;
- relay_record.relay_delivery_json = Some(json!({
- "state": "observed",
- "observed_relays": ["ws://127.0.0.1:1234/"]
- }));
- let relay_record = events
- .append_record(&relay_record)
- .expect("append relay order decision");
-
- let report = app_store
- .import_local_event_records(&[app_record, relay_record])
- .expect("import duplicate order decision records");
- let imported = app_store
- .load_local_interop_records()
- .expect("load imported records");
- let stored = imported
- .iter()
- .find(|record| record.event_id.as_deref() == Some(decision_event.id_str()))
- .expect("app order decision evidence");
-
- assert_eq!(report.imported_records, 1);
- assert_eq!(report.skipped_records, 1);
- assert_eq!(
- imported
- .iter()
- .filter(|record| record.event_id.as_deref() == Some(decision_event.id_str()))
- .count(),
- 1
- );
- assert_eq!(
- stored.record_id,
- "app:signed_event:order-decision:duplicate"
- );
- assert_eq!(stored.source_runtime, SourceRuntime::App.as_str());
- assert_eq!(stored.owner_account_id.as_deref(), Some("acct_seller"));
- assert_eq!(
- stored.outbox_status,
- PublishOutboxStatus::Acknowledged.as_str()
- );
- assert_eq!(stored.listing_addr.as_deref(), Some(listing_addr.as_str()));
- }
-
- #[test]
fn local_work_farm_import_preserves_duplicate_relay_signed_ready_farm() {
let app_store =
AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs
@@ -35,7 +35,6 @@ use sqlx::Row;
pub use error::AppSqliteError;
pub use interop::{
AppLocalInteropImportReport, AppLocalInteropRepository, StoredLocalInteropRecord,
- projected_order_id_from_trade_request,
};
pub use migrations::latest_schema_version;
pub use repo::{
@@ -926,10 +925,14 @@ mod tests {
));
assert!(column_exists(connection, "products", "category"));
assert!(column_exists(connection, "products", "listing_bin_id"));
- assert!(column_exists(connection, "buyer_carts", "buyer_email"));
- assert!(column_exists(connection, "buyer_carts", "buyer_phone"));
- assert!(column_exists(connection, "buyer_carts", "buyer_order_note"));
- assert!(column_exists(
+ assert!(!column_exists(connection, "buyer_carts", "buyer_email"));
+ assert!(!column_exists(connection, "buyer_carts", "buyer_phone"));
+ assert!(!column_exists(
+ connection,
+ "buyer_carts",
+ "buyer_order_note"
+ ));
+ assert!(!column_exists(
connection,
"buyer_carts",
"buyer_order_note_public_confirmed"
@@ -961,10 +964,10 @@ mod tests {
"listing_relays_json"
));
assert!(column_exists(connection, "orders", "buyer_context_key"));
- assert!(column_exists(connection, "orders", "buyer_email"));
- assert!(column_exists(connection, "orders", "buyer_phone"));
- assert!(column_exists(connection, "orders", "buyer_order_note"));
- assert!(column_exists(
+ assert!(!column_exists(connection, "orders", "buyer_email"));
+ assert!(!column_exists(connection, "orders", "buyer_phone"));
+ assert!(!column_exists(connection, "orders", "buyer_order_note"));
+ assert!(!column_exists(
connection,
"orders",
"buyer_order_note_public_confirmed"
@@ -1199,18 +1202,18 @@ mod tests {
workflow_agreement,
workflow_inventory
) VALUES (
- 'order_agreed_pending_validation',
+ 'order_contested',
'farm_schema',
- 'agreed pending validation',
+ 'contested',
'Buyer',
'needs_action',
'2026-01-01T00:00:00Z',
- 'agreed_pending_validation',
+ 'contested',
'reserved'
)",
crate::empty_params(),
)
- .expect("pending validation workflow projection should insert");
+ .expect("contested workflow projection should insert");
let invalid_result = connection.execute(
"INSERT INTO orders (
@@ -1271,10 +1274,7 @@ mod tests {
customer_display_name,
status,
updated_at,
- buyer_context_key,
- buyer_email,
- buyer_phone,
- buyer_order_note
+ buyer_context_key
) VALUES (
'order_status',
'farm_status',
@@ -1283,10 +1283,7 @@ mod tests {
'Schema Buyer',
'needs_action',
'2026-01-01T00:00:00Z',
- 'account:buyer',
- '',
- '',
- ''
+ 'account:buyer'
)",
crate::empty_params(),
)
@@ -1360,10 +1357,10 @@ mod tests {
assert_eq!(status, "needs_review");
connection
.execute(
- "UPDATE orders SET workflow_agreement = 'agreed_pending_validation' WHERE id = 'order_status'",
+ "UPDATE orders SET workflow_agreement = 'contested' WHERE id = 'order_status'",
crate::empty_params(),
)
- .expect("agreed pending validation agreement should satisfy current check");
+ .expect("contested agreement should satisfy current check");
}
fn table_exists(connection: &AppSqliteDatabase, table_name: &str) -> bool {
diff --git a/crates/store/src/migrations.rs b/crates/store/src/migrations.rs
@@ -126,6 +126,10 @@ const MIGRATIONS: &[Migration] = &[
version: 30,
sql: include_str!("../migrations/0030_buyer_order_public_note_confirmation.sql"),
},
+ Migration {
+ version: 31,
+ sql: include_str!("../migrations/0031_drop_default_buyer_private_fields.sql"),
+ },
];
pub fn latest_schema_version() -> u32 {
diff --git a/crates/store/src/repo/buyer.rs b/crates/store/src/repo/buyer.rs
@@ -90,10 +90,6 @@ pub struct BuyerOrderRuntimeStoreExport {
pub status: String,
pub buyer_context_key: String,
pub buyer_name: String,
- pub buyer_email: String,
- pub buyer_phone: String,
- pub buyer_order_note: String,
- pub buyer_order_note_public_confirmed: bool,
pub updated_at: String,
pub fulfillment_window_id: Option<FulfillmentWindowId>,
pub fulfillment_window_label: Option<String>,
@@ -310,7 +306,6 @@ impl<'a> AppBuyerRepository<'a> {
"update buyer_carts
set
farm_id = null,
- buyer_order_note = '',
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
where buyer_context_key = ?1",
crate::app_sqlite_params![context_key.as_str()],
@@ -382,20 +377,9 @@ impl<'a> AppBuyerRepository<'a> {
"update buyer_carts
set
buyer_name = ?2,
- buyer_email = ?3,
- buyer_phone = ?4,
- buyer_order_note = ?5,
- buyer_order_note_public_confirmed = ?6,
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
where buyer_context_key = ?1",
- crate::app_sqlite_params![
- context_key.as_str(),
- draft.name.trim(),
- draft.email.trim(),
- draft.phone.trim(),
- draft.order_note.trim(),
- draft.confirm_public_note,
- ],
+ crate::app_sqlite_params![context_key.as_str(), draft.name.trim()],
)
.map_err(|source| AppSqliteError::Query {
operation: "save buyer order review draft",
@@ -447,11 +431,7 @@ impl<'a> AppBuyerRepository<'a> {
customer_display_name,
status,
updated_at,
- buyer_context_key,
- buyer_email,
- buyer_phone,
- buyer_order_note,
- buyer_order_note_public_confirmed
+ buyer_context_key
) values (
?1,
?2,
@@ -460,11 +440,7 @@ impl<'a> AppBuyerRepository<'a> {
?5,
'needs_action',
strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
- ?6,
- ?7,
- ?8,
- ?9,
- ?10
+ ?6
)",
crate::app_sqlite_params![
order_id.to_string(),
@@ -473,10 +449,6 @@ impl<'a> AppBuyerRepository<'a> {
order_number,
order_review.draft.name.trim(),
context_key.as_str(),
- order_review.draft.email.trim(),
- order_review.draft.phone.trim(),
- order_review.draft.order_note.trim(),
- order_review.draft.confirm_public_note,
],
)
.map_err(|source| AppSqliteError::Query {
@@ -543,8 +515,6 @@ impl<'a> AppBuyerRepository<'a> {
"update buyer_carts
set
farm_id = null,
- buyer_order_note = '',
- buyer_order_note_public_confirmed = 0,
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
where buyer_context_key = ?1",
crate::app_sqlite_params![context_key.as_str()],
@@ -954,7 +924,6 @@ impl<'a> AppBuyerRepository<'a> {
o.farm_id,
o.order_number,
o.status,
- o.buyer_order_note,
o.workflow_revision,
o.workflow_agreement,
o.workflow_inventory,
@@ -984,12 +953,11 @@ impl<'a> AppBuyerRepository<'a> {
row.try_get::<String, _>(5)?,
row.try_get::<String, _>(6)?,
row.try_get::<String, _>(7)?,
- row.try_get::<String, _>(8)?,
- row.try_get::<Option<String>, _>(9)?,
- row.try_get::<String, _>(10)?,
+ row.try_get::<Option<String>, _>(8)?,
+ row.try_get::<String, _>(9)?,
+ row.try_get::<Option<String>, _>(10)?,
row.try_get::<Option<String>, _>(11)?,
row.try_get::<Option<String>, _>(12)?,
- row.try_get::<Option<String>, _>(13)?,
))
})
.optional()
@@ -1005,7 +973,6 @@ impl<'a> AppBuyerRepository<'a> {
farm_id,
order_number,
status,
- order_note,
workflow_revision,
workflow_agreement,
workflow_inventory,
@@ -1050,7 +1017,7 @@ impl<'a> AppBuyerRepository<'a> {
economics,
workflow,
validation_receipts,
- order_note: empty_string_to_none(order_note),
+ order_note: None,
repeat_demand: self.build_repeat_demand_handoff(
order_id,
farm_id,
@@ -1079,10 +1046,6 @@ impl<'a> AppBuyerRepository<'a> {
o.status,
o.buyer_context_key,
o.customer_display_name,
- o.buyer_email,
- o.buyer_phone,
- o.buyer_order_note,
- o.buyer_order_note_public_confirmed,
o.updated_at,
f.display_name,
fw.id,
@@ -1105,14 +1068,10 @@ impl<'a> AppBuyerRepository<'a> {
row.try_get::<String, _>(5)?,
row.try_get::<String, _>(6)?,
row.try_get::<String, _>(7)?,
- row.try_get::<String, _>(8)?,
- row.try_get::<bool, _>(9)?,
- row.try_get::<String, _>(10)?,
- row.try_get::<String, _>(11)?,
- row.try_get::<Option<String>, _>(12)?,
- row.try_get::<Option<String>, _>(13)?,
- row.try_get::<Option<String>, _>(14)?,
- row.try_get::<Option<String>, _>(15)?,
+ row.try_get::<Option<String>, _>(8)?,
+ row.try_get::<Option<String>, _>(9)?,
+ row.try_get::<Option<String>, _>(10)?,
+ row.try_get::<Option<String>, _>(11)?,
))
},
)
@@ -1131,10 +1090,6 @@ impl<'a> AppBuyerRepository<'a> {
status,
buyer_context_key,
buyer_name,
- buyer_email,
- buyer_phone,
- buyer_order_note,
- buyer_order_note_public_confirmed,
updated_at,
farm_display_name,
fulfillment_window_id,
@@ -1154,10 +1109,6 @@ impl<'a> AppBuyerRepository<'a> {
status,
buyer_context_key: buyer_context_key.unwrap_or_else(|| context_key.clone()),
buyer_name,
- buyer_email,
- buyer_phone,
- buyer_order_note,
- buyer_order_note_public_confirmed,
updated_at,
fulfillment_window_id: parse_optional_typed_id(
"orders.fulfillment_window_id",
@@ -1582,11 +1533,7 @@ impl<'a> AppBuyerRepository<'a> {
.query_row(
"select
farm_id,
- buyer_name,
- buyer_email,
- buyer_phone,
- buyer_order_note,
- buyer_order_note_public_confirmed
+ buyer_name
from buyer_carts
where buyer_context_key = ?1
limit 1",
@@ -1595,10 +1542,6 @@ impl<'a> AppBuyerRepository<'a> {
Ok((
row.try_get::<Option<String>, _>(0)?,
row.try_get::<String, _>(1)?,
- row.try_get::<String, _>(2)?,
- row.try_get::<String, _>(3)?,
- row.try_get::<String, _>(4)?,
- row.try_get::<bool, _>(5)?,
))
},
)
@@ -1607,25 +1550,12 @@ impl<'a> AppBuyerRepository<'a> {
operation: "load buyer cart header",
source,
})?
- .map(
- |(
- farm_id,
+ .map(|(farm_id, buyer_name)| {
+ Ok(BuyerCartHeader {
+ farm_id: parse_optional_typed_id("buyer_carts.farm_id", farm_id)?,
buyer_name,
- buyer_email,
- buyer_phone,
- buyer_order_note,
- buyer_order_note_public_confirmed,
- )| {
- Ok(BuyerCartHeader {
- farm_id: parse_optional_typed_id("buyer_carts.farm_id", farm_id)?,
- buyer_name,
- buyer_email,
- buyer_phone,
- buyer_order_note,
- buyer_order_note_public_confirmed,
- })
- },
- )
+ })
+ })
.transpose()
}
@@ -2230,20 +2160,16 @@ impl<'a> AppBuyerRepository<'a> {
struct BuyerCartHeader {
farm_id: Option<FarmId>,
buyer_name: String,
- buyer_email: String,
- buyer_phone: String,
- buyer_order_note: String,
- buyer_order_note_public_confirmed: bool,
}
impl BuyerCartHeader {
fn into_order_review_draft(self) -> BuyerOrderReviewDraft {
BuyerOrderReviewDraft {
name: self.buyer_name,
- email: self.buyer_email,
- phone: self.buyer_phone,
- order_note: self.buyer_order_note,
- confirm_public_note: self.buyer_order_note_public_confirmed,
+ email: String::new(),
+ phone: String::new(),
+ order_note: String::new(),
+ confirm_public_note: false,
}
}
}
@@ -2678,12 +2604,6 @@ fn buyer_order_review_disabled_reason(
if draft.name.trim().is_empty() {
return Some(BuyerOrderReviewDisabledReason::MissingName);
}
- if draft.email.trim().is_empty() {
- return Some(BuyerOrderReviewDisabledReason::MissingEmail);
- }
- if !draft.order_note.trim().is_empty() && !draft.confirm_public_note {
- return Some(BuyerOrderReviewDisabledReason::PublicNoteConfirmationRequired);
- }
if matches!(context, BuyerContext::Guest) {
return Some(BuyerOrderReviewDisabledReason::AccountRequired);
}
@@ -3002,7 +2922,7 @@ mod tests {
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
#[test]
- fn buyer_order_review_requires_public_note_confirmation() {
+ fn buyer_order_review_does_not_require_private_note_confirmation_in_default_store() {
let cart = BuyerCartProjection {
farm_id: Some(FarmId::generate()),
farm_display_name: Some("Willow Farm".to_owned()),
@@ -3039,7 +2959,7 @@ mod tests {
Some(&"Friday pickup".to_owned()),
&draft,
),
- Some(BuyerOrderReviewDisabledReason::PublicNoteConfirmationRequired)
+ None
);
assert_eq!(
super::buyer_order_review_disabled_reason(
@@ -4235,9 +4155,9 @@ mod tests {
order_number: &str,
status: &str,
buyer_context_key: Option<&str>,
- buyer_email: &str,
- buyer_phone: &str,
- buyer_order_note: &str,
+ _buyer_email: &str,
+ _buyer_phone: &str,
+ _buyer_order_note: &str,
) {
connection
.execute(
@@ -4249,20 +4169,14 @@ mod tests {
customer_display_name,
status,
updated_at,
- buyer_context_key,
- buyer_email,
- buyer_phone,
- buyer_order_note
- ) values (?1, ?2, null, ?3, 'Casey', ?4, '2026-04-20T10:00:00Z', ?5, ?6, ?7, ?8)",
+ buyer_context_key
+ ) values (?1, ?2, null, ?3, 'Casey', ?4, '2026-04-20T10:00:00Z', ?5)",
crate::app_sqlite_params![
order_id.to_string(),
farm_id.to_string(),
order_number,
status,
buyer_context_key,
- buyer_email,
- buyer_phone,
- buyer_order_note,
],
)
.expect("order insert should succeed");
diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs
@@ -7,7 +7,7 @@ use radroots_studio_app_view::{
PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry,
PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow,
PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow,
- PackDayScreenQueryState, ProductId, TradeAgreementStatus, TradeWorkflowProjection,
+ PackDayScreenQueryState, ProductId, TradeWorkflowProjection,
};
use sqlx::Row;
@@ -1177,14 +1177,9 @@ fn summarize_orders(records: &[OrderRecord]) -> OrdersListSummary {
fn primary_action_for_order(
status: OrderStatus,
- workflow: &TradeWorkflowProjection,
+ _workflow: &TradeWorkflowProjection,
) -> Option<OrderPrimaryAction> {
match status {
- OrderStatus::NeedsAction
- if workflow.agreement == TradeAgreementStatus::AgreedPendingValidation =>
- {
- None
- }
OrderStatus::NeedsAction => Some(OrderPrimaryAction::Review),
OrderStatus::Scheduled
| OrderStatus::Packed
diff --git a/crates/store/src/repo/workflow.rs b/crates/store/src/repo/workflow.rs
@@ -1,5 +1,6 @@
use radroots_studio_app_view::{
- OrderId, TradeAgreementStatus, TradeEconomicsProjection, TradeInventoryStatus,
+ OrderId, TradeAgreementStatus, TradeAttestationStatus, TradeConflictStatus,
+ TradeEconomicsProjection, TradeEvidenceStatus, TradeInventoryStatus, TradePrivateTermsStatus,
TradeProvenanceProjection, TradeRevisionStatus, TradeWorkflowProjection, TradeWorkflowSource,
};
@@ -25,6 +26,11 @@ pub(super) fn trade_workflow_projection_from_storage(
revision: snapshot.revision,
economics: snapshot.economics,
inventory: parse_trade_inventory_status("orders.workflow_inventory", snapshot.inventory)?,
+ evidence: TradeEvidenceStatus::Missing,
+ conflict: TradeConflictStatus::None,
+ private_terms: TradePrivateTermsStatus::NotRequired,
+ attestation: TradeAttestationStatus::None,
+ projection_digest: None,
provenance: TradeProvenanceProjection::from_primary_source(parse_trade_workflow_source(
"orders.workflow_provenance_source",
snapshot.provenance_source,
@@ -39,11 +45,10 @@ fn parse_trade_agreement_status(
) -> Result<TradeAgreementStatus, AppSqliteError> {
match value.as_str() {
"requested" => Ok(TradeAgreementStatus::Requested),
- "agreed_pending_validation" => Ok(TradeAgreementStatus::AgreedPendingValidation),
"committed" => Ok(TradeAgreementStatus::Committed),
+ "contested" => Ok(TradeAgreementStatus::Contested),
"declined" => Ok(TradeAgreementStatus::Declined),
"cancelled" => Ok(TradeAgreementStatus::Cancelled),
- "validation_expired" => Ok(TradeAgreementStatus::ValidationExpired),
"invalid" => Ok(TradeAgreementStatus::Invalid),
_ => Err(AppSqliteError::DecodeEnum { field, value }),
}
diff --git a/crates/sync/src/lib.rs b/crates/sync/src/lib.rs
@@ -3,11 +3,11 @@
mod publish;
pub use publish::{
- AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload,
- AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload, AppOrderDecisionPublishPayload,
- AppOrderRequestItemPayload, AppOrderRequestPublishPayload, AppPublishContext,
- AppPublishPayload, AppPublishPayloadJsonError, AppPublishValidationFailure,
- AppPublishValidationFailureSet, AppPublishWorkKind,
+ AppFarmProfilePublishPayload, AppListingPublishPayload, AppPublishContext, AppPublishPayload,
+ AppPublishPayloadJsonError, AppPublishValidationFailure, AppPublishValidationFailureSet,
+ AppPublishWorkKind, AppTradeCancellationPublishPayload,
+ AppTradeCandidateDecisionPublishPayload, AppTradeProposalPublishPayload,
+ AppTradeRevisionProposalPublishPayload,
};
use radroots_studio_app_view::{FarmId, FulfillmentWindowId, OrderId, ProductId};
diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs
@@ -1,6 +1,8 @@
+use radroots_event::trade::{RadrootsTradeMutationBodyV1, RadrootsTradeMutationEnvelopeV1};
use radroots_sdk::{
- FARM_PUBLISH_OPERATION_KIND, LISTING_PUBLISH_OPERATION_KIND, TRADE_CANCELLATION_OPERATION_KIND,
- TRADE_DECISION_OPERATION_KIND, TRADE_SUBMIT_OPERATION_KIND,
+ FARM_PUBLISH_OPERATION_KIND, LISTING_PUBLISH_OPERATION_KIND, TRADE_CANCEL_OPERATION_KIND,
+ TRADE_DECIDE_CANDIDATE_OPERATION_KIND, TRADE_PROPOSE_REVISION_OPERATION_KIND,
+ TRADE_SUBMIT_PROPOSAL_OPERATION_KIND,
};
use radroots_studio_app_view::{
FarmId, FarmReadiness, FulfillmentWindowId, OrderId, ProductId, ProductStatus,
@@ -15,9 +17,10 @@ use crate::{PendingSyncOperation, PendingSyncOperationState, SyncAggregateRef, S
pub enum AppPublishWorkKind {
FarmProfile,
Listing,
- OrderRequest,
- OrderDecision,
- OrderCancellation,
+ TradeProposal,
+ TradeRevisionProposal,
+ TradeCandidateDecision,
+ TradeCancellation,
}
impl AppPublishWorkKind {
@@ -25,9 +28,10 @@ impl AppPublishWorkKind {
match self {
Self::FarmProfile => "farm_profile",
Self::Listing => "listing",
- Self::OrderRequest => "order_request",
- Self::OrderDecision => "order_decision",
- Self::OrderCancellation => "order_cancellation",
+ Self::TradeProposal => "trade_proposal",
+ Self::TradeRevisionProposal => "trade_revision_proposal",
+ Self::TradeCandidateDecision => "trade_candidate_decision",
+ Self::TradeCancellation => "trade_cancellation",
}
}
@@ -35,9 +39,10 @@ impl AppPublishWorkKind {
match self {
Self::FarmProfile => FARM_PUBLISH_OPERATION_KIND,
Self::Listing => LISTING_PUBLISH_OPERATION_KIND,
- Self::OrderRequest => TRADE_SUBMIT_OPERATION_KIND,
- Self::OrderDecision => TRADE_DECISION_OPERATION_KIND,
- Self::OrderCancellation => TRADE_CANCELLATION_OPERATION_KIND,
+ Self::TradeProposal => TRADE_SUBMIT_PROPOSAL_OPERATION_KIND,
+ Self::TradeRevisionProposal => TRADE_PROPOSE_REVISION_OPERATION_KIND,
+ Self::TradeCandidateDecision => TRADE_DECIDE_CANDIDATE_OPERATION_KIND,
+ Self::TradeCancellation => TRADE_CANCEL_OPERATION_KIND,
}
}
}
@@ -106,83 +111,36 @@ pub struct AppListingPublishPayload {
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub struct AppOrderRequestItemPayload {
- pub product_id: ProductId,
- pub quantity: u32,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub struct AppOrderRequestPublishPayload {
+pub struct AppTradeProposalPublishPayload {
pub context: AppPublishContext,
pub order_id: OrderId,
pub farm_id: FarmId,
- pub status: Option<String>,
- pub order_document_json: Option<serde_json::Value>,
- pub listing_addr: Option<String>,
- pub listing_event_id: Option<String>,
- pub listing_relays: Vec<String>,
- pub buyer_pubkey: Option<String>,
- pub seller_pubkey: Option<String>,
- pub items: Vec<AppOrderRequestItemPayload>,
- pub currency_code: Option<String>,
- pub total_minor_units: Option<u32>,
- pub note: Option<String>,
- pub confirm_public_note: bool,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub struct AppOrderDecisionInventoryCommitment {
- pub bin_id: String,
- pub bin_count: u32,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case", tag = "decision")]
-pub enum AppOrderDecisionPayload {
- Accepted {
- inventory_commitments: Vec<AppOrderDecisionInventoryCommitment>,
- },
- Declined {
- reason: String,
- },
-}
-
-impl AppOrderDecisionPayload {
- pub const fn storage_key(&self) -> &'static str {
- match self {
- Self::Accepted { .. } => "accepted",
- Self::Declined { .. } => "declined",
- }
- }
+pub struct AppTradeRevisionProposalPublishPayload {
+ pub context: AppPublishContext,
+ pub order_id: OrderId,
+ pub farm_id: FarmId,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub struct AppOrderDecisionPublishPayload {
+pub struct AppTradeCandidateDecisionPublishPayload {
pub context: AppPublishContext,
- pub app_order_id: OrderId,
+ pub order_id: OrderId,
pub farm_id: FarmId,
- pub trade_order_id: String,
- pub request_event_id: String,
- pub listing_event_id: Option<String>,
- pub listing_addr: String,
- pub buyer_pubkey: String,
- pub seller_pubkey: String,
- pub decision: AppOrderDecisionPayload,
- pub confirm_public_note: bool,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
+ pub acknowledge_private_terms: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub struct AppOrderCancellationPublishPayload {
+pub struct AppTradeCancellationPublishPayload {
pub context: AppPublishContext,
- pub app_order_id: OrderId,
+ pub order_id: OrderId,
pub farm_id: FarmId,
- pub trade_order_id: String,
- pub request_event_id: String,
- pub listing_addr: String,
- pub buyer_pubkey: String,
- pub seller_pubkey: String,
- pub reason: String,
- pub confirm_public_note: bool,
+ pub envelope: RadrootsTradeMutationEnvelopeV1,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@@ -190,9 +148,10 @@ pub struct AppOrderCancellationPublishPayload {
pub enum AppPublishPayload {
FarmProfile(AppFarmProfilePublishPayload),
Listing(AppListingPublishPayload),
- OrderRequest(AppOrderRequestPublishPayload),
- OrderDecision(AppOrderDecisionPublishPayload),
- OrderCancellation(AppOrderCancellationPublishPayload),
+ TradeProposal(AppTradeProposalPublishPayload),
+ TradeRevisionProposal(AppTradeRevisionProposalPublishPayload),
+ TradeCandidateDecision(AppTradeCandidateDecisionPublishPayload),
+ TradeCancellation(AppTradeCancellationPublishPayload),
}
impl AppPublishPayload {
@@ -200,9 +159,10 @@ impl AppPublishPayload {
match self {
Self::FarmProfile(_) => AppPublishWorkKind::FarmProfile,
Self::Listing(_) => AppPublishWorkKind::Listing,
- Self::OrderRequest(_) => AppPublishWorkKind::OrderRequest,
- Self::OrderDecision(_) => AppPublishWorkKind::OrderDecision,
- Self::OrderCancellation(_) => AppPublishWorkKind::OrderCancellation,
+ Self::TradeProposal(_) => AppPublishWorkKind::TradeProposal,
+ Self::TradeRevisionProposal(_) => AppPublishWorkKind::TradeRevisionProposal,
+ Self::TradeCandidateDecision(_) => AppPublishWorkKind::TradeCandidateDecision,
+ Self::TradeCancellation(_) => AppPublishWorkKind::TradeCancellation,
}
}
@@ -214,9 +174,10 @@ impl AppPublishPayload {
match self {
Self::FarmProfile(payload) => SyncAggregateRef::Farm(payload.farm_id),
Self::Listing(payload) => SyncAggregateRef::Product(payload.product_id),
- Self::OrderRequest(payload) => SyncAggregateRef::Order(payload.order_id),
- Self::OrderDecision(payload) => SyncAggregateRef::Order(payload.app_order_id),
- Self::OrderCancellation(payload) => SyncAggregateRef::Order(payload.app_order_id),
+ Self::TradeProposal(payload) => SyncAggregateRef::Order(payload.order_id),
+ Self::TradeRevisionProposal(payload) => SyncAggregateRef::Order(payload.order_id),
+ Self::TradeCandidateDecision(payload) => SyncAggregateRef::Order(payload.order_id),
+ Self::TradeCancellation(payload) => SyncAggregateRef::Order(payload.order_id),
}
}
@@ -291,110 +252,37 @@ impl AppPublishPayload {
failures.push(AppPublishValidationFailure::MissingListingFulfillmentLocation);
}
}
- Self::OrderRequest(payload) => {
+ Self::TradeProposal(payload) => {
payload.context.validation_failures(&mut failures);
- if payload.order_document_json.is_none() {
- failures.push(AppPublishValidationFailure::MissingOrderDocument);
- }
- if payload
- .listing_addr
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- {
- failures.push(AppPublishValidationFailure::MissingOrderListingAddress);
- }
- if payload
- .listing_event_id
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- {
- failures.push(AppPublishValidationFailure::MissingOrderListingEventId);
- }
- if payload
- .listing_relays
- .iter()
- .all(|relay| relay.trim().is_empty())
- {
- failures.push(AppPublishValidationFailure::MissingOrderListingRelay);
- }
- if payload
- .buyer_pubkey
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- {
- failures.push(AppPublishValidationFailure::MissingOrderBuyerPubkey);
- }
- if payload
- .seller_pubkey
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- {
- failures.push(AppPublishValidationFailure::MissingOrderSellerPubkey);
- }
- if payload.items.is_empty() || payload.items.iter().any(|item| item.quantity == 0) {
- failures.push(AppPublishValidationFailure::MissingOrderItems);
- }
- if payload
- .currency_code
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- {
- failures.push(AppPublishValidationFailure::MissingOrderCurrency);
- }
- if payload.total_minor_units.is_none() {
- failures.push(AppPublishValidationFailure::MissingOrderTotal);
- }
+ validate_trade_envelope_kind(
+ &payload.envelope,
+ TradeMutationPayloadKind::Proposal,
+ &mut failures,
+ );
}
- Self::OrderDecision(payload) => {
+ Self::TradeRevisionProposal(payload) => {
payload.context.validation_failures(&mut failures);
- if payload.trade_order_id.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderTradeOrderId);
- }
- if payload.request_event_id.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderRequestEventId);
- }
- if payload.listing_addr.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderListingAddress);
- }
- if payload.buyer_pubkey.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderBuyerPubkey);
- }
- if payload.seller_pubkey.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderSellerPubkey);
- }
- match &payload.decision {
- AppOrderDecisionPayload::Accepted {
- inventory_commitments,
- } => {
- if inventory_commitments.is_empty()
- || inventory_commitments.iter().any(|commitment| {
- commitment.bin_id.trim().is_empty() || commitment.bin_count == 0
- })
- {
- failures
- .push(AppPublishValidationFailure::MissingOrderDecisionInventory);
- }
- }
- AppOrderDecisionPayload::Declined { reason } => {
- if reason.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderDeclineReason);
- }
- }
- }
+ validate_trade_envelope_kind(
+ &payload.envelope,
+ TradeMutationPayloadKind::RevisionProposal,
+ &mut failures,
+ );
}
- Self::OrderCancellation(payload) => {
- validate_lifecycle_order_fields(
- &payload.context,
- payload.trade_order_id.as_str(),
- payload.request_event_id.as_str(),
- payload.listing_addr.as_str(),
- payload.buyer_pubkey.as_str(),
- payload.seller_pubkey.as_str(),
+ Self::TradeCandidateDecision(payload) => {
+ payload.context.validation_failures(&mut failures);
+ validate_trade_envelope_kind(
+ &payload.envelope,
+ TradeMutationPayloadKind::CandidateDecision,
+ &mut failures,
+ );
+ }
+ Self::TradeCancellation(payload) => {
+ payload.context.validation_failures(&mut failures);
+ validate_trade_envelope_kind(
+ &payload.envelope,
+ TradeMutationPayloadKind::Cancellation,
&mut failures,
);
- if payload.reason.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderCancellationReason);
- }
}
}
@@ -417,30 +305,36 @@ impl AppPublishPayload {
}
}
-fn validate_lifecycle_order_fields(
- context: &AppPublishContext,
- trade_order_id: &str,
- request_event_id: &str,
- listing_addr: &str,
- buyer_pubkey: &str,
- seller_pubkey: &str,
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum TradeMutationPayloadKind {
+ Proposal,
+ RevisionProposal,
+ CandidateDecision,
+ Cancellation,
+}
+
+fn validate_trade_envelope_kind(
+ envelope: &RadrootsTradeMutationEnvelopeV1,
+ expected: TradeMutationPayloadKind,
failures: &mut Vec<AppPublishValidationFailure>,
) {
- context.validation_failures(failures);
- if trade_order_id.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderTradeOrderId);
- }
- if request_event_id.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderRequestEventId);
- }
- if listing_addr.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderListingAddress);
+ if envelope.validate().is_err() {
+ failures.push(AppPublishValidationFailure::InvalidTradeMutationEnvelope);
+ return;
}
- if buyer_pubkey.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderBuyerPubkey);
- }
- if seller_pubkey.trim().is_empty() {
- failures.push(AppPublishValidationFailure::MissingOrderSellerPubkey);
+ let actual = match &envelope.body {
+ RadrootsTradeMutationBodyV1::Proposal { .. } => TradeMutationPayloadKind::Proposal,
+ RadrootsTradeMutationBodyV1::RevisionProposal { .. } => {
+ TradeMutationPayloadKind::RevisionProposal
+ }
+ RadrootsTradeMutationBodyV1::Decision { .. }
+ | RadrootsTradeMutationBodyV1::RevisionDecision { .. } => {
+ TradeMutationPayloadKind::CandidateDecision
+ }
+ RadrootsTradeMutationBodyV1::Cancellation { .. } => TradeMutationPayloadKind::Cancellation,
+ };
+ if actual != expected {
+ failures.push(AppPublishValidationFailure::WrongTradeMutationKind);
}
}
@@ -461,20 +355,8 @@ pub enum AppPublishValidationFailure {
MissingListingStock,
MissingListingFulfillmentMethod,
MissingListingFulfillmentLocation,
- MissingOrderDocument,
- MissingOrderListingAddress,
- MissingOrderListingEventId,
- MissingOrderListingRelay,
- MissingOrderBuyerPubkey,
- MissingOrderSellerPubkey,
- MissingOrderItems,
- MissingOrderCurrency,
- MissingOrderTotal,
- MissingOrderTradeOrderId,
- MissingOrderRequestEventId,
- MissingOrderDecisionInventory,
- MissingOrderDeclineReason,
- MissingOrderCancellationReason,
+ InvalidTradeMutationEnvelope,
+ WrongTradeMutationKind,
}
impl AppPublishValidationFailure {
@@ -494,20 +376,8 @@ impl AppPublishValidationFailure {
Self::MissingListingStock => "missing_listing_stock",
Self::MissingListingFulfillmentMethod => "missing_listing_fulfillment_method",
Self::MissingListingFulfillmentLocation => "missing_listing_fulfillment_location",
- Self::MissingOrderDocument => "missing_order_document",
- Self::MissingOrderListingAddress => "missing_order_listing_address",
- Self::MissingOrderListingEventId => "missing_order_listing_event_id",
- Self::MissingOrderListingRelay => "missing_order_listing_relay",
- Self::MissingOrderBuyerPubkey => "missing_order_buyer_pubkey",
- Self::MissingOrderSellerPubkey => "missing_order_seller_pubkey",
- Self::MissingOrderItems => "missing_order_items",
- Self::MissingOrderCurrency => "missing_order_currency",
- Self::MissingOrderTotal => "missing_order_total",
- Self::MissingOrderTradeOrderId => "missing_order_trade_order_id",
- Self::MissingOrderRequestEventId => "missing_order_request_event_id",
- Self::MissingOrderDecisionInventory => "missing_order_decision_inventory",
- Self::MissingOrderDeclineReason => "missing_order_decline_reason",
- Self::MissingOrderCancellationReason => "missing_order_cancellation_reason",
+ Self::InvalidTradeMutationEnvelope => "invalid_trade_mutation_envelope",
+ Self::WrongTradeMutationKind => "wrong_trade_mutation_kind",
}
}
}
@@ -559,15 +429,31 @@ impl PendingSyncOperation {
#[cfg(test)]
mod tests {
use super::{
- AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload,
- AppOrderDecisionPayload, AppOrderDecisionPublishPayload, AppOrderRequestItemPayload,
- AppOrderRequestPublishPayload, AppPublishContext, AppPublishPayload,
- AppPublishValidationFailure, AppPublishWorkKind, FARM_PUBLISH_OPERATION_KIND,
- TRADE_CANCELLATION_OPERATION_KIND, TRADE_DECISION_OPERATION_KIND,
+ AppFarmProfilePublishPayload, AppListingPublishPayload, AppPublishContext,
+ AppPublishPayload, AppPublishValidationFailure, AppPublishWorkKind,
+ AppTradeCancellationPublishPayload, AppTradeCandidateDecisionPublishPayload,
+ AppTradeProposalPublishPayload, AppTradeRevisionProposalPublishPayload,
+ FARM_PUBLISH_OPERATION_KIND, TRADE_CANCEL_OPERATION_KIND,
+ TRADE_DECIDE_CANDIDATE_OPERATION_KIND, TRADE_PROPOSE_REVISION_OPERATION_KIND,
};
use crate::{
PendingSyncOperation, PendingSyncOperationState, SyncAggregateRef, SyncOperationKind,
};
+ use radroots_event::{
+ ids::{
+ RadrootsAddressableCoordinate, RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId,
+ RadrootsPublicKey, RadrootsTradeId,
+ },
+ trade::{
+ RADROOTS_TRADE_CANCELLATION_CONTRACT_ID, RADROOTS_TRADE_DECISION_CONTRACT_ID,
+ RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID,
+ RADROOTS_TRADE_SCHEMA_VERSION, RadrootsFulfillmentProfileV1,
+ RadrootsTradeCancellationProfileV1, RadrootsTradeCandidateLineV1,
+ RadrootsTradeCandidateTermsV1, RadrootsTradeDecisionV1,
+ RadrootsTradeEconomicsProfileV1, RadrootsTradeMutationBodyV1,
+ RadrootsTradeMutationEnvelopeV1, canonical_trade_mutation_content,
+ },
+ };
use radroots_studio_app_view::{FarmId, FarmReadiness, OrderId, ProductId, ProductStatus};
#[test]
@@ -609,17 +495,19 @@ mod tests {
let work_kinds = [
AppPublishWorkKind::FarmProfile,
AppPublishWorkKind::Listing,
- AppPublishWorkKind::OrderRequest,
- AppPublishWorkKind::OrderDecision,
- AppPublishWorkKind::OrderCancellation,
+ AppPublishWorkKind::TradeProposal,
+ AppPublishWorkKind::TradeRevisionProposal,
+ AppPublishWorkKind::TradeCandidateDecision,
+ AppPublishWorkKind::TradeCancellation,
];
- assert_eq!(work_kinds.len(), 5);
+ assert_eq!(work_kinds.len(), 6);
assert_eq!(work_kinds[0].storage_key(), "farm_profile");
assert_eq!(work_kinds[1].storage_key(), "listing");
- assert_eq!(work_kinds[2].storage_key(), "order_request");
- assert_eq!(work_kinds[3].storage_key(), "order_decision");
- assert_eq!(work_kinds[4].storage_key(), "order_cancellation");
+ assert_eq!(work_kinds[2].storage_key(), "trade_proposal");
+ assert_eq!(work_kinds[3].storage_key(), "trade_revision_proposal");
+ assert_eq!(work_kinds[4].storage_key(), "trade_candidate_decision");
+ assert_eq!(work_kinds[5].storage_key(), "trade_cancellation");
}
#[test]
@@ -673,73 +561,108 @@ mod tests {
}
#[test]
- fn order_request_publish_payload_requires_sdk_publish_inputs() {
- let payload = AppPublishPayload::OrderRequest(AppOrderRequestPublishPayload {
- context: AppPublishContext::new("acct_buyer", "place_personal_order"),
- order_id: OrderId::generate(),
+ fn trade_proposal_publish_payload_uses_sdk_envelope() {
+ let order_id = OrderId::generate();
+ let proposal = proposal_envelope();
+ let payload = AppPublishPayload::TradeProposal(AppTradeProposalPublishPayload {
+ context: AppPublishContext::new("acct_buyer", "trade_proposal"),
+ order_id,
farm_id: FarmId::generate(),
- status: Some("needs_action".to_owned()),
- order_document_json: None,
- listing_addr: Some(String::new()),
- listing_event_id: None,
- listing_relays: vec![],
- buyer_pubkey: None,
- seller_pubkey: Some(" ".to_owned()),
- items: vec![AppOrderRequestItemPayload {
- product_id: ProductId::generate(),
- quantity: 0,
- }],
- currency_code: None,
- total_minor_units: None,
- note: None,
- confirm_public_note: false,
+ envelope: proposal.clone(),
});
- let reason_codes: Vec<&str> = payload
- .validation_failures()
- .into_iter()
- .map(AppPublishValidationFailure::storage_key)
- .collect();
+ assert_eq!(payload.validation_failures(), Vec::new());
+ assert_eq!(payload.work_kind().storage_key(), "trade_proposal");
+ assert_eq!(
+ payload.work_kind().sdk_operation(),
+ super::TRADE_SUBMIT_PROPOSAL_OPERATION_KIND
+ );
+ let operation =
+ PendingSyncOperation::from_publish_payload(payload.clone(), "2026-04-20T18:00:00Z")
+ .expect("typed trade payload should serialize");
+
+ assert_eq!(operation.aggregate, SyncAggregateRef::Order(order_id));
+ assert_eq!(operation.operation_key, format!("order:{order_id}:upsert"));
assert_eq!(
- reason_codes,
- vec![
- "missing_order_document",
- "missing_order_listing_address",
- "missing_order_listing_event_id",
- "missing_order_listing_relay",
- "missing_order_buyer_pubkey",
- "missing_order_seller_pubkey",
- "missing_order_items",
- "missing_order_currency",
- "missing_order_total",
- ]
+ operation.publish_payload().expect("payload should parse"),
+ AppPublishPayload::TradeProposal(AppTradeProposalPublishPayload {
+ context: AppPublishContext::new("acct_buyer", "trade_proposal"),
+ order_id,
+ farm_id: match payload {
+ AppPublishPayload::TradeProposal(payload) => payload.farm_id,
+ _ => unreachable!(),
+ },
+ envelope: proposal,
+ })
);
}
#[test]
- fn order_decision_publish_payload_reports_stable_validation_reason_codes() {
- let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload {
+ fn trade_revision_and_decision_payloads_validate_expected_mutation_kind() {
+ let order_id = OrderId::generate();
+ let farm_id = FarmId::generate();
+ let proposal = proposal_envelope();
+ let revision = revision_proposal_envelope(&proposal);
+ let decision = decision_envelope(&revision);
+
+ let revision_payload =
+ AppPublishPayload::TradeRevisionProposal(AppTradeRevisionProposalPublishPayload {
+ context: AppPublishContext::new("acct_buyer", "trade_revision_proposal"),
+ order_id,
+ farm_id,
+ envelope: revision,
+ });
+ assert_eq!(revision_payload.validation_failures(), Vec::new());
+ assert_eq!(
+ revision_payload.work_kind().sdk_operation(),
+ TRADE_PROPOSE_REVISION_OPERATION_KIND
+ );
+
+ let decision_payload =
+ AppPublishPayload::TradeCandidateDecision(AppTradeCandidateDecisionPublishPayload {
+ context: AppPublishContext::new("acct_seller", "trade_candidate_decision"),
+ order_id,
+ farm_id,
+ envelope: decision,
+ acknowledge_private_terms: true,
+ });
+ assert_eq!(decision_payload.validation_failures(), Vec::new());
+ assert_eq!(
+ decision_payload.work_kind().sdk_operation(),
+ TRADE_DECIDE_CANDIDATE_OPERATION_KIND
+ );
+
+ let wrong_kind =
+ AppPublishPayload::TradeCandidateDecision(AppTradeCandidateDecisionPublishPayload {
+ context: AppPublishContext::new("acct_seller", "trade_candidate_decision"),
+ order_id,
+ farm_id,
+ envelope: proposal_envelope(),
+ acknowledge_private_terms: false,
+ });
+
+ assert_eq!(
+ wrong_kind
+ .validation_failures()
+ .into_iter()
+ .map(AppPublishValidationFailure::storage_key)
+ .collect::<Vec<_>>(),
+ vec!["wrong_trade_mutation_kind"]
+ );
+ }
+
+ #[test]
+ fn trade_payload_reports_stable_invalid_envelope_reason_code() {
+ let mut envelope = proposal_envelope();
+ envelope.contract_id = RADROOTS_TRADE_DECISION_CONTRACT_ID.to_owned();
+ let payload = AppPublishPayload::TradeProposal(AppTradeProposalPublishPayload {
context: AppPublishContext::new("", ""),
- app_order_id: OrderId::generate(),
+ order_id: OrderId::generate(),
farm_id: FarmId::generate(),
- trade_order_id: " ".to_owned(),
- request_event_id: String::new(),
- listing_event_id: None,
- listing_addr: String::new(),
- buyer_pubkey: String::new(),
- seller_pubkey: String::new(),
- decision: AppOrderDecisionPayload::Declined {
- reason: " ".to_owned(),
- },
- confirm_public_note: false,
+ envelope,
});
- assert_eq!(payload.work_kind().storage_key(), "order_decision");
- assert_eq!(
- payload.work_kind().sdk_operation(),
- TRADE_DECISION_OPERATION_KIND
- );
let reason_codes: Vec<&str> = payload
.validation_failures()
.into_iter()
@@ -751,92 +674,45 @@ mod tests {
vec![
"missing_account_id",
"missing_source",
- "missing_order_trade_order_id",
- "missing_order_request_event_id",
- "missing_order_listing_address",
- "missing_order_buyer_pubkey",
- "missing_order_seller_pubkey",
- "missing_order_decline_reason",
+ "invalid_trade_mutation_envelope",
]
);
}
#[test]
- fn cancellation_publish_payload_reports_stable_validation_reason_codes() {
+ fn cancellation_publish_payload_round_trips_semantic_envelope() {
let order_id = OrderId::generate();
let farm_id = FarmId::generate();
+ let proposal = proposal_envelope();
+ let cancellation_envelope = cancellation_envelope(&proposal);
let cancellation =
- AppPublishPayload::OrderCancellation(AppOrderCancellationPublishPayload {
- context: AppPublishContext::new("", ""),
- app_order_id: order_id,
+ AppPublishPayload::TradeCancellation(AppTradeCancellationPublishPayload {
+ context: AppPublishContext::new("acct_buyer", "trade_cancellation"),
+ order_id,
farm_id,
- trade_order_id: " ".to_owned(),
- request_event_id: String::new(),
- listing_addr: String::new(),
- buyer_pubkey: String::new(),
- seller_pubkey: String::new(),
- reason: " ".to_owned(),
- confirm_public_note: false,
+ envelope: cancellation_envelope.clone(),
});
assert_eq!(
cancellation.work_kind().sdk_operation(),
- TRADE_CANCELLATION_OPERATION_KIND
+ TRADE_CANCEL_OPERATION_KIND
);
+ assert_eq!(cancellation.validation_failures(), Vec::new());
- let cancellation_reason_codes: Vec<&str> = cancellation
- .validation_failures()
- .into_iter()
- .map(AppPublishValidationFailure::storage_key)
- .collect();
-
- assert_eq!(
- cancellation_reason_codes,
- vec![
- "missing_account_id",
- "missing_source",
- "missing_order_trade_order_id",
- "missing_order_request_event_id",
- "missing_order_listing_address",
- "missing_order_buyer_pubkey",
- "missing_order_seller_pubkey",
- "missing_order_cancellation_reason",
- ]
- );
-
- let operation = PendingSyncOperation::from_publish_payload(
- AppPublishPayload::OrderCancellation(AppOrderCancellationPublishPayload {
- context: AppPublishContext::new("acct_local", "buyer_order_cancellation"),
- app_order_id: order_id,
- farm_id,
- trade_order_id: "order-1".to_owned(),
- request_event_id: "request-event-1".to_owned(),
- listing_addr: "30402:seller:listing".to_owned(),
- buyer_pubkey: "buyer".to_owned(),
- seller_pubkey: "seller".to_owned(),
- reason: "buyer cancelled order".to_owned(),
- confirm_public_note: false,
- }),
- "2026-04-20T18:00:00Z",
- )
- .expect("typed lifecycle payload should serialize");
+ let operation =
+ PendingSyncOperation::from_publish_payload(cancellation, "2026-04-20T18:00:00Z")
+ .expect("typed lifecycle payload should serialize");
assert_eq!(operation.aggregate, SyncAggregateRef::Order(order_id));
assert_eq!(operation.operation_key, format!("order:{order_id}:upsert"));
assert_eq!(operation.operation, SyncOperationKind::Upsert);
assert_eq!(
operation.publish_payload().expect("payload should parse"),
- AppPublishPayload::OrderCancellation(AppOrderCancellationPublishPayload {
- context: AppPublishContext::new("acct_local", "buyer_order_cancellation"),
- app_order_id: order_id,
+ AppPublishPayload::TradeCancellation(AppTradeCancellationPublishPayload {
+ context: AppPublishContext::new("acct_buyer", "trade_cancellation"),
+ order_id,
farm_id,
- trade_order_id: "order-1".to_owned(),
- request_event_id: "request-event-1".to_owned(),
- listing_addr: "30402:seller:listing".to_owned(),
- buyer_pubkey: "buyer".to_owned(),
- seller_pubkey: "seller".to_owned(),
- reason: "buyer cancelled order".to_owned(),
- confirm_public_note: false,
+ envelope: cancellation_envelope,
})
);
}
@@ -858,4 +734,205 @@ mod tests {
assert!(!pending_operation.is_retry());
assert!(pending_operation.publish_payload().is_err());
}
+
+ fn hex_64(character: char) -> String {
+ core::iter::repeat_n(character, 64).collect()
+ }
+
+ fn hex_32(character: char) -> String {
+ core::iter::repeat_n(character, 32).collect()
+ }
+
+ fn pubkey(character: char) -> RadrootsPublicKey {
+ RadrootsPublicKey::parse(hex_64(character)).expect("pubkey")
+ }
+
+ fn event_id(character: char) -> RadrootsEventId {
+ RadrootsEventId::parse(hex_64(character)).expect("event id")
+ }
+
+ fn trade_id() -> RadrootsTradeId {
+ RadrootsTradeId::parse(hex_32('1')).expect("trade id")
+ }
+
+ fn candidate() -> RadrootsTradeCandidateTermsV1 {
+ RadrootsTradeCandidateTermsV1 {
+ candidate_id: None,
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ base_candidate_id: None,
+ supersession_intent: None,
+ buyer_pubkey: pubkey('a'),
+ seller_pubkey: pubkey('b'),
+ farm_id: RadrootsDTag::parse("farm-1").expect("farm d tag"),
+ lines: vec![RadrootsTradeCandidateLineV1 {
+ line_id: RadrootsDTag::parse("line-1").expect("line d tag"),
+ listing_addr: RadrootsAddressableCoordinate::parse(format!(
+ "30402:{}:listing-1",
+ hex_64('b')
+ ))
+ .expect("listing addr"),
+ listing_event_id: event_id('c'),
+ listing_snapshot_sha256: hex_64('d'),
+ product_id: "carrots".to_owned(),
+ option_id: None,
+ bin_id: RadrootsInventoryBinId::parse("bin-1").expect("inventory bin"),
+ quantity_mantissa: "2".to_owned(),
+ quantity_scale: 0,
+ unit_code: "count".to_owned(),
+ unit_profile: "mvp-count".to_owned(),
+ unit_price_mantissa: "500".to_owned(),
+ currency_code: "USD".to_owned(),
+ line_subtotal_mantissa: "1000".to_owned(),
+ replaces_line_id: None,
+ }],
+ line_tombstones: Vec::new(),
+ economics: RadrootsTradeEconomicsProfileV1 {
+ profile_id: "mvp-fixed".to_owned(),
+ currency_code: "USD".to_owned(),
+ currency_exponent: 2,
+ rounding_profile: "half-even".to_owned(),
+ subtotal_mantissa: "1000".to_owned(),
+ discount_total_mantissa: "0".to_owned(),
+ adjustment_total_mantissa: "0".to_owned(),
+ total_mantissa: "1000".to_owned(),
+ adjustments: Vec::new(),
+ },
+ fulfillment: RadrootsFulfillmentProfileV1 {
+ profile_id: "market-pickup".to_owned(),
+ method: "pickup".to_owned(),
+ starts_at_unix_s: 1_800_000_000,
+ ends_at_unix_s: 1_800_003_600,
+ timezone: "America/New_York".to_owned(),
+ utc_offset_seconds: -18_000,
+ fold: 0,
+ location_class: "farmstand".to_owned(),
+ requires_private_terms: false,
+ },
+ cancellation: RadrootsTradeCancellationProfileV1 {
+ profile_id: "buyer-pre-agreement".to_owned(),
+ buyer_pre_agreement: true,
+ post_agreement_cutoff_unix_s: None,
+ },
+ private_terms: None,
+ proposal_expires_at_unix_s: 1_799_999_000,
+ }
+ }
+
+ fn proposal_envelope() -> RadrootsTradeMutationEnvelopeV1 {
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: trade_id(),
+ root_mutation_id: None,
+ buyer_pubkey: pubkey('a'),
+ seller_pubkey: pubkey('b'),
+ farm_id: RadrootsDTag::parse("farm-1").expect("farm d tag"),
+ parent_mutation_ids: Vec::new(),
+ author_pubkey: pubkey('a'),
+ counterparty_pubkey: pubkey('b'),
+ authored_at_unix_s: 1_799_000_000,
+ body: RadrootsTradeMutationBodyV1::Proposal {
+ candidate: candidate(),
+ },
+ })
+ .expect("canonical proposal")
+ .envelope
+ }
+
+ fn revision_proposal_envelope(
+ root: &RadrootsTradeMutationEnvelopeV1,
+ ) -> RadrootsTradeMutationEnvelopeV1 {
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: root.trade_id.clone(),
+ root_mutation_id: root.mutation_id.clone(),
+ buyer_pubkey: pubkey('a'),
+ seller_pubkey: pubkey('b'),
+ farm_id: RadrootsDTag::parse("farm-1").expect("farm d tag"),
+ parent_mutation_ids: vec![root.mutation_id.clone().expect("root mutation id")],
+ author_pubkey: pubkey('a'),
+ counterparty_pubkey: pubkey('b'),
+ authored_at_unix_s: 1_799_000_100,
+ body: RadrootsTradeMutationBodyV1::RevisionProposal {
+ candidate: candidate(),
+ },
+ })
+ .expect("canonical revision")
+ .envelope
+ }
+
+ fn decision_envelope(
+ proposal: &RadrootsTradeMutationEnvelopeV1,
+ ) -> RadrootsTradeMutationEnvelopeV1 {
+ let proposal_mutation_id = proposal.mutation_id.clone().expect("proposal mutation id");
+ let candidate_id = match &proposal.body {
+ RadrootsTradeMutationBodyV1::Proposal { candidate }
+ | RadrootsTradeMutationBodyV1::RevisionProposal { candidate } => {
+ candidate.candidate_id.clone().expect("candidate id")
+ }
+ _ => unreachable!(),
+ };
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_DECISION_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: proposal.trade_id.clone(),
+ root_mutation_id: proposal
+ .root_mutation_id
+ .clone()
+ .or(proposal.mutation_id.clone()),
+ buyer_pubkey: pubkey('a'),
+ seller_pubkey: pubkey('b'),
+ farm_id: RadrootsDTag::parse("farm-1").expect("farm d tag"),
+ parent_mutation_ids: vec![proposal_mutation_id.clone()],
+ author_pubkey: pubkey('b'),
+ counterparty_pubkey: pubkey('a'),
+ authored_at_unix_s: 1_799_000_200,
+ body: RadrootsTradeMutationBodyV1::Decision {
+ proposal_mutation_id,
+ candidate_id,
+ decision: RadrootsTradeDecisionV1::Declined {
+ reason: "not available".to_owned(),
+ },
+ },
+ })
+ .expect("canonical decision")
+ .envelope
+ }
+
+ fn cancellation_envelope(
+ root: &RadrootsTradeMutationEnvelopeV1,
+ ) -> RadrootsTradeMutationEnvelopeV1 {
+ let root_mutation_id = root.mutation_id.clone().expect("root mutation id");
+ let candidate_id = match &root.body {
+ RadrootsTradeMutationBodyV1::Proposal { candidate } => {
+ candidate.candidate_id.clone().expect("candidate id")
+ }
+ _ => unreachable!(),
+ };
+ canonical_trade_mutation_content(RadrootsTradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: RADROOTS_TRADE_CANCELLATION_CONTRACT_ID.to_owned(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: root.trade_id.clone(),
+ root_mutation_id: Some(root_mutation_id.clone()),
+ buyer_pubkey: pubkey('a'),
+ seller_pubkey: pubkey('b'),
+ farm_id: RadrootsDTag::parse("farm-1").expect("farm d tag"),
+ parent_mutation_ids: vec![root_mutation_id],
+ author_pubkey: pubkey('a'),
+ counterparty_pubkey: pubkey('b'),
+ authored_at_unix_s: 1_799_000_300,
+ body: RadrootsTradeMutationBodyV1::Cancellation {
+ target_candidate_id: Some(candidate_id),
+ target_claim_mutation_id: None,
+ reason: "buyer cancelled".to_owned(),
+ },
+ })
+ .expect("canonical cancellation")
+ .envelope
+ }
}
diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs
@@ -8,7 +8,11 @@ use radroots_trade::validation_receipt::{
RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult,
RadrootsValidationReceiptType,
};
-use radroots_trade::{order::RadrootsOrderProjection, workflow::RadrootsTradeWorkflowState};
+use radroots_trade::workflow::{
+ RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, RadrootsTradeConflictStateV1,
+ RadrootsTradeEvidenceStateV1, RadrootsTradeNegotiationStateV1,
+ RadrootsTradePrivateTermsStateV1, RadrootsTradeProjectionV1,
+};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeSet, error::Error, fmt, str::FromStr};
use url::Url;
@@ -1076,11 +1080,10 @@ pub struct BuyerOrderReviewProjection {
pub enum TradeAgreementStatus {
#[default]
Requested,
- AgreedPendingValidation,
Committed,
+ Contested,
Declined,
Cancelled,
- ValidationExpired,
Invalid,
}
@@ -1088,11 +1091,10 @@ impl TradeAgreementStatus {
pub const fn storage_key(self) -> &'static str {
match self {
Self::Requested => "requested",
- Self::AgreedPendingValidation => "agreed_pending_validation",
Self::Committed => "committed",
+ Self::Contested => "contested",
Self::Declined => "declined",
Self::Cancelled => "cancelled",
- Self::ValidationExpired => "validation_expired",
Self::Invalid => "invalid",
}
}
@@ -1100,34 +1102,32 @@ impl TradeAgreementStatus {
pub const fn label_key_id(self) -> &'static str {
match self {
Self::Requested => "messages.trade.workflow.agreement.requested",
- Self::AgreedPendingValidation => {
- "messages.trade.workflow.agreement.agreed_pending_validation"
- }
Self::Committed => "messages.trade.workflow.agreement.committed",
+ Self::Contested => "messages.trade.workflow.agreement.contested",
Self::Declined => "messages.trade.workflow.agreement.declined",
Self::Cancelled => "messages.trade.workflow.agreement.cancelled",
- Self::ValidationExpired => "messages.trade.workflow.agreement.validation_expired",
Self::Invalid => "messages.trade.workflow.agreement.invalid",
}
}
- pub const fn from_active_order_status(status: &RadrootsTradeWorkflowState) -> Self {
- match status {
- RadrootsTradeWorkflowState::Missing => Self::Invalid,
- RadrootsTradeWorkflowState::Requested => Self::Requested,
- RadrootsTradeWorkflowState::AgreedPendingValidation => Self::AgreedPendingValidation,
- RadrootsTradeWorkflowState::Committed => Self::Committed,
- RadrootsTradeWorkflowState::Declined => Self::Declined,
- RadrootsTradeWorkflowState::Cancelled => Self::Cancelled,
- RadrootsTradeWorkflowState::ValidationExpired => Self::ValidationExpired,
- RadrootsTradeWorkflowState::Invalid => Self::Invalid,
+ pub const fn from_trade_projection(projection: &RadrootsTradeProjectionV1) -> Self {
+ match projection.agreement_state {
+ RadrootsTradeAgreementStateV1::Agreed => Self::Committed,
+ RadrootsTradeAgreementStateV1::Contested => Self::Contested,
+ RadrootsTradeAgreementStateV1::Cancelled => Self::Cancelled,
+ RadrootsTradeAgreementStateV1::None => match projection.negotiation_state {
+ RadrootsTradeNegotiationStateV1::Open => Self::Requested,
+ RadrootsTradeNegotiationStateV1::ClosedDeclined => Self::Declined,
+ RadrootsTradeNegotiationStateV1::ClosedExpired => Self::Invalid,
+ RadrootsTradeNegotiationStateV1::None => Self::Invalid,
+ },
}
}
}
-impl From<&RadrootsTradeWorkflowState> for TradeAgreementStatus {
- fn from(status: &RadrootsTradeWorkflowState) -> Self {
- Self::from_active_order_status(status)
+impl From<&RadrootsTradeProjectionV1> for TradeAgreementStatus {
+ fn from(projection: &RadrootsTradeProjectionV1) -> Self {
+ Self::from_trade_projection(projection)
}
}
@@ -1221,17 +1221,214 @@ impl TradeInventoryStatus {
}
}
- pub fn from_active_order_projection(projection: &RadrootsOrderProjection) -> Self {
- match projection.status {
- RadrootsTradeWorkflowState::Requested => Self::NeedsReview,
- RadrootsTradeWorkflowState::AgreedPendingValidation
- | RadrootsTradeWorkflowState::Committed => Self::Reserved,
- RadrootsTradeWorkflowState::Declined | RadrootsTradeWorkflowState::Cancelled => {
- Self::Available
+ pub const fn from_trade_projection(projection: &RadrootsTradeProjectionV1) -> Self {
+ match projection.agreement_state {
+ RadrootsTradeAgreementStateV1::Agreed => match (
+ projection.evidence_state,
+ projection.conflict_state,
+ projection.private_terms_state,
+ ) {
+ (
+ RadrootsTradeEvidenceStateV1::Complete,
+ RadrootsTradeConflictStateV1::None,
+ RadrootsTradePrivateTermsStateV1::NotRequired
+ | RadrootsTradePrivateTermsStateV1::AvailableVerified,
+ ) => Self::Reserved,
+ _ => Self::NeedsReview,
+ },
+ RadrootsTradeAgreementStateV1::Cancelled => Self::Available,
+ RadrootsTradeAgreementStateV1::None => match projection.negotiation_state {
+ RadrootsTradeNegotiationStateV1::ClosedDeclined
+ | RadrootsTradeNegotiationStateV1::ClosedExpired => Self::Available,
+ RadrootsTradeNegotiationStateV1::None | RadrootsTradeNegotiationStateV1::Open => {
+ Self::NeedsReview
+ }
+ },
+ RadrootsTradeAgreementStateV1::Contested => Self::NeedsReview,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeEvidenceStatus {
+ Complete,
+ #[default]
+ Missing,
+ QueryPartial,
+ UnsupportedVersion,
+}
+
+impl TradeEvidenceStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::Complete => "complete",
+ Self::Missing => "missing",
+ Self::QueryPartial => "query_partial",
+ Self::UnsupportedVersion => "unsupported_version",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::Complete => "messages.trade.workflow.evidence.complete",
+ Self::Missing => "messages.trade.workflow.evidence.missing",
+ Self::QueryPartial => "messages.trade.workflow.evidence.query_partial",
+ Self::UnsupportedVersion => "messages.trade.workflow.evidence.unsupported_version",
+ }
+ }
+}
+
+impl From<RadrootsTradeEvidenceStateV1> for TradeEvidenceStatus {
+ fn from(value: RadrootsTradeEvidenceStateV1) -> Self {
+ match value {
+ RadrootsTradeEvidenceStateV1::Complete => Self::Complete,
+ RadrootsTradeEvidenceStateV1::Missing => Self::Missing,
+ RadrootsTradeEvidenceStateV1::QueryPartial => Self::QueryPartial,
+ RadrootsTradeEvidenceStateV1::UnsupportedVersion => Self::UnsupportedVersion,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeConflictStatus {
+ #[default]
+ None,
+ ConcurrentCandidates,
+ DoubleAcceptance,
+ DecisionConflict,
+ CancellationConflict,
+ InvalidCausalChain,
+ InventoryAuthorityConflict,
+}
+
+impl TradeConflictStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::None => "none",
+ Self::ConcurrentCandidates => "concurrent_candidates",
+ Self::DoubleAcceptance => "double_acceptance",
+ Self::DecisionConflict => "decision_conflict",
+ Self::CancellationConflict => "cancellation_conflict",
+ Self::InvalidCausalChain => "invalid_causal_chain",
+ Self::InventoryAuthorityConflict => "inventory_authority_conflict",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::None => "messages.trade.workflow.conflict.none",
+ Self::ConcurrentCandidates => "messages.trade.workflow.conflict.concurrent_candidates",
+ Self::DoubleAcceptance => "messages.trade.workflow.conflict.double_acceptance",
+ Self::DecisionConflict => "messages.trade.workflow.conflict.decision_conflict",
+ Self::CancellationConflict => "messages.trade.workflow.conflict.cancellation_conflict",
+ Self::InvalidCausalChain => "messages.trade.workflow.conflict.invalid_causal_chain",
+ Self::InventoryAuthorityConflict => {
+ "messages.trade.workflow.conflict.inventory_authority_conflict"
}
- RadrootsTradeWorkflowState::Missing
- | RadrootsTradeWorkflowState::ValidationExpired
- | RadrootsTradeWorkflowState::Invalid => Self::NeedsReview,
+ }
+ }
+}
+
+impl From<RadrootsTradeConflictStateV1> for TradeConflictStatus {
+ fn from(value: RadrootsTradeConflictStateV1) -> Self {
+ match value {
+ RadrootsTradeConflictStateV1::None => Self::None,
+ RadrootsTradeConflictStateV1::ConcurrentCandidates => Self::ConcurrentCandidates,
+ RadrootsTradeConflictStateV1::DoubleAcceptance => Self::DoubleAcceptance,
+ RadrootsTradeConflictStateV1::DecisionConflict => Self::DecisionConflict,
+ RadrootsTradeConflictStateV1::CancellationConflict => Self::CancellationConflict,
+ RadrootsTradeConflictStateV1::InvalidCausalChain => Self::InvalidCausalChain,
+ RadrootsTradeConflictStateV1::InventoryAuthorityConflict => {
+ Self::InventoryAuthorityConflict
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradePrivateTermsStatus {
+ #[default]
+ NotRequired,
+ AvailableVerified,
+ Missing,
+ Undecryptable,
+ CommitmentMismatch,
+}
+
+impl TradePrivateTermsStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::NotRequired => "not_required",
+ Self::AvailableVerified => "available_verified",
+ Self::Missing => "missing",
+ Self::Undecryptable => "undecryptable",
+ Self::CommitmentMismatch => "commitment_mismatch",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::NotRequired => "messages.trade.workflow.private_terms.not_required",
+ Self::AvailableVerified => "messages.trade.workflow.private_terms.available_verified",
+ Self::Missing => "messages.trade.workflow.private_terms.missing",
+ Self::Undecryptable => "messages.trade.workflow.private_terms.undecryptable",
+ Self::CommitmentMismatch => "messages.trade.workflow.private_terms.commitment_mismatch",
+ }
+ }
+}
+
+impl From<RadrootsTradePrivateTermsStateV1> for TradePrivateTermsStatus {
+ fn from(value: RadrootsTradePrivateTermsStateV1) -> Self {
+ match value {
+ RadrootsTradePrivateTermsStateV1::NotRequired => Self::NotRequired,
+ RadrootsTradePrivateTermsStateV1::AvailableVerified => Self::AvailableVerified,
+ RadrootsTradePrivateTermsStateV1::Missing => Self::Missing,
+ RadrootsTradePrivateTermsStateV1::Undecryptable => Self::Undecryptable,
+ RadrootsTradePrivateTermsStateV1::CommitmentMismatch => Self::CommitmentMismatch,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TradeAttestationStatus {
+ #[default]
+ None,
+ PresentValid,
+ PresentInvalid,
+ Conflicting,
+}
+
+impl TradeAttestationStatus {
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::None => "none",
+ Self::PresentValid => "present_valid",
+ Self::PresentInvalid => "present_invalid",
+ Self::Conflicting => "conflicting",
+ }
+ }
+
+ pub const fn label_key_id(self) -> &'static str {
+ match self {
+ Self::None => "messages.trade.workflow.attestation.none",
+ Self::PresentValid => "messages.trade.workflow.attestation.present_valid",
+ Self::PresentInvalid => "messages.trade.workflow.attestation.present_invalid",
+ Self::Conflicting => "messages.trade.workflow.attestation.conflicting",
+ }
+ }
+}
+
+impl From<RadrootsTradeAttestationStateV1> for TradeAttestationStatus {
+ fn from(value: RadrootsTradeAttestationStateV1) -> Self {
+ match value {
+ RadrootsTradeAttestationStateV1::None => Self::None,
+ RadrootsTradeAttestationStateV1::PresentValid => Self::PresentValid,
+ RadrootsTradeAttestationStateV1::PresentInvalid => Self::PresentInvalid,
+ RadrootsTradeAttestationStateV1::Conflicting => Self::Conflicting,
}
}
}
@@ -1472,6 +1669,11 @@ pub struct TradeWorkflowProjection {
pub revision: TradeRevisionStatus,
pub economics: TradeEconomicsProjection,
pub inventory: TradeInventoryStatus,
+ pub evidence: TradeEvidenceStatus,
+ pub conflict: TradeConflictStatus,
+ pub private_terms: TradePrivateTermsStatus,
+ pub attestation: TradeAttestationStatus,
+ pub projection_digest: Option<String>,
pub provenance: TradeProvenanceProjection,
}
@@ -1483,29 +1685,36 @@ impl TradeWorkflowProjection {
revision: TradeRevisionStatus::None,
economics: TradeEconomicsProjection::default(),
inventory: TradeInventoryStatus::NeedsReview,
+ evidence: TradeEvidenceStatus::Missing,
+ conflict: TradeConflictStatus::None,
+ private_terms: TradePrivateTermsStatus::NotRequired,
+ attestation: TradeAttestationStatus::None,
+ projection_digest: None,
provenance: TradeProvenanceProjection::default(),
}
}
- pub fn from_active_order_projection(
+ pub fn from_trade_projection(
order_id: OrderId,
- projection: &RadrootsOrderProjection,
+ projection: &RadrootsTradeProjectionV1,
+ economics: TradeEconomicsProjection,
revision: TradeRevisionStatus,
provenance: TradeProvenanceProjection,
) -> Self {
let mut workflow = Self::new(
order_id,
- TradeAgreementStatus::from_active_order_status(&projection.status),
+ TradeAgreementStatus::from_trade_projection(projection),
);
workflow.revision = revision;
- workflow.economics = projection
- .economics
- .as_ref()
- .map(TradeEconomicsProjection::from_trade_order_economics)
- .unwrap_or_default();
- workflow.inventory = TradeInventoryStatus::from_active_order_projection(projection);
- workflow.provenance = provenance
- .with_last_event_id(projection.last_event_id.as_ref().map(ToString::to_string));
+ workflow.economics = economics;
+ workflow.inventory = TradeInventoryStatus::from_trade_projection(projection);
+ workflow.evidence = projection.evidence_state.into();
+ workflow.conflict = projection.conflict_state.into();
+ workflow.private_terms = projection.private_terms_state.into();
+ workflow.attestation = projection.attestation_state.into();
+ workflow.projection_digest = (!projection.projection_digest.is_empty())
+ .then(|| projection.projection_digest.clone());
+ workflow.provenance = provenance;
workflow
}
@@ -1580,19 +1789,19 @@ impl TradeWorkflowProjection {
}
}
-pub fn order_status_from_active_order_projection(
- projection: &RadrootsOrderProjection,
+pub fn order_status_from_trade_projection(
+ projection: &RadrootsTradeProjectionV1,
) -> Option<OrderStatus> {
- match projection.status {
- RadrootsTradeWorkflowState::Missing => None,
- RadrootsTradeWorkflowState::Requested
- | RadrootsTradeWorkflowState::AgreedPendingValidation
- | RadrootsTradeWorkflowState::ValidationExpired => Some(OrderStatus::NeedsAction),
- RadrootsTradeWorkflowState::Committed => Some(OrderStatus::Scheduled),
- RadrootsTradeWorkflowState::Declined | RadrootsTradeWorkflowState::Cancelled => {
- Some(OrderStatus::Declined)
- }
- RadrootsTradeWorkflowState::Invalid => Some(OrderStatus::NeedsAction),
+ match projection.agreement_state {
+ RadrootsTradeAgreementStateV1::Agreed => Some(OrderStatus::Scheduled),
+ RadrootsTradeAgreementStateV1::Contested => Some(OrderStatus::NeedsAction),
+ RadrootsTradeAgreementStateV1::Cancelled => Some(OrderStatus::Declined),
+ RadrootsTradeAgreementStateV1::None => match projection.negotiation_state {
+ RadrootsTradeNegotiationStateV1::None => None,
+ RadrootsTradeNegotiationStateV1::Open => Some(OrderStatus::NeedsAction),
+ RadrootsTradeNegotiationStateV1::ClosedDeclined => Some(OrderStatus::Declined),
+ RadrootsTradeNegotiationStateV1::ClosedExpired => Some(OrderStatus::NeedsAction),
+ },
}
}
@@ -2128,8 +2337,7 @@ mod tests {
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
};
use radroots_event::ids::{
- RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
- RadrootsOrderQuoteId, RadrootsPublicKey,
+ RadrootsInventoryBinId, RadrootsOrderQuoteId, RadrootsPublicKey, RadrootsTradeId,
};
use radroots_event::order::{
RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderPricingBasis,
@@ -2138,7 +2346,13 @@ mod tests {
RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult,
RadrootsValidationReceiptType,
};
- use radroots_trade::{order::RadrootsOrderProjection, workflow::RadrootsTradeWorkflowState};
+ use radroots_trade::workflow::{
+ RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION,
+ RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1,
+ RadrootsTradeConflictStateV1, RadrootsTradeEvidenceStateV1,
+ RadrootsTradeFulfillmentStateV1, RadrootsTradeNegotiationStateV1,
+ RadrootsTradePaymentStateV1, RadrootsTradePrivateTermsStateV1, RadrootsTradeProjectionV1,
+ };
use super::{
AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface,
@@ -2173,11 +2387,12 @@ mod tests {
RepeatDemandHandoffProjection, SelectedAccountProjection, SelectedSurfaceProjection,
SettingsPreference, SettingsSection, ShellSection, StartupSignerEntryProjection,
StartupSignerSource, StartupSignerSourceKind, TodayAgendaProjection, TodaySetupTask,
- TodaySetupTaskKind, TodaySummary, TradeAgreementStatus, TradeEconomicsProjection,
- TradeInventoryStatus, TradeProvenanceProjection, TradeRevisionStatus,
+ TodaySetupTaskKind, TodaySummary, TradeAgreementStatus, TradeAttestationStatus,
+ TradeConflictStatus, TradeEconomicsProjection, TradeEvidenceStatus, TradeInventoryStatus,
+ TradePrivateTermsStatus, TradeProvenanceProjection, TradeRevisionStatus,
TradeValidationReceiptProofSystem, TradeValidationReceiptResult,
TradeValidationReceiptType, TradeWorkflowProjection, TradeWorkflowSource,
- order_status_from_active_order_projection,
+ order_status_from_trade_projection,
};
use std::{collections::BTreeSet, str::FromStr};
use uuid::Uuid;
@@ -2798,10 +3013,6 @@ mod tests {
RadrootsCoreMoney::new(test_decimal(raw), RadrootsCoreCurrency::USD)
}
- fn test_order_id(raw: &str) -> RadrootsOrderId {
- raw.parse().expect("test order id should parse")
- }
-
fn test_quote_id(raw: &str) -> RadrootsOrderQuoteId {
raw.parse().expect("test quote id should parse")
}
@@ -2810,16 +3021,12 @@ mod tests {
raw.parse().expect("test bin id should parse")
}
- fn test_event_id(raw: &str) -> RadrootsEventId {
- raw.parse().expect("test event id should parse")
- }
-
fn test_pubkey(raw: &str) -> RadrootsPublicKey {
raw.parse().expect("test pubkey should parse")
}
- fn test_listing_addr(raw: &str) -> RadrootsListingAddress {
- raw.parse().expect("test listing address should parse")
+ fn test_trade_id(raw: &str) -> RadrootsTradeId {
+ raw.parse().expect("test trade id should parse")
}
fn test_trade_economics() -> RadrootsOrderEconomics {
@@ -2846,68 +3053,51 @@ mod tests {
}
}
- fn test_active_order_projection(status: RadrootsTradeWorkflowState) -> RadrootsOrderProjection {
- RadrootsOrderProjection {
- order_id: test_order_id("ord_AAAAAAAAAAAAAAAAAAAAAg"),
- status,
- request_event_id: Some(test_event_id(
- "1111111111111111111111111111111111111111111111111111111111111111",
- )),
- decision_event_id: Some(test_event_id(
- "2222222222222222222222222222222222222222222222222222222222222222",
- )),
- validation_receipt_event_id: None,
- cancellation_event_id: None,
- lifecycle_terminal: false,
- economics: Some(test_trade_economics()),
- agreement_event_id: Some(test_event_id(
- "2222222222222222222222222222222222222222222222222222222222222222",
- )),
- pending_revision_event_id: None,
- pending_inventory_reservations: Vec::new(),
- committed_inventory_reservations: Vec::new(),
- listing_addr: Some(test_listing_addr(
- "30402:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:AAAAAAAAAAAAAAAAAAAAAg",
- )),
+ fn test_trade_projection(
+ agreement_state: RadrootsTradeAgreementStateV1,
+ negotiation_state: RadrootsTradeNegotiationStateV1,
+ evidence_state: RadrootsTradeEvidenceStateV1,
+ conflict_state: RadrootsTradeConflictStateV1,
+ private_terms_state: RadrootsTradePrivateTermsStateV1,
+ attestation_state: RadrootsTradeAttestationStateV1,
+ ) -> RadrootsTradeProjectionV1 {
+ RadrootsTradeProjectionV1 {
+ reducer_contract_id: RADROOTS_TRADE_REDUCER_CONTRACT_ID.to_owned(),
+ reducer_version: RADROOTS_TRADE_REDUCER_VERSION,
+ trade_id: test_trade_id("11111111111111111111111111111111"),
+ root_mutation_id: None,
buyer_pubkey: Some(test_pubkey(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)),
seller_pubkey: Some(test_pubkey(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
)),
- last_event_id: Some(test_event_id(
- "3333333333333333333333333333333333333333333333333333333333333333",
- )),
+ farm_id: None,
+ negotiation_state,
+ agreement_state,
+ evidence_state,
+ conflict_state,
+ private_terms_state,
+ attestation_state,
+ fulfillment_state: RadrootsTradeFulfillmentStateV1::NotStarted,
+ payment_state: RadrootsTradePaymentStateV1::NotTracked,
+ candidate_heads: Vec::new(),
+ agreement_claims: Vec::new(),
+ active_agreement_claim_ids: Vec::new(),
+ contested_claim_ids: Vec::new(),
+ cancelled_claim_ids: Vec::new(),
+ declined_candidate_ids: Vec::new(),
+ missing_parent_ids: Vec::new(),
+ missing_proposal_ids: Vec::new(),
+ unsupported_mutation_ids: Vec::new(),
issues: Vec::new(),
+ attestations: Vec::new(),
+ projection_digest: "projection-digest-v1".to_owned(),
}
}
#[test]
- fn trade_workflow_projection_maps_shared_active_order_projection_to_product_axes() {
- assert_eq!(
- TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Requested),
- TradeAgreementStatus::Requested
- );
- assert_eq!(
- TradeAgreementStatus::from_active_order_status(
- &RadrootsTradeWorkflowState::AgreedPendingValidation
- ),
- TradeAgreementStatus::AgreedPendingValidation
- );
- assert_eq!(
- TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Committed),
- TradeAgreementStatus::Committed
- );
- assert_eq!(
- TradeAgreementStatus::from_active_order_status(
- &RadrootsTradeWorkflowState::ValidationExpired
- ),
- TradeAgreementStatus::ValidationExpired
- );
- assert_eq!(
- TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Invalid),
- TradeAgreementStatus::Invalid
- );
+ fn trade_workflow_projection_maps_release_product_reducer_axes() {
assert_eq!(
TradeRevisionStatus::try_from_storage_key("none"),
Ok(TradeRevisionStatus::None)
@@ -2936,10 +3126,20 @@ mod tests {
);
let order_id = OrderId::generate();
- let active_order = test_active_order_projection(RadrootsTradeWorkflowState::Committed);
- let projection = TradeWorkflowProjection::from_active_order_projection(
+ let economics =
+ TradeEconomicsProjection::from_trade_order_economics(&test_trade_economics());
+ let agreed_projection = test_trade_projection(
+ RadrootsTradeAgreementStateV1::Agreed,
+ RadrootsTradeNegotiationStateV1::Open,
+ RadrootsTradeEvidenceStateV1::Complete,
+ RadrootsTradeConflictStateV1::None,
+ RadrootsTradePrivateTermsStateV1::AvailableVerified,
+ RadrootsTradeAttestationStateV1::PresentValid,
+ );
+ let projection = TradeWorkflowProjection::from_trade_projection(
order_id,
- &active_order,
+ &agreed_projection,
+ economics,
TradeRevisionStatus::Updated,
TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::RuntimeStore),
);
@@ -2949,54 +3149,111 @@ mod tests {
assert_eq!(projection.inventory, TradeInventoryStatus::Reserved);
assert_eq!(projection.economics.total_minor_units, Some(1234));
assert_eq!(projection.economics.currency_code.as_deref(), Some("USD"));
+ assert_eq!(projection.evidence, TradeEvidenceStatus::Complete);
+ assert_eq!(projection.conflict, TradeConflictStatus::None);
+ assert_eq!(
+ projection.private_terms,
+ TradePrivateTermsStatus::AvailableVerified
+ );
+ assert_eq!(projection.attestation, TradeAttestationStatus::PresentValid);
+ assert_eq!(
+ projection.projection_digest.as_deref(),
+ Some("projection-digest-v1")
+ );
assert_eq!(
projection.provenance,
TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::RuntimeStore)
- .with_last_event_id(Some(
- "3333333333333333333333333333333333333333333333333333333333333333".to_owned()
- ))
);
assert_eq!(
- order_status_from_active_order_projection(&active_order),
+ order_status_from_trade_projection(&agreed_projection),
Some(OrderStatus::Scheduled)
);
- let pending_validation_order =
- test_active_order_projection(RadrootsTradeWorkflowState::AgreedPendingValidation);
- let pending_validation_projection = TradeWorkflowProjection::from_active_order_projection(
+ let contested_projection = test_trade_projection(
+ RadrootsTradeAgreementStateV1::Contested,
+ RadrootsTradeNegotiationStateV1::Open,
+ RadrootsTradeEvidenceStateV1::Complete,
+ RadrootsTradeConflictStateV1::DecisionConflict,
+ RadrootsTradePrivateTermsStateV1::AvailableVerified,
+ RadrootsTradeAttestationStateV1::Conflicting,
+ );
+ let contested_workflow = TradeWorkflowProjection::from_trade_projection(
order_id,
- &pending_validation_order,
+ &contested_projection,
+ TradeEconomicsProjection::default(),
TradeRevisionStatus::None,
TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::RuntimeStore),
);
assert_eq!(
- pending_validation_projection.agreement,
- TradeAgreementStatus::AgreedPendingValidation
+ contested_workflow.agreement,
+ TradeAgreementStatus::Contested
);
assert_eq!(
- pending_validation_projection.inventory,
- TradeInventoryStatus::Reserved
+ contested_workflow.inventory,
+ TradeInventoryStatus::NeedsReview
+ );
+ assert_eq!(
+ contested_workflow.conflict,
+ TradeConflictStatus::DecisionConflict
+ );
+ assert_eq!(
+ contested_workflow.attestation,
+ TradeAttestationStatus::Conflicting
);
assert_eq!(
- order_status_from_active_order_projection(&pending_validation_order),
+ order_status_from_trade_projection(&contested_projection),
Some(OrderStatus::NeedsAction)
);
- let requested_order = test_active_order_projection(RadrootsTradeWorkflowState::Requested);
- let requested_projection = TradeWorkflowProjection::from_active_order_projection(
+ let requested_projection = test_trade_projection(
+ RadrootsTradeAgreementStateV1::None,
+ RadrootsTradeNegotiationStateV1::Open,
+ RadrootsTradeEvidenceStateV1::Missing,
+ RadrootsTradeConflictStateV1::None,
+ RadrootsTradePrivateTermsStateV1::Missing,
+ RadrootsTradeAttestationStateV1::None,
+ );
+ let requested_workflow = TradeWorkflowProjection::from_trade_projection(
order_id,
- &requested_order,
+ &requested_projection,
+ TradeEconomicsProjection::default(),
TradeRevisionStatus::None,
TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::RuntimeStore),
);
assert_eq!(
- requested_projection.agreement,
+ requested_workflow.agreement,
TradeAgreementStatus::Requested
);
assert_eq!(
- requested_projection.inventory,
+ requested_workflow.inventory,
TradeInventoryStatus::NeedsReview
);
+ assert_eq!(requested_workflow.evidence, TradeEvidenceStatus::Missing);
+ assert_eq!(
+ requested_workflow.private_terms,
+ TradePrivateTermsStatus::Missing
+ );
+
+ let declined_projection = test_trade_projection(
+ RadrootsTradeAgreementStateV1::None,
+ RadrootsTradeNegotiationStateV1::ClosedDeclined,
+ RadrootsTradeEvidenceStateV1::Complete,
+ RadrootsTradeConflictStateV1::None,
+ RadrootsTradePrivateTermsStateV1::NotRequired,
+ RadrootsTradeAttestationStateV1::None,
+ );
+ let declined_workflow = TradeWorkflowProjection::from_trade_projection(
+ order_id,
+ &declined_projection,
+ TradeEconomicsProjection::default(),
+ TradeRevisionStatus::None,
+ TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::RuntimeStore),
+ );
+ assert_eq!(declined_workflow.agreement, TradeAgreementStatus::Declined);
+ assert_eq!(
+ order_status_from_trade_projection(&declined_projection),
+ Some(OrderStatus::Declined)
+ );
}
#[test]
@@ -3054,21 +3311,9 @@ mod tests {
#[test]
fn trade_workflow_projection_uses_localization_key_ids_for_visible_status_labels() {
- assert_eq!(
- TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Requested)
- .storage_key(),
- "requested"
- );
assert_eq!(TradeAgreementStatus::Requested.storage_key(), "requested");
- assert_eq!(
- TradeAgreementStatus::AgreedPendingValidation.storage_key(),
- "agreed_pending_validation"
- );
assert_eq!(TradeAgreementStatus::Committed.storage_key(), "committed");
- assert_eq!(
- TradeAgreementStatus::ValidationExpired.storage_key(),
- "validation_expired"
- );
+ assert_eq!(TradeAgreementStatus::Contested.storage_key(), "contested");
assert_eq!(TradeAgreementStatus::Invalid.storage_key(), "invalid");
assert_eq!(
TradeRevisionStatus::KeptAsPlaced.storage_key(),
@@ -3076,6 +3321,22 @@ mod tests {
);
assert_eq!(TradeInventoryStatus::Reserved.storage_key(), "reserved");
assert_eq!(
+ TradeEvidenceStatus::QueryPartial.storage_key(),
+ "query_partial"
+ );
+ assert_eq!(
+ TradeConflictStatus::CancellationConflict.storage_key(),
+ "cancellation_conflict"
+ );
+ assert_eq!(
+ TradePrivateTermsStatus::CommitmentMismatch.storage_key(),
+ "commitment_mismatch"
+ );
+ assert_eq!(
+ TradeAttestationStatus::PresentInvalid.storage_key(),
+ "present_invalid"
+ );
+ assert_eq!(
TradeWorkflowSource::RuntimeStore.storage_key(),
"runtime_store"
);
@@ -3085,12 +3346,8 @@ mod tests {
"messages.trade.workflow.agreement.requested"
);
assert_eq!(
- TradeAgreementStatus::AgreedPendingValidation.label_key_id(),
- "messages.trade.workflow.agreement.agreed_pending_validation"
- );
- assert_eq!(
- TradeAgreementStatus::ValidationExpired.label_key_id(),
- "messages.trade.workflow.agreement.validation_expired"
+ TradeAgreementStatus::Contested.label_key_id(),
+ "messages.trade.workflow.agreement.contested"
);
assert_eq!(
TradeAgreementStatus::Invalid.label_key_id(),
@@ -3105,6 +3362,22 @@ mod tests {
"messages.trade.workflow.inventory.sold_out"
);
assert_eq!(
+ TradeEvidenceStatus::UnsupportedVersion.label_key_id(),
+ "messages.trade.workflow.evidence.unsupported_version"
+ );
+ assert_eq!(
+ TradeConflictStatus::InventoryAuthorityConflict.label_key_id(),
+ "messages.trade.workflow.conflict.inventory_authority_conflict"
+ );
+ assert_eq!(
+ TradePrivateTermsStatus::Undecryptable.label_key_id(),
+ "messages.trade.workflow.private_terms.undecryptable"
+ );
+ assert_eq!(
+ TradeAttestationStatus::Conflicting.label_key_id(),
+ "messages.trade.workflow.attestation.conflicting"
+ );
+ assert_eq!(
TradeWorkflowSource::Cli.label_key_id(),
"messages.trade.workflow.provenance.cli"
);
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -411,13 +411,16 @@
"trade.workflow.axis.agreement": "Agreement",
"trade.workflow.axis.revision": "Change",
"trade.workflow.axis.inventory": "Stock",
+ "trade.workflow.axis.evidence": "Evidence",
+ "trade.workflow.axis.conflict": "Conflict",
+ "trade.workflow.axis.private_terms": "Private terms",
+ "trade.workflow.axis.attestation": "Attestation",
"trade.workflow.axis.source": "Source",
"trade.workflow.agreement.requested": "Requested",
- "trade.workflow.agreement.agreed_pending_validation": "Pending validation",
"trade.workflow.agreement.committed": "Committed",
+ "trade.workflow.agreement.contested": "Contested",
"trade.workflow.agreement.declined": "Declined",
"trade.workflow.agreement.cancelled": "Cancelled",
- "trade.workflow.agreement.validation_expired": "Validation expired",
"trade.workflow.agreement.invalid": "Invalid",
"trade.workflow.revision.none": "No change",
"trade.workflow.revision.change_proposed": "Change proposed",
@@ -427,6 +430,26 @@
"trade.workflow.inventory.reserved": "Reserved",
"trade.workflow.inventory.sold_out": "Sold out",
"trade.workflow.inventory.needs_review": "Needs review",
+ "trade.workflow.evidence.complete": "Complete",
+ "trade.workflow.evidence.missing": "Missing",
+ "trade.workflow.evidence.query_partial": "Partial",
+ "trade.workflow.evidence.unsupported_version": "Unsupported",
+ "trade.workflow.conflict.none": "None",
+ "trade.workflow.conflict.concurrent_candidates": "Concurrent candidates",
+ "trade.workflow.conflict.double_acceptance": "Double acceptance",
+ "trade.workflow.conflict.decision_conflict": "Decision conflict",
+ "trade.workflow.conflict.cancellation_conflict": "Cancellation conflict",
+ "trade.workflow.conflict.invalid_causal_chain": "Invalid causal chain",
+ "trade.workflow.conflict.inventory_authority_conflict": "Inventory authority conflict",
+ "trade.workflow.private_terms.not_required": "Not required",
+ "trade.workflow.private_terms.available_verified": "Verified",
+ "trade.workflow.private_terms.missing": "Missing",
+ "trade.workflow.private_terms.undecryptable": "Undecryptable",
+ "trade.workflow.private_terms.commitment_mismatch": "Commitment mismatch",
+ "trade.workflow.attestation.none": "None",
+ "trade.workflow.attestation.present_valid": "Valid",
+ "trade.workflow.attestation.present_invalid": "Invalid",
+ "trade.workflow.attestation.conflicting": "Conflicting",
"trade.workflow.provenance.app": "App",
"trade.workflow.provenance.cli": "CLI",
"trade.workflow.provenance.relay": "Relay",