commit c63dae0f1db19f4662f758f4f9bc7cd025af23d3
parent 3bc687d567220d4ee133504ff3ebb008860f8c45
Author: triesap <tyson@radroots.org>
Date: Thu, 16 Jul 2026 11:37:52 +0000
runtime: add SDK journal recovery
- add release-product operation journal, recovery receipts, reservations, and projection checkpoints
- move workflow enqueue through command-hash idempotency and signer-safe journal states
- remove retired order runtime modules and old public trade runtime tests
- align trade binding DTO roots and generated TypeScript with canonical trade projections
Diffstat:
33 files changed, 1337 insertions(+), 14578 deletions(-)
diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs
@@ -11,7 +11,7 @@ use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_sdk::{
ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient,
RadrootsSdkError, RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp,
- SdkIdempotencyKey, TargetPolicy, TradeStatusRequest,
+ SdkIdempotencyKey, TargetPolicy,
};
const RELAY: &str = "wss://relay.example.com";
@@ -47,11 +47,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.sync()
.push_outbox(PushOutboxRequest::new().with_limit(1))
.await;
- let order_status = sdk
- .trades()
- .status(TradeStatusRequest::parse("example-order-1")?)
- .await?;
-
assert_eq!(
prepared.public_listing_addr.as_str(),
enqueue.public_listing_addr.as_str()
@@ -66,7 +61,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
push,
Err(RadrootsSdkError::ProductSyncUnsupported { .. })
));
- assert!(!order_status.found);
Ok(())
}
diff --git a/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs b/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs
@@ -11,7 +11,7 @@ use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_sdk::{
ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient,
RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SdkIdempotencyKey,
- TargetPolicy, TargetSet, TradeStatusRequest,
+ TargetPolicy, TargetSet,
};
use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsNostrTransport};
@@ -55,14 +55,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.sync()
.push_outbox_with_transport(&transport, PushOutboxRequest::new().with_limit(1))
.await?;
- let order_status = sdk
- .trades()
- .status(TradeStatusRequest::parse("example-order-1")?)
- .await?;
println!("queued listing event: {}", enqueue.signed_event_id.as_str());
println!("published events: {}", push.published_events);
- println!("order found: {}", order_status.found);
Ok(())
}
diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs
@@ -510,17 +510,6 @@ impl RadrootsSdkError {
}
}
- pub(crate) fn trade_status_limit_invalid(limit: u32, min: u32, max: u32) -> Self {
- Self::TradeStatusLimitInvalid { limit, min, max }
- }
-
- pub(crate) fn invalid_trade_id(value: impl Into<String>, message: impl Into<String>) -> Self {
- Self::InvalidTradeId {
- value: value.into(),
- message: message.into(),
- }
- }
-
pub(crate) fn missing_geonames_config() -> Self {
Self::GeoNames {
kind: RadrootsSdkGeoNamesErrorKind::Configuration,
@@ -786,22 +775,6 @@ impl From<radroots_trade::listing::RadrootsListingMutationError> for RadrootsSdk
}
#[cfg(feature = "runtime")]
-impl From<radroots_trade::projection::RadrootsTradeProjectionError> for RadrootsSdkError {
- fn from(error: radroots_trade::projection::RadrootsTradeProjectionError) -> Self {
- match error {
- radroots_trade::projection::RadrootsTradeProjectionError::InvalidLimit { max } => {
- Self::InvalidRequest {
- message: format!("projection query limit must be between 1 and {max}"),
- }
- }
- error => Self::Projection {
- message: error.to_string(),
- },
- }
- }
-}
-
-#[cfg(feature = "runtime")]
impl From<radroots_outbox::RadrootsOutboxError> for RadrootsSdkError {
fn from(error: radroots_outbox::RadrootsOutboxError) -> Self {
match error {
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -33,12 +33,6 @@ pub mod knowledge;
#[cfg(feature = "runtime")]
mod listings_runtime;
#[cfg(feature = "runtime")]
-mod market_runtime;
-#[cfg(feature = "runtime")]
-mod order;
-#[cfg(feature = "runtime")]
-mod orders_runtime;
-#[cfg(feature = "runtime")]
mod privacy;
#[cfg(feature = "runtime")]
mod private_store;
@@ -56,8 +50,6 @@ mod studio_store;
#[cfg(feature = "runtime")]
mod sync_runtime;
#[cfg(feature = "runtime")]
-mod trade_storage;
-#[cfg(feature = "runtime")]
pub mod transport;
#[cfg(feature = "runtime")]
mod workflow_runtime;
@@ -136,63 +128,21 @@ pub use crate::listings_runtime::{
ListingPreparePublishRequest, ListingPublishPlan, SdkMutationState,
};
#[cfg(feature = "runtime")]
-pub use crate::market_runtime::{
- MARKET_SEARCH_DEFAULT_LIMIT, MarketListingSearchRow, MarketSearchReceipt, MarketSearchRequest,
- MarketSearchSource,
-};
-#[cfg(feature = "runtime")]
-pub use crate::orders_runtime::{
- SdkTradeStatusIssue, SdkTradeStatusIssueKind, SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT,
- TRADE_STATUS_MAX_LIMIT, TRADE_STATUS_ROOT_SELECTOR_SEPARATOR,
- TRADE_STATUS_WATCH_DEFAULT_CAPACITY, TRADE_STATUS_WATCH_DEFAULT_REFRESH_INTERVAL_MS,
- TRADE_STATUS_WATCH_MAX_CAPACITY, TRADE_STATUS_WATCH_MAX_REFRESH_INTERVAL_MS,
- TradeEvidenceBranchReceipt, TradeEvidenceIngestReceipt, TradeEvidenceIngestRequest,
- TradeEvidenceNostrRelayFilter, TradeEvidenceNostrRelayTagFilter, TradeEvidenceQueryBranch,
- TradeEvidenceQueryBranchKind, TradeEvidenceQueryPlan, TradeRequestEvidenceIngestReceipt,
- TradeRequestEvidenceIngestRequest, TradeResyncEventImportReceipt, TradeResyncEvidenceReceipt,
- TradeResyncNostrRelayOutcomeKind, TradeResyncNostrRelayOutcomeReceipt,
- TradeResyncNostrRelayTransportOutcomeKind, TradeResyncReceipt, TradeResyncRequest,
- TradeSellerInboxReceipt, TradeSellerInboxRequest, TradeStatusAmbiguityCandidate,
- TradeStatusEligibility, TradeStatusEvidenceSummary, TradeStatusKind, TradeStatusNextActionKind,
- TradeStatusReceipt, TradeStatusRequest, TradeStatusWatch, TradeStatusWatchCancelReceipt,
- TradeStatusWatchCancelState, TradeStatusWatchRequest, TradeStatusWatchUpdate,
- TradeValidationReceiptEvent, TradeValidationReceiptInspectReceipt,
- TradeValidationReceiptInspectRequest, TradeValidationReceiptInvalidCandidate,
- TradeValidationReceiptListReceipt, TradeValidationReceiptListRequest,
- TradeValidationReceiptNostrEvidenceReceipt, TradeValidationReceiptNostrRelayOutcomeKind,
- TradeValidationReceiptNostrRelayOutcomeReceipt,
- TradeValidationReceiptNostrRelayTransportOutcomeKind, TradeValidationReceiptTags,
- TradeValidationReceiptVerifyRequest, TradeValidationTrustDecision,
-};
-#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
-pub use crate::orders_runtime::{
- TRADE_CANCELLATION_OPERATION_KIND, TRADE_DECISION_OPERATION_KIND, TRADE_SUBMIT_OPERATION_KIND,
- TradeAcceptRequest, TradeCancelRequest, TradeCancellationPlan, TradeCancellationReceipt,
- TradeDecisionPlan, TradeDecisionReceipt, TradeDeclineRequest, TradeEvidenceMode,
- TradeMutationOutcome, TradeProposeRequest, TradeSubmitPlan, TradeSubmitReceipt,
- TradeWorkflowEnqueueReceipt, TradeWorkflowIdempotencyReceipt, TradeWorkflowKind,
- TradeWorkflowPlan, TradeWorkflowRetryAdvice,
-};
-#[cfg(feature = "runtime")]
pub use crate::privacy::{
PrivacyPreflightConfirmation, PrivacyPreflightReceipt, PrivacyPreflightStatus,
ProductSensitivityField,
};
-#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
-pub use crate::product_clients::TradeBuyerClient;
#[cfg(feature = "runtime")]
pub use crate::product_clients::{
- FarmsClient, GeoNamesClient, ListingsClient, MarketClient, SyncClient, TradeResyncClient,
- TradeSellerClient, TradeValidationReceiptsClient, TradesClient,
+ FarmsClient, GeoNamesClient, ListingsClient, MarketClient, SyncClient, TradesClient,
};
#[cfg(feature = "runtime")]
pub use crate::runtime::{
- BackupReceipt, BackupRequest, IntegrityReceipt, IntegrityRequest, QuarantineResetReceipt,
- QuarantineResetRequest, RadrootsClient, RadrootsClientBuilder, RadrootsSdkClock,
- RadrootsSdkStorageConfig, RadrootsSdkStoragePaths, RadrootsSdkTimestamp, RestoreArchive,
- RestoreReceipt, RestoreRequest, SdkBackupManifest, SdkBackupManifestKind, SdkBackupState,
- SdkBackupVerification, SdkEventStoreStorageStatus, SdkOutboxStorageStatus,
- SdkPrivateStoreStorageStatus, SdkRestoreState, SdkSqliteStoreStatus,
+ BackupReceipt, BackupRequest, IntegrityReceipt, IntegrityRequest, RadrootsClient,
+ RadrootsClientBuilder, RadrootsSdkClock, RadrootsSdkStorageConfig, RadrootsSdkStoragePaths,
+ RadrootsSdkTimestamp, RestoreArchive, RestoreReceipt, RestoreRequest, SdkBackupManifest,
+ SdkBackupManifestKind, SdkBackupState, SdkBackupVerification, SdkEventStoreStorageStatus,
+ SdkOutboxStorageStatus, SdkPrivateStoreStorageStatus, SdkRestoreState, SdkSqliteStoreStatus,
SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, SdkStudioStoreStorageStatus,
StorageCheckpointReceipt, StorageCheckpointRequest, StorageStatusReceipt, StorageStatusRequest,
};
@@ -220,11 +170,6 @@ pub use crate::sync_runtime::{
SyncTransportStatusSummary, SyncTransportTargetSummary,
};
#[cfg(feature = "runtime")]
-pub use crate::trade_storage::{
- SDK_TRADE_PROJECTION_CACHE_VERSION, SdkTradeProjectionCache, SdkTradeProjectionCacheKey,
- SdkTradeProjectionCacheRecord,
-};
-#[cfg(feature = "runtime")]
pub use crate::transport::{
MeshScopeId, MultiTargetProfile, NostrProfile, NostrRelayUrlPolicy, PublishMode,
RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryTargetStatus, RadrootsTransportKind,
diff --git a/crates/sdk/src/market_runtime.rs b/crates/sdk/src/market_runtime.rs
@@ -1,156 +0,0 @@
-#[cfg(feature = "runtime")]
-use crate::sync_runtime::refresh_product_projections_for_sdk;
-#[cfg(feature = "runtime")]
-use crate::{
- MarketClient, RadrootsSdkError, SyncProjectionRefreshReceipt, SyncProjectionRefreshRequest,
-};
-#[cfg(feature = "runtime")]
-use radroots_event::ids::{RadrootsEventId, RadrootsListingAddress, RadrootsPublicKey};
-#[cfg(feature = "runtime")]
-use radroots_trade::projection::{
- RadrootsListingProjectionRow, RadrootsListingSearchRequest, search_listing_projection,
-};
-
-#[cfg(feature = "runtime")]
-pub const MARKET_SEARCH_DEFAULT_LIMIT: u32 = 50;
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[non_exhaustive]
-pub struct MarketSearchRequest {
- pub query: String,
- pub limit: u32,
- pub projection_refresh: SyncProjectionRefreshRequest,
-}
-
-#[cfg(feature = "runtime")]
-impl MarketSearchRequest {
- pub fn new(query: impl Into<String>) -> Self {
- Self {
- query: query.into(),
- limit: MARKET_SEARCH_DEFAULT_LIMIT,
- projection_refresh: SyncProjectionRefreshRequest::new(),
- }
- }
-
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = limit;
- self
- }
-
- pub fn with_projection_refresh(mut self, refresh: SyncProjectionRefreshRequest) -> Self {
- self.projection_refresh = refresh;
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct MarketSearchReceipt {
- pub source: MarketSearchSource,
- pub refresh: SyncProjectionRefreshReceipt,
- pub listings: Vec<MarketListingSearchRow>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum MarketSearchSource {
- LocalProjectionFts,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct MarketListingSearchRow {
- pub listing_addr: RadrootsListingAddress,
- pub listing_event_id: RadrootsEventId,
- pub seller_pubkey: RadrootsPublicKey,
- pub title: String,
- pub description: String,
- pub product_type: String,
- pub price_amount: String,
- pub price_currency: String,
- pub inventory_available: String,
- pub delivery_method: String,
- pub locality_primary: String,
- pub locality_city: Option<String>,
- pub locality_region: Option<String>,
- pub locality_country: Option<String>,
- pub geohash5: String,
- pub updated_at_ms: i64,
-}
-
-#[cfg(feature = "runtime")]
-impl<'sdk> MarketClient<'sdk> {
- pub async fn search(
- &self,
- request: MarketSearchRequest,
- ) -> Result<MarketSearchReceipt, RadrootsSdkError> {
- let refresh =
- refresh_product_projections_for_sdk(self.sdk, request.projection_refresh).await?;
- let rows = search_listing_projection(
- &self.sdk._event_store,
- &RadrootsListingSearchRequest::new(request.query).with_limit(request.limit),
- )
- .await?;
- let listings = rows
- .into_iter()
- .map(MarketListingSearchRow::try_from_projection_row)
- .collect::<Result<Vec<_>, _>>()?;
- Ok(MarketSearchReceipt {
- source: MarketSearchSource::LocalProjectionFts,
- refresh,
- listings,
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-impl MarketListingSearchRow {
- fn try_from_projection_row(
- row: RadrootsListingProjectionRow,
- ) -> Result<Self, RadrootsSdkError> {
- let listing_addr = row.listing_addr;
- let listing_event_id =
- RadrootsEventId::parse(row.listing_event_id.as_str()).map_err(|source| {
- RadrootsSdkError::Projection {
- message: format!(
- "stored listing projection event id `{}` is invalid: {source}",
- row.listing_event_id
- ),
- }
- })?;
- let seller_pubkey =
- RadrootsPublicKey::parse(row.seller_pubkey.as_str()).map_err(|source| {
- RadrootsSdkError::Projection {
- message: format!(
- "stored listing projection seller pubkey `{}` is invalid: {source}",
- row.seller_pubkey
- ),
- }
- })?;
- Ok(Self {
- listing_addr,
- listing_event_id,
- seller_pubkey,
- title: row.title,
- description: row.description,
- product_type: row.product_type,
- price_amount: row.price_amount,
- price_currency: row.price_currency,
- inventory_available: row.inventory_available,
- delivery_method: row.delivery_method,
- locality_primary: row.locality_primary,
- locality_city: row.locality_city,
- locality_region: row.locality_region,
- locality_country: row.locality_country,
- geohash5: row.geohash5,
- updated_at_ms: row.updated_at_ms,
- })
- }
-}
-
-#[cfg(test)]
-#[path = "../tests/unit/market_runtime_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/order.rs b/crates/sdk/src/order.rs
@@ -1,112 +0,0 @@
-pub use radroots_event::order::*;
-#[cfg(feature = "signer-adapters")]
-pub use radroots_event_codec::error::EventEncodeError;
-pub use radroots_event_codec::order::RadrootsOrderEnvelopeParseError;
-
-use radroots_event::RadrootsEventEnvelope;
-#[cfg(feature = "signer-adapters")]
-use radroots_event::wire::RadrootsNip01EventWireParts;
-#[cfg(feature = "signer-adapters")]
-use radroots_event::{RadrootsEventPtr, ids::RadrootsEventId, tags::TAG_E};
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Debug, Clone)]
-pub struct RadrootsOrderRequestDraft {
- parts: RadrootsNip01EventWireParts,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Debug, Clone)]
-pub struct RadrootsOrderDecisionDraft {
- parts: RadrootsNip01EventWireParts,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Debug, Clone)]
-pub struct RadrootsOrderCancellationDraft {
- parts: RadrootsNip01EventWireParts,
-}
-
-#[cfg(feature = "signer-adapters")]
-impl RadrootsOrderRequestDraft {
- pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
- self.parts
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-impl RadrootsOrderDecisionDraft {
- pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
- self.parts
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-impl RadrootsOrderCancellationDraft {
- pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
- self.parts
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn with_contract_root_event_tag(
- mut parts: RadrootsNip01EventWireParts,
- root_event_id: &RadrootsEventId,
-) -> RadrootsNip01EventWireParts {
- parts
- .tags
- .push(vec![TAG_E.to_owned(), root_event_id.as_str().to_owned()]);
- parts
-}
-
-#[cfg(feature = "signer-adapters")]
-pub fn build_order_request_draft(
- listing_event: &RadrootsEventPtr,
- payload: &RadrootsOrderRequest,
-) -> Result<RadrootsOrderRequestDraft, EventEncodeError> {
- Ok(RadrootsOrderRequestDraft {
- parts: radroots_event_codec::order::order_request_event_build(listing_event, payload)?,
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-pub fn build_order_decision_draft(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderDecision,
-) -> Result<RadrootsOrderDecisionDraft, EventEncodeError> {
- Ok(RadrootsOrderDecisionDraft {
- parts: with_contract_root_event_tag(
- radroots_event_codec::order::order_decision_event_build(
- root_event_id,
- prev_event_id,
- payload,
- )?,
- root_event_id,
- ),
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-pub fn build_order_cancellation_draft(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderCancellation,
-) -> Result<RadrootsOrderCancellationDraft, EventEncodeError> {
- Ok(RadrootsOrderCancellationDraft {
- parts: with_contract_root_event_tag(
- radroots_event_codec::order::order_cancellation_event_build(
- root_event_id,
- prev_event_id,
- payload,
- )?,
- root_event_id,
- ),
- })
-}
-
-pub fn parse_order_request(
- event: &RadrootsEventEnvelope,
-) -> Result<RadrootsOrderEnvelope<RadrootsOrderRequest>, RadrootsOrderEnvelopeParseError> {
- radroots_event_codec::order::order_request_from_event(event)
-}
diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs
@@ -1,5695 +0,0 @@
-#[cfg(feature = "signer-adapters")]
-use crate::TradeBuyerClient;
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use crate::runtime::sdk_now_ms;
-#[cfg(feature = "runtime")]
-use crate::sync_runtime::SyncProjectionRefreshReceipt;
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use crate::sync_runtime::{SyncProjectionRefreshRequest, refresh_product_projections_for_sdk};
-#[cfg(feature = "signer-adapters")]
-use crate::workflow_runtime::enqueue_configured_signed_workflow;
-#[cfg(feature = "signer-adapters")]
-use crate::{
- PrivacyPreflightConfirmation, PrivacyPreflightReceipt, ProductSensitivityField, PublishMode,
- PushOutboxReceipt, PushOutboxRequest, RadrootsSdkRecoveryAction, SatisfactionPolicy,
- SdkIdempotencyKey, SdkMutationState, TargetPolicy, workflow_runtime::SdkWorkflowEnqueueRequest,
-};
-#[cfg(feature = "runtime")]
-use crate::{
- RadrootsSdkError, RadrootsSdkTimestamp, TradeResyncClient, TradeSellerClient,
- TradeValidationReceiptsClient, TradesClient, order,
-};
-#[cfg(feature = "runtime")]
-use radroots_authority::RadrootsActorContext;
-#[cfg(feature = "signer-adapters")]
-use radroots_event::wire::RadrootsNip01EventWireParts;
-#[cfg(feature = "runtime")]
-use radroots_event::{
- RadrootsEventEnvelope, RadrootsEventEnvelopeParts,
- contract::RadrootsActorRole,
- draft::RadrootsSignedEvent,
- ids::RadrootsEventId,
- kinds::{
- KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST,
- KIND_TRADE_VALIDATION_RECEIPT,
- },
- tags::TAG_P,
-};
-#[cfg(feature = "signer-adapters")]
-use radroots_event::{
- RadrootsEventPtr,
- draft::RadrootsEventDraft,
- order::{
- RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
- RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderRequest,
- },
-};
-#[cfg(any(feature = "runtime", feature = "signer-adapters", test))]
-use radroots_event::{
- ids::{RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey},
- order::RadrootsOrderEconomics,
-};
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use radroots_event::{
- kinds::KIND_LISTING,
- tags::{TAG_D, TAG_E},
-};
-#[cfg(feature = "runtime")]
-use radroots_event_codec::order::{
- order_cancellation_from_event, order_decision_from_event, order_request_from_event,
-};
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use radroots_event_store::RadrootsStoredEventTag;
-#[cfg(feature = "runtime")]
-use radroots_event_store::{RadrootsEventIngest, RadrootsStoredEvent};
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use radroots_nostr::prelude::{
- RadrootsNostrEventId, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey,
- radroots_nostr_filter_tag,
-};
-#[cfg(feature = "runtime")]
-use radroots_trade::identity::{RadrootsTradeLocator, RadrootsTradeLocatorCandidate};
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use radroots_trade::listing::parse_listing_address;
-#[cfg(feature = "signer-adapters")]
-use radroots_trade::order::{
- RadrootsOrderCanonicalizationError, RadrootsOrderProjectionQueryResult,
- canonicalize_order_decision_for_signer, canonicalize_order_request_for_signer,
- order_projection_query_for_order_id,
-};
-#[cfg(feature = "runtime")]
-use radroots_trade::order::{
- RadrootsOrderEventRecord, RadrootsOrderIssue, RadrootsOrderProjection,
- RadrootsOrderStoreQueryError, RadrootsTradeLocatorProjectionQueryResult,
- RadrootsTradeLocatorProjectionResolution, order_event_record_from_event,
- order_projection_query_for_trade_locator,
-};
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use radroots_trade::validation_receipt::RadrootsValidationReceiptError;
-#[cfg(feature = "runtime")]
-use radroots_trade::validation_receipt::{
- RadrootsTradeCommitmentConfidence, RadrootsTradeValidationAuthority,
- RadrootsTradeValidationReceipt, RadrootsTradeValidationTrustPolicy,
- RadrootsTradeValidationTrustState, RadrootsValidationReceiptExpectedBinding,
- RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult,
- RadrootsValidationReceiptTags, verify_validation_receipt_event,
-};
-#[cfg(feature = "runtime")]
-use radroots_trade::workflow::RadrootsTradeWorkflowState;
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use radroots_transport_nostr::{
- RadrootsNostrClientFetchAdapter, RadrootsRelayFetchAdapter, RadrootsRelayFetchEventReceipt,
- RadrootsRelayFetchOutcomeKind, RadrootsRelayFetchReceipt, RadrootsRelayFetchRelayOutcome,
- RadrootsRelayFetchRequest, RadrootsRelayOutcomeKind, fetch_and_ingest_relay_events,
-};
-#[cfg(feature = "runtime")]
-use serde::ser::SerializeStruct;
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-use std::collections::{BTreeMap, BTreeSet};
-#[cfg(feature = "runtime")]
-use std::time::Duration;
-#[cfg(feature = "runtime")]
-use tokio::{
- sync::{mpsc, oneshot},
- task::JoinHandle,
-};
-#[cfg(feature = "runtime")]
-pub const TRADE_STATUS_DEFAULT_LIMIT: u32 = 500;
-#[cfg(feature = "runtime")]
-pub const TRADE_STATUS_MAX_LIMIT: u32 = 1_000;
-#[cfg(feature = "runtime")]
-pub const TRADE_STATUS_ROOT_SELECTOR_SEPARATOR: char = '@';
-#[cfg(feature = "runtime")]
-pub const TRADE_STATUS_WATCH_DEFAULT_CAPACITY: usize = 8;
-#[cfg(feature = "runtime")]
-pub const TRADE_STATUS_WATCH_MAX_CAPACITY: usize = 128;
-#[cfg(feature = "runtime")]
-pub const TRADE_STATUS_WATCH_DEFAULT_REFRESH_INTERVAL_MS: u64 = 1_000;
-#[cfg(feature = "runtime")]
-pub const TRADE_STATUS_WATCH_MAX_REFRESH_INTERVAL_MS: u64 = 60_000;
-#[cfg(feature = "signer-adapters")]
-pub const TRADE_SUBMIT_OPERATION_KIND: &str = "trade.submit.v1";
-#[cfg(feature = "signer-adapters")]
-pub const TRADE_DECISION_OPERATION_KIND: &str = "trade.decision.v1";
-#[cfg(feature = "signer-adapters")]
-pub const TRADE_CANCELLATION_OPERATION_KIND: &str = "trade.cancellation.v1";
-
-#[cfg(feature = "signer-adapters")]
-const TRADE_SUBMIT_CONTRACT_ID: &str = "radroots.order.request.v1";
-#[cfg(feature = "signer-adapters")]
-const TRADE_DECISION_CONTRACT_ID: &str = "radroots.order.decision.v1";
-#[cfg(feature = "signer-adapters")]
-const TRADE_CANCELLATION_CONTRACT_ID: &str = "radroots.order.cancellation.v1";
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeWorkflowKind {
- Submit,
- Decision,
- Cancellation,
-}
-
-#[cfg(feature = "signer-adapters")]
-impl TradeWorkflowKind {
- pub fn operation_kind(self) -> &'static str {
- match self {
- Self::Submit => TRADE_SUBMIT_OPERATION_KIND,
- Self::Decision => TRADE_DECISION_OPERATION_KIND,
- Self::Cancellation => TRADE_CANCELLATION_OPERATION_KIND,
- }
- }
-
- pub fn contract_id(self) -> &'static str {
- match self {
- Self::Submit => TRADE_SUBMIT_CONTRACT_ID,
- Self::Decision => TRADE_DECISION_CONTRACT_ID,
- Self::Cancellation => TRADE_CANCELLATION_CONTRACT_ID,
- }
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeWorkflowPlan {
- pub kind: TradeWorkflowKind,
- pub operation_kind: &'static str,
- pub contract_id: &'static str,
- pub expected_event_id: RadrootsEventId,
- pub created_at: RadrootsSdkTimestamp,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeWorkflowEnqueueReceipt {
- pub kind: TradeWorkflowKind,
- pub operation_kind: &'static str,
- pub expected_event_id: RadrootsEventId,
- pub signed_event_id: RadrootsEventId,
- pub local_event_seq: i64,
- pub outbox_operation_id: i64,
- pub outbox_event_id: i64,
- pub state: SdkMutationState,
- pub idempotency_digest_prefix: Option<String>,
- pub idempotency: TradeWorkflowIdempotencyReceipt,
- pub retry: TradeWorkflowRetryAdvice,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeWorkflowIdempotencyReceipt {
- pub digest_prefix: Option<String>,
- pub replayed_existing_operation: bool,
- pub safe_to_retry_with_same_idempotency_key: bool,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeWorkflowRetryAdvice {
- pub retryable_after_error: bool,
- pub safe_to_retry_enqueue_with_same_idempotency_key: bool,
- pub recovery_actions: Vec<RadrootsSdkRecoveryAction>,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeSubmitPrepareRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub listing_event: RadrootsEventPtr,
- pub order: RadrootsOrderRequest,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeSubmitPlan {
- pub workflow: TradeWorkflowPlan,
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub listing_event_id: RadrootsEventId,
- pub expected_event_id: RadrootsEventId,
- pub frozen_draft: RadrootsEventDraft,
- pub created_at: RadrootsSdkTimestamp,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeSubmitReceipt {
- pub workflow: TradeWorkflowEnqueueReceipt,
- pub locator: RadrootsTradeLocator,
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub listing_event_id: RadrootsEventId,
- pub expected_event_id: RadrootsEventId,
- pub signed_event_id: RadrootsEventId,
- pub local_event_seq: i64,
- pub outbox_operation_id: i64,
- pub outbox_event_id: i64,
- pub state: SdkMutationState,
- pub idempotency_digest_prefix: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeRequestEvidenceIngestRequest {
- pub event: RadrootsSignedEvent,
- pub observed_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeRequestEvidenceIngestRequest {
- pub fn new(event: RadrootsSignedEvent) -> Self {
- Self {
- event,
- observed_at: None,
- }
- }
-
- pub fn with_observed_at(mut self, observed_at: RadrootsSdkTimestamp) -> Self {
- self.observed_at = Some(observed_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeRequestEvidenceIngestReceipt {
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub request_event_id: RadrootsEventId,
- pub local_event_seq: i64,
- pub inserted: bool,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeEvidenceIngestRequest {
- pub event: RadrootsSignedEvent,
- pub observed_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeEvidenceIngestRequest {
- pub fn new(event: RadrootsSignedEvent) -> Self {
- Self {
- event,
- observed_at: None,
- }
- }
-
- pub fn with_observed_at(mut self, observed_at: RadrootsSdkTimestamp) -> Self {
- self.observed_at = Some(observed_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeEvidenceIngestReceipt {
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub event_id: RadrootsEventId,
- pub event_kind: u32,
- pub local_event_seq: i64,
- pub inserted: bool,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeDecisionPrepareRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub request_event: RadrootsEventPtr,
- pub decision: RadrootsOrderDecision,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeDecisionPlan {
- pub workflow: TradeWorkflowPlan,
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub request_event_id: RadrootsEventId,
- pub expected_event_id: RadrootsEventId,
- pub frozen_draft: RadrootsEventDraft,
- pub created_at: RadrootsSdkTimestamp,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeDecisionReceipt {
- pub workflow: TradeWorkflowEnqueueReceipt,
- pub locator: RadrootsTradeLocator,
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub request_event_id: RadrootsEventId,
- pub expected_event_id: RadrootsEventId,
- pub signed_event_id: RadrootsEventId,
- pub local_event_seq: i64,
- pub outbox_operation_id: i64,
- pub outbox_event_id: i64,
- pub state: SdkMutationState,
- pub idempotency_digest_prefix: Option<String>,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeCancellationPrepareRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub root_event: RadrootsEventPtr,
- pub previous_event: RadrootsEventPtr,
- pub cancellation: RadrootsOrderCancellation,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeCancellationPlan {
- pub workflow: TradeWorkflowPlan,
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub root_event_id: RadrootsEventId,
- pub previous_event_id: RadrootsEventId,
- pub expected_event_id: RadrootsEventId,
- pub frozen_draft: RadrootsEventDraft,
- pub created_at: RadrootsSdkTimestamp,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeCancellationReceipt {
- pub workflow: TradeWorkflowEnqueueReceipt,
- pub locator: RadrootsTradeLocator,
- pub order_id: RadrootsOrderId,
- pub listing_addr: RadrootsListingAddress,
- pub buyer_pubkey: RadrootsPublicKey,
- pub seller_pubkey: RadrootsPublicKey,
- pub root_event_id: RadrootsEventId,
- pub previous_event_id: RadrootsEventId,
- pub expected_event_id: RadrootsEventId,
- pub signed_event_id: RadrootsEventId,
- pub local_event_seq: i64,
- pub outbox_operation_id: i64,
- pub outbox_event_id: i64,
- pub state: SdkMutationState,
- pub idempotency_digest_prefix: Option<String>,
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case", tag = "mode")]
-pub enum TradeMutationOutcome<Plan, Receipt> {
- DryRun {
- plan: Plan,
- },
- Enqueued {
- receipt: Receipt,
- },
- Published {
- receipt: Receipt,
- publish: PushOutboxReceipt,
- },
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeEvidenceMode {
- LocalOnly,
- ResyncBeforeMutation,
- RequireExplicitEvidence {
- evidence: Vec<TradeEvidenceIngestRequest>,
- },
-}
-
-#[cfg(feature = "signer-adapters")]
-impl TradeEvidenceMode {
- pub fn require_explicit_evidence(
- evidence: impl IntoIterator<Item = TradeEvidenceIngestRequest>,
- ) -> Self {
- Self::RequireExplicitEvidence {
- evidence: evidence.into_iter().collect(),
- }
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeProposeRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- 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 target_policy: TargetPolicy,
- pub publish_mode: PublishMode,
- pub satisfaction_policy: SatisfactionPolicy,
- pub privacy_confirmation: PrivacyPreflightConfirmation,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "signer-adapters")]
-impl TradeProposeRequest {
- pub fn new(
- actor: RadrootsActorContext,
- listing_event: RadrootsEventPtr,
- order: RadrootsOrderRequest,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- ) -> Self {
- Self {
- actor,
- listing_event,
- order_id: order.order_id,
- listing_addr: order.listing_addr,
- seller_pubkey: order.seller_pubkey,
- items: order.items,
- economics: order.economics,
- public_note: None,
- target_policy,
- publish_mode,
- satisfaction_policy,
- privacy_confirmation: PrivacyPreflightConfirmation::new(),
- idempotency_key: None,
- created_at: None,
- }
- }
-
- pub fn with_public_note(mut self, public_note: impl Into<String>) -> Self {
- self.public_note = Some(public_note.into());
- self
- }
-
- pub fn with_optional_public_note(mut self, public_note: Option<String>) -> Self {
- self.public_note = public_note;
- self
- }
-
- pub fn with_privacy_confirmation(
- mut self,
- privacy_confirmation: PrivacyPreflightConfirmation,
- ) -> Self {
- self.privacy_confirmation = privacy_confirmation;
- self
- }
-
- pub fn try_with_idempotency_key(
- mut self,
- idempotency_key: impl AsRef<str>,
- ) -> Result<Self, RadrootsSdkError> {
- self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?);
- Ok(self)
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeAcceptRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub locator: RadrootsTradeLocator,
- pub inventory_commitments: Vec<RadrootsOrderInventoryCommitment>,
- pub target_policy: TargetPolicy,
- pub publish_mode: PublishMode,
- pub satisfaction_policy: SatisfactionPolicy,
- pub evidence_mode: TradeEvidenceMode,
- pub privacy_confirmation: PrivacyPreflightConfirmation,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "signer-adapters")]
-impl TradeAcceptRequest {
- pub fn new(
- actor: RadrootsActorContext,
- locator: RadrootsTradeLocator,
- inventory_commitments: Vec<RadrootsOrderInventoryCommitment>,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- evidence_mode: TradeEvidenceMode,
- ) -> Self {
- Self {
- actor,
- locator,
- inventory_commitments,
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation: PrivacyPreflightConfirmation::new(),
- idempotency_key: None,
- created_at: None,
- }
- }
-
- pub fn with_privacy_confirmation(
- mut self,
- privacy_confirmation: PrivacyPreflightConfirmation,
- ) -> Self {
- self.privacy_confirmation = privacy_confirmation;
- self
- }
-
- pub fn try_with_idempotency_key(
- mut self,
- idempotency_key: impl AsRef<str>,
- ) -> Result<Self, RadrootsSdkError> {
- self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?);
- Ok(self)
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeDeclineRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub locator: RadrootsTradeLocator,
- pub reason: String,
- pub target_policy: TargetPolicy,
- pub publish_mode: PublishMode,
- pub satisfaction_policy: SatisfactionPolicy,
- pub evidence_mode: TradeEvidenceMode,
- pub privacy_confirmation: PrivacyPreflightConfirmation,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "signer-adapters")]
-impl TradeDeclineRequest {
- pub fn new(
- actor: RadrootsActorContext,
- locator: RadrootsTradeLocator,
- reason: impl Into<String>,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- evidence_mode: TradeEvidenceMode,
- ) -> Self {
- Self {
- actor,
- locator,
- reason: reason.into(),
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation: PrivacyPreflightConfirmation::new(),
- idempotency_key: None,
- created_at: None,
- }
- }
-
- pub fn with_privacy_confirmation(
- mut self,
- privacy_confirmation: PrivacyPreflightConfirmation,
- ) -> Self {
- self.privacy_confirmation = privacy_confirmation;
- self
- }
-
- pub fn try_with_idempotency_key(
- mut self,
- idempotency_key: impl AsRef<str>,
- ) -> Result<Self, RadrootsSdkError> {
- self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?);
- Ok(self)
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeCancelRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub locator: RadrootsTradeLocator,
- pub reason: String,
- pub target_policy: TargetPolicy,
- pub publish_mode: PublishMode,
- pub satisfaction_policy: SatisfactionPolicy,
- pub evidence_mode: TradeEvidenceMode,
- pub privacy_confirmation: PrivacyPreflightConfirmation,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "signer-adapters")]
-impl TradeCancelRequest {
- pub fn new(
- actor: RadrootsActorContext,
- locator: RadrootsTradeLocator,
- reason: impl Into<String>,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- evidence_mode: TradeEvidenceMode,
- ) -> Self {
- Self {
- actor,
- locator,
- reason: reason.into(),
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation: PrivacyPreflightConfirmation::new(),
- idempotency_key: None,
- created_at: None,
- }
- }
-
- pub fn with_privacy_confirmation(
- mut self,
- privacy_confirmation: PrivacyPreflightConfirmation,
- ) -> Self {
- self.privacy_confirmation = privacy_confirmation;
- self
- }
-
- pub fn try_with_idempotency_key(
- mut self,
- idempotency_key: impl AsRef<str>,
- ) -> Result<Self, RadrootsSdkError> {
- self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?);
- Ok(self)
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeSellerInboxRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub limit: u32,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeSellerInboxRequest {
- pub fn new(actor: RadrootsActorContext) -> Self {
- Self {
- actor,
- limit: TRADE_STATUS_DEFAULT_LIMIT,
- }
- }
-
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = limit;
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeSellerInboxReceipt {
- pub seller_pubkey: RadrootsPublicKey,
- pub statuses: Vec<TradeStatusReceipt>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeResyncRequest {
- pub locator: RadrootsTradeLocator,
- pub limit: u32,
- pub validation_trust_policy: RadrootsTradeValidationTrustPolicy,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeResyncRequest {
- pub fn new(locator: RadrootsTradeLocator) -> Self {
- Self {
- locator,
- limit: TRADE_STATUS_DEFAULT_LIMIT,
- validation_trust_policy: RadrootsTradeValidationTrustPolicy::production(),
- }
- }
-
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = limit;
- self
- }
-
- pub fn with_validation_trust_policy(
- mut self,
- policy: RadrootsTradeValidationTrustPolicy,
- ) -> Self {
- self.validation_trust_policy = policy;
- self
- }
-
- #[cfg(feature = "transport-nostr-runtime")]
- fn validate(&self) -> Result<(), RadrootsSdkError> {
- if self.limit == 0 || self.limit > TRADE_STATUS_MAX_LIMIT {
- return Err(RadrootsSdkError::trade_status_limit_invalid(
- self.limit,
- 1,
- TRADE_STATUS_MAX_LIMIT,
- ));
- }
- Ok(())
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeResyncReceipt {
- pub nostr_relay_urls: Vec<String>,
- pub evidence: TradeResyncEvidenceReceipt,
- pub refresh: SyncProjectionRefreshReceipt,
- pub status: TradeStatusReceipt,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeResyncEvidenceReceipt {
- pub query_plan: TradeEvidenceQueryPlan,
- pub inserted_count: usize,
- pub duplicate_count: usize,
- pub malformed_count: usize,
- pub out_of_filter_count: usize,
- pub skipped_over_limit_count: usize,
- pub unsupported_count: usize,
- pub eose_count: usize,
- pub closed_count: usize,
- pub notice_count: usize,
- pub branches: Vec<TradeEvidenceBranchReceipt>,
- pub events: Vec<TradeResyncEventImportReceipt>,
- pub nostr_relay_outcomes: Vec<TradeResyncNostrRelayOutcomeReceipt>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeEvidenceQueryPlan {
- pub locator: RadrootsTradeLocator,
- pub limit: u32,
- pub branches: Vec<TradeEvidenceQueryBranch>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeEvidenceQueryBranch {
- pub kind: TradeEvidenceQueryBranchKind,
- pub filter: TradeEvidenceNostrRelayFilter,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeEvidenceQueryBranchKind {
- RequestRoots,
- LifecycleChain,
- ValidationReceipts,
- ListingSnapshot,
- RejectedEvidence,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeEvidenceNostrRelayFilter {
- pub active: bool,
- pub event_kinds: Vec<u32>,
- pub author_pubkey: Option<String>,
- pub tag: Option<TradeEvidenceNostrRelayTagFilter>,
- pub limit: u32,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeEvidenceNostrRelayTagFilter {
- pub tag_name: String,
- pub values: Vec<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeEvidenceBranchReceipt {
- pub branch: TradeEvidenceQueryBranchKind,
- pub accepted_count: usize,
- pub inserted_count: usize,
- pub duplicate_count: usize,
- pub malformed_count: usize,
- pub out_of_filter_count: usize,
- pub skipped_over_limit_count: usize,
- pub unsupported_count: usize,
- pub nostr_relay_failure_count: usize,
- pub empty_result: bool,
- pub events: Vec<TradeResyncEventImportReceipt>,
- pub nostr_relay_outcomes: Vec<TradeResyncNostrRelayOutcomeReceipt>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeResyncEventImportReceipt {
- pub nostr_relay_url: String,
- pub event_id: Option<String>,
- pub inserted: bool,
- pub duplicate: bool,
- pub unsupported: bool,
- pub malformed: bool,
- pub out_of_filter: bool,
- pub skipped_over_limit: bool,
- pub projection_eligible: bool,
- pub verification_status: Option<String>,
- pub message: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeResyncNostrRelayOutcomeReceipt {
- pub nostr_relay_url: String,
- pub outcome_kind: TradeResyncNostrRelayOutcomeKind,
- pub transport_outcome_kind: Option<TradeResyncNostrRelayTransportOutcomeKind>,
- pub message: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeResyncNostrRelayOutcomeKind {
- Eose,
- Closed,
- Notice,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeResyncNostrRelayOutcomeKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Eose => "eose",
- Self::Closed => "closed",
- Self::Notice => "notice",
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeResyncNostrRelayTransportOutcomeKind {
- Accepted,
- DuplicateAccepted,
- Blocked,
- RateLimited,
- Invalid,
- PowRequired,
- Restricted,
- AuthRequired,
- Muted,
- Unsupported,
- PaymentRequired,
- Error,
- Timeout,
- ConnectionFailed,
- RelayUrlRejected,
- SkippedAlreadyAccepted,
- Unknown,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeResyncNostrRelayTransportOutcomeKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Accepted => "accepted",
- Self::DuplicateAccepted => "duplicate_accepted",
- Self::Blocked => "blocked",
- Self::RateLimited => "rate_limited",
- Self::Invalid => "invalid",
- Self::PowRequired => "pow_required",
- Self::Restricted => "restricted",
- Self::AuthRequired => "auth_required",
- Self::Muted => "muted",
- Self::Unsupported => "unsupported",
- Self::PaymentRequired => "payment_required",
- Self::Error => "error",
- Self::Timeout => "timeout",
- Self::ConnectionFailed => "connection_failed",
- Self::RelayUrlRejected => "relay_url_rejected",
- Self::SkippedAlreadyAccepted => "skipped_already_accepted",
- Self::Unknown => "unknown",
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeValidationReceiptListRequest {
- pub order_id: RadrootsOrderId,
- pub limit: u32,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeValidationReceiptListRequest {
- pub fn new(order_id: RadrootsOrderId) -> Self {
- Self {
- order_id,
- limit: TRADE_STATUS_DEFAULT_LIMIT,
- }
- }
-
- pub fn parse(order_id: &str) -> Result<Self, RadrootsSdkError> {
- RadrootsOrderId::parse(order_id)
- .map(Self::new)
- .map_err(|error| RadrootsSdkError::invalid_trade_id(order_id, error.to_string()))
- }
-
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = limit;
- self
- }
-
- #[cfg(feature = "transport-nostr-runtime")]
- fn validate(&self) -> Result<(), RadrootsSdkError> {
- validate_validation_receipt_limit(self.limit)
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeValidationReceiptInspectRequest {
- pub receipt_event_id: RadrootsEventId,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeValidationReceiptInspectRequest {
- pub fn new(receipt_event_id: RadrootsEventId) -> Self {
- Self { receipt_event_id }
- }
-
- pub fn parse(receipt_event_id: &str) -> Result<Self, RadrootsSdkError> {
- RadrootsEventId::parse(receipt_event_id)
- .map(Self::new)
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!(
- "invalid validation receipt event id `{receipt_event_id}`: {error}"
- ),
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeValidationReceiptVerifyRequest {
- pub receipt_event_id: RadrootsEventId,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeValidationReceiptVerifyRequest {
- pub fn new(receipt_event_id: RadrootsEventId) -> Self {
- Self { receipt_event_id }
- }
-
- pub fn parse(receipt_event_id: &str) -> Result<Self, RadrootsSdkError> {
- RadrootsEventId::parse(receipt_event_id)
- .map(Self::new)
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!(
- "invalid validation receipt event id `{receipt_event_id}`: {error}"
- ),
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptListReceipt {
- pub nostr_relay_urls: Vec<String>,
- pub nostr_evidence: TradeValidationReceiptNostrEvidenceReceipt,
- pub order_id: RadrootsOrderId,
- pub receipts: Vec<TradeValidationReceiptEvent>,
- pub invalid_receipts: Vec<TradeValidationReceiptInvalidCandidate>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptInspectReceipt {
- pub nostr_relay_urls: Vec<String>,
- pub nostr_evidence: TradeValidationReceiptNostrEvidenceReceipt,
- pub receipt_event_id: RadrootsEventId,
- pub receipt: Option<TradeValidationReceiptEvent>,
- pub invalid_receipt: Option<TradeValidationReceiptInvalidCandidate>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptEvent {
- pub event: RadrootsEventEnvelope,
- pub receipt: RadrootsTradeValidationReceipt,
- pub tags: TradeValidationReceiptTags,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptInvalidCandidate {
- pub event: RadrootsEventEnvelope,
- pub reason_code: String,
- pub reason: String,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptTags {
- pub order_id: String,
- pub event_set_root: String,
- pub listing_event_id: String,
- pub reducer_output_root: String,
- pub public_values_hash: String,
- pub proof_system: String,
- pub receipt_type: String,
- pub root_event_id: String,
- pub target_event_id: String,
- pub validator_set_addr: String,
- pub validator_set_event_id: String,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptNostrEvidenceReceipt {
- pub inserted_count: usize,
- pub duplicate_count: usize,
- pub malformed_count: usize,
- pub out_of_filter_count: usize,
- pub skipped_over_limit_count: usize,
- pub unsupported_count: usize,
- pub eose_count: usize,
- pub closed_count: usize,
- pub notice_count: usize,
- pub events: Vec<TradeResyncEventImportReceipt>,
- pub nostr_relay_outcomes: Vec<TradeValidationReceiptNostrRelayOutcomeReceipt>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptNostrRelayOutcomeReceipt {
- pub nostr_relay_url: String,
- pub outcome_kind: TradeValidationReceiptNostrRelayOutcomeKind,
- pub transport_outcome_kind: Option<TradeValidationReceiptNostrRelayTransportOutcomeKind>,
- pub message: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeValidationReceiptNostrRelayOutcomeKind {
- Eose,
- Closed,
- Notice,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeValidationReceiptNostrRelayOutcomeKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Eose => "eose",
- Self::Closed => "closed",
- Self::Notice => "notice",
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeValidationReceiptNostrRelayTransportOutcomeKind {
- Accepted,
- DuplicateAccepted,
- Blocked,
- RateLimited,
- Invalid,
- PowRequired,
- Restricted,
- AuthRequired,
- Muted,
- Unsupported,
- PaymentRequired,
- Error,
- Timeout,
- ConnectionFailed,
- RelayUrlRejected,
- SkippedAlreadyAccepted,
- Unknown,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeValidationReceiptNostrRelayTransportOutcomeKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Accepted => "accepted",
- Self::DuplicateAccepted => "duplicate_accepted",
- Self::Blocked => "blocked",
- Self::RateLimited => "rate_limited",
- Self::Invalid => "invalid",
- Self::PowRequired => "pow_required",
- Self::Restricted => "restricted",
- Self::AuthRequired => "auth_required",
- Self::Muted => "muted",
- Self::Unsupported => "unsupported",
- Self::PaymentRequired => "payment_required",
- Self::Error => "error",
- Self::Timeout => "timeout",
- Self::ConnectionFailed => "connection_failed",
- Self::RelayUrlRejected => "relay_url_rejected",
- Self::SkippedAlreadyAccepted => "skipped_already_accepted",
- Self::Unknown => "unknown",
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeStatusRequest {
- pub locator: RadrootsTradeLocator,
- pub limit: u32,
- pub source: SdkTradeStatusSource,
- pub validation_trust_policy: RadrootsTradeValidationTrustPolicy,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeStatusRequest {
- pub fn new(locator: RadrootsTradeLocator) -> Self {
- Self {
- locator,
- limit: TRADE_STATUS_DEFAULT_LIMIT,
- source: SdkTradeStatusSource::LocalOnly,
- validation_trust_policy: RadrootsTradeValidationTrustPolicy::production(),
- }
- }
-
- pub fn parse(selector: &str) -> Result<Self, RadrootsSdkError> {
- let (trade_id, root_event_id) = trade_status_selector_parts(selector)?;
- let locator = RadrootsOrderId::parse(trade_id)
- .map(RadrootsTradeLocator::from_order_id)
- .map_err(|error| RadrootsSdkError::invalid_trade_id(trade_id, error.to_string()))?;
- let locator = match root_event_id {
- Some(root_event_id) => {
- locator.with_root_event_id(root_event_id.parse().map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!(
- "invalid trade status root selector `{selector}`: {error}"
- ),
- }
- })?)
- }
- None => locator,
- };
- Ok(Self::new(locator))
- }
-
- pub fn locator_selector(locator: &RadrootsTradeLocator) -> String {
- match locator.root_event_id.as_ref() {
- Some(root_event_id) => format!(
- "{}{TRADE_STATUS_ROOT_SELECTOR_SEPARATOR}{}",
- locator.trade_id.as_str(),
- root_event_id.as_str()
- ),
- None => locator.trade_id.as_str().to_owned(),
- }
- }
-
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = limit;
- self
- }
-
- pub fn with_source(mut self, source: SdkTradeStatusSource) -> Self {
- self.source = source;
- self
- }
-
- pub fn with_validation_trust_policy(
- mut self,
- policy: RadrootsTradeValidationTrustPolicy,
- ) -> Self {
- self.validation_trust_policy = policy;
- self
- }
-
- fn validate(&self) -> Result<(), RadrootsSdkError> {
- if self.limit == 0 || self.limit > TRADE_STATUS_MAX_LIMIT {
- return Err(RadrootsSdkError::trade_status_limit_invalid(
- self.limit,
- 1,
- TRADE_STATUS_MAX_LIMIT,
- ));
- }
- Ok(())
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeStatusWatchRequest {
- pub status: TradeStatusRequest,
- pub capacity: usize,
- pub refresh_interval_ms: u64,
- pub refresh_limit: Option<u32>,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeStatusWatchRequest {
- pub fn new(status: TradeStatusRequest) -> Self {
- Self {
- status,
- capacity: TRADE_STATUS_WATCH_DEFAULT_CAPACITY,
- refresh_interval_ms: TRADE_STATUS_WATCH_DEFAULT_REFRESH_INTERVAL_MS,
- refresh_limit: None,
- }
- }
-
- pub fn parse(selector: &str) -> Result<Self, RadrootsSdkError> {
- TradeStatusRequest::parse(selector).map(Self::new)
- }
-
- pub fn with_capacity(mut self, capacity: usize) -> Self {
- self.capacity = capacity;
- self
- }
-
- pub fn with_refresh_interval_ms(mut self, refresh_interval_ms: u64) -> Self {
- self.refresh_interval_ms = refresh_interval_ms;
- self
- }
-
- pub fn with_refresh_limit(mut self, refresh_limit: u32) -> Self {
- self.refresh_limit = Some(refresh_limit);
- self
- }
-
- pub fn without_refresh_limit(mut self) -> Self {
- self.refresh_limit = None;
- self
- }
-
- fn validate(&self) -> Result<(), RadrootsSdkError> {
- self.status.validate()?;
- if self.capacity == 0 || self.capacity > TRADE_STATUS_WATCH_MAX_CAPACITY {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "trade status watch capacity must be between 1 and {TRADE_STATUS_WATCH_MAX_CAPACITY}"
- ),
- });
- }
- if self.refresh_interval_ms == 0
- || self.refresh_interval_ms > TRADE_STATUS_WATCH_MAX_REFRESH_INTERVAL_MS
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "trade status watch refresh interval must be between 1 and {TRADE_STATUS_WATCH_MAX_REFRESH_INTERVAL_MS} milliseconds"
- ),
- });
- }
- if self.refresh_limit == Some(0) {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "trade status watch refresh limit must be greater than zero".to_owned(),
- });
- }
- Ok(())
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeStatusWatchUpdate {
- pub sequence: u64,
- pub status: TradeStatusReceipt,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeStatusWatchCancelReceipt {
- pub state: TradeStatusWatchCancelState,
- pub buffered_updates_dropped: usize,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeStatusWatchCancelState {
- Cancelled,
- AlreadyFinished,
-}
-
-#[cfg(feature = "runtime")]
-pub struct TradeStatusWatch {
- receiver: mpsc::Receiver<Result<TradeStatusWatchUpdate, RadrootsSdkError>>,
- cancel: Option<oneshot::Sender<()>>,
- producer: Option<JoinHandle<()>>,
- capacity: usize,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeStatusWatch {
- pub fn capacity(&self) -> usize {
- self.capacity
- }
-
- pub fn buffered_len(&self) -> usize {
- self.receiver.len()
- }
-
- pub async fn next(&mut self) -> Result<Option<TradeStatusWatchUpdate>, RadrootsSdkError> {
- match self.receiver.recv().await {
- Some(Ok(update)) => Ok(Some(update)),
- Some(Err(error)) => Err(error),
- None => Ok(None),
- }
- }
-
- pub async fn cancel(&mut self) -> TradeStatusWatchCancelReceipt {
- let producer_active = self
- .producer
- .as_ref()
- .map(|producer| !producer.is_finished())
- .unwrap_or(false);
- let buffered_updates_dropped = self.drain_buffered_updates();
- let cancel_sent = self
- .cancel
- .take()
- .map(|sender| sender.send(()).is_ok())
- .unwrap_or(false);
- let producer_finished = match self.producer.take() {
- Some(producer) => producer.await.is_ok(),
- None => true,
- };
- let state = if producer_active || cancel_sent || !producer_finished {
- TradeStatusWatchCancelState::Cancelled
- } else {
- TradeStatusWatchCancelState::AlreadyFinished
- };
- TradeStatusWatchCancelReceipt {
- state,
- buffered_updates_dropped,
- }
- }
-
- fn drain_buffered_updates(&mut self) -> usize {
- self.receiver.close();
- let mut dropped = 0;
- while self.receiver.try_recv().is_ok() {
- dropped += 1;
- }
- dropped
- }
-}
-
-#[cfg(feature = "runtime")]
-impl Drop for TradeStatusWatch {
- fn drop(&mut self) {
- self.drain_buffered_updates();
- if let Some(cancel) = self.cancel.take() {
- let _ = cancel.send(());
- }
- if let Some(producer) = self.producer.take() {
- producer.abort();
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-fn trade_status_selector_parts(selector: &str) -> Result<(&str, Option<&str>), RadrootsSdkError> {
- let selector = selector.trim();
- if selector.is_empty() {
- return Err(RadrootsSdkError::invalid_trade_id(
- selector,
- "empty trade id",
- ));
- }
- let Some((trade_id, root_event_id)) = selector.split_once(TRADE_STATUS_ROOT_SELECTOR_SEPARATOR)
- else {
- return Ok((selector, None));
- };
- if trade_id.trim().is_empty()
- || root_event_id.trim().is_empty()
- || root_event_id.contains(TRADE_STATUS_ROOT_SELECTOR_SEPARATOR)
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!("invalid trade status selector `{selector}`"),
- });
- }
- Ok((trade_id, Some(root_event_id)))
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeStatusReceipt {
- pub locator: RadrootsTradeLocator,
- pub order_id: RadrootsOrderId,
- pub root_event_id: Option<RadrootsEventId>,
- pub ambiguity_candidates: Vec<TradeStatusAmbiguityCandidate>,
- pub source: SdkTradeStatusSource,
- pub found: bool,
- pub event_count: usize,
- pub limit_applied: u32,
- pub status: TradeStatusKind,
- pub lifecycle_terminal: bool,
- pub listing_addr: Option<RadrootsListingAddress>,
- pub buyer_pubkey: Option<RadrootsPublicKey>,
- pub seller_pubkey: Option<RadrootsPublicKey>,
- pub economics: Option<RadrootsOrderEconomics>,
- pub evidence: TradeStatusEvidenceSummary,
- pub validation_trust: Option<TradeValidationTrustDecision>,
- pub online_evidence: Option<TradeResyncEvidenceReceipt>,
- pub eligibility: TradeStatusEligibility,
- pub next_action: TradeStatusNextActionKind,
- pub event_ids: Vec<RadrootsEventId>,
- pub request_event_id: Option<RadrootsEventId>,
- pub decision_event_id: Option<RadrootsEventId>,
- pub agreement_event_id: Option<RadrootsEventId>,
- pub rhi_receipt_event_id: Option<RadrootsEventId>,
- pub cancellation_event_id: Option<RadrootsEventId>,
- pub last_event_id: Option<RadrootsEventId>,
- pub issues: Vec<SdkTradeStatusIssue>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeStatusAmbiguityCandidate {
- pub locator: RadrootsTradeLocator,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeStatusEvidenceSummary {
- pub event_count: usize,
- pub limit_applied: u32,
- pub has_request: bool,
- pub has_decision: bool,
- pub has_agreement: bool,
- pub has_validation_receipt: bool,
- pub has_cancellation: bool,
- pub has_issues: bool,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationTrustDecision {
- pub state: RadrootsTradeValidationTrustState,
- pub validator_count: usize,
- pub validator_set_addr: Option<String>,
- pub validator_set_event_id: Option<String>,
- pub require_cryptographic_proof: bool,
- pub receipt_event_id: Option<RadrootsEventId>,
- pub receipt_author: Option<RadrootsPublicKey>,
- pub result_event_id: Option<RadrootsEventId>,
- pub result_author: Option<RadrootsPublicKey>,
- pub proof_system: Option<String>,
- pub validation_authority: Option<RadrootsTradeValidationAuthority>,
- pub commitment_confidence: Option<RadrootsTradeCommitmentConfidence>,
- pub cryptographic_proof_required: bool,
- pub cryptographic_proof_verified: bool,
- pub production_committed: bool,
- pub reason_code: Option<String>,
- pub reason: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeStatusEligibility {
- pub can_decide: bool,
- pub can_cancel: bool,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeStatusNextActionKind {
- NoLocalOrder,
- InspectEvidenceIssues,
- AwaitSellerDecision,
- AwaitValidation,
- Terminal,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum SdkTradeStatusSource {
- LocalOnly,
- ResyncThenLocal,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeStatusKind {
- Missing,
- Ambiguous,
- Requested,
- AgreedPendingValidation,
- Committed,
- Declined,
- Cancelled,
- ValidationExpired,
- Invalid,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct SdkTradeStatusIssue {
- pub kind: SdkTradeStatusIssueKind,
- pub event_ids: Vec<RadrootsEventId>,
-}
-
-#[cfg(feature = "runtime")]
-impl SdkTradeStatusIssue {
- fn new(kind: SdkTradeStatusIssueKind, event_ids: Vec<RadrootsEventId>) -> Self {
- Self { kind, event_ids }
- }
-
- fn single(kind: SdkTradeStatusIssueKind, event_id: RadrootsEventId) -> Self {
- Self::new(kind, vec![event_id])
- }
-
- pub fn code(&self) -> String {
- self.kind.code()
- }
-}
-
-#[cfg(feature = "runtime")]
-impl serde::Serialize for SdkTradeStatusIssue {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- let mut state = serializer.serialize_struct("SdkTradeStatusIssue", 3)?;
- state.serialize_field("code", &self.code())?;
- state.serialize_field("kind", &self.kind)?;
- state.serialize_field("event_ids", &self.event_ids)?;
- state.end()
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum SdkTradeStatusIssueKind {
- MissingRequest,
- MultipleRequests,
- RequestPayloadInvalid,
- RequestOrderIdMismatch,
- RequestAuthorMismatch,
- RequestListingAddressInvalid,
- RequestSellerListingMismatch,
- DecisionPayloadInvalid,
- DecisionOrderIdMismatch,
- DecisionAuthorMismatch,
- DecisionCounterpartyMismatch,
- DecisionBuyerMismatch,
- DecisionSellerMismatch,
- DecisionListingAddressInvalid,
- DecisionListingMismatch,
- DecisionRootMismatch,
- DecisionPreviousMismatch,
- DecisionMissingInventoryCommitments,
- DecisionInventoryCommitmentMismatch,
- DecisionMissingReason,
- ConflictingDecisions,
- CancellationWithoutCancellableOrder,
- CancellationPayloadInvalid,
- CancellationOrderIdMismatch,
- CancellationAuthorMismatch,
- CancellationCounterpartyMismatch,
- CancellationBuyerMismatch,
- CancellationSellerMismatch,
- CancellationListingAddressInvalid,
- CancellationListingMismatch,
- CancellationRootMismatch,
- CancellationPreviousMismatch,
- ForkedLifecycle,
- ValidationReceiptWithoutPendingAgreement,
- ValidationReceiptOrderIdMismatch,
- ValidationReceiptTypeMismatch,
- ValidationReceiptRootMismatch,
- ValidationReceiptTargetMismatch,
- ValidationReceiptListingMismatch,
- ConflictingValidationReceipts,
- DeterministicValidationFailure,
- StaleListingEvent,
-}
-
-#[cfg(feature = "runtime")]
-impl SdkTradeStatusIssueKind {
- pub fn code(self) -> String {
- camel_to_snake(format!("{self:?}").as_str())
- }
-}
-
-#[cfg(feature = "runtime")]
-impl<'sdk> TradesClient<'sdk> {
- pub async fn ingest_evidence(
- &self,
- request: TradeEvidenceIngestRequest,
- ) -> Result<TradeEvidenceIngestReceipt, RadrootsSdkError> {
- let evidence = parse_order_evidence(request.event.envelope())?;
- let observed_at = self.resolved_created_at(request.observed_at)?;
- let observed_at_ms = sdk_timestamp_ms(observed_at)?;
- let receipt = self
- .sdk
- ._event_store
- .ingest_event(RadrootsEventIngest::new(request.event, observed_at_ms))
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Ok(TradeEvidenceIngestReceipt {
- order_id: evidence.order_id,
- listing_addr: evidence.listing_addr,
- event_id: evidence.event_id,
- event_kind: evidence.event_kind,
- local_event_seq: receipt.seq,
- inserted: receipt.inserted,
- })
- }
-
- pub async fn ingest_request_evidence(
- &self,
- request: TradeRequestEvidenceIngestRequest,
- ) -> Result<TradeRequestEvidenceIngestReceipt, RadrootsSdkError> {
- let evidence = parse_order_request_evidence(request.event.envelope())?;
- let observed_at = self.resolved_created_at(request.observed_at)?;
- let observed_at_ms = sdk_timestamp_ms(observed_at)?;
- let receipt = self
- .sdk
- ._event_store
- .ingest_event(RadrootsEventIngest::new(request.event, observed_at_ms))
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Ok(TradeRequestEvidenceIngestReceipt {
- order_id: evidence.order_id,
- listing_addr: evidence.listing_addr,
- buyer_pubkey: evidence.buyer_pubkey,
- seller_pubkey: evidence.seller_pubkey,
- request_event_id: evidence.request_event_id,
- local_event_seq: receipt.seq,
- inserted: receipt.inserted,
- })
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) fn prepare_submit(
- &self,
- request: TradeSubmitPrepareRequest,
- ) -> Result<TradeSubmitPlan, RadrootsSdkError> {
- let created_at = self.resolved_created_at(request.created_at)?;
- order_submit_plan(
- &request.actor,
- request.listing_event,
- request.order,
- created_at,
- )
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) async fn enqueue_prepared_submit(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeSubmitPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- ) -> Result<TradeSubmitReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let enqueue = enqueue_configured_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::Submit.operation_kind(),
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy,
- idempotency_key,
- },
- )
- .await?;
- Ok(order_submit_receipt(plan, enqueue))
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) fn prepare_decision(
- &self,
- request: TradeDecisionPrepareRequest,
- ) -> Result<TradeDecisionPlan, RadrootsSdkError> {
- let created_at = self.resolved_created_at(request.created_at)?;
- order_decision_plan(
- &request.actor,
- request.request_event,
- request.decision,
- created_at,
- )
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) async fn enqueue_prepared_decision(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeDecisionPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- ) -> Result<TradeDecisionReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- if !self
- .prepared_order_event_exists(&plan.expected_event_id)
- .await?
- {
- self.require_decision_preflight(&plan).await?;
- }
- let enqueue = enqueue_configured_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::Decision.operation_kind(),
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy,
- idempotency_key,
- },
- )
- .await?;
- Ok(order_decision_receipt(plan, enqueue))
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) fn prepare_cancellation(
- &self,
- request: TradeCancellationPrepareRequest,
- ) -> Result<TradeCancellationPlan, RadrootsSdkError> {
- let created_at = self.resolved_created_at(request.created_at)?;
- order_cancellation_plan(
- &request.actor,
- request.root_event,
- request.previous_event,
- request.cancellation,
- created_at,
- )
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) async fn enqueue_prepared_cancellation(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeCancellationPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- ) -> Result<TradeCancellationReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- if !self
- .prepared_order_event_exists(&plan.expected_event_id)
- .await?
- {
- self.require_cancellation_preflight(&plan).await?;
- }
- let enqueue = enqueue_configured_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::Cancellation.operation_kind(),
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy,
- idempotency_key,
- },
- )
- .await?;
- Ok(order_cancellation_receipt(plan, enqueue))
- }
-
- pub async fn status(
- &self,
- request: TradeStatusRequest,
- ) -> Result<TradeStatusReceipt, RadrootsSdkError> {
- match request.source {
- SdkTradeStatusSource::LocalOnly => self.local_status(request).await,
- SdkTradeStatusSource::ResyncThenLocal => {
- #[cfg(feature = "transport-nostr-runtime")]
- {
- let adapter = RadrootsNostrClientFetchAdapter;
- return self.status_with_fetch_adapter(request, &adapter).await;
- }
- #[cfg(not(feature = "transport-nostr-runtime"))]
- {
- let _ = request;
- Err(RadrootsSdkError::ProductSyncUnsupported {
- operation: "trade.status.resync_then_local",
- required_feature: "transport-nostr-runtime",
- })
- }
- }
- }
- }
-
- pub async fn watch(
- &self,
- request: TradeStatusWatchRequest,
- ) -> Result<TradeStatusWatch, RadrootsSdkError> {
- request.validate()?;
- let handle = tokio::runtime::Handle::try_current().map_err(|_| {
- RadrootsSdkError::InvalidRequest {
- message: "trade status watch requires an active Tokio runtime".to_owned(),
- }
- })?;
- let (updates, receiver) = mpsc::channel(request.capacity);
- let (cancel, cancel_receiver) = oneshot::channel();
- let sdk = self.sdk.clone();
- let capacity = request.capacity;
- let producer = handle.spawn(run_trade_status_watch(
- sdk,
- request,
- updates,
- cancel_receiver,
- ));
- Ok(TradeStatusWatch {
- receiver,
- cancel: Some(cancel),
- producer: Some(producer),
- capacity,
- })
- }
-
- #[cfg(all(feature = "signer-adapters", feature = "transport-nostr-runtime"))]
- pub async fn status_with_fetch_adapter<A>(
- &self,
- request: TradeStatusRequest,
- adapter: &A,
- ) -> Result<TradeStatusReceipt, RadrootsSdkError>
- where
- A: RadrootsRelayFetchAdapter,
- {
- match request.source {
- SdkTradeStatusSource::LocalOnly => self.local_status(request).await,
- SdkTradeStatusSource::ResyncThenLocal => {
- request.validate()?;
- let execution = execute_trade_resync_with_fetch_adapter(
- self.sdk,
- request.locator.clone(),
- request.limit,
- adapter,
- "trade.status.resync_then_local",
- )
- .await?;
- let mut status = self
- .local_status(
- TradeStatusRequest::new(request.locator)
- .with_limit(request.limit)
- .with_validation_trust_policy(request.validation_trust_policy),
- )
- .await?;
- status.source = SdkTradeStatusSource::ResyncThenLocal;
- status.online_evidence = Some(execution.evidence);
- Ok(status)
- }
- }
- }
-
- async fn local_status(
- &self,
- request: TradeStatusRequest,
- ) -> Result<TradeStatusReceipt, RadrootsSdkError> {
- request.validate()?;
- let query_result = order_projection_query_for_trade_locator(
- &self.sdk._event_store,
- &request.locator,
- request.limit,
- )
- .await
- .map_err(projection_error)?;
- let mut receipt =
- TradeStatusReceipt::from_locator_query_result(request.locator, query_result);
- receipt.source = request.source;
- apply_trade_status_validation_trust(
- self.sdk,
- &mut receipt,
- &request.validation_trust_policy,
- )
- .await?;
- Ok(receipt)
- }
-
- fn resolved_created_at(
- &self,
- created_at: Option<RadrootsSdkTimestamp>,
- ) -> Result<RadrootsSdkTimestamp, RadrootsSdkError> {
- match created_at {
- Some(created_at) => Ok(created_at),
- None => self.sdk.now(),
- }
- }
-
- #[cfg(feature = "signer-adapters")]
- async fn require_decision_preflight(
- &self,
- plan: &TradeDecisionPlan,
- ) -> Result<(), RadrootsSdkError> {
- let query_result = self.query_order_projection(&plan.order_id).await?;
- require_decision_request_evidence(plan, &query_result.projection)
- }
-
- #[cfg(feature = "signer-adapters")]
- async fn require_cancellation_preflight(
- &self,
- plan: &TradeCancellationPlan,
- ) -> Result<(), RadrootsSdkError> {
- let query_result = self.query_order_projection(&plan.order_id).await?;
- require_cancellation_state(plan, &query_result.projection)
- }
-
- #[cfg(feature = "signer-adapters")]
- async fn query_order_projection(
- &self,
- order_id: &RadrootsOrderId,
- ) -> Result<RadrootsOrderProjectionQueryResult, RadrootsSdkError> {
- order_projection_query_for_order_id(
- &self.sdk._event_store,
- order_id,
- TRADE_STATUS_MAX_LIMIT,
- )
- .await
- .map_err(projection_error)
- }
-
- #[cfg(feature = "signer-adapters")]
- async fn prepared_order_event_exists(
- &self,
- expected_event_id: &RadrootsEventId,
- ) -> Result<bool, RadrootsSdkError> {
- self.sdk
- ._event_store
- .get_event(expected_event_id.as_str())
- .await
- .map(|event| event.is_some())
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-async fn run_trade_status_watch(
- sdk: crate::RadrootsClient,
- request: TradeStatusWatchRequest,
- updates: mpsc::Sender<Result<TradeStatusWatchUpdate, RadrootsSdkError>>,
- mut cancel: oneshot::Receiver<()>,
-) {
- let refresh_interval = Duration::from_millis(request.refresh_interval_ms);
- let mut sequence = 0_u64;
- loop {
- let status_result = sdk.trades().status(request.status.clone()).await;
- let stop_after_update = status_result
- .as_ref()
- .map(|status| status.lifecycle_terminal)
- .unwrap_or(true);
- sequence += 1;
- let update_result = status_result.map(|status| TradeStatusWatchUpdate { sequence, status });
- let send_result = tokio::select! {
- _ = &mut cancel => break,
- send_result = updates.send(update_result) => send_result,
- };
- if send_result.is_err() || stop_after_update {
- break;
- }
- if request
- .refresh_limit
- .map(|refresh_limit| sequence >= u64::from(refresh_limit))
- .unwrap_or(false)
- {
- break;
- }
- tokio::select! {
- _ = &mut cancel => break,
- _ = tokio::time::sleep(refresh_interval) => {}
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-impl<'sdk> TradeResyncClient<'sdk> {
- pub async fn resync(
- &self,
- request: TradeResyncRequest,
- ) -> Result<TradeResyncReceipt, RadrootsSdkError> {
- #[cfg(feature = "transport-nostr-runtime")]
- {
- let adapter = RadrootsNostrClientFetchAdapter;
- return self.resync_with_fetch_adapter(request, &adapter).await;
- }
- #[cfg(not(feature = "transport-nostr-runtime"))]
- {
- let _ = self.sdk;
- let _ = request;
- Err(RadrootsSdkError::ProductSyncUnsupported {
- operation: "trade.resync",
- required_feature: "transport-nostr-runtime",
- })
- }
- }
-
- #[cfg(feature = "transport-nostr-runtime")]
- pub async fn resync_with_fetch_adapter<A>(
- &self,
- request: TradeResyncRequest,
- adapter: &A,
- ) -> Result<TradeResyncReceipt, RadrootsSdkError>
- where
- A: RadrootsRelayFetchAdapter,
- {
- request.validate()?;
- let execution = execute_trade_resync_with_fetch_adapter(
- self.sdk,
- request.locator.clone(),
- request.limit,
- adapter,
- "trade.resync",
- )
- .await?;
- let status = trades_client(self.sdk)
- .status(
- TradeStatusRequest::new(request.locator.clone())
- .with_limit(request.limit)
- .with_validation_trust_policy(request.validation_trust_policy.clone()),
- )
- .await?;
- if status.status == TradeStatusKind::Ambiguous {
- return Err(RadrootsSdkError::TradeAmbiguous {
- operation: "trade.resync".to_owned(),
- locator: Box::new(request.locator),
- candidates: status
- .ambiguity_candidates
- .iter()
- .map(|candidate| candidate.locator.clone())
- .collect(),
- });
- }
- Ok(TradeResyncReceipt {
- nostr_relay_urls: execution.nostr_relay_urls,
- evidence: execution.evidence,
- refresh: execution.refresh,
- status,
- })
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-struct TradeResyncExecution {
- nostr_relay_urls: Vec<String>,
- evidence: TradeResyncEvidenceReceipt,
- refresh: SyncProjectionRefreshReceipt,
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-async fn execute_trade_resync_with_fetch_adapter<A>(
- sdk: &crate::RadrootsClient,
- locator: RadrootsTradeLocator,
- limit: u32,
- adapter: &A,
- operation: &'static str,
-) -> Result<TradeResyncExecution, RadrootsSdkError>
-where
- A: RadrootsRelayFetchAdapter,
-{
- let nostr_relay_urls = sdk.configured_nostr_relay_urls().to_vec();
- if nostr_relay_urls.is_empty() {
- return Err(RadrootsSdkError::empty_transport_targets(operation));
- }
- let query_plan = trade_evidence_query_plan(locator, limit)?;
- let fetch_request = trade_evidence_fetch_request(sdk, &query_plan, &nostr_relay_urls)?;
- let fetch_receipt =
- fetch_and_ingest_relay_events(adapter, &sdk._event_store, fetch_request).await?;
- if trade_resync_total_relay_failure(&fetch_receipt, nostr_relay_urls.len()) {
- return Err(RadrootsSdkError::ProductSyncTransportSetupFailure {
- message: trade_resync_total_failure_message(operation, &fetch_receipt),
- });
- }
- let evidence = TradeResyncEvidenceReceipt::from_fetch(sdk, query_plan, fetch_receipt).await?;
- let refresh = refresh_product_projections_for_sdk(
- sdk,
- SyncProjectionRefreshRequest::new().with_limit(limit),
- )
- .await?;
- Ok(TradeResyncExecution {
- nostr_relay_urls,
- evidence,
- refresh,
- })
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_evidence_fetch_request(
- sdk: &crate::RadrootsClient,
- query_plan: &TradeEvidenceQueryPlan,
- nostr_relay_urls: &[String],
-) -> Result<RadrootsRelayFetchRequest, RadrootsSdkError> {
- let mut filters = Vec::new();
- for branch in query_plan
- .branches
- .iter()
- .filter(|branch| branch.filter.active)
- {
- filters.extend(trade_evidence_branch_filters(branch)?);
- }
- if filters.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "trade evidence query plan has no active relay filters".to_owned(),
- });
- }
- Ok(
- RadrootsRelayFetchRequest::fetch(sdk_now_ms(sdk)?, query_plan.limit as usize, filters)?
- .with_relay_urls(nostr_relay_urls.iter().cloned()),
- )
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_evidence_query_plan(
- locator: RadrootsTradeLocator,
- limit: u32,
-) -> Result<TradeEvidenceQueryPlan, RadrootsSdkError> {
- let order_id = locator.order_id().as_str().to_owned();
- let root_event_id = locator
- .root_event_id
- .as_ref()
- .map(|value| value.as_str().to_owned());
- let lifecycle_tag = root_event_id
- .map(|event_id| (TAG_E, vec![event_id]))
- .unwrap_or_else(|| (TAG_D, vec![order_id.clone()]));
- let listing_filter = locator
- .listing_addr
- .as_ref()
- .map(|value| listing_snapshot_filter_parts(value.as_str()))
- .transpose()?;
- let branches = vec![
- trade_evidence_branch(
- TradeEvidenceQueryBranchKind::RequestRoots,
- vec![KIND_ORDER_REQUEST],
- Some((TAG_D, vec![order_id.clone()])),
- None,
- limit,
- true,
- ),
- trade_evidence_branch(
- TradeEvidenceQueryBranchKind::LifecycleChain,
- vec![KIND_ORDER_DECISION, KIND_ORDER_CANCELLATION],
- Some(lifecycle_tag),
- None,
- limit,
- true,
- ),
- trade_evidence_branch(
- TradeEvidenceQueryBranchKind::ValidationReceipts,
- vec![KIND_TRADE_VALIDATION_RECEIPT],
- Some((TAG_D, vec![order_id])),
- None,
- limit,
- true,
- ),
- trade_evidence_branch(
- TradeEvidenceQueryBranchKind::ListingSnapshot,
- vec![KIND_LISTING],
- listing_filter
- .as_ref()
- .map(|parts| (TAG_D, vec![parts.listing_id.clone()])),
- listing_filter
- .as_ref()
- .map(|parts| parts.seller_pubkey.clone()),
- limit,
- locator.listing_addr.is_some(),
- ),
- ];
- Ok(TradeEvidenceQueryPlan {
- locator,
- limit,
- branches,
- })
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_evidence_branch(
- kind: TradeEvidenceQueryBranchKind,
- event_kinds: Vec<u32>,
- tag: Option<(&'static str, Vec<String>)>,
- author_pubkey: Option<String>,
- limit: u32,
- active: bool,
-) -> TradeEvidenceQueryBranch {
- TradeEvidenceQueryBranch {
- kind,
- filter: TradeEvidenceNostrRelayFilter {
- active,
- event_kinds,
- author_pubkey,
- tag: tag.map(|(tag_name, values)| TradeEvidenceNostrRelayTagFilter {
- tag_name: tag_name.to_owned(),
- values,
- }),
- limit,
- },
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_evidence_branch_filters(
- branch: &TradeEvidenceQueryBranch,
-) -> Result<Vec<RadrootsNostrFilter>, RadrootsSdkError> {
- if branch.filter.event_kinds.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "trade evidence branch has no event kinds".to_owned(),
- });
- }
- branch
- .filter
- .event_kinds
- .iter()
- .map(|kind| trade_evidence_branch_filter(branch, *kind))
- .collect()
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_evidence_branch_filter(
- branch: &TradeEvidenceQueryBranch,
- kind: u32,
-) -> Result<RadrootsNostrFilter, RadrootsSdkError> {
- let mut filter = RadrootsNostrFilter::new().limit(branch.filter.limit as usize);
- let nostr_kind = u16::try_from(kind).map_err(|_| RadrootsSdkError::InvalidRequest {
- message: format!("trade evidence event kind {kind} exceeds Nostr filter range"),
- })?;
- filter = filter.kind(RadrootsNostrKind::Custom(nostr_kind));
- if let Some(author_pubkey) = branch.filter.author_pubkey.as_ref() {
- let author = author_pubkey
- .parse::<RadrootsNostrPublicKey>()
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("trade evidence filter author invalid: {error}"),
- })?;
- filter = filter.author(author);
- }
- match branch.filter.tag.as_ref() {
- Some(tag) => radroots_nostr_filter_tag(filter, tag.tag_name.as_str(), tag.values.clone())
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("trade evidence filter invalid: {error}"),
- }),
- None => Ok(filter),
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-struct ListingSnapshotFilterParts {
- seller_pubkey: String,
- listing_id: String,
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn listing_snapshot_filter_parts(
- listing_addr: &str,
-) -> Result<ListingSnapshotFilterParts, RadrootsSdkError> {
- let parts =
- parse_listing_address(listing_addr).map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("trade listing snapshot filter invalid: {error}"),
- })?;
- Ok(ListingSnapshotFilterParts {
- seller_pubkey: parts.seller_pubkey.as_str().to_owned(),
- listing_id: parts.listing_id.as_str().to_owned(),
- })
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_resync_total_relay_failure(
- receipt: &RadrootsRelayFetchReceipt,
- relay_count: usize,
-) -> bool {
- relay_count > 0 && receipt.eose_count == 0 && receipt.closed_count >= relay_count
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_resync_total_failure_message(
- operation: &str,
- receipt: &RadrootsRelayFetchReceipt,
-) -> String {
- format!(
- "{operation} failed for all configured relays: closed_count={}, notice_count={}, malformed_count={}",
- receipt.closed_count, receipt.notice_count, receipt.malformed_count
- )
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl TradeResyncEvidenceReceipt {
- async fn from_fetch(
- sdk: &crate::RadrootsClient,
- query_plan: TradeEvidenceQueryPlan,
- receipt: RadrootsRelayFetchReceipt,
- ) -> Result<Self, RadrootsSdkError> {
- let events = receipt
- .events
- .into_iter()
- .map(Into::into)
- .collect::<Vec<_>>();
- let nostr_relay_outcomes = receipt
- .relay_outcomes
- .into_iter()
- .map(Into::into)
- .collect::<Vec<_>>();
- let branches = trade_evidence_branch_receipts(
- sdk,
- &query_plan,
- events.as_slice(),
- nostr_relay_outcomes.as_slice(),
- )
- .await?;
- Ok(Self {
- query_plan,
- inserted_count: receipt.inserted_count,
- duplicate_count: receipt.duplicate_count,
- malformed_count: receipt.malformed_count,
- out_of_filter_count: receipt.out_of_filter_count,
- skipped_over_limit_count: receipt.skipped_over_limit_count,
- unsupported_count: receipt.unsupported_count,
- eose_count: receipt.eose_count,
- closed_count: receipt.closed_count,
- notice_count: receipt.notice_count,
- branches,
- events,
- nostr_relay_outcomes,
- })
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-async fn trade_evidence_branch_receipts(
- sdk: &crate::RadrootsClient,
- query_plan: &TradeEvidenceQueryPlan,
- events: &[TradeResyncEventImportReceipt],
- nostr_relay_outcomes: &[TradeResyncNostrRelayOutcomeReceipt],
-) -> Result<Vec<TradeEvidenceBranchReceipt>, RadrootsSdkError> {
- let mut branch_events = query_plan
- .branches
- .iter()
- .filter(|branch| branch.filter.active)
- .map(|branch| (branch.kind, Vec::<TradeResyncEventImportReceipt>::new()))
- .collect::<BTreeMap<_, _>>();
- branch_events
- .entry(TradeEvidenceQueryBranchKind::RejectedEvidence)
- .or_default();
- for event in events {
- let branch = trade_evidence_event_branch(sdk, query_plan, event).await?;
- branch_events.entry(branch).or_default().push(event.clone());
- }
- let nostr_relay_failure_count = nostr_relay_outcomes
- .iter()
- .filter(|relay| relay.outcome_kind != TradeResyncNostrRelayOutcomeKind::Eose)
- .count();
- Ok(branch_events
- .into_iter()
- .map(|(branch, events)| {
- TradeEvidenceBranchReceipt::from_parts(
- branch,
- events,
- nostr_relay_outcomes.to_vec(),
- nostr_relay_failure_count,
- )
- })
- .collect())
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-async fn trade_evidence_event_branch(
- sdk: &crate::RadrootsClient,
- query_plan: &TradeEvidenceQueryPlan,
- event: &TradeResyncEventImportReceipt,
-) -> Result<TradeEvidenceQueryBranchKind, RadrootsSdkError> {
- if event.malformed || event.out_of_filter || event.skipped_over_limit {
- return Ok(TradeEvidenceQueryBranchKind::RejectedEvidence);
- }
- let Some(event_id) = event.event_id.as_deref() else {
- return Ok(TradeEvidenceQueryBranchKind::RejectedEvidence);
- };
- let Some(stored_event) = sdk
- ._event_store
- .get_event(event_id)
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?
- else {
- return Ok(TradeEvidenceQueryBranchKind::RejectedEvidence);
- };
- let stored_tags = sdk
- ._event_store
- .tags_for_event(event_id)
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Ok(query_plan
- .branches
- .iter()
- .filter(|branch| branch.filter.active)
- .find(|branch| trade_evidence_branch_matches_event(branch, &stored_event, &stored_tags))
- .map(|branch| branch.kind)
- .unwrap_or(TradeEvidenceQueryBranchKind::RejectedEvidence))
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn trade_evidence_branch_matches_event(
- branch: &TradeEvidenceQueryBranch,
- stored_event: &RadrootsStoredEvent,
- stored_tags: &[RadrootsStoredEventTag],
-) -> bool {
- if !branch.filter.event_kinds.contains(&stored_event.kind) {
- return false;
- }
- if let Some(author_pubkey) = branch.filter.author_pubkey.as_ref()
- && stored_event.pubkey != *author_pubkey
- {
- return false;
- }
- match branch.filter.tag.as_ref() {
- Some(tag) => stored_tags.iter().any(|stored_tag| {
- stored_tag.tag_name == tag.tag_name
- && stored_tag
- .tag_value
- .as_ref()
- .is_some_and(|value| tag.values.iter().any(|expected| expected == value))
- }),
- None => true,
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl TradeEvidenceBranchReceipt {
- fn from_parts(
- branch: TradeEvidenceQueryBranchKind,
- events: Vec<TradeResyncEventImportReceipt>,
- nostr_relay_outcomes: Vec<TradeResyncNostrRelayOutcomeReceipt>,
- nostr_relay_failure_count: usize,
- ) -> Self {
- let inserted_count = events.iter().filter(|event| event.inserted).count();
- let duplicate_count = events.iter().filter(|event| event.duplicate).count();
- let malformed_count = events.iter().filter(|event| event.malformed).count();
- let out_of_filter_count = events.iter().filter(|event| event.out_of_filter).count();
- let skipped_over_limit_count = events
- .iter()
- .filter(|event| event.skipped_over_limit)
- .count();
- let unsupported_count = events.iter().filter(|event| event.unsupported).count();
- let accepted_count = events
- .iter()
- .filter(|event| {
- !event.malformed
- && !event.out_of_filter
- && !event.skipped_over_limit
- && !event.unsupported
- && (event.inserted || event.duplicate)
- })
- .count();
- let empty_result = accepted_count == 0
- && duplicate_count == 0
- && malformed_count == 0
- && out_of_filter_count == 0
- && skipped_over_limit_count == 0
- && unsupported_count == 0;
- Self {
- branch,
- accepted_count,
- inserted_count,
- duplicate_count,
- malformed_count,
- out_of_filter_count,
- skipped_over_limit_count,
- unsupported_count,
- nostr_relay_failure_count,
- empty_result,
- events,
- nostr_relay_outcomes,
- }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl From<RadrootsRelayFetchEventReceipt> for TradeResyncEventImportReceipt {
- fn from(receipt: RadrootsRelayFetchEventReceipt) -> Self {
- Self {
- nostr_relay_url: receipt.relay_url,
- event_id: receipt.event_id,
- inserted: receipt.inserted,
- duplicate: receipt.duplicate,
- unsupported: receipt.unsupported,
- malformed: receipt.malformed,
- out_of_filter: receipt.out_of_filter,
- skipped_over_limit: receipt.skipped_over_limit,
- projection_eligible: receipt.projection_eligible,
- verification_status: receipt.verification_status,
- message: receipt.message,
- }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl From<RadrootsRelayFetchRelayOutcome> for TradeResyncNostrRelayOutcomeReceipt {
- fn from(receipt: RadrootsRelayFetchRelayOutcome) -> Self {
- Self {
- nostr_relay_url: receipt.relay_url,
- outcome_kind: receipt.kind.into(),
- transport_outcome_kind: receipt.relay_outcome.map(|outcome| outcome.kind.into()),
- message: receipt.message,
- }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl From<RadrootsRelayFetchOutcomeKind> for TradeResyncNostrRelayOutcomeKind {
- fn from(kind: RadrootsRelayFetchOutcomeKind) -> Self {
- match kind {
- RadrootsRelayFetchOutcomeKind::Eose => Self::Eose,
- RadrootsRelayFetchOutcomeKind::Closed => Self::Closed,
- RadrootsRelayFetchOutcomeKind::Notice => Self::Notice,
- }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl From<RadrootsRelayOutcomeKind> for TradeResyncNostrRelayTransportOutcomeKind {
- fn from(kind: RadrootsRelayOutcomeKind) -> Self {
- match kind {
- RadrootsRelayOutcomeKind::Accepted => Self::Accepted,
- RadrootsRelayOutcomeKind::DuplicateAccepted => Self::DuplicateAccepted,
- RadrootsRelayOutcomeKind::Blocked => Self::Blocked,
- RadrootsRelayOutcomeKind::RateLimited => Self::RateLimited,
- RadrootsRelayOutcomeKind::Invalid => Self::Invalid,
- RadrootsRelayOutcomeKind::PowRequired => Self::PowRequired,
- RadrootsRelayOutcomeKind::Restricted => Self::Restricted,
- RadrootsRelayOutcomeKind::AuthRequired => Self::AuthRequired,
- RadrootsRelayOutcomeKind::Muted => Self::Muted,
- RadrootsRelayOutcomeKind::Unsupported => Self::Unsupported,
- RadrootsRelayOutcomeKind::PaymentRequired => Self::PaymentRequired,
- RadrootsRelayOutcomeKind::Error => Self::Error,
- RadrootsRelayOutcomeKind::Timeout => Self::Timeout,
- RadrootsRelayOutcomeKind::ConnectionFailed => Self::ConnectionFailed,
- RadrootsRelayOutcomeKind::RelayUrlRejected => Self::RelayUrlRejected,
- RadrootsRelayOutcomeKind::SkippedAlreadyAccepted => Self::SkippedAlreadyAccepted,
- RadrootsRelayOutcomeKind::Unknown => Self::Unknown,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-impl<'sdk> TradeValidationReceiptsClient<'sdk> {
- pub async fn list(
- &self,
- request: TradeValidationReceiptListRequest,
- ) -> Result<TradeValidationReceiptListReceipt, RadrootsSdkError> {
- #[cfg(feature = "transport-nostr-runtime")]
- {
- let adapter = RadrootsNostrClientFetchAdapter;
- return self.list_with_fetch_adapter(request, &adapter).await;
- }
- #[cfg(not(feature = "transport-nostr-runtime"))]
- {
- let _ = self.sdk;
- let _ = request;
- Err(RadrootsSdkError::ProductSyncUnsupported {
- operation: "trade.validation_receipts.list",
- required_feature: "transport-nostr-runtime",
- })
- }
- }
-
- #[cfg(feature = "transport-nostr-runtime")]
- pub async fn list_with_fetch_adapter<A>(
- &self,
- request: TradeValidationReceiptListRequest,
- adapter: &A,
- ) -> Result<TradeValidationReceiptListReceipt, RadrootsSdkError>
- where
- A: RadrootsRelayFetchAdapter,
- {
- request.validate()?;
- let nostr_relay_urls =
- validation_receipt_nostr_relay_urls(self.sdk, "trade.validation_receipts.list")?;
- let fetch_request =
- validation_receipt_list_fetch_request(self.sdk, &request, &nostr_relay_urls)?;
- let fetch_receipt =
- fetch_and_ingest_relay_events(adapter, &self.sdk._event_store, fetch_request).await?;
- let nostr_evidence = TradeValidationReceiptNostrEvidenceReceipt::from_fetch(fetch_receipt);
- let events = validation_receipt_events_from_fetch(
- self.sdk,
- &nostr_evidence.events,
- KIND_TRADE_VALIDATION_RECEIPT,
- )
- .await?;
- let (mut receipts, mut invalid_receipts) =
- classify_validation_receipts(events, Some(request.order_id.as_str()))?;
- receipts.sort_by(validation_receipt_event_order);
- invalid_receipts.sort_by(validation_receipt_invalid_order);
- Ok(TradeValidationReceiptListReceipt {
- nostr_relay_urls,
- nostr_evidence,
- order_id: request.order_id,
- receipts,
- invalid_receipts,
- })
- }
-
- pub async fn inspect(
- &self,
- request: TradeValidationReceiptInspectRequest,
- ) -> Result<TradeValidationReceiptInspectReceipt, RadrootsSdkError> {
- #[cfg(feature = "transport-nostr-runtime")]
- {
- let adapter = RadrootsNostrClientFetchAdapter;
- return self.inspect_with_fetch_adapter(request, &adapter).await;
- }
- #[cfg(not(feature = "transport-nostr-runtime"))]
- {
- let _ = self.sdk;
- let _ = request;
- Err(RadrootsSdkError::ProductSyncUnsupported {
- operation: "trade.validation_receipts.inspect",
- required_feature: "transport-nostr-runtime",
- })
- }
- }
-
- #[cfg(feature = "transport-nostr-runtime")]
- pub async fn inspect_with_fetch_adapter<A>(
- &self,
- request: TradeValidationReceiptInspectRequest,
- adapter: &A,
- ) -> Result<TradeValidationReceiptInspectReceipt, RadrootsSdkError>
- where
- A: RadrootsRelayFetchAdapter,
- {
- validation_receipt_inspect(
- self.sdk,
- request.receipt_event_id,
- adapter,
- "trade.validation_receipts.inspect",
- )
- .await
- }
-
- pub async fn verify(
- &self,
- request: TradeValidationReceiptVerifyRequest,
- ) -> Result<TradeValidationReceiptInspectReceipt, RadrootsSdkError> {
- #[cfg(feature = "transport-nostr-runtime")]
- {
- let adapter = RadrootsNostrClientFetchAdapter;
- return self.verify_with_fetch_adapter(request, &adapter).await;
- }
- #[cfg(not(feature = "transport-nostr-runtime"))]
- {
- let _ = self.sdk;
- let _ = request;
- Err(RadrootsSdkError::ProductSyncUnsupported {
- operation: "trade.validation_receipts.verify",
- required_feature: "transport-nostr-runtime",
- })
- }
- }
-
- #[cfg(feature = "transport-nostr-runtime")]
- pub async fn verify_with_fetch_adapter<A>(
- &self,
- request: TradeValidationReceiptVerifyRequest,
- adapter: &A,
- ) -> Result<TradeValidationReceiptInspectReceipt, RadrootsSdkError>
- where
- A: RadrootsRelayFetchAdapter,
- {
- validation_receipt_inspect(
- self.sdk,
- request.receipt_event_id,
- adapter,
- "trade.validation_receipts.verify",
- )
- .await
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-async fn validation_receipt_inspect<A>(
- sdk: &crate::RadrootsClient,
- receipt_event_id: RadrootsEventId,
- adapter: &A,
- operation: &'static str,
-) -> Result<TradeValidationReceiptInspectReceipt, RadrootsSdkError>
-where
- A: RadrootsRelayFetchAdapter,
-{
- let nostr_relay_urls = validation_receipt_nostr_relay_urls(sdk, operation)?;
- let fetch_request =
- validation_receipt_inspect_fetch_request(sdk, &receipt_event_id, &nostr_relay_urls)?;
- let fetch_receipt =
- fetch_and_ingest_relay_events(adapter, &sdk._event_store, fetch_request).await?;
- let nostr_evidence = TradeValidationReceiptNostrEvidenceReceipt::from_fetch(fetch_receipt);
- let events = validation_receipt_events_from_fetch(
- sdk,
- &nostr_evidence.events,
- KIND_TRADE_VALIDATION_RECEIPT,
- )
- .await?;
- let (mut receipts, mut invalid_receipts) = classify_validation_receipts(events, None)?;
- receipts.retain(|receipt| receipt.event.id_str() == receipt_event_id.as_str());
- invalid_receipts.retain(|receipt| receipt.event.id_str() == receipt_event_id.as_str());
- receipts.sort_by(validation_receipt_event_order);
- invalid_receipts.sort_by(validation_receipt_invalid_order);
- Ok(TradeValidationReceiptInspectReceipt {
- nostr_relay_urls,
- nostr_evidence,
- receipt_event_id,
- receipt: receipts.into_iter().next(),
- invalid_receipt: invalid_receipts.into_iter().next(),
- })
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validation_receipt_nostr_relay_urls(
- sdk: &crate::RadrootsClient,
- operation: impl Into<String>,
-) -> Result<Vec<String>, RadrootsSdkError> {
- let nostr_relay_urls = sdk.configured_nostr_relay_urls().to_vec();
- if nostr_relay_urls.is_empty() {
- return Err(RadrootsSdkError::empty_transport_targets(operation));
- }
- Ok(nostr_relay_urls)
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validation_receipt_list_fetch_request(
- sdk: &crate::RadrootsClient,
- request: &TradeValidationReceiptListRequest,
- nostr_relay_urls: &[String],
-) -> Result<RadrootsRelayFetchRequest, RadrootsSdkError> {
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(
- KIND_TRADE_VALIDATION_RECEIPT as u16,
- ))
- .limit(request.limit as usize);
- let filter =
- radroots_nostr_filter_tag(filter, TAG_D, vec![request.order_id.as_str().to_owned()])
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("validation receipt list filter invalid: {error}"),
- })?;
- Ok(
- RadrootsRelayFetchRequest::fetch(sdk_now_ms(sdk)?, request.limit as usize, [filter])?
- .with_relay_urls(nostr_relay_urls.iter().cloned()),
- )
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validation_receipt_inspect_fetch_request(
- sdk: &crate::RadrootsClient,
- receipt_event_id: &RadrootsEventId,
- nostr_relay_urls: &[String],
-) -> Result<RadrootsRelayFetchRequest, RadrootsSdkError> {
- let nostr_event_id =
- RadrootsNostrEventId::parse(receipt_event_id.as_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!(
- "validation receipt event id `{}` cannot build relay filter: {error}",
- receipt_event_id.as_str()
- ),
- }
- })?;
- let filter = RadrootsNostrFilter::new().id(nostr_event_id);
- Ok(
- RadrootsRelayFetchRequest::fetch(sdk_now_ms(sdk)?, 1, [filter])?
- .with_relay_urls(nostr_relay_urls.iter().cloned()),
- )
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-async fn validation_receipt_events_from_fetch(
- sdk: &crate::RadrootsClient,
- events: &[TradeResyncEventImportReceipt],
- expected_kind: u32,
-) -> Result<Vec<RadrootsEventEnvelope>, RadrootsSdkError> {
- let mut event_ids = events
- .iter()
- .filter(|event| !event.malformed)
- .filter(|event| !event.out_of_filter)
- .filter_map(|event| event.event_id.as_deref())
- .collect::<BTreeSet<_>>()
- .into_iter()
- .collect::<Vec<_>>();
- event_ids.sort();
- let mut fetched = Vec::new();
- for event_id in event_ids {
- let Some(stored_event) = sdk
- ._event_store
- .get_event(event_id)
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?
- else {
- continue;
- };
- if stored_event.kind == expected_kind {
- fetched.push(stored_event_to_nostr_event(&stored_event)?);
- }
- }
- Ok(fetched)
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn classify_validation_receipts(
- events: Vec<RadrootsEventEnvelope>,
- expected_order_id: Option<&str>,
-) -> Result<
- (
- Vec<TradeValidationReceiptEvent>,
- Vec<TradeValidationReceiptInvalidCandidate>,
- ),
- RadrootsSdkError,
-> {
- let mut receipts = Vec::new();
- let mut invalid_receipts = Vec::new();
- for event in events {
- let expected = RadrootsValidationReceiptExpectedBinding {
- order_id: expected_order_id,
- ..RadrootsValidationReceiptExpectedBinding::default()
- };
- match verify_validation_receipt_event(&event, expected) {
- Ok(verified) => receipts.push(TradeValidationReceiptEvent {
- event,
- receipt: verified.receipt,
- tags: TradeValidationReceiptTags::from(verified.tags),
- }),
- Err(error) => invalid_receipts.push(TradeValidationReceiptInvalidCandidate {
- event,
- reason_code: validation_receipt_invalid_reason_code(&error).to_owned(),
- reason: error.to_string(),
- }),
- }
- }
- Ok((receipts, invalid_receipts))
-}
-
-#[cfg(feature = "runtime")]
-async fn apply_trade_status_validation_trust(
- sdk: &crate::RadrootsClient,
- status: &mut TradeStatusReceipt,
- policy: &RadrootsTradeValidationTrustPolicy,
-) -> Result<(), RadrootsSdkError> {
- if !status.found
- || matches!(
- status.status,
- TradeStatusKind::Missing | TradeStatusKind::Ambiguous
- )
- {
- return Ok(());
- }
- let decision = trade_status_validation_trust_decision(sdk, status, policy).await?;
- status.validation_trust = Some(decision);
- apply_validation_trust_decision_to_status(status);
- Ok(())
-}
-
-#[cfg(feature = "runtime")]
-async fn trade_status_validation_trust_decision(
- sdk: &crate::RadrootsClient,
- status: &TradeStatusReceipt,
- policy: &RadrootsTradeValidationTrustPolicy,
-) -> Result<TradeValidationTrustDecision, RadrootsSdkError> {
- let Some(receipt_event_id) = status.rhi_receipt_event_id.clone() else {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- TradeValidationTrustEvidence::none(),
- false,
- Some(TradeValidationTrustReason::new(
- "validation_receipt_missing",
- "validation receipt is not present in local product evidence",
- )),
- ));
- };
- let Some(stored_event) = sdk
- ._event_store
- .get_event(receipt_event_id.as_str())
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?
- else {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- TradeValidationTrustEvidence::receipt_event(receipt_event_id),
- false,
- Some(TradeValidationTrustReason::new(
- "validation_receipt_event_missing",
- "validation receipt event is referenced by projection but missing from storage",
- )),
- ));
- };
- if stored_event.kind != KIND_TRADE_VALIDATION_RECEIPT {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Invalid,
- TradeValidationTrustEvidence::receipt_event(receipt_event_id),
- false,
- Some(TradeValidationTrustReason::new(
- "validation_receipt_event_kind_invalid",
- "validation receipt reference does not point to a validation receipt event",
- )),
- ));
- }
- let event = stored_event_to_nostr_event(&stored_event)?;
- let receipt_author = match RadrootsPublicKey::parse(event.author_str()) {
- Ok(author) => author,
- Err(_) => {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Invalid,
- TradeValidationTrustEvidence::receipt_event(receipt_event_id),
- false,
- Some(TradeValidationTrustReason::new(
- "validation_receipt_author_invalid",
- "validation receipt author is not a valid public key",
- )),
- ));
- }
- };
- let expected = RadrootsValidationReceiptExpectedBinding {
- order_id: Some(status.order_id.as_str()),
- root_event_id: status
- .request_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- target_event_id: status
- .agreement_event_id
- .as_ref()
- .or(status.decision_event_id.as_ref())
- .map(RadrootsEventId::as_str),
- validator_set_addr: policy.validator_set_addr.as_ref().map(|addr| addr.as_str()),
- validator_set_event_id: policy.validator_set_event_id.as_deref(),
- ..RadrootsValidationReceiptExpectedBinding::default()
- };
- let verified = match verify_validation_receipt_event(&event, expected) {
- Ok(verified) => verified,
- Err(error) => {
- let reason = error.to_string();
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Invalid,
- TradeValidationTrustEvidence::receipt_author(receipt_event_id, receipt_author),
- false,
- Some(TradeValidationTrustReason::new(
- "validation_receipt_invalid",
- reason.as_str(),
- )),
- ));
- }
- };
- let receipt = TradeValidationReceiptEvent {
- event,
- receipt: verified.receipt,
- tags: TradeValidationReceiptTags::from(verified.tags),
- };
- if receipt.receipt.result == RadrootsValidationReceiptResult::Invalid {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Invalid,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- None,
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validation_receipt_result_invalid",
- "validation receipt reports an invalid trade transition",
- )),
- ));
- }
- let Some(validator_set) = policy.validator_set.as_ref() else {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Untrusted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- None,
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validator_set_required",
- "validation trust policy has no configured validator set",
- )),
- ));
- };
- let Some(validator_set_addr) = policy.validator_set_addr.as_ref() else {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Untrusted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- None,
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validator_set_address_required",
- "validation trust policy has no validator set address",
- )),
- ));
- };
- let Some(validator_set_event_id) = policy.validator_set_event_id.as_ref() else {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Untrusted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- None,
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validator_set_event_id_required",
- "validation trust policy has no validator set event id",
- )),
- ));
- };
- if receipt.receipt.statement.validator_set_addr != *validator_set_addr
- || receipt.receipt.statement.validator_set_event_id != *validator_set_event_id
- {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Untrusted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- None,
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validator_set_binding_mismatch",
- "validation receipt does not bind the configured validator set",
- )),
- ));
- }
- if !policy.trusts_validator_pubkey(&receipt_author) {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Untrusted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- None,
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validator_set_author_untrusted",
- "validation receipt author is not the configured validator set key",
- )),
- ));
- }
- let receipt_created_at = receipt.event.created_at_u64();
- if receipt_created_at < validator_set.valid_from
- || receipt_created_at > validator_set.valid_until
- {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Untrusted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- None,
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validator_set_not_valid_at_receipt_time",
- "validation receipt was authored outside the configured validator set validity window",
- )),
- ));
- }
- Ok(evaluate_trade_validation_trust_receipt(
- policy,
- receipt_event_id,
- receipt_author,
- &receipt.receipt,
- ))
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-struct TradeValidationTrustMetadata {
- validation_authority: RadrootsTradeValidationAuthority,
- commitment_confidence: RadrootsTradeCommitmentConfidence,
- cryptographic_proof_verified: bool,
-}
-
-#[cfg(feature = "runtime")]
-struct TradeValidationTrustEvidence<'a> {
- receipt_event_id: Option<RadrootsEventId>,
- receipt_author: Option<RadrootsPublicKey>,
- receipt: Option<&'a RadrootsTradeValidationReceipt>,
- metadata: Option<TradeValidationTrustMetadata>,
-}
-
-#[cfg(feature = "runtime")]
-impl<'a> TradeValidationTrustEvidence<'a> {
- fn none() -> Self {
- Self {
- receipt_event_id: None,
- receipt_author: None,
- receipt: None,
- metadata: None,
- }
- }
-
- fn receipt_event(receipt_event_id: RadrootsEventId) -> Self {
- Self {
- receipt_event_id: Some(receipt_event_id),
- receipt_author: None,
- receipt: None,
- metadata: None,
- }
- }
-
- fn receipt_author(
- receipt_event_id: RadrootsEventId,
- receipt_author: RadrootsPublicKey,
- ) -> Self {
- Self {
- receipt_event_id: Some(receipt_event_id),
- receipt_author: Some(receipt_author),
- receipt: None,
- metadata: None,
- }
- }
-
- fn receipt(
- receipt_event_id: RadrootsEventId,
- receipt_author: RadrootsPublicKey,
- receipt: &'a RadrootsTradeValidationReceipt,
- metadata: Option<TradeValidationTrustMetadata>,
- ) -> Self {
- Self {
- receipt_event_id: Some(receipt_event_id),
- receipt_author: Some(receipt_author),
- receipt: Some(receipt),
- metadata,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-struct TradeValidationTrustReason<'a> {
- code: &'a str,
- message: &'a str,
-}
-
-#[cfg(feature = "runtime")]
-impl<'a> TradeValidationTrustReason<'a> {
- fn new(code: &'a str, message: &'a str) -> Self {
- Self { code, message }
- }
-}
-
-#[cfg(feature = "runtime")]
-fn evaluate_trade_validation_trust_receipt(
- policy: &RadrootsTradeValidationTrustPolicy,
- receipt_event_id: RadrootsEventId,
- receipt_author: RadrootsPublicKey,
- receipt: &RadrootsTradeValidationReceipt,
-) -> TradeValidationTrustDecision {
- let metadata = validation_receipt_trust_metadata(receipt);
- let authority = metadata.validation_authority;
- let confidence = metadata.commitment_confidence;
- if authority == RadrootsTradeValidationAuthority::ValidatorSetDeterministic
- || confidence == RadrootsTradeCommitmentConfidence::CommittedByValidatorSet
- {
- if policy.require_cryptographic_proof {
- return trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- receipt,
- Some(metadata),
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "cryptographic_proof_required",
- "active policy requires cryptographic proof for committed confidence",
- )),
- );
- }
- return trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::ValidatorSetCommitted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- receipt,
- Some(metadata),
- ),
- true,
- None,
- );
- }
- let cryptographic_metadata = matches!(
- (authority, confidence),
- (
- RadrootsTradeValidationAuthority::CryptographicProofVerified,
- RadrootsTradeCommitmentConfidence::CommittedByCryptographicProof
- ) | (
- RadrootsTradeValidationAuthority::ValidatorSetAndProofVerified,
- RadrootsTradeCommitmentConfidence::CommittedByValidatorSetAndProof
- )
- );
- if cryptographic_metadata
- && receipt.proof.system != RadrootsValidationReceiptProofSystem::None
- && metadata.cryptographic_proof_verified
- {
- return trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::CryptographicCommitted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- receipt,
- Some(metadata),
- ),
- true,
- None,
- );
- }
- if policy.require_cryptographic_proof {
- return trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- receipt,
- Some(metadata),
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "cryptographic_proof_required",
- "active policy requires trusted cryptographic proof metadata",
- )),
- );
- }
- if matches!(
- (authority, confidence),
- (
- RadrootsTradeValidationAuthority::ValidatorSetAndProofVerified,
- RadrootsTradeCommitmentConfidence::CommittedByValidatorSetAndProof
- ) | (
- RadrootsTradeValidationAuthority::CryptographicProofVerified,
- RadrootsTradeCommitmentConfidence::CommittedByCryptographicProof
- )
- ) {
- return trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::ValidatorSetCommitted,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- receipt,
- Some(metadata),
- ),
- false,
- None,
- );
- }
- trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- TradeValidationTrustEvidence::receipt(
- receipt_event_id,
- receipt_author,
- receipt,
- Some(metadata),
- ),
- false,
- Some(TradeValidationTrustReason::new(
- "validation_trust_metadata_insufficient",
- "trusted validation result metadata does not satisfy the active policy",
- )),
- )
-}
-
-#[cfg(feature = "runtime")]
-fn validation_receipt_trust_metadata(
- receipt: &RadrootsTradeValidationReceipt,
-) -> TradeValidationTrustMetadata {
- if receipt.proof.system == RadrootsValidationReceiptProofSystem::None {
- TradeValidationTrustMetadata {
- validation_authority: RadrootsTradeValidationAuthority::ValidatorSetDeterministic,
- commitment_confidence: RadrootsTradeCommitmentConfidence::CommittedByValidatorSet,
- cryptographic_proof_verified: false,
- }
- } else {
- TradeValidationTrustMetadata {
- validation_authority: RadrootsTradeValidationAuthority::ValidatorSetAndProofVerified,
- commitment_confidence:
- RadrootsTradeCommitmentConfidence::CommittedByValidatorSetAndProof,
- cryptographic_proof_verified: true,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-fn trade_validation_trust_decision(
- policy: &RadrootsTradeValidationTrustPolicy,
- state: RadrootsTradeValidationTrustState,
- evidence: TradeValidationTrustEvidence<'_>,
- production_committed: bool,
- reason: Option<TradeValidationTrustReason<'_>>,
-) -> TradeValidationTrustDecision {
- let (reason_code, reason) = match reason {
- Some(reason) => (
- Some(reason.code.to_owned()),
- Some(reason.message.to_owned()),
- ),
- None => (None, None),
- };
- TradeValidationTrustDecision {
- state,
- validator_count: policy.validator_count(),
- validator_set_addr: policy
- .validator_set_addr
- .as_ref()
- .map(|addr| addr.as_str().to_owned()),
- validator_set_event_id: policy.validator_set_event_id.clone(),
- require_cryptographic_proof: policy.require_cryptographic_proof,
- receipt_event_id: evidence.receipt_event_id,
- receipt_author: evidence.receipt_author,
- result_event_id: None,
- result_author: None,
- proof_system: evidence
- .receipt
- .map(|receipt| receipt.proof.system.as_str().to_owned()),
- validation_authority: evidence
- .metadata
- .map(|metadata| metadata.validation_authority),
- commitment_confidence: evidence
- .metadata
- .map(|metadata| metadata.commitment_confidence),
- cryptographic_proof_required: policy.require_cryptographic_proof,
- cryptographic_proof_verified: evidence
- .metadata
- .is_some_and(|metadata| metadata.cryptographic_proof_verified),
- production_committed,
- reason_code,
- reason,
- }
-}
-
-#[cfg(feature = "runtime")]
-fn apply_validation_trust_decision_to_status(status: &mut TradeStatusReceipt) {
- let Some(decision) = status.validation_trust.as_ref() else {
- return;
- };
- match decision.state {
- RadrootsTradeValidationTrustState::Pending
- | RadrootsTradeValidationTrustState::Untrusted => {
- if status.status == TradeStatusKind::Committed {
- status.status = TradeStatusKind::AgreedPendingValidation;
- status.lifecycle_terminal = false;
- status.next_action = TradeStatusNextActionKind::AwaitValidation;
- status.last_event_id = status
- .agreement_event_id
- .clone()
- .or_else(|| status.decision_event_id.clone())
- .or_else(|| status.request_event_id.clone());
- }
- }
- RadrootsTradeValidationTrustState::Invalid => {
- status.status = TradeStatusKind::Invalid;
- status.lifecycle_terminal = true;
- status.next_action = TradeStatusNextActionKind::InspectEvidenceIssues;
- }
- RadrootsTradeValidationTrustState::ValidatorSetCommitted
- | RadrootsTradeValidationTrustState::CryptographicCommitted => {}
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl TradeValidationReceiptNostrEvidenceReceipt {
- fn from_fetch(receipt: RadrootsRelayFetchReceipt) -> Self {
- Self {
- inserted_count: receipt.inserted_count,
- duplicate_count: receipt.duplicate_count,
- malformed_count: receipt.malformed_count,
- out_of_filter_count: receipt.out_of_filter_count,
- skipped_over_limit_count: receipt.skipped_over_limit_count,
- unsupported_count: receipt.unsupported_count,
- eose_count: receipt.eose_count,
- closed_count: receipt.closed_count,
- notice_count: receipt.notice_count,
- events: receipt.events.into_iter().map(Into::into).collect(),
- nostr_relay_outcomes: receipt.relay_outcomes.into_iter().map(Into::into).collect(),
- }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl From<RadrootsRelayFetchRelayOutcome> for TradeValidationReceiptNostrRelayOutcomeReceipt {
- fn from(receipt: RadrootsRelayFetchRelayOutcome) -> Self {
- Self {
- nostr_relay_url: receipt.relay_url,
- outcome_kind: receipt.kind.into(),
- transport_outcome_kind: receipt.relay_outcome.map(|outcome| outcome.kind.into()),
- message: receipt.message,
- }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl From<RadrootsRelayFetchOutcomeKind> for TradeValidationReceiptNostrRelayOutcomeKind {
- fn from(kind: RadrootsRelayFetchOutcomeKind) -> Self {
- match kind {
- RadrootsRelayFetchOutcomeKind::Eose => Self::Eose,
- RadrootsRelayFetchOutcomeKind::Closed => Self::Closed,
- RadrootsRelayFetchOutcomeKind::Notice => Self::Notice,
- }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-impl From<RadrootsRelayOutcomeKind> for TradeValidationReceiptNostrRelayTransportOutcomeKind {
- fn from(kind: RadrootsRelayOutcomeKind) -> Self {
- match kind {
- RadrootsRelayOutcomeKind::Accepted => Self::Accepted,
- RadrootsRelayOutcomeKind::DuplicateAccepted => Self::DuplicateAccepted,
- RadrootsRelayOutcomeKind::Blocked => Self::Blocked,
- RadrootsRelayOutcomeKind::RateLimited => Self::RateLimited,
- RadrootsRelayOutcomeKind::Invalid => Self::Invalid,
- RadrootsRelayOutcomeKind::PowRequired => Self::PowRequired,
- RadrootsRelayOutcomeKind::Restricted => Self::Restricted,
- RadrootsRelayOutcomeKind::AuthRequired => Self::AuthRequired,
- RadrootsRelayOutcomeKind::Muted => Self::Muted,
- RadrootsRelayOutcomeKind::Unsupported => Self::Unsupported,
- RadrootsRelayOutcomeKind::PaymentRequired => Self::PaymentRequired,
- RadrootsRelayOutcomeKind::Error => Self::Error,
- RadrootsRelayOutcomeKind::Timeout => Self::Timeout,
- RadrootsRelayOutcomeKind::ConnectionFailed => Self::ConnectionFailed,
- RadrootsRelayOutcomeKind::RelayUrlRejected => Self::RelayUrlRejected,
- RadrootsRelayOutcomeKind::SkippedAlreadyAccepted => Self::SkippedAlreadyAccepted,
- RadrootsRelayOutcomeKind::Unknown => Self::Unknown,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-impl From<RadrootsValidationReceiptTags> for TradeValidationReceiptTags {
- fn from(tags: RadrootsValidationReceiptTags) -> Self {
- Self {
- order_id: tags.order_id,
- event_set_root: tags.event_set_root,
- listing_event_id: tags.listing_event_id,
- reducer_output_root: tags.reducer_output_root,
- public_values_hash: tags.public_values_hash,
- proof_system: tags.proof_system.as_str().to_owned(),
- receipt_type: tags.receipt_type.as_str().to_owned(),
- root_event_id: tags.root_event_id,
- target_event_id: tags.target_event_id,
- validator_set_addr: tags.validator_set_addr.as_str().to_owned(),
- validator_set_event_id: tags.validator_set_event_id,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-fn stored_event_to_nostr_event(
- stored_event: &RadrootsStoredEvent,
-) -> Result<RadrootsEventEnvelope, RadrootsSdkError> {
- let tags = serde_json::from_str(&stored_event.tags_json).map_err(|error| {
- RadrootsSdkError::EventStore {
- message: format!(
- "stored event {} contains invalid tags_json: {error}",
- stored_event.event_id
- ),
- }
- })?;
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: stored_event.event_id.clone(),
- author: stored_event.pubkey.clone(),
- created_at: stored_event.created_at,
- kind: stored_event.kind,
- tags,
- content: stored_event.content.clone(),
- sig: stored_event.sig.clone(),
- })
- .map_err(|error| RadrootsSdkError::EventStore {
- message: format!(
- "stored event {} contains invalid envelope: {error}",
- stored_event.event_id
- ),
- })
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validation_receipt_event_order(
- left: &TradeValidationReceiptEvent,
- right: &TradeValidationReceiptEvent,
-) -> core::cmp::Ordering {
- left.event
- .created_at()
- .cmp(&right.event.created_at())
- .then_with(|| left.event.id().cmp(right.event.id()))
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validation_receipt_invalid_order(
- left: &TradeValidationReceiptInvalidCandidate,
- right: &TradeValidationReceiptInvalidCandidate,
-) -> core::cmp::Ordering {
- left.event
- .created_at()
- .cmp(&right.event.created_at())
- .then_with(|| left.event.id().cmp(right.event.id()))
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validate_validation_receipt_limit(limit: u32) -> Result<(), RadrootsSdkError> {
- if limit == 0 || limit > TRADE_STATUS_MAX_LIMIT {
- return Err(RadrootsSdkError::trade_status_limit_invalid(
- limit,
- 1,
- TRADE_STATUS_MAX_LIMIT,
- ));
- }
- Ok(())
-}
-
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validation_receipt_invalid_reason_code(error: &RadrootsValidationReceiptError) -> &'static str {
- match error {
- RadrootsValidationReceiptError::InvalidProofMetadata("proof.material")
- | RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_missing") => {
- "sp1_proof_material_missing"
- }
- RadrootsValidationReceiptError::InvalidProofMetadata("proof.material_conflict") => {
- "sp1_proof_material_conflict"
- }
- RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") => {
- "sp1_inline_proof_invalid"
- }
- RadrootsValidationReceiptError::InvalidProofMetadata("proof.proof_reference") => {
- "sp1_proof_reference_invalid"
- }
- RadrootsValidationReceiptError::TagMismatch("public_values_hash")
- | RadrootsValidationReceiptError::ExpectedBindingMismatch("public_values_hash") => {
- "public_values_hash_mismatch"
- }
- RadrootsValidationReceiptError::ExpectedBindingMismatch("program_hash") => {
- "sp1_program_hash_mismatch"
- }
- RadrootsValidationReceiptError::ExpectedBindingMismatch("verifying_key_hash") => {
- "sp1_verifying_key_hash_mismatch"
- }
- _ => "validation_receipt_invalid",
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
-impl<'sdk> TradeBuyerClient<'sdk> {
- pub async fn propose_trade(
- &self,
- request: TradeProposeRequest,
- ) -> Result<TradeMutationOutcome<TradeSubmitPlan, TradeSubmitReceipt>, RadrootsSdkError> {
- validate_trade_product_publish_policy(request.publish_mode, &request.satisfaction_policy)?;
- require_trade_product_privacy_preflight(
- "trade.propose",
- trade_propose_privacy_fields(&request),
- &request.privacy_confirmation,
- )?;
- let TradeProposeRequest {
- actor,
- listing_event,
- order_id,
- listing_addr,
- seller_pubkey,
- items,
- economics,
- public_note: _,
- target_policy,
- publish_mode,
- satisfaction_policy,
- privacy_confirmation: _,
- idempotency_key,
- created_at,
- } = request;
- let order = RadrootsOrderRequest {
- order_id,
- listing_addr,
- buyer_pubkey: actor.pubkey().clone(),
- seller_pubkey,
- items,
- economics,
- };
- let client = trades_client(self.sdk);
- let plan = client.prepare_submit(TradeSubmitPrepareRequest {
- actor: actor.clone(),
- listing_event,
- order,
- created_at,
- })?;
- if publish_mode == PublishMode::DryRun {
- return Ok(TradeMutationOutcome::DryRun { plan });
- }
- let receipt = client
- .enqueue_prepared_submit(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy.clone(),
- idempotency_key,
- )
- .await?;
- trade_product_post_enqueue_outcome(
- self.sdk,
- publish_mode,
- satisfaction_policy,
- receipt.outbox_event_id,
- receipt,
- )
- .await
- }
-
- pub async fn cancel_trade(
- &self,
- request: TradeCancelRequest,
- ) -> Result<
- TradeMutationOutcome<TradeCancellationPlan, TradeCancellationReceipt>,
- RadrootsSdkError,
- > {
- validate_trade_product_publish_policy(request.publish_mode, &request.satisfaction_policy)?;
- let TradeCancelRequest {
- actor,
- locator,
- reason,
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation,
- idempotency_key,
- created_at,
- } = request;
- let context =
- trade_mutation_context(self.sdk, locator, evidence_mode, "trade.cancel").await?;
- let cancellation = RadrootsOrderCancellation {
- order_id: context.order_id.clone(),
- listing_addr: context.listing_addr.clone(),
- buyer_pubkey: context.buyer_pubkey.clone(),
- seller_pubkey: context.seller_pubkey.clone(),
- reason,
- };
- require_trade_product_privacy_preflight(
- "trade.cancel",
- trade_reason_privacy_fields(&cancellation.reason),
- &privacy_confirmation,
- )?;
- let client = trades_client(self.sdk);
- let plan = client.prepare_cancellation(TradeCancellationPrepareRequest {
- actor: actor.clone(),
- root_event: event_ptr(&context.root_event_id),
- previous_event: event_ptr(&context.previous_event_id),
- cancellation,
- created_at,
- })?;
- if publish_mode == PublishMode::DryRun {
- return Ok(TradeMutationOutcome::DryRun { plan });
- }
- let receipt = client
- .enqueue_prepared_cancellation(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy.clone(),
- idempotency_key,
- )
- .await?;
- trade_product_post_enqueue_outcome(
- self.sdk,
- publish_mode,
- satisfaction_policy,
- receipt.outbox_event_id,
- receipt,
- )
- .await
- }
-}
-
-#[cfg(feature = "runtime")]
-impl<'sdk> TradeSellerClient<'sdk> {
- pub async fn inbox(
- &self,
- request: TradeSellerInboxRequest,
- ) -> Result<TradeSellerInboxReceipt, RadrootsSdkError> {
- require_seller_actor(&request.actor, "trade.inbox")?;
- if request.limit == 0 || request.limit > TRADE_STATUS_MAX_LIMIT {
- return Err(RadrootsSdkError::trade_status_limit_invalid(
- request.limit,
- 1,
- TRADE_STATUS_MAX_LIMIT,
- ));
- }
- let seller_pubkey =
- RadrootsPublicKey::parse(request.actor.pubkey().as_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("seller actor pubkey is invalid: {error}"),
- }
- })?;
- let events = self
- .sdk
- ._event_store
- .events_by_tag(TAG_P, request.actor.pubkey().as_str(), request.limit)
- .await?;
- let mut locators = Vec::new();
- for event in events {
- let tags = serde_json::from_str(&event.tags_json).map_err(|error| {
- RadrootsSdkError::Projection {
- message: format!(
- "stored trade inbox event {} contains invalid tags_json: {error}",
- event.event_id
- ),
- }
- })?;
- let nostr_event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: event.event_id,
- author: event.pubkey,
- created_at: event.created_at,
- kind: event.kind,
- tags,
- content: event.content,
- sig: event.sig,
- })
- .map_err(|error| RadrootsSdkError::Projection {
- message: format!("stored trade inbox event envelope is invalid: {error}"),
- })?;
- if let Ok(RadrootsOrderEventRecord::Request(record)) =
- order_event_record_from_event(&nostr_event)
- && record.payload.seller_pubkey == seller_pubkey
- {
- locators.push(
- RadrootsTradeLocator::from_order_id(record.payload.order_id)
- .with_root_event_id(record.event_id)
- .with_listing_addr(record.payload.listing_addr)
- .with_buyer_pubkey(record.payload.buyer_pubkey)
- .with_seller_pubkey(record.payload.seller_pubkey),
- );
- }
- }
- locators.sort_by(|left, right| left.root_event_id.cmp(&right.root_event_id));
- locators.dedup_by(|left, right| left.root_event_id == right.root_event_id);
- let mut statuses = Vec::with_capacity(locators.len());
- for locator in locators {
- statuses.push(
- trades_client(self.sdk)
- .status(TradeStatusRequest::new(locator))
- .await?,
- );
- }
- Ok(TradeSellerInboxReceipt {
- seller_pubkey,
- statuses,
- })
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn accept_trade(
- &self,
- request: TradeAcceptRequest,
- ) -> Result<TradeMutationOutcome<TradeDecisionPlan, TradeDecisionReceipt>, RadrootsSdkError>
- {
- validate_trade_product_publish_policy(request.publish_mode, &request.satisfaction_policy)?;
- let TradeAcceptRequest {
- actor,
- locator,
- inventory_commitments,
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation,
- idempotency_key,
- created_at,
- } = request;
- let context =
- trade_mutation_context(self.sdk, locator, evidence_mode, "trade.accept").await?;
- let decision = RadrootsOrderDecision {
- order_id: context.order_id.clone(),
- listing_addr: context.listing_addr.clone(),
- buyer_pubkey: context.buyer_pubkey.clone(),
- seller_pubkey: context.seller_pubkey.clone(),
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments,
- },
- };
- require_trade_product_privacy_preflight(
- "trade.accept",
- trade_decision_privacy_fields(&decision),
- &privacy_confirmation,
- )?;
- let client = trades_client(self.sdk);
- let plan = client.prepare_decision(TradeDecisionPrepareRequest {
- actor: actor.clone(),
- request_event: event_ptr(&context.root_event_id),
- decision,
- created_at,
- })?;
- if publish_mode == PublishMode::DryRun {
- return Ok(TradeMutationOutcome::DryRun { plan });
- }
- let receipt = client
- .enqueue_prepared_decision(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy.clone(),
- idempotency_key,
- )
- .await?;
- trade_product_post_enqueue_outcome(
- self.sdk,
- publish_mode,
- satisfaction_policy,
- receipt.outbox_event_id,
- receipt,
- )
- .await
- }
-
- #[cfg(feature = "transport-nostr-runtime")]
- pub async fn accept_trade_with_fetch_adapter<A>(
- &self,
- request: TradeAcceptRequest,
- adapter: &A,
- ) -> Result<TradeMutationOutcome<TradeDecisionPlan, TradeDecisionReceipt>, RadrootsSdkError>
- where
- A: RadrootsRelayFetchAdapter,
- {
- validate_trade_product_publish_policy(request.publish_mode, &request.satisfaction_policy)?;
- let TradeAcceptRequest {
- actor,
- locator,
- inventory_commitments,
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation,
- idempotency_key,
- created_at,
- } = request;
- let context = trade_mutation_context_with_fetch_adapter(
- self.sdk,
- locator,
- evidence_mode,
- "trade.accept",
- adapter,
- )
- .await?;
- let decision = RadrootsOrderDecision {
- order_id: context.order_id.clone(),
- listing_addr: context.listing_addr.clone(),
- buyer_pubkey: context.buyer_pubkey.clone(),
- seller_pubkey: context.seller_pubkey.clone(),
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments,
- },
- };
- require_trade_product_privacy_preflight(
- "trade.accept",
- trade_decision_privacy_fields(&decision),
- &privacy_confirmation,
- )?;
- let client = trades_client(self.sdk);
- let plan = client.prepare_decision(TradeDecisionPrepareRequest {
- actor: actor.clone(),
- request_event: event_ptr(&context.root_event_id),
- decision,
- created_at,
- })?;
- if publish_mode == PublishMode::DryRun {
- return Ok(TradeMutationOutcome::DryRun { plan });
- }
- let receipt = client
- .enqueue_prepared_decision(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy.clone(),
- idempotency_key,
- )
- .await?;
- trade_product_post_enqueue_outcome(
- self.sdk,
- publish_mode,
- satisfaction_policy,
- receipt.outbox_event_id,
- receipt,
- )
- .await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn decline_trade(
- &self,
- request: TradeDeclineRequest,
- ) -> Result<TradeMutationOutcome<TradeDecisionPlan, TradeDecisionReceipt>, RadrootsSdkError>
- {
- validate_trade_product_publish_policy(request.publish_mode, &request.satisfaction_policy)?;
- let TradeDeclineRequest {
- actor,
- locator,
- reason,
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation,
- idempotency_key,
- created_at,
- } = request;
- let context =
- trade_mutation_context(self.sdk, locator, evidence_mode, "trade.decline").await?;
- let decision = RadrootsOrderDecision {
- order_id: context.order_id.clone(),
- listing_addr: context.listing_addr.clone(),
- buyer_pubkey: context.buyer_pubkey.clone(),
- seller_pubkey: context.seller_pubkey.clone(),
- decision: RadrootsOrderDecisionOutcome::Declined { reason },
- };
- require_trade_product_privacy_preflight(
- "trade.decline",
- trade_decision_privacy_fields(&decision),
- &privacy_confirmation,
- )?;
- let client = trades_client(self.sdk);
- let plan = client.prepare_decision(TradeDecisionPrepareRequest {
- actor: actor.clone(),
- request_event: event_ptr(&context.root_event_id),
- decision,
- created_at,
- })?;
- if publish_mode == PublishMode::DryRun {
- return Ok(TradeMutationOutcome::DryRun { plan });
- }
- let receipt = client
- .enqueue_prepared_decision(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy.clone(),
- idempotency_key,
- )
- .await?;
- trade_product_post_enqueue_outcome(
- self.sdk,
- publish_mode,
- satisfaction_policy,
- receipt.outbox_event_id,
- receipt,
- )
- .await
- }
-}
-
-#[cfg(feature = "runtime")]
-fn trades_client(sdk: &crate::RadrootsClient) -> TradesClient<'_> {
- TradesClient { sdk }
-}
-
-#[cfg(feature = "signer-adapters")]
-struct TradeProductMutationContext {
- order_id: RadrootsOrderId,
- listing_addr: RadrootsListingAddress,
- buyer_pubkey: RadrootsPublicKey,
- seller_pubkey: RadrootsPublicKey,
- root_event_id: RadrootsEventId,
- previous_event_id: RadrootsEventId,
-}
-
-#[cfg(feature = "signer-adapters")]
-async fn trade_mutation_context(
- sdk: &crate::RadrootsClient,
- locator: RadrootsTradeLocator,
- evidence_mode: TradeEvidenceMode,
- operation: &'static str,
-) -> Result<TradeProductMutationContext, RadrootsSdkError> {
- let status = trade_mutation_status(sdk, locator.clone(), &evidence_mode, operation).await?;
- trade_mutation_context_from_status(locator, &evidence_mode, operation, status)
-}
-
-#[cfg(feature = "signer-adapters")]
-async fn trade_mutation_status(
- sdk: &crate::RadrootsClient,
- locator: RadrootsTradeLocator,
- evidence_mode: &TradeEvidenceMode,
- operation: &'static str,
-) -> Result<TradeStatusReceipt, RadrootsSdkError> {
- match evidence_mode {
- TradeEvidenceMode::LocalOnly => {
- trades_client(sdk)
- .status(
- TradeStatusRequest::new(locator).with_source(SdkTradeStatusSource::LocalOnly),
- )
- .await
- }
- TradeEvidenceMode::RequireExplicitEvidence { evidence } => {
- ingest_explicit_trade_mutation_evidence(sdk, operation, evidence).await?;
- trades_client(sdk)
- .status(
- TradeStatusRequest::new(locator).with_source(SdkTradeStatusSource::LocalOnly),
- )
- .await
- }
- TradeEvidenceMode::ResyncBeforeMutation => {
- #[cfg(feature = "transport-nostr-runtime")]
- {
- let adapter = RadrootsNostrClientFetchAdapter;
- let status = trades_client(sdk)
- .status_with_fetch_adapter(
- TradeStatusRequest::new(locator)
- .with_source(SdkTradeStatusSource::ResyncThenLocal),
- &adapter,
- )
- .await?;
- require_trade_mutation_online_evidence_clean(operation, &status)?;
- Ok(status)
- }
- #[cfg(not(feature = "transport-nostr-runtime"))]
- {
- let _ = locator;
- Err(RadrootsSdkError::ProductSyncUnsupported {
- operation,
- required_feature: "transport-nostr-runtime",
- })
- }
- }
- }
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "transport-nostr-runtime"))]
-async fn trade_mutation_context_with_fetch_adapter<A>(
- sdk: &crate::RadrootsClient,
- locator: RadrootsTradeLocator,
- evidence_mode: TradeEvidenceMode,
- operation: &'static str,
- adapter: &A,
-) -> Result<TradeProductMutationContext, RadrootsSdkError>
-where
- A: RadrootsRelayFetchAdapter,
-{
- let status = match evidence_mode {
- TradeEvidenceMode::LocalOnly => {
- trades_client(sdk)
- .status(
- TradeStatusRequest::new(locator.clone())
- .with_source(SdkTradeStatusSource::LocalOnly),
- )
- .await?
- }
- TradeEvidenceMode::RequireExplicitEvidence { ref evidence } => {
- ingest_explicit_trade_mutation_evidence(sdk, operation, evidence).await?;
- trades_client(sdk)
- .status(
- TradeStatusRequest::new(locator.clone())
- .with_source(SdkTradeStatusSource::LocalOnly),
- )
- .await?
- }
- TradeEvidenceMode::ResyncBeforeMutation => {
- let status = trades_client(sdk)
- .status_with_fetch_adapter(
- TradeStatusRequest::new(locator.clone())
- .with_source(SdkTradeStatusSource::ResyncThenLocal),
- adapter,
- )
- .await?;
- require_trade_mutation_online_evidence_clean(operation, &status)?;
- status
- }
- };
- trade_mutation_context_from_status(locator, &evidence_mode, operation, status)
-}
-
-#[cfg(feature = "signer-adapters")]
-fn trade_mutation_context_from_status(
- locator: RadrootsTradeLocator,
- evidence_mode: &TradeEvidenceMode,
- operation: &'static str,
- status: TradeStatusReceipt,
-) -> Result<TradeProductMutationContext, RadrootsSdkError> {
- if status.status == TradeStatusKind::Ambiguous {
- return Err(RadrootsSdkError::TradeAmbiguous {
- operation: operation.to_owned(),
- locator: Box::new(locator),
- candidates: status
- .ambiguity_candidates
- .into_iter()
- .map(|candidate| candidate.locator)
- .collect(),
- });
- }
- if !status.found {
- let evidence_requirement = match evidence_mode {
- TradeEvidenceMode::LocalOnly => "locally projected",
- TradeEvidenceMode::ResyncBeforeMutation => "resynced",
- TradeEvidenceMode::RequireExplicitEvidence { .. } => "explicitly ingested",
- };
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires a {evidence_requirement} trade"),
- });
- }
- let root_event_id =
- status
- .root_event_id
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires a trade root event id"),
- })?;
- let previous_event_id =
- status
- .last_event_id
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires local lifecycle evidence"),
- })?;
- let listing_addr =
- status
- .listing_addr
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires listing address evidence"),
- })?;
- let buyer_pubkey =
- status
- .buyer_pubkey
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires buyer pubkey evidence"),
- })?;
- let seller_pubkey =
- status
- .seller_pubkey
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires seller pubkey evidence"),
- })?;
- Ok(TradeProductMutationContext {
- order_id: status.order_id,
- listing_addr,
- buyer_pubkey,
- seller_pubkey,
- root_event_id,
- previous_event_id,
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-async fn ingest_explicit_trade_mutation_evidence(
- sdk: &crate::RadrootsClient,
- operation: &'static str,
- evidence: &[TradeEvidenceIngestRequest],
-) -> Result<(), RadrootsSdkError> {
- if evidence.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires explicit trade evidence"),
- });
- }
- let client = trades_client(sdk);
- for event in evidence {
- client.ingest_evidence(event.clone()).await?;
- }
- Ok(())
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "transport-nostr-runtime"))]
-fn require_trade_mutation_online_evidence_clean(
- operation: &'static str,
- status: &TradeStatusReceipt,
-) -> Result<(), RadrootsSdkError> {
- let evidence =
- status
- .online_evidence
- .as_ref()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!("{operation} requires online mutation evidence"),
- })?;
- if evidence.malformed_count > 0
- || evidence.out_of_filter_count > 0
- || evidence.skipped_over_limit_count > 0
- || evidence.unsupported_count > 0
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "{operation} refused online mutation evidence: malformed_count={}, out_of_filter_count={}, skipped_over_limit_count={}, unsupported_count={}",
- evidence.malformed_count,
- evidence.out_of_filter_count,
- evidence.skipped_over_limit_count,
- evidence.unsupported_count
- ),
- });
- }
- Ok(())
-}
-
-#[cfg(feature = "signer-adapters")]
-fn event_ptr(event_id: &RadrootsEventId) -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: event_id.as_str().to_owned(),
- relays: None,
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-async fn trade_product_post_enqueue_outcome<Plan, Receipt>(
- sdk: &crate::RadrootsClient,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- outbox_event_id: i64,
- receipt: Receipt,
-) -> Result<TradeMutationOutcome<Plan, Receipt>, RadrootsSdkError> {
- match publish_mode {
- PublishMode::DryRun => Err(RadrootsSdkError::InvalidRequest {
- message: "trade product dry-run must return before enqueue".to_owned(),
- }),
- PublishMode::EnqueueOnly => Ok(TradeMutationOutcome::Enqueued { receipt }),
- PublishMode::EnqueueAndPublish => {
- let publish = sdk
- .sync()
- .push_outbox(push_request_for_satisfaction_policy(
- satisfaction_policy,
- outbox_event_id,
- )?)
- .await?;
- Ok(TradeMutationOutcome::Published { receipt, publish })
- }
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn push_request_for_satisfaction_policy(
- satisfaction_policy: SatisfactionPolicy,
- outbox_event_id: i64,
-) -> Result<PushOutboxRequest, RadrootsSdkError> {
- let request = PushOutboxRequest::new().with_outbox_event_id(outbox_event_id);
- if satisfaction_policy.is_no_wait() {
- Err(RadrootsSdkError::InvalidRequest {
- message: "trade enqueue-and-publish requires a transport satisfaction policy"
- .to_owned(),
- })
- } else {
- Ok(request)
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn validate_trade_product_publish_policy(
- publish_mode: PublishMode,
- satisfaction_policy: &SatisfactionPolicy,
-) -> Result<(), RadrootsSdkError> {
- match publish_mode {
- PublishMode::DryRun | PublishMode::EnqueueOnly if !satisfaction_policy.is_no_wait() => {
- Err(RadrootsSdkError::InvalidRequest {
- message: "trade dry-run and enqueue-only modes require no-wait satisfaction"
- .to_owned(),
- })
- }
- PublishMode::EnqueueAndPublish if satisfaction_policy.is_no_wait() => {
- Err(RadrootsSdkError::InvalidRequest {
- message: "trade enqueue-and-publish requires a transport satisfaction policy"
- .to_owned(),
- })
- }
- _ => Ok(()),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_trade_product_privacy_preflight(
- operation: &'static str,
- fields: Vec<ProductSensitivityField>,
- confirmation: &PrivacyPreflightConfirmation,
-) -> Result<(), RadrootsSdkError> {
- PrivacyPreflightReceipt::evaluate(fields)
- .require_public_publish_allowed(operation, confirmation)
-}
-
-#[cfg(feature = "signer-adapters")]
-fn trade_propose_privacy_fields(request: &TradeProposeRequest) -> Vec<ProductSensitivityField> {
- let mut fields = Vec::new();
- if !request.items.is_empty() || !request.economics.items.is_empty() {
- fields.push(ProductSensitivityField::ProtocolMinimizedInventoryFields);
- }
- if let Some(public_note) = request.public_note.as_deref()
- && !public_note.trim().is_empty()
- {
- fields.push(ProductSensitivityField::PublicButSensitiveNotes);
- if trade_reason_contains_private_coordination(public_note) {
- fields.push(ProductSensitivityField::SensitiveFulfillmentDetails);
- }
- }
- fields
-}
-
-#[cfg(feature = "signer-adapters")]
-fn trade_decision_privacy_fields(decision: &RadrootsOrderDecision) -> Vec<ProductSensitivityField> {
- match &decision.decision {
- RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments,
- } if !inventory_commitments.is_empty() => {
- vec![ProductSensitivityField::ProtocolMinimizedInventoryFields]
- }
- RadrootsOrderDecisionOutcome::Accepted { .. } => Vec::new(),
- RadrootsOrderDecisionOutcome::Declined { reason } => trade_reason_privacy_fields(reason),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn trade_reason_privacy_fields(reason: &str) -> Vec<ProductSensitivityField> {
- if reason.trim().is_empty() {
- return Vec::new();
- }
- let mut fields = vec![ProductSensitivityField::PublicButSensitiveNotes];
- if trade_reason_contains_private_coordination(reason) {
- fields.push(ProductSensitivityField::SensitiveFulfillmentDetails);
- }
- fields
-}
-
-#[cfg(feature = "signer-adapters")]
-fn trade_reason_contains_private_coordination(reason: &str) -> bool {
- let reason = reason.to_ascii_lowercase();
- [
- "pickup address",
- "delivery address",
- "street address",
- "exact location",
- "private location",
- "gate code",
- "door code",
- "latitude",
- "longitude",
- " gps",
- "farm gate",
- ]
- .iter()
- .any(|marker| reason.contains(marker))
-}
-
-#[cfg(feature = "signer-adapters")]
-fn validate_trade_enqueue_policy(
- publish_mode: PublishMode,
- satisfaction_policy: &SatisfactionPolicy,
-) -> Result<(), RadrootsSdkError> {
- if publish_mode == PublishMode::DryRun {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "trade dry-run publish mode must use a prepare request".to_owned(),
- });
- }
- if publish_mode == PublishMode::EnqueueOnly && !satisfaction_policy.is_no_wait() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "trade enqueue-only publish mode only supports no-wait satisfaction"
- .to_owned(),
- });
- }
- if publish_mode == PublishMode::EnqueueAndPublish && satisfaction_policy.is_no_wait() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "trade enqueue-and-publish requires a transport satisfaction policy"
- .to_owned(),
- });
- }
- Ok(())
-}
-
-#[cfg(feature = "runtime")]
-struct ParsedOrderEvidence {
- order_id: RadrootsOrderId,
- listing_addr: RadrootsListingAddress,
- event_id: RadrootsEventId,
- event_kind: u32,
-}
-
-#[cfg(feature = "runtime")]
-#[inline(never)]
-fn parse_order_evidence(
- event: &RadrootsEventEnvelope,
-) -> Result<ParsedOrderEvidence, RadrootsSdkError> {
- let event_id = RadrootsEventId::parse(event.id_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("order evidence event id is invalid: {error}"),
- }
- })?;
- let (order_id, listing_addr) = match event.kind_u32() {
- KIND_ORDER_REQUEST => {
- let payload = order_request_from_event(event)
- .map_err(order_evidence_parse_error)?
- .payload;
- (payload.order_id, payload.listing_addr)
- }
- KIND_ORDER_DECISION => {
- let payload = order_decision_from_event(event)
- .map_err(order_evidence_parse_error)?
- .payload;
- (payload.order_id, payload.listing_addr)
- }
- KIND_ORDER_CANCELLATION => {
- let payload = order_cancellation_from_event(event)
- .map_err(order_evidence_parse_error)?
- .payload;
- (payload.order_id, payload.listing_addr)
- }
- other => {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!("order evidence event kind {other} is not supported"),
- });
- }
- };
-
- Ok(ParsedOrderEvidence {
- order_id,
- listing_addr,
- event_id,
- event_kind: event.kind_u32(),
- })
-}
-
-#[cfg(feature = "runtime")]
-fn order_evidence_parse_error(
- error: radroots_event_codec::order::RadrootsOrderEnvelopeParseError,
-) -> RadrootsSdkError {
- RadrootsSdkError::InvalidRequest {
- message: format!("order evidence event is invalid: {error}"),
- }
-}
-
-#[cfg(feature = "runtime")]
-impl TradeStatusReceipt {
- fn from_locator_query_result(
- locator: RadrootsTradeLocator,
- query_result: RadrootsTradeLocatorProjectionQueryResult,
- ) -> Self {
- match query_result.resolution {
- RadrootsTradeLocatorProjectionResolution::Projected {
- locator,
- projection,
- } => Self::from_projection(
- locator.clone(),
- locator.root_event_id,
- Vec::new(),
- projection,
- query_result.event_count,
- query_result.limit_applied,
- query_result.event_ids,
- ),
- RadrootsTradeLocatorProjectionResolution::Ambiguous { candidates, .. } => {
- Self::ambiguous(
- locator,
- candidates,
- query_result.event_count,
- query_result.limit_applied,
- query_result.event_ids,
- )
- }
- RadrootsTradeLocatorProjectionResolution::Missing { .. } => Self::missing(
- locator,
- query_result.event_count,
- query_result.limit_applied,
- query_result.event_ids,
- ),
- }
- }
-
- fn from_projection(
- locator: RadrootsTradeLocator,
- root_event_id: Option<RadrootsEventId>,
- ambiguity_candidates: Vec<TradeStatusAmbiguityCandidate>,
- projection: RadrootsOrderProjection,
- event_count: usize,
- limit_applied: u32,
- event_ids: Vec<RadrootsEventId>,
- ) -> Self {
- let found = projection.status != RadrootsTradeWorkflowState::Missing;
- let evidence =
- TradeStatusEvidenceSummary::from_projection(&projection, event_count, limit_applied);
- let eligibility = TradeStatusEligibility::from_projection(&projection);
- let next_action = TradeStatusNextActionKind::from_projection(&projection, &eligibility);
- Self {
- locator,
- order_id: projection.order_id,
- root_event_id,
- ambiguity_candidates,
- source: SdkTradeStatusSource::LocalOnly,
- found,
- event_count,
- limit_applied,
- status: projection.status.into(),
- lifecycle_terminal: projection.lifecycle_terminal,
- listing_addr: projection.listing_addr,
- buyer_pubkey: projection.buyer_pubkey,
- seller_pubkey: projection.seller_pubkey,
- economics: projection.economics,
- evidence,
- validation_trust: None,
- online_evidence: None,
- eligibility,
- next_action,
- event_ids,
- request_event_id: projection.request_event_id,
- decision_event_id: projection.decision_event_id,
- agreement_event_id: projection.agreement_event_id,
- rhi_receipt_event_id: projection.validation_receipt_event_id,
- cancellation_event_id: projection.cancellation_event_id,
- last_event_id: projection.last_event_id,
- issues: projection.issues.into_iter().map(Into::into).collect(),
- }
- }
-
- fn missing(
- locator: RadrootsTradeLocator,
- event_count: usize,
- limit_applied: u32,
- event_ids: Vec<RadrootsEventId>,
- ) -> Self {
- Self::from_projection(
- locator.clone(),
- locator.root_event_id.clone(),
- Vec::new(),
- RadrootsOrderProjection {
- order_id: locator.order_id().clone(),
- status: RadrootsTradeWorkflowState::Missing,
- request_event_id: None,
- decision_event_id: None,
- cancellation_event_id: None,
- validation_receipt_event_id: None,
- lifecycle_terminal: false,
- economics: None,
- agreement_event_id: None,
- pending_inventory_reservations: Vec::new(),
- committed_inventory_reservations: Vec::new(),
- listing_addr: locator.listing_addr.clone(),
- buyer_pubkey: locator.buyer_pubkey.clone(),
- seller_pubkey: locator.seller_pubkey.clone(),
- last_event_id: None,
- issues: Vec::new(),
- },
- event_count,
- limit_applied,
- event_ids,
- )
- }
-
- fn ambiguous(
- locator: RadrootsTradeLocator,
- candidates: Vec<RadrootsTradeLocatorCandidate>,
- event_count: usize,
- limit_applied: u32,
- event_ids: Vec<RadrootsEventId>,
- ) -> Self {
- let ambiguity_candidates = candidates
- .into_iter()
- .map(|candidate| TradeStatusAmbiguityCandidate {
- locator: candidate.locator(),
- })
- .collect::<Vec<_>>();
- let mut receipt = Self::missing(locator, event_count, limit_applied, event_ids);
- receipt.status = TradeStatusKind::Ambiguous;
- receipt.next_action = TradeStatusNextActionKind::InspectEvidenceIssues;
- receipt.ambiguity_candidates = ambiguity_candidates;
- receipt
- }
-}
-
-#[cfg(feature = "runtime")]
-impl TradeStatusEvidenceSummary {
- fn from_projection(
- projection: &RadrootsOrderProjection,
- event_count: usize,
- limit_applied: u32,
- ) -> Self {
- Self {
- event_count,
- limit_applied,
- has_request: projection.request_event_id.is_some(),
- has_decision: projection.decision_event_id.is_some(),
- has_agreement: projection.agreement_event_id.is_some(),
- has_validation_receipt: projection.validation_receipt_event_id.is_some(),
- has_cancellation: projection.cancellation_event_id.is_some(),
- has_issues: !projection.issues.is_empty(),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-impl TradeStatusEligibility {
- fn from_projection(projection: &RadrootsOrderProjection) -> Self {
- let clean = projection.issues.is_empty();
- let open = clean && !projection.lifecycle_terminal;
- let requested = projection.status == RadrootsTradeWorkflowState::Requested;
-
- Self {
- can_decide: open && requested && projection.decision_event_id.is_none(),
- can_cancel: open && requested,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-impl TradeStatusNextActionKind {
- fn from_projection(
- projection: &RadrootsOrderProjection,
- eligibility: &TradeStatusEligibility,
- ) -> Self {
- if projection.status == RadrootsTradeWorkflowState::Missing {
- return Self::NoLocalOrder;
- }
- if !projection.issues.is_empty() || projection.status == RadrootsTradeWorkflowState::Invalid
- {
- return Self::InspectEvidenceIssues;
- }
- if projection.status == RadrootsTradeWorkflowState::AgreedPendingValidation {
- return Self::AwaitValidation;
- }
- if projection.lifecycle_terminal {
- return Self::Terminal;
- }
- if eligibility.can_decide {
- return Self::AwaitSellerDecision;
- }
- Self::Terminal
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_submit_plan(
- actor: &RadrootsActorContext,
- listing_event: RadrootsEventPtr,
- order_request: RadrootsOrderRequest,
- created_at: RadrootsSdkTimestamp,
-) -> Result<TradeSubmitPlan, RadrootsSdkError> {
- require_buyer_actor(actor, "trade.prepare_submit")?;
- let listing_event_id = listing_event_id(&listing_event)?;
- let order_request =
- canonicalize_order_request_for_signer(order_request, actor.pubkey().as_str())
- .map_err(order_canonicalization_error)?;
- let created_at_nostr = created_at.try_into_nostr_created_at()?;
- let order_id = order_request.order_id.clone();
- let listing_addr = order_request.listing_addr.clone();
- let buyer_pubkey = order_request.buyer_pubkey.clone();
- let seller_pubkey = order_request.seller_pubkey.clone();
- let draft =
- order::build_order_request_draft(&listing_event, &order_request).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("order submit draft encode failed: {error}"),
- }
- })?;
- let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
- draft.into_wire_parts(),
- TRADE_SUBMIT_CONTRACT_ID,
- order_request.buyer_pubkey.as_str(),
- created_at_nostr,
- "trade.prepare_submit",
- );
- Ok(TradeSubmitPlan {
- workflow: order_workflow_plan(
- TradeWorkflowKind::Submit,
- expected_event_id.clone(),
- created_at,
- ),
- order_id,
- listing_addr,
- buyer_pubkey,
- seller_pubkey,
- listing_event_id,
- expected_event_id,
- frozen_draft,
- created_at,
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_decision_plan(
- actor: &RadrootsActorContext,
- request_event: RadrootsEventPtr,
- decision: RadrootsOrderDecision,
- created_at: RadrootsSdkTimestamp,
-) -> Result<TradeDecisionPlan, RadrootsSdkError> {
- require_seller_actor(actor, "trade.prepare_decision")?;
- let request_event_id = request_event_id(&request_event)?;
- if decision.seller_pubkey.as_str() != actor.pubkey().as_str() {
- return Err(RadrootsSdkError::UnauthorizedActor {
- operation: "trade.prepare_decision".to_owned(),
- reason: "actor pubkey must match order seller_pubkey".to_owned(),
- });
- }
- let decision = canonicalize_order_decision_for_signer(decision, actor.pubkey().as_str())
- .map_err(order_decision_canonicalization_error)?;
- let created_at_nostr = created_at.try_into_nostr_created_at()?;
- let order_id = decision.order_id.clone();
- let listing_addr = decision.listing_addr.clone();
- let buyer_pubkey = decision.buyer_pubkey.clone();
- let seller_pubkey = decision.seller_pubkey.clone();
- validate_order_payload(&decision, "order decision")
- .expect("canonical order decision payload validates");
- let draft = order::build_order_decision_draft(&request_event_id, &request_event_id, &decision)
- .expect("validated order decision draft encodes");
- let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
- draft.into_wire_parts(),
- TRADE_DECISION_CONTRACT_ID,
- decision.seller_pubkey.as_str(),
- created_at_nostr,
- "trade.prepare_decision",
- );
- Ok(TradeDecisionPlan {
- workflow: order_workflow_plan(
- TradeWorkflowKind::Decision,
- expected_event_id.clone(),
- created_at,
- ),
- order_id,
- listing_addr,
- buyer_pubkey,
- seller_pubkey,
- request_event_id,
- expected_event_id,
- frozen_draft,
- created_at,
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_cancellation_plan(
- actor: &RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- cancellation: RadrootsOrderCancellation,
- created_at: RadrootsSdkTimestamp,
-) -> Result<TradeCancellationPlan, RadrootsSdkError> {
- require_buyer_actor(actor, "trade.prepare_cancellation")?;
- let root_event_id = order_reference_event_id(&root_event, "root")?;
- let previous_event_id = order_reference_event_id(&previous_event, "previous")?;
- if cancellation.buyer_pubkey.as_str() != actor.pubkey().as_str() {
- return Err(RadrootsSdkError::UnauthorizedActor {
- operation: "trade.prepare_cancellation".to_owned(),
- reason: "actor pubkey must match order buyer_pubkey".to_owned(),
- });
- }
- let created_at_nostr = created_at.try_into_nostr_created_at()?;
- let order_id = cancellation.order_id.clone();
- let listing_addr = cancellation.listing_addr.clone();
- let buyer_pubkey = cancellation.buyer_pubkey.clone();
- let seller_pubkey = cancellation.seller_pubkey.clone();
- validate_order_payload(&cancellation, "order cancellation")?;
- let draft =
- order::build_order_cancellation_draft(&root_event_id, &previous_event_id, &cancellation)
- .expect("validated order cancellation draft encodes");
- let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
- draft.into_wire_parts(),
- TRADE_CANCELLATION_CONTRACT_ID,
- buyer_pubkey.as_str(),
- created_at_nostr,
- "order cancellation",
- );
- Ok(TradeCancellationPlan {
- workflow: order_workflow_plan(
- TradeWorkflowKind::Cancellation,
- expected_event_id.clone(),
- created_at,
- ),
- order_id,
- listing_addr,
- buyer_pubkey,
- seller_pubkey,
- root_event_id,
- previous_event_id,
- expected_event_id,
- frozen_draft,
- created_at,
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_workflow_plan(
- kind: TradeWorkflowKind,
- expected_event_id: RadrootsEventId,
- created_at: RadrootsSdkTimestamp,
-) -> TradeWorkflowPlan {
- TradeWorkflowPlan {
- kind,
- operation_kind: kind.operation_kind(),
- contract_id: kind.contract_id(),
- expected_event_id,
- created_at,
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_submit_receipt(
- plan: TradeSubmitPlan,
- enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> TradeSubmitReceipt {
- let locator = RadrootsTradeLocator::from_order_id(plan.order_id.clone())
- .with_root_event_id(enqueue.signed_event_id.clone())
- .with_listing_addr(plan.listing_addr.clone())
- .with_buyer_pubkey(plan.buyer_pubkey.clone())
- .with_seller_pubkey(plan.seller_pubkey.clone());
- TradeSubmitReceipt {
- workflow: order_workflow_enqueue_receipt(
- TradeWorkflowKind::Submit,
- plan.expected_event_id.clone(),
- &enqueue,
- ),
- locator,
- order_id: plan.order_id,
- listing_addr: plan.listing_addr,
- buyer_pubkey: plan.buyer_pubkey,
- seller_pubkey: plan.seller_pubkey,
- listing_event_id: plan.listing_event_id,
- expected_event_id: plan.expected_event_id,
- signed_event_id: enqueue.signed_event_id,
- local_event_seq: enqueue.local_event_seq,
- outbox_operation_id: enqueue.outbox_operation_id,
- outbox_event_id: enqueue.outbox_event_id,
- state: enqueue.state.into(),
- idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_decision_receipt(
- plan: TradeDecisionPlan,
- enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> TradeDecisionReceipt {
- let locator = RadrootsTradeLocator::from_order_id(plan.order_id.clone())
- .with_root_event_id(plan.request_event_id.clone())
- .with_listing_addr(plan.listing_addr.clone())
- .with_buyer_pubkey(plan.buyer_pubkey.clone())
- .with_seller_pubkey(plan.seller_pubkey.clone());
- TradeDecisionReceipt {
- workflow: order_workflow_enqueue_receipt(
- TradeWorkflowKind::Decision,
- plan.expected_event_id.clone(),
- &enqueue,
- ),
- locator,
- order_id: plan.order_id,
- listing_addr: plan.listing_addr,
- buyer_pubkey: plan.buyer_pubkey,
- seller_pubkey: plan.seller_pubkey,
- request_event_id: plan.request_event_id,
- expected_event_id: plan.expected_event_id,
- signed_event_id: enqueue.signed_event_id,
- local_event_seq: enqueue.local_event_seq,
- outbox_operation_id: enqueue.outbox_operation_id,
- outbox_event_id: enqueue.outbox_event_id,
- state: enqueue.state.into(),
- idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_cancellation_receipt(
- plan: TradeCancellationPlan,
- enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> TradeCancellationReceipt {
- let locator = RadrootsTradeLocator::from_order_id(plan.order_id.clone())
- .with_root_event_id(plan.root_event_id.clone())
- .with_listing_addr(plan.listing_addr.clone())
- .with_buyer_pubkey(plan.buyer_pubkey.clone())
- .with_seller_pubkey(plan.seller_pubkey.clone());
- TradeCancellationReceipt {
- workflow: order_workflow_enqueue_receipt(
- TradeWorkflowKind::Cancellation,
- plan.expected_event_id.clone(),
- &enqueue,
- ),
- locator,
- order_id: plan.order_id,
- listing_addr: plan.listing_addr,
- buyer_pubkey: plan.buyer_pubkey,
- seller_pubkey: plan.seller_pubkey,
- root_event_id: plan.root_event_id,
- previous_event_id: plan.previous_event_id,
- expected_event_id: plan.expected_event_id,
- signed_event_id: enqueue.signed_event_id,
- local_event_seq: enqueue.local_event_seq,
- outbox_operation_id: enqueue.outbox_operation_id,
- outbox_event_id: enqueue.outbox_event_id,
- state: enqueue.state.into(),
- idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_workflow_enqueue_receipt(
- kind: TradeWorkflowKind,
- expected_event_id: RadrootsEventId,
- enqueue: &crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> TradeWorkflowEnqueueReceipt {
- let state = SdkMutationState::from(enqueue.state);
- let digest_prefix = Some(enqueue.idempotency_digest_prefix.clone());
- let safe_retry_same_key = true;
- let replayed_existing_operation = state == SdkMutationState::AlreadyQueued;
- TradeWorkflowEnqueueReceipt {
- kind,
- operation_kind: kind.operation_kind(),
- expected_event_id,
- signed_event_id: enqueue.signed_event_id.clone(),
- local_event_seq: enqueue.local_event_seq,
- outbox_operation_id: enqueue.outbox_operation_id,
- outbox_event_id: enqueue.outbox_event_id,
- state,
- idempotency_digest_prefix: digest_prefix.clone(),
- idempotency: TradeWorkflowIdempotencyReceipt {
- digest_prefix,
- replayed_existing_operation,
- safe_to_retry_with_same_idempotency_key: safe_retry_same_key,
- },
- retry: TradeWorkflowRetryAdvice {
- retryable_after_error: false,
- safe_to_retry_enqueue_with_same_idempotency_key: safe_retry_same_key,
- recovery_actions: Vec::new(),
- },
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn freeze_order_workflow_draft(
- parts: RadrootsNip01EventWireParts,
- contract_id: &str,
- expected_pubkey: &str,
- created_at: u32,
- _operation: &'static str,
-) -> (RadrootsEventDraft, RadrootsEventId) {
- let frozen_draft = RadrootsEventDraft::new(
- contract_id,
- parts.kind,
- u64::from(created_at),
- parts.tags,
- parts.content,
- expected_pubkey,
- )
- .expect("validated order workflow draft freezes");
- let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id_str())
- .expect("frozen order workflow draft produces a valid event id");
- (frozen_draft, expected_event_id)
-}
-
-#[cfg(feature = "signer-adapters")]
-fn validate_order_payload<T>(payload: &T, operation: &'static str) -> Result<(), RadrootsSdkError>
-where
- T: OrderPayloadValidate,
-{
- payload
- .validate_order_payload()
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("{operation} payload is invalid: {error}"),
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-trait OrderPayloadValidate {
- fn validate_order_payload(
- &self,
- ) -> Result<(), radroots_event::order::RadrootsOrderPayloadError>;
-}
-
-#[cfg(feature = "signer-adapters")]
-impl OrderPayloadValidate for RadrootsOrderDecision {
- fn validate_order_payload(
- &self,
- ) -> Result<(), radroots_event::order::RadrootsOrderPayloadError> {
- self.validate()
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-impl OrderPayloadValidate for RadrootsOrderCancellation {
- fn validate_order_payload(
- &self,
- ) -> Result<(), radroots_event::order::RadrootsOrderPayloadError> {
- self.validate()
- }
-}
-
-#[cfg(feature = "runtime")]
-struct TradeRequestEvidence {
- order_id: RadrootsOrderId,
- listing_addr: RadrootsListingAddress,
- buyer_pubkey: RadrootsPublicKey,
- seller_pubkey: RadrootsPublicKey,
- request_event_id: RadrootsEventId,
-}
-
-#[cfg(feature = "runtime")]
-fn parse_order_request_evidence(
- event: &RadrootsEventEnvelope,
-) -> Result<TradeRequestEvidence, RadrootsSdkError> {
- let request_event_id = RadrootsEventId::parse(event.id_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("order request evidence event id is invalid: {error}"),
- }
- })?;
- let envelope =
- order::parse_order_request(event).map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("order request evidence decode failed: {error}"),
- })?;
- let payload = envelope.payload;
- Ok(TradeRequestEvidence {
- order_id: payload.order_id,
- listing_addr: payload.listing_addr,
- buyer_pubkey: payload.buyer_pubkey,
- seller_pubkey: payload.seller_pubkey,
- request_event_id,
- })
-}
-
-#[cfg(feature = "runtime")]
-fn sdk_timestamp_ms(timestamp: RadrootsSdkTimestamp) -> Result<i64, RadrootsSdkError> {
- let seconds = timestamp.unix_seconds();
- let millis = seconds
- .checked_mul(1_000)
- .ok_or(RadrootsSdkError::TimestampOutOfRange { value: seconds })?;
- i64::try_from(millis).map_err(|_| RadrootsSdkError::TimestampOutOfRange { value: seconds })
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_decision_request_evidence(
- plan: &TradeDecisionPlan,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- let Some(request_event_id) = &projection.request_event_id else {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "order decision requires local request evidence for order {}",
- plan.order_id
- ),
- });
- };
- if request_event_id != &plan.request_event_id {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "order decision request evidence {} does not match local request {} for order {}",
- plan.request_event_id, request_event_id, plan.order_id
- ),
- });
- }
- if !matches!(&projection.status, RadrootsTradeWorkflowState::Requested) {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "order decision requires requested local state for order {}; current state is {:?}",
- plan.order_id, projection.status
- ),
- });
- }
- if !projection.issues.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "order decision request evidence for order {} has {} reducer issue(s)",
- plan.order_id,
- projection.issues.len()
- ),
- });
- }
- require_projection_match(
- "order decision",
- "listing_addr",
- projection.listing_addr.as_ref(),
- &plan.listing_addr,
- &plan.order_id,
- )?;
- require_projection_match(
- "order decision",
- "buyer_pubkey",
- projection.buyer_pubkey.as_ref(),
- &plan.buyer_pubkey,
- &plan.order_id,
- )?;
- require_projection_match(
- "order decision",
- "seller_pubkey",
- projection.seller_pubkey.as_ref(),
- &plan.seller_pubkey,
- &plan.order_id,
- )?;
- Ok(())
-}
-
-#[cfg(feature = "signer-adapters")]
-#[derive(Clone, Copy)]
-struct OrderLifecycleReferences<'a> {
- operation: &'static str,
- order_id: &'a RadrootsOrderId,
- listing_addr: &'a RadrootsListingAddress,
- buyer_pubkey: &'a RadrootsPublicKey,
- seller_pubkey: &'a RadrootsPublicKey,
- root_event_id: &'a RadrootsEventId,
- previous_event_id: &'a RadrootsEventId,
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_cancellation_state(
- plan: &TradeCancellationPlan,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- let refs = OrderLifecycleReferences {
- operation: "order cancellation",
- order_id: &plan.order_id,
- listing_addr: &plan.listing_addr,
- buyer_pubkey: &plan.buyer_pubkey,
- seller_pubkey: &plan.seller_pubkey,
- root_event_id: &plan.root_event_id,
- previous_event_id: &plan.previous_event_id,
- };
- require_clean_lifecycle_projection(refs, projection)?;
- if projection.status != RadrootsTradeWorkflowState::Requested {
- return Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- format!(
- "cancellation requires requested local state; current state is {:?}",
- projection.status
- ),
- ));
- }
- require_no_lifecycle_terminal(&refs, projection)?;
- require_lifecycle_previous_is_current(&refs, projection)
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_clean_lifecycle_projection(
- refs: OrderLifecycleReferences<'_>,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- let Some(request_event_id) = &projection.request_event_id else {
- return Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- "requires local order request evidence",
- ));
- };
- if request_event_id != refs.root_event_id {
- return Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- format!(
- "root event {} does not match local request {}",
- refs.root_event_id, request_event_id
- ),
- ));
- }
- if !projection.issues.is_empty() {
- return Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- format!(
- "local order evidence has {} reducer issue(s)",
- projection.issues.len()
- ),
- ));
- }
- require_projection_match(
- refs.operation,
- "listing_addr",
- projection.listing_addr.as_ref(),
- refs.listing_addr,
- refs.order_id,
- )?;
- require_projection_match(
- refs.operation,
- "buyer_pubkey",
- projection.buyer_pubkey.as_ref(),
- refs.buyer_pubkey,
- refs.order_id,
- )?;
- require_projection_match(
- refs.operation,
- "seller_pubkey",
- projection.seller_pubkey.as_ref(),
- refs.seller_pubkey,
- refs.order_id,
- )
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_no_lifecycle_terminal(
- refs: &OrderLifecycleReferences<'_>,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- if projection.lifecycle_terminal {
- Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- "requires non-terminal local order state",
- ))
- } else {
- Ok(())
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_lifecycle_previous_is_current(
- refs: &OrderLifecycleReferences<'_>,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- match projection.last_event_id.as_ref() {
- Some(last_event_id) if last_event_id == refs.previous_event_id => Ok(()),
- Some(last_event_id) => Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- format!(
- "previous event {} does not match current lifecycle event {}",
- refs.previous_event_id, last_event_id
- ),
- )),
- None => Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- "requires current lifecycle event evidence",
- )),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn lifecycle_invalid(
- operation: &'static str,
- order_id: &RadrootsOrderId,
- reason: impl Into<String>,
-) -> RadrootsSdkError {
- RadrootsSdkError::InvalidRequest {
- message: format!("{operation} for order {order_id} {}", reason.into()),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_projection_match<T>(
- operation: &'static str,
- field: &'static str,
- actual: Option<&T>,
- expected: &T,
- order_id: &RadrootsOrderId,
-) -> Result<(), RadrootsSdkError>
-where
- T: core::fmt::Display + PartialEq,
-{
- match actual {
- Some(actual) if actual == expected => Ok(()),
- Some(actual) => Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "{operation} {field} {expected} does not match local request {actual} for order {order_id}"
- ),
- }),
- None => Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "{operation} request evidence is missing {field} for order {order_id}"
- ),
- }),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn require_buyer_actor(
- actor: &RadrootsActorContext,
- operation: &'static str,
-) -> Result<(), RadrootsSdkError> {
- if actor.satisfies(RadrootsActorRole::Buyer) {
- Ok(())
- } else {
- Err(RadrootsSdkError::UnauthorizedActor {
- operation: operation.to_owned(),
- reason: "missing role Buyer".to_owned(),
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-fn require_seller_actor(
- actor: &RadrootsActorContext,
- operation: &'static str,
-) -> Result<(), RadrootsSdkError> {
- if actor.satisfies(RadrootsActorRole::Seller) {
- Ok(())
- } else {
- Err(RadrootsSdkError::UnauthorizedActor {
- operation: operation.to_owned(),
- reason: "missing role Seller".to_owned(),
- })
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn listing_event_id(listing_event: &RadrootsEventPtr) -> Result<RadrootsEventId, RadrootsSdkError> {
- RadrootsEventId::parse(listing_event.id.as_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("listing evidence event id is invalid: {error}"),
- }
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-fn request_event_id(request_event: &RadrootsEventPtr) -> Result<RadrootsEventId, RadrootsSdkError> {
- RadrootsEventId::parse(request_event.id.as_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("order request evidence event id is invalid: {error}"),
- }
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_reference_event_id(
- event: &RadrootsEventPtr,
- label: &'static str,
-) -> Result<RadrootsEventId, RadrootsSdkError> {
- RadrootsEventId::parse(event.id.as_str()).map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("order {label} evidence event id is invalid: {error}"),
- })
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_canonicalization_error(error: RadrootsOrderCanonicalizationError) -> RadrootsSdkError {
- match error {
- RadrootsOrderCanonicalizationError::InvalidBuyerSigner => {
- RadrootsSdkError::UnauthorizedActor {
- operation: "trade.prepare_submit".to_owned(),
- reason: "actor pubkey must match order buyer_pubkey".to_owned(),
- }
- }
- error => RadrootsSdkError::InvalidRequest {
- message: format!("order submit request is invalid: {error}"),
- },
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn order_decision_canonicalization_error(
- error: RadrootsOrderCanonicalizationError,
-) -> RadrootsSdkError {
- RadrootsSdkError::InvalidRequest {
- message: format!("order decision request is invalid: {error}"),
- }
-}
-
-#[cfg(feature = "runtime")]
-impl From<RadrootsTradeWorkflowState> for TradeStatusKind {
- fn from(status: RadrootsTradeWorkflowState) -> Self {
- match status {
- RadrootsTradeWorkflowState::Missing => Self::Missing,
- 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,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-impl From<RadrootsOrderIssue> for SdkTradeStatusIssue {
- fn from(issue: RadrootsOrderIssue) -> Self {
- match issue {
- RadrootsOrderIssue::MissingRequest => {
- Self::new(SdkTradeStatusIssueKind::MissingRequest, Vec::new())
- }
- RadrootsOrderIssue::MultipleRequests { event_ids } => {
- Self::new(SdkTradeStatusIssueKind::MultipleRequests, event_ids)
- }
- RadrootsOrderIssue::RequestPayloadInvalid { event_id } => {
- Self::single(SdkTradeStatusIssueKind::RequestPayloadInvalid, event_id)
- }
- RadrootsOrderIssue::RequestOrderIdMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::RequestOrderIdMismatch, event_id)
- }
- RadrootsOrderIssue::RequestAuthorMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::RequestAuthorMismatch, event_id)
- }
- RadrootsOrderIssue::RequestListingAddressInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::RequestListingAddressInvalid,
- event_id,
- ),
- RadrootsOrderIssue::RequestSellerListingMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RequestSellerListingMismatch,
- event_id,
- ),
- RadrootsOrderIssue::DecisionPayloadInvalid { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionPayloadInvalid, event_id)
- }
- RadrootsOrderIssue::DecisionOrderIdMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionOrderIdMismatch, event_id)
- }
- RadrootsOrderIssue::DecisionAuthorMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionAuthorMismatch, event_id)
- }
- RadrootsOrderIssue::DecisionCounterpartyMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::DecisionCounterpartyMismatch,
- event_id,
- ),
- RadrootsOrderIssue::DecisionBuyerMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionBuyerMismatch, event_id)
- }
- RadrootsOrderIssue::DecisionSellerMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionSellerMismatch, event_id)
- }
- RadrootsOrderIssue::DecisionListingAddressInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::DecisionListingAddressInvalid,
- event_id,
- ),
- RadrootsOrderIssue::DecisionListingMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionListingMismatch, event_id)
- }
- RadrootsOrderIssue::DecisionRootMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionRootMismatch, event_id)
- }
- RadrootsOrderIssue::DecisionPreviousMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionPreviousMismatch, event_id)
- }
- RadrootsOrderIssue::DecisionMissingInventoryCommitments { event_id } => Self::single(
- SdkTradeStatusIssueKind::DecisionMissingInventoryCommitments,
- event_id,
- ),
- RadrootsOrderIssue::DecisionInventoryCommitmentMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::DecisionInventoryCommitmentMismatch,
- event_id,
- ),
- RadrootsOrderIssue::DecisionMissingReason { event_id } => {
- Self::single(SdkTradeStatusIssueKind::DecisionMissingReason, event_id)
- }
- RadrootsOrderIssue::ConflictingDecisions { event_ids } => {
- Self::new(SdkTradeStatusIssueKind::ConflictingDecisions, event_ids)
- }
- RadrootsOrderIssue::CancellationWithoutCancellableOrder { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationWithoutCancellableOrder,
- event_id,
- ),
- RadrootsOrderIssue::CancellationPayloadInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationPayloadInvalid,
- event_id,
- ),
- RadrootsOrderIssue::CancellationOrderIdMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationOrderIdMismatch,
- event_id,
- ),
- RadrootsOrderIssue::CancellationAuthorMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationAuthorMismatch,
- event_id,
- ),
- RadrootsOrderIssue::CancellationCounterpartyMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationCounterpartyMismatch,
- event_id,
- ),
- RadrootsOrderIssue::CancellationBuyerMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::CancellationBuyerMismatch, event_id)
- }
- RadrootsOrderIssue::CancellationSellerMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationSellerMismatch,
- event_id,
- ),
- RadrootsOrderIssue::CancellationListingAddressInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationListingAddressInvalid,
- event_id,
- ),
- RadrootsOrderIssue::CancellationListingMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationListingMismatch,
- event_id,
- ),
- RadrootsOrderIssue::CancellationRootMismatch { event_id } => {
- Self::single(SdkTradeStatusIssueKind::CancellationRootMismatch, event_id)
- }
- RadrootsOrderIssue::CancellationPreviousMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::CancellationPreviousMismatch,
- event_id,
- ),
- RadrootsOrderIssue::ForkedLifecycle { event_ids } => {
- Self::new(SdkTradeStatusIssueKind::ForkedLifecycle, event_ids)
- }
- RadrootsOrderIssue::ValidationReceiptWithoutPendingAgreement { event_id } => {
- Self::single(
- SdkTradeStatusIssueKind::ValidationReceiptWithoutPendingAgreement,
- event_id,
- )
- }
- RadrootsOrderIssue::ValidationReceiptOrderIdMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::ValidationReceiptOrderIdMismatch,
- event_id,
- ),
- RadrootsOrderIssue::ValidationReceiptTypeMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::ValidationReceiptTypeMismatch,
- event_id,
- ),
- RadrootsOrderIssue::ValidationReceiptRootMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::ValidationReceiptRootMismatch,
- event_id,
- ),
- RadrootsOrderIssue::ValidationReceiptTargetMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::ValidationReceiptTargetMismatch,
- event_id,
- ),
- RadrootsOrderIssue::ValidationReceiptListingMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::ValidationReceiptListingMismatch,
- event_id,
- ),
- RadrootsOrderIssue::ConflictingValidationReceipts { event_ids } => Self::new(
- SdkTradeStatusIssueKind::ConflictingValidationReceipts,
- event_ids,
- ),
- RadrootsOrderIssue::DeterministicValidationFailure { event_id, .. } => Self::single(
- SdkTradeStatusIssueKind::DeterministicValidationFailure,
- event_id,
- ),
- RadrootsOrderIssue::StaleListingEvent {
- expected_event_id,
- current_event_id,
- } => Self::new(
- SdkTradeStatusIssueKind::StaleListingEvent,
- vec![expected_event_id, current_event_id],
- ),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-fn projection_error(error: RadrootsOrderStoreQueryError) -> RadrootsSdkError {
- let message = match error {
- RadrootsOrderStoreQueryError::Store(_) => "order status store query failed",
- RadrootsOrderStoreQueryError::InvalidStoredTagsJson { .. } => {
- "stored order event tags could not be decoded"
- }
- RadrootsOrderStoreQueryError::Decode { .. } => {
- "stored order event could not decode as order record"
- }
- RadrootsOrderStoreQueryError::InvalidStoredEnvelope { .. } => {
- "stored order event envelope is invalid"
- }
- RadrootsOrderStoreQueryError::Projection(error) => return error.into(),
- };
- RadrootsSdkError::Projection {
- message: message.to_owned(),
- }
-}
-
-#[cfg(feature = "runtime")]
-fn camel_to_snake(value: &str) -> String {
- let mut output = String::new();
- for (index, character) in value.chars().enumerate() {
- if character.is_ascii_uppercase() {
- if index > 0 {
- output.push('_');
- }
- output.push(character.to_ascii_lowercase());
- } else {
- output.push(character);
- }
- }
- output
-}
-
-#[cfg(all(test, feature = "runtime", feature = "signer-adapters"))]
-#[path = "../tests/unit/orders_runtime_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/private_store.rs b/crates/sdk/src/private_store.rs
@@ -441,8 +441,7 @@ impl SdkPrivateStore {
if exists != 0 {
return Err(RadrootsSdkError::UnsupportedProfileSchema {
path: Path::new("private.sqlite").to_path_buf(),
- message: "pre-V1 SDK private store is unsupported; use explicit quarantine reset"
- .to_owned(),
+ message: "pre-V1 SDK private store is unsupported by release-product v1".to_owned(),
});
}
Ok(())
diff --git a/crates/sdk/src/product_clients.rs b/crates/sdk/src/product_clients.rs
@@ -30,13 +30,13 @@ impl<'client> ListingsClient<'client> {
#[cfg(feature = "runtime")]
#[derive(Clone, Copy)]
pub struct MarketClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
+ pub(crate) _sdk: &'client RadrootsClient,
}
#[cfg(feature = "runtime")]
impl<'client> MarketClient<'client> {
pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
+ Self { _sdk: sdk }
}
}
@@ -56,55 +56,14 @@ impl<'client> GeoNamesClient<'client> {
#[cfg(feature = "runtime")]
#[derive(Clone, Copy)]
pub struct TradesClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
+ pub(crate) _sdk: &'client RadrootsClient,
}
#[cfg(feature = "runtime")]
impl<'client> TradesClient<'client> {
pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-
- #[cfg(feature = "signer-adapters")]
- pub fn buyer(&self) -> TradeBuyerClient<'client> {
- TradeBuyerClient { sdk: self.sdk }
- }
-
- pub fn seller(&self) -> TradeSellerClient<'client> {
- TradeSellerClient { sdk: self.sdk }
+ Self { _sdk: sdk }
}
-
- pub fn resync(&self) -> TradeResyncClient<'client> {
- TradeResyncClient { sdk: self.sdk }
- }
-
- pub fn validation_receipts(&self) -> TradeValidationReceiptsClient<'client> {
- TradeValidationReceiptsClient { sdk: self.sdk }
- }
-}
-
-#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
-#[derive(Clone, Copy)]
-pub struct TradeBuyerClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct TradeSellerClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct TradeResyncClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct TradeValidationReceiptsClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
}
#[cfg(feature = "runtime")]
diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs
@@ -52,39 +52,101 @@ const BACKUP_MANIFEST_FILE: &str = "manifest.json";
#[cfg(feature = "runtime")]
const PRE_V1_RUNTIME_FILES: [&str; 2] = ["event_store.sqlite", "outbox.sqlite"];
#[cfg(feature = "runtime")]
-const PRE_V1_RUNTIME_ARTIFACTS: [&str; 6] = [
- "event_store.sqlite",
- "event_store.sqlite-wal",
- "event_store.sqlite-shm",
- "outbox.sqlite",
- "outbox.sqlite-wal",
- "outbox.sqlite-shm",
-];
-#[cfg(feature = "runtime")]
const SDK_RUNTIME_MIGRATION_UP: &str = r#"
CREATE TABLE IF NOT EXISTS sdk_runtime_operation_journal (
journal_id INTEGER PRIMARY KEY AUTOINCREMENT,
contract_version TEXT NOT NULL,
- operation_id TEXT NOT NULL,
+ operation_kind TEXT NOT NULL,
actor_pubkey TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
- request_digest_sha256_hex TEXT NOT NULL,
+ command_payload_hash TEXT NOT NULL CHECK (length(command_payload_hash) = 64),
+ frozen_draft_json TEXT NOT NULL,
+ expected_transport_id TEXT NOT NULL,
+ mutation_id TEXT,
+ state TEXT NOT NULL CHECK (state IN ('prepared','signature_pending','committed','rejected','failed_recoverable')),
+ result_json TEXT,
+ last_error_code TEXT,
+ last_error_detail TEXT,
+ created_at_ms INTEGER NOT NULL,
+ updated_at_ms INTEGER NOT NULL,
+ UNIQUE(contract_version, operation_kind, actor_pubkey, idempotency_key),
+ UNIQUE(mutation_id)
+) STRICT;
+
+CREATE INDEX IF NOT EXISTS sdk_runtime_operation_journal_state_idx
+ON sdk_runtime_operation_journal(state, updated_at_ms, journal_id);
+
+CREATE TABLE IF NOT EXISTS sdk_runtime_recovery_receipt (
+ recovery_receipt_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ recovery_code TEXT NOT NULL CHECK (recovery_code IN ('signer_timeout','projection_stale','relay_failure','reservation_expiry','idempotency_conflict')),
+ operation_kind TEXT,
+ actor_pubkey TEXT,
+ idempotency_key TEXT,
+ recovery_action TEXT NOT NULL,
+ detail_json TEXT NOT NULL,
+ created_at_ms INTEGER NOT NULL
+) STRICT;
+
+CREATE INDEX IF NOT EXISTS sdk_runtime_recovery_receipt_code_idx
+ON sdk_runtime_recovery_receipt(recovery_code, created_at_ms, recovery_receipt_id);
+
+CREATE TABLE IF NOT EXISTS sdk_seller_inventory_reservation (
+ reservation_id TEXT PRIMARY KEY NOT NULL,
+ farm_id TEXT NOT NULL,
+ candidate_id TEXT NOT NULL,
+ authority_id TEXT NOT NULL,
+ inventory_epoch INTEGER NOT NULL CHECK (inventory_epoch >= 0),
+ assertion_commitment TEXT NOT NULL UNIQUE,
+ state TEXT NOT NULL CHECK (state IN ('prepared','bound','released','expired','conflict')),
+ lease_until_ms INTEGER NOT NULL,
+ bound_mutation_id TEXT UNIQUE,
created_at_ms INTEGER NOT NULL,
- completed_at_ms INTEGER,
- UNIQUE(contract_version, operation_id, actor_pubkey, idempotency_key)
-);
+ updated_at_ms INTEGER NOT NULL
+) STRICT;
+
+CREATE TABLE IF NOT EXISTS sdk_seller_inventory_reservation_line (
+ reservation_id TEXT NOT NULL REFERENCES sdk_seller_inventory_reservation(reservation_id) ON DELETE CASCADE,
+ farm_id TEXT NOT NULL,
+ candidate_id TEXT NOT NULL,
+ line_id TEXT NOT NULL,
+ bin_id TEXT NOT NULL,
+ quantity_mantissa TEXT NOT NULL,
+ quantity_scale INTEGER NOT NULL CHECK (quantity_scale BETWEEN 0 AND 9),
+ unit_code TEXT NOT NULL,
+ PRIMARY KEY (reservation_id, line_id),
+ UNIQUE(farm_id, candidate_id, line_id, bin_id)
+) STRICT;
+
+CREATE INDEX IF NOT EXISTS sdk_seller_inventory_reservation_expiring_idx
+ON sdk_seller_inventory_reservation(lease_until_ms, reservation_id)
+WHERE state = 'prepared';
+
+CREATE INDEX IF NOT EXISTS sdk_seller_inventory_reservation_candidate_idx
+ON sdk_seller_inventory_reservation(candidate_id, state, reservation_id);
+
+CREATE TABLE IF NOT EXISTS sdk_runtime_trade_projection_checkpoint (
+ projection_name TEXT PRIMARY KEY NOT NULL,
+ reducer_contract_id TEXT NOT NULL,
+ reducer_version INTEGER NOT NULL,
+ last_ingest_seq INTEGER NOT NULL,
+ source_digest TEXT NOT NULL,
+ projection_digest TEXT NOT NULL,
+ completeness_state TEXT NOT NULL CHECK (completeness_state IN ('current','partial','stale','rebuilding','failed')),
+ rebuilt_at_ms INTEGER,
+ updated_at_ms INTEGER NOT NULL
+) STRICT;
CREATE TABLE IF NOT EXISTS sdk_runtime_health_state (
key TEXT PRIMARY KEY NOT NULL,
value_json TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL
-);
+) STRICT;
CREATE TABLE IF NOT EXISTS sdk_runtime_projection_generation (
projection_name TEXT PRIMARY KEY NOT NULL,
generation INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
-);
+) STRICT;
"#;
#[cfg(feature = "runtime")]
@@ -466,40 +528,6 @@ pub struct RestoreReceipt {
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-#[non_exhaustive]
-pub struct QuarantineResetRequest {
- pub profile: PathBuf,
- pub quarantine: PathBuf,
- pub overwrite: bool,
-}
-
-#[cfg(feature = "runtime")]
-impl QuarantineResetRequest {
- pub fn new(profile: impl Into<PathBuf>, quarantine: impl Into<PathBuf>) -> Self {
- Self {
- profile: profile.into(),
- quarantine: quarantine.into(),
- overwrite: false,
- }
- }
-
- pub fn with_overwrite(mut self, overwrite: bool) -> Self {
- self.overwrite = overwrite;
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct QuarantineResetReceipt {
- pub profile: PathBuf,
- pub quarantine: PathBuf,
- pub quarantined_paths: Vec<PathBuf>,
- pub reset_paths: RadrootsSdkStoragePaths,
-}
-
-#[cfg(feature = "runtime")]
#[derive(Clone)]
pub struct RadrootsClientBuilder {
storage: RadrootsSdkStorageConfig,
@@ -575,7 +603,8 @@ impl RadrootsClientBuilder {
}
pub async fn build(self) -> Result<RadrootsClient, RadrootsSdkError> {
- let storage = open_storage(&self.storage).await?;
+ let recovery_now_ms = clock_recovery_now_ms(&self.clock);
+ let storage = open_storage(&self.storage, recovery_now_ms).await?;
Ok(RadrootsClient {
_event_store: storage.event_store,
_outbox: storage.outbox,
@@ -916,12 +945,6 @@ impl RadrootsClient {
restored_paths,
})
}
-
- pub async fn quarantine_reset_storage(
- request: QuarantineResetRequest,
- ) -> Result<QuarantineResetReceipt, RadrootsSdkError> {
- quarantine_reset_storage(request).await
- }
}
#[cfg(feature = "runtime")]
@@ -1802,19 +1825,25 @@ struct OpenedRuntimeStorage {
#[cfg(feature = "runtime")]
async fn open_storage(
storage: &RadrootsSdkStorageConfig,
+ recovery_now_ms: i64,
) -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
match storage {
- RadrootsSdkStorageConfig::Memory => open_memory_storage().await,
- RadrootsSdkStorageConfig::Directory(path) => open_directory_storage(path).await,
+ RadrootsSdkStorageConfig::Memory => open_memory_storage(recovery_now_ms).await,
+ RadrootsSdkStorageConfig::Directory(path) => {
+ open_directory_storage(path, recovery_now_ms).await
+ }
}
}
#[cfg(feature = "runtime")]
-async fn open_memory_storage() -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
+async fn open_memory_storage(
+ recovery_now_ms: i64,
+) -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
let runtime_pool = open_runtime_memory_pool().await?;
+ reject_newer_sdk_runtime_schema(&runtime_pool, Path::new(":memory:")).await?;
let event_store = RadrootsEventStore::open_pool(runtime_pool.clone(), false).await?;
let outbox = RadrootsOutbox::open_pool(runtime_pool.clone(), false).await?;
- apply_sdk_runtime_schema(&runtime_pool).await?;
+ apply_sdk_runtime_schema(&runtime_pool, Path::new(":memory:"), recovery_now_ms).await?;
Ok(OpenedRuntimeStorage {
event_store,
outbox,
@@ -1825,7 +1854,10 @@ async fn open_memory_storage() -> Result<OpenedRuntimeStorage, RadrootsSdkError>
}
#[cfg(feature = "runtime")]
-async fn open_directory_storage(path: &Path) -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
+async fn open_directory_storage(
+ path: &Path,
+ recovery_now_ms: i64,
+) -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
reject_pre_v1_profile(path)?;
fs::create_dir_all(path).map_err(|error| RadrootsSdkError::Io {
path: path.to_path_buf(),
@@ -1833,9 +1865,10 @@ async fn open_directory_storage(path: &Path) -> Result<OpenedRuntimeStorage, Rad
})?;
let paths = storage_paths_for_directory(path);
let runtime_pool = open_runtime_file_pool(&paths.runtime_path).await?;
+ reject_newer_sdk_runtime_schema(&runtime_pool, &paths.runtime_path).await?;
let event_store = RadrootsEventStore::open_pool(runtime_pool.clone(), true).await?;
let outbox = RadrootsOutbox::open_pool(runtime_pool.clone(), true).await?;
- apply_sdk_runtime_schema(&runtime_pool).await?;
+ apply_sdk_runtime_schema(&runtime_pool, &paths.runtime_path, recovery_now_ms).await?;
Ok(OpenedRuntimeStorage {
event_store,
outbox,
@@ -1846,6 +1879,17 @@ async fn open_directory_storage(path: &Path) -> Result<OpenedRuntimeStorage, Rad
}
#[cfg(feature = "runtime")]
+fn clock_recovery_now_ms(clock: &RadrootsSdkClock) -> i64 {
+ let Ok(timestamp) = clock.now() else {
+ return 0;
+ };
+ let Some(millis) = timestamp.unix_seconds().checked_mul(1_000) else {
+ return i64::MAX;
+ };
+ i64::try_from(millis).unwrap_or(i64::MAX)
+}
+
+#[cfg(feature = "runtime")]
fn storage_paths_for_directory(path: &Path) -> RadrootsSdkStoragePaths {
RadrootsSdkStoragePaths {
runtime_path: path.join(RUNTIME_SQLITE_FILE),
@@ -1878,11 +1922,17 @@ async fn open_runtime_file_pool(path: &Path) -> Result<SqlitePool, RadrootsSdkEr
}
#[cfg(feature = "runtime")]
-async fn apply_sdk_runtime_schema(pool: &SqlitePool) -> Result<(), RadrootsSdkError> {
+async fn apply_sdk_runtime_schema(
+ pool: &SqlitePool,
+ path: &Path,
+ recovery_now_ms: i64,
+) -> Result<(), RadrootsSdkError> {
sqlx::raw_sql(SDK_RUNTIME_MIGRATION_UP)
.execute(pool)
.await
.map_err(|error| runtime_store_error(error.to_string()))?;
+ validate_sdk_runtime_schema(pool, path).await?;
+ recover_sdk_runtime_state(pool, recovery_now_ms).await?;
sqlx::query("PRAGMA user_version = 1")
.execute(pool)
.await
@@ -1891,130 +1941,234 @@ async fn apply_sdk_runtime_schema(pool: &SqlitePool) -> Result<(), RadrootsSdkEr
}
#[cfg(feature = "runtime")]
-fn reject_pre_v1_profile(path: &Path) -> Result<(), RadrootsSdkError> {
- for file_name in PRE_V1_RUNTIME_FILES {
- let candidate = path.join(file_name);
- if fs::symlink_metadata(&candidate).is_ok() {
- return Err(RadrootsSdkError::UnsupportedProfileSchema {
- path: candidate,
- message: "pre-V1 SDK runtime file is unsupported; use explicit quarantine reset"
- .to_owned(),
- });
- }
+async fn reject_newer_sdk_runtime_schema(
+ pool: &SqlitePool,
+ path: &Path,
+) -> Result<(), RadrootsSdkError> {
+ let version =
+ sqlite_query_i64(pool, "PRAGMA user_version", SqliteStoreRole::RuntimeStore).await?;
+ if version > SDK_RUNTIME_SCHEMA_VERSION {
+ return Err(RadrootsSdkError::UnsupportedProfileSchema {
+ path: path.to_path_buf(),
+ message: format!(
+ "runtime schema version {version} is newer than supported version {SDK_RUNTIME_SCHEMA_VERSION}"
+ ),
+ });
}
Ok(())
}
#[cfg(feature = "runtime")]
-async fn quarantine_reset_storage(
- request: QuarantineResetRequest,
-) -> Result<QuarantineResetReceipt, RadrootsSdkError> {
- let moves = preflight_quarantine_reset(&request)?;
- fs::create_dir_all(&request.quarantine).map_err(|error| RadrootsSdkError::Io {
- path: request.quarantine.clone(),
- message: error.to_string(),
- })?;
- let mut quarantined_paths = Vec::with_capacity(moves.len());
- for (source, destination) in moves {
- if request.overwrite {
- remove_existing_restore_path(&destination)?;
+async fn validate_sdk_runtime_schema(
+ pool: &SqlitePool,
+ path: &Path,
+) -> Result<(), RadrootsSdkError> {
+ for (table, columns) in [
+ (
+ "sdk_runtime_operation_journal",
+ &[
+ "operation_kind",
+ "command_payload_hash",
+ "frozen_draft_json",
+ "expected_transport_id",
+ "state",
+ "result_json",
+ "updated_at_ms",
+ ][..],
+ ),
+ (
+ "sdk_seller_inventory_reservation",
+ &[
+ "reservation_id",
+ "state",
+ "lease_until_ms",
+ "bound_mutation_id",
+ ][..],
+ ),
+ (
+ "sdk_runtime_trade_projection_checkpoint",
+ &["projection_name", "completeness_state", "updated_at_ms"][..],
+ ),
+ ] {
+ let actual = sqlite_table_columns(pool, table).await?;
+ for required in columns {
+ if !actual.iter().any(|column| column == required) {
+ return Err(RadrootsSdkError::UnsupportedProfileSchema {
+ path: path.to_path_buf(),
+ message: format!(
+ "runtime table `{table}` is missing required column `{required}`"
+ ),
+ });
+ }
}
- rename_restore_path(&source, &destination, "quarantine reset")?;
- quarantined_paths.push(destination);
- }
- let storage = open_directory_storage(&request.profile).await?;
- let reset_paths = storage
- .paths
- .expect("directory storage reset always returns paths");
- Ok(QuarantineResetReceipt {
- profile: request.profile,
- quarantine: request.quarantine,
- quarantined_paths,
- reset_paths,
- })
-}
-
-#[cfg(feature = "runtime")]
-fn preflight_quarantine_reset(
- request: &QuarantineResetRequest,
-) -> Result<Vec<(PathBuf, PathBuf)>, RadrootsSdkError> {
- if request.profile.as_os_str().is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "quarantine reset profile path must not be empty".to_owned(),
- });
}
- if request.quarantine.as_os_str().is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "quarantine reset destination must not be empty".to_owned(),
+ let integrity =
+ sqlite_query_string(pool, "PRAGMA quick_check", SqliteStoreRole::RuntimeStore).await?;
+ if integrity != "ok" {
+ return Err(RadrootsSdkError::UnsupportedProfileSchema {
+ path: path.to_path_buf(),
+ message: format!("runtime quick_check failed: {integrity}"),
});
}
- match fs::symlink_metadata(&request.profile) {
- Ok(metadata) if metadata.file_type().is_symlink() => {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "quarantine reset profile must not be a symbolic link".to_owned(),
- });
- }
- Ok(metadata) if metadata.is_dir() => {}
- Ok(_) => {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "quarantine reset profile must be a directory".to_owned(),
- });
+ Ok(())
+}
+
+#[cfg(feature = "runtime")]
+async fn sqlite_table_columns(
+ pool: &SqlitePool,
+ table: &str,
+) -> Result<Vec<String>, RadrootsSdkError> {
+ let sql = match table {
+ "sdk_runtime_operation_journal" => "PRAGMA table_info(sdk_runtime_operation_journal)",
+ "sdk_seller_inventory_reservation" => "PRAGMA table_info(sdk_seller_inventory_reservation)",
+ "sdk_runtime_trade_projection_checkpoint" => {
+ "PRAGMA table_info(sdk_runtime_trade_projection_checkpoint)"
}
- Err(error) => {
- return Err(RadrootsSdkError::Io {
- path: request.profile.clone(),
- message: error.to_string(),
+ _ => {
+ return Err(RadrootsSdkError::UnsupportedProfileSchema {
+ path: PathBuf::from(":memory:"),
+ message: format!("runtime schema validation requested unknown table `{table}`"),
});
}
+ };
+ let rows = sqlx::query(sql)
+ .fetch_all(pool)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))?;
+ rows.into_iter()
+ .map(|row| {
+ row.try_get::<String, _>("name")
+ .map_err(|error| runtime_store_error(error.to_string()))
+ })
+ .collect()
+}
+
+#[cfg(feature = "runtime")]
+async fn recover_sdk_runtime_state(pool: &SqlitePool, now_ms: i64) -> Result<(), RadrootsSdkError> {
+ let operation_recovery = sqlx::query(
+ "UPDATE sdk_runtime_operation_journal SET state = 'failed_recoverable', last_error_code = 'signer_timeout', last_error_detail = 'operation was incomplete at SDK startup', updated_at_ms = ? WHERE state IN ('prepared','signature_pending')",
+ )
+ .bind(now_ms)
+ .execute(pool)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))?;
+ if operation_recovery.rows_affected() > 0 {
+ record_runtime_recovery_receipt(
+ pool,
+ "signer_timeout",
+ None,
+ None,
+ None,
+ "retry_operation_with_same_idempotency_key",
+ serde_json::json!({ "recovered_operations": operation_recovery.rows_affected() }),
+ now_ms,
+ )
+ .await?;
}
- match fs::symlink_metadata(&request.quarantine) {
- Ok(metadata) if metadata.file_type().is_symlink() => {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "quarantine reset destination must not be a symbolic link".to_owned(),
- });
- }
- Ok(metadata) if metadata.is_dir() => {}
- Ok(_) => {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "quarantine reset destination must be a directory".to_owned(),
- });
- }
- Err(error) if error.kind() == ErrorKind::NotFound => {}
- Err(error) => {
- return Err(RadrootsSdkError::Io {
- path: request.quarantine.clone(),
- message: error.to_string(),
- });
- }
+ let reservation_expiry = sqlx::query(
+ "UPDATE sdk_seller_inventory_reservation SET state = 'expired', updated_at_ms = ? WHERE state = 'prepared' AND lease_until_ms <= ?",
+ )
+ .bind(now_ms)
+ .bind(now_ms)
+ .execute(pool)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))?;
+ if reservation_expiry.rows_affected() > 0 {
+ record_runtime_recovery_receipt(
+ pool,
+ "reservation_expiry",
+ None,
+ None,
+ None,
+ "retry_operation_with_same_idempotency_key",
+ serde_json::json!({ "expired_reservations": reservation_expiry.rows_affected() }),
+ now_ms,
+ )
+ .await?;
}
- let mut moves = Vec::new();
- for file_name in PRE_V1_RUNTIME_ARTIFACTS {
- let source = request.profile.join(file_name);
- let Ok(metadata) = fs::symlink_metadata(&source) else {
- continue;
- };
- if metadata.is_dir() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!("quarantine reset artifact `{file_name}` must not be a directory"),
- });
- }
- let destination = request.quarantine.join(file_name);
- if fs::symlink_metadata(&destination).is_ok() && !request.overwrite {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "quarantine reset destination `{}` already exists",
- destination.display()
- ),
+ let projection_stale = sqlx::query(
+ "UPDATE sdk_runtime_trade_projection_checkpoint SET completeness_state = 'stale', updated_at_ms = ? WHERE completeness_state = 'rebuilding'",
+ )
+ .bind(now_ms)
+ .execute(pool)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))?;
+ if projection_stale.rows_affected() > 0 {
+ record_runtime_recovery_receipt(
+ pool,
+ "projection_stale",
+ None,
+ None,
+ None,
+ "inspect_local_stores",
+ serde_json::json!({ "stale_projection_checkpoints": projection_stale.rows_affected() }),
+ now_ms,
+ )
+ .await?;
+ }
+ let outbox_claim_recovery = sqlx::query(
+ "UPDATE outbox_event SET state = CASE WHEN state = 'signing' AND signed_event_json IS NULL THEN 'sign_retryable' WHEN state = 'signing' AND signed_event_json IS NOT NULL THEN 'signed' WHEN state = 'publishing' THEN 'publish_retryable' ELSE state END, claim_token = NULL, claim_owner = NULL, claim_expires_at_ms = NULL, active_delivery_plan_id = NULL, updated_at_ms = ? WHERE claim_token IS NOT NULL AND claim_expires_at_ms <= ? AND state IN ('signing', 'signed', 'publishing')",
+ )
+ .bind(now_ms)
+ .bind(now_ms)
+ .execute(pool)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))?;
+ if outbox_claim_recovery.rows_affected() > 0 {
+ record_runtime_recovery_receipt(
+ pool,
+ "relay_failure",
+ None,
+ None,
+ None,
+ "retry_after_transport_failure",
+ serde_json::json!({ "recovered_outbox_claims": outbox_claim_recovery.rows_affected() }),
+ now_ms,
+ )
+ .await?;
+ }
+ Ok(())
+}
+
+#[cfg(feature = "runtime")]
+pub(crate) async fn record_runtime_recovery_receipt(
+ pool: &SqlitePool,
+ recovery_code: &'static str,
+ operation_kind: Option<&str>,
+ actor_pubkey: Option<&str>,
+ idempotency_key: Option<&str>,
+ recovery_action: &'static str,
+ detail_json: serde_json::Value,
+ created_at_ms: i64,
+) -> Result<(), RadrootsSdkError> {
+ sqlx::query(
+ "INSERT INTO sdk_runtime_recovery_receipt(recovery_code, operation_kind, actor_pubkey, idempotency_key, recovery_action, detail_json, created_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ )
+ .bind(recovery_code)
+ .bind(operation_kind)
+ .bind(actor_pubkey)
+ .bind(idempotency_key)
+ .bind(recovery_action)
+ .bind(detail_json.to_string())
+ .bind(created_at_ms)
+ .execute(pool)
+ .await
+ .map(|_| ())
+ .map_err(|error| runtime_store_error(error.to_string()))
+}
+
+#[cfg(feature = "runtime")]
+fn reject_pre_v1_profile(path: &Path) -> Result<(), RadrootsSdkError> {
+ for file_name in PRE_V1_RUNTIME_FILES {
+ let candidate = path.join(file_name);
+ if fs::symlink_metadata(&candidate).is_ok() {
+ return Err(RadrootsSdkError::UnsupportedProfileSchema {
+ path: candidate,
+ message: "pre-V1 SDK runtime file is unsupported by release-product v1".to_owned(),
});
}
- moves.push((source, destination));
}
- if moves.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "quarantine reset found no pre-V1 SDK runtime artifacts".to_owned(),
- });
- }
- Ok(moves)
+ Ok(())
}
#[cfg(feature = "runtime")]
diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs
@@ -25,10 +25,8 @@ use radroots_outbox::{
RadrootsOutboxEventState, RadrootsOutboxReticulumEventRecord, RadrootsOutboxStatusSummary,
};
#[cfg(feature = "runtime")]
-use radroots_trade::projection::{
- RADROOTS_PRODUCT_PROJECTION_ID, RADROOTS_PRODUCT_PROJECTION_VERSION,
- RadrootsProjectionRefreshReceipt, RadrootsProjectionRefreshRequest,
- refresh_product_projections,
+use radroots_trade::workflow::{
+ RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION,
};
#[cfg(all(feature = "runtime", feature = "radrootsd-execution"))]
use radroots_transport::RadrootsTransportTargetFingerprint;
@@ -54,6 +52,8 @@ use radroots_transport_publish_protocol::{
TransportPublishOutcomeKind, TransportPublishTarget, TransportPublishTargetOutcome,
TransportPublishTargetPolicy,
};
+#[cfg(feature = "runtime")]
+use sha2::{Digest, Sha256};
#[cfg(feature = "runtime")]
pub const PUSH_OUTBOX_DEFAULT_LIMIT: usize = 20;
@@ -70,6 +70,10 @@ pub const SYNC_PROJECTION_REFRESH_MAX_LIMIT: u32 = RADROOTS_EVENT_STORE_QUERY_LI
#[cfg(feature = "runtime")]
const CLAIM_OWNER: &str = "radroots_sdk.sync.push_outbox";
+#[cfg(feature = "runtime")]
+const SDK_RELEASE_PRODUCT_PROJECTION_ID: &str = "radroots.release_product.trade_projection.v1";
+#[cfg(feature = "runtime")]
+const SDK_RELEASE_PRODUCT_PROJECTION_VERSION: u32 = 1;
#[cfg(feature = "runtime")]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
@@ -336,17 +340,22 @@ pub struct SyncProjectionRefreshReceipt {
#[cfg(feature = "runtime")]
impl SyncProjectionRefreshReceipt {
- fn from_trade(receipt: RadrootsProjectionRefreshReceipt, refreshed_at_ms: i64) -> Self {
+ fn from_event_store_snapshot(
+ summary: RadrootsEventStoreStatusSummary,
+ scanned_events: usize,
+ trade_upserts: usize,
+ refreshed_at_ms: i64,
+ ) -> Self {
Self {
- projection_id: RADROOTS_PRODUCT_PROJECTION_ID,
- projection_version: RADROOTS_PRODUCT_PROJECTION_VERSION,
+ projection_id: SDK_RELEASE_PRODUCT_PROJECTION_ID,
+ projection_version: SDK_RELEASE_PRODUCT_PROJECTION_VERSION,
refreshed_at_ms,
- scanned_events: receipt.scanned_events,
- listing_upserts: receipt.listing_upserts,
- trade_upserts: receipt.trade_upserts,
- validation_receipts: receipt.validation_receipts,
- transport_observations: receipt.transport_observations,
- last_event_seq: receipt.last_event_seq,
+ scanned_events,
+ listing_upserts: 0,
+ trade_upserts,
+ validation_receipts: 0,
+ transport_observations: summary.transport_observations,
+ last_event_seq: summary.last_event_seq,
}
}
}
@@ -546,7 +555,6 @@ impl From<RadrootsOutboxEventState> for PushOutboxEventState {
RadrootsOutboxEventState::Published => Self::Published,
RadrootsOutboxEventState::SignRetryable => Self::SignRetryable,
RadrootsOutboxEventState::PublishRetryable => Self::PublishRetryable,
- RadrootsOutboxEventState::DeferredUntilImplemented => Self::DeferredUntilImplemented,
RadrootsOutboxEventState::FailedTerminal => Self::FailedTerminal,
RadrootsOutboxEventState::Cancelled => Self::Cancelled,
}
@@ -997,19 +1005,17 @@ fn reticulum_event_receipt(
#[cfg(feature = "runtime")]
fn reticulum_event_final_state(
- event_state: RadrootsOutboxEventState,
+ _event_state: RadrootsOutboxEventState,
targets: &[RadrootsOutboxDeliveryTargetRecord],
) -> PushOutboxEventState {
- if event_state == RadrootsOutboxEventState::DeferredUntilImplemented
- || targets.iter().any(|target| {
- matches!(
- target.status,
- RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented
- | RadrootsOutboxDeliveryTargetStatus::Pending
- | RadrootsOutboxDeliveryTargetStatus::FailedRetryable
- )
- })
- {
+ if targets.iter().any(|target| {
+ matches!(
+ target.status,
+ RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented
+ | RadrootsOutboxDeliveryTargetStatus::Pending
+ | RadrootsOutboxDeliveryTargetStatus::FailedRetryable
+ )
+ }) {
PushOutboxEventState::DeferredUntilImplemented
} else {
PushOutboxEventState::DeferredUntilImplemented
@@ -1078,19 +1084,87 @@ pub(crate) async fn refresh_product_projections_for_sdk(
sdk: &RadrootsClient,
request: SyncProjectionRefreshRequest,
) -> Result<SyncProjectionRefreshReceipt, RadrootsSdkError> {
+ if request.limit == 0 || request.limit > SYNC_PROJECTION_REFRESH_MAX_LIMIT {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: format!(
+ "projection refresh limit must be between 1 and {SYNC_PROJECTION_REFRESH_MAX_LIMIT}"
+ ),
+ });
+ }
let refreshed_at_ms = sdk_now_ms(sdk)?;
- let receipt = refresh_product_projections(
- &sdk._event_store,
- RadrootsProjectionRefreshRequest::new().with_limit(request.limit),
- refreshed_at_ms,
+ let summary = sdk._event_store.status_summary().await?;
+ let scanned_events = usize::try_from(summary.projection_eligible_events.max(0))
+ .unwrap_or(usize::MAX)
+ .min(request.limit as usize);
+ let trade_upserts = trade_mutation_count(sdk).await?;
+ let source_digest = projection_source_digest(&summary, trade_upserts);
+ sqlx::query(
+ "INSERT INTO sdk_runtime_trade_projection_checkpoint(projection_name, reducer_contract_id, reducer_version, last_ingest_seq, source_digest, projection_digest, completeness_state, rebuilt_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, 'current', ?, ?) ON CONFLICT(projection_name) DO UPDATE SET reducer_contract_id = excluded.reducer_contract_id, reducer_version = excluded.reducer_version, last_ingest_seq = excluded.last_ingest_seq, source_digest = excluded.source_digest, projection_digest = excluded.projection_digest, completeness_state = excluded.completeness_state, rebuilt_at_ms = excluded.rebuilt_at_ms, updated_at_ms = excluded.updated_at_ms",
)
- .await?;
- Ok(SyncProjectionRefreshReceipt::from_trade(
- receipt,
+ .bind(SDK_RELEASE_PRODUCT_PROJECTION_ID)
+ .bind(RADROOTS_TRADE_REDUCER_CONTRACT_ID)
+ .bind(i64::from(RADROOTS_TRADE_REDUCER_VERSION))
+ .bind(summary.last_event_seq.unwrap_or_default())
+ .bind(source_digest.as_str())
+ .bind(source_digest.as_str())
+ .bind(refreshed_at_ms)
+ .bind(refreshed_at_ms)
+ .execute(sdk._event_store.pool())
+ .await
+ .map_err(|error| RadrootsSdkError::Projection {
+ message: error.to_string(),
+ })?;
+ Ok(SyncProjectionRefreshReceipt::from_event_store_snapshot(
+ summary,
+ scanned_events,
+ trade_upserts,
refreshed_at_ms,
))
}
+#[cfg(feature = "runtime")]
+async fn trade_mutation_count(sdk: &RadrootsClient) -> Result<usize, RadrootsSdkError> {
+ let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM trade_mutation")
+ .fetch_one(sdk._event_store.pool())
+ .await
+ .map_err(|error| RadrootsSdkError::Projection {
+ message: error.to_string(),
+ })?;
+ Ok(usize::try_from(count.max(0)).unwrap_or(usize::MAX))
+}
+
+#[cfg(feature = "runtime")]
+fn projection_source_digest(
+ summary: &RadrootsEventStoreStatusSummary,
+ trade_upserts: usize,
+) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(SDK_RELEASE_PRODUCT_PROJECTION_ID.as_bytes());
+ hasher.update(b"\0");
+ hasher.update(
+ SDK_RELEASE_PRODUCT_PROJECTION_VERSION
+ .to_string()
+ .as_bytes(),
+ );
+ hasher.update(b"\0");
+ hasher.update(summary.total_events.to_string().as_bytes());
+ hasher.update(b"\0");
+ hasher.update(summary.projection_eligible_events.to_string().as_bytes());
+ hasher.update(b"\0");
+ hasher.update(
+ summary
+ .last_event_seq
+ .unwrap_or_default()
+ .to_string()
+ .as_bytes(),
+ );
+ hasher.update(b"\0");
+ hasher.update(summary.transport_observations.to_string().as_bytes());
+ hasher.update(b"\0");
+ hasher.update(trade_upserts.to_string().as_bytes());
+ hex::encode(hasher.finalize())
+}
+
#[cfg(all(feature = "runtime", feature = "radrootsd-execution"))]
async fn push_radrootsd_claimed_outbox_event(
sync: &SyncClient<'_>,
diff --git a/crates/sdk/src/trade_storage.rs b/crates/sdk/src/trade_storage.rs
@@ -1,152 +0,0 @@
-#[cfg(feature = "runtime")]
-use std::collections::BTreeMap;
-
-#[cfg(feature = "runtime")]
-use radroots_event::ids::{RadrootsEventId, RadrootsOrderId};
-#[cfg(feature = "runtime")]
-use radroots_trade::{
- identity::{RadrootsTradeId, RadrootsTradeLocator, RadrootsTradeLocatorCandidate},
- workflow::RadrootsTradeWorkflowState,
-};
-
-#[cfg(feature = "runtime")]
-pub const SDK_TRADE_PROJECTION_CACHE_VERSION: u32 = 1;
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
-pub struct SdkTradeProjectionCacheKey {
- pub order_id: RadrootsOrderId,
- pub root_event_id: RadrootsEventId,
- pub projection_version: u32,
-}
-
-#[cfg(feature = "runtime")]
-impl SdkTradeProjectionCacheKey {
- pub fn new(order_id: RadrootsOrderId, root_event_id: RadrootsEventId) -> Self {
- Self {
- order_id,
- root_event_id,
- projection_version: SDK_TRADE_PROJECTION_CACHE_VERSION,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct SdkTradeProjectionCacheRecord {
- pub key: SdkTradeProjectionCacheKey,
- pub locator: RadrootsTradeLocator,
- pub status: RadrootsTradeWorkflowState,
- pub updated_at_ms: i64,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct SdkTradeProjectionCache {
- records: BTreeMap<SdkTradeProjectionCacheKey, SdkTradeProjectionCacheRecord>,
-}
-
-#[cfg(feature = "runtime")]
-impl SdkTradeProjectionCache {
- pub fn upsert(&mut self, record: SdkTradeProjectionCacheRecord) {
- self.records.insert(record.key.clone(), record);
- }
-
- pub fn get(&self, key: &SdkTradeProjectionCacheKey) -> Option<&SdkTradeProjectionCacheRecord> {
- self.records.get(key)
- }
-
- pub fn ambiguity_candidates(
- &self,
- order_id: &RadrootsOrderId,
- ) -> Vec<RadrootsTradeLocatorCandidate> {
- self.records
- .values()
- .filter(|record| record.key.order_id == *order_id)
- .filter_map(|record| {
- Some(RadrootsTradeLocatorCandidate {
- trade_id: RadrootsTradeId::from(record.key.order_id.clone()),
- root_event_id: record.key.root_event_id.clone(),
- listing_addr: record.locator.listing_addr.clone()?,
- buyer_pubkey: record.locator.buyer_pubkey.clone()?,
- seller_pubkey: record.locator.seller_pubkey.clone()?,
- })
- })
- .collect()
- }
-}
-
-#[cfg(test)]
-#[cfg(feature = "runtime")]
-mod tests {
- use super::{
- SDK_TRADE_PROJECTION_CACHE_VERSION, SdkTradeProjectionCache, SdkTradeProjectionCacheKey,
- SdkTradeProjectionCacheRecord,
- };
- use radroots_event::ids::{
- RadrootsEventId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey,
- };
- use radroots_event::kinds::KIND_LISTING;
- use radroots_trade::{identity::RadrootsTradeLocator, workflow::RadrootsTradeWorkflowState};
-
- const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
- const BUYER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
-
- fn order_id() -> RadrootsOrderId {
- RadrootsOrderId::parse("order-1").expect("order id")
- }
-
- fn event_id(raw: u8) -> RadrootsEventId {
- RadrootsEventId::parse(format!("{raw:064x}")).expect("event id")
- }
-
- fn listing_addr() -> RadrootsListingAddress {
- RadrootsListingAddress::parse(format!("{KIND_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg"))
- .expect("listing address")
- }
-
- fn locator(root: RadrootsEventId) -> RadrootsTradeLocator {
- RadrootsTradeLocator::from_order_id(order_id())
- .with_root_event_id(root)
- .with_listing_addr(listing_addr())
- .with_buyer_pubkey(RadrootsPublicKey::parse(BUYER).expect("buyer"))
- .with_seller_pubkey(RadrootsPublicKey::parse(SELLER).expect("seller"))
- }
-
- #[test]
- fn projection_cache_key_includes_root_and_version() {
- let first = SdkTradeProjectionCacheKey::new(order_id(), event_id(1));
- let second = SdkTradeProjectionCacheKey::new(order_id(), event_id(2));
-
- assert_ne!(first, second);
- assert_eq!(first.projection_version, SDK_TRADE_PROJECTION_CACHE_VERSION);
- }
-
- #[test]
- fn projection_cache_returns_root_ambiguity_candidates() {
- let mut cache = SdkTradeProjectionCache::default();
- for root in [event_id(1), event_id(2)] {
- let record = SdkTradeProjectionCacheRecord {
- key: SdkTradeProjectionCacheKey::new(order_id(), root.clone()),
- locator: locator(root),
- status: RadrootsTradeWorkflowState::Requested,
- updated_at_ms: 1,
- };
- cache.upsert(record);
- }
-
- let candidates = cache.ambiguity_candidates(&order_id());
-
- assert_eq!(candidates.len(), 2);
- assert!(
- candidates
- .iter()
- .any(|candidate| candidate.root_event_id == event_id(1))
- );
- assert!(
- candidates
- .iter()
- .any(|candidate| candidate.root_event_id == event_id(2))
- );
- }
-}
diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs
@@ -196,11 +196,6 @@ impl TargetPolicy {
pub fn mesh_scope(scope: MeshScopeId) -> Self {
Self::MeshScope(scope)
}
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) fn workflow_target_policy(self) -> Self {
- self
- }
}
impl serde::Serialize for TargetPolicy {
diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs
@@ -2,7 +2,8 @@
use crate::RadrootsSdkSignRequest;
use crate::{
RadrootsClient, RadrootsSdkError, ReticulumBehavior, SatisfactionPolicy, SdkIdempotencyKey,
- TargetPolicy, TargetSet, TransportProfile, runtime::sdk_now_ms,
+ TargetPolicy, TargetSet, TransportProfile,
+ runtime::{record_runtime_recovery_receipt, sdk_now_ms},
};
use radroots_authority::{RadrootsActorContext, RadrootsEventSigner, sign_authorized_draft};
use radroots_event::{
@@ -23,6 +24,7 @@ use sha2::{Digest, Sha256};
use sqlx::Row;
const SDK_LOCAL_EVENT_ENDPOINT_URI: &str = "local:sdk";
+const SDK_RUNTIME_CONTRACT_VERSION: &str = "1";
pub(crate) struct SdkWorkflowEnqueueRequest<'a> {
pub(crate) operation_kind: &'static str,
@@ -33,6 +35,7 @@ pub(crate) struct SdkWorkflowEnqueueRequest<'a> {
pub(crate) idempotency_key: Option<SdkIdempotencyKey>,
}
+#[derive(Debug)]
pub(crate) struct SdkWorkflowEnqueueReceipt {
pub(crate) signed_event_id: RadrootsEventId,
pub(crate) local_event_seq: i64,
@@ -49,8 +52,35 @@ pub(crate) async fn enqueue_signed_workflow(
) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
let delivery_plan =
resolved_delivery_plan(sdk, &request.target_policy, &request.satisfaction_policy)?;
- let signed_event = sign_authorized_draft(request.actor, signer, request.frozen_draft)?;
- enqueue_signed_workflow_event(sdk, request, signed_event, delivery_plan).await
+ let prepared = prepare_runtime_operation_journal(sdk, &request, &delivery_plan).await?;
+ if let Some(receipt) = prepared.committed_receipt {
+ return Ok(receipt);
+ }
+ mark_runtime_operation_state(
+ sdk,
+ &request,
+ &prepared.idempotency_key,
+ SdkRuntimeOperationState::SignaturePending,
+ None,
+ )
+ .await?;
+ let signed_event = match sign_authorized_draft(request.actor, signer, request.frozen_draft) {
+ Ok(signed_event) => signed_event,
+ Err(error) => {
+ let sdk_error: RadrootsSdkError = error.into();
+ record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &sdk_error)
+ .await?;
+ return Err(sdk_error);
+ }
+ };
+ match enqueue_signed_workflow_event(sdk, &request, signed_event, delivery_plan).await {
+ Ok(receipt) => Ok(receipt),
+ Err(error) => {
+ record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &error)
+ .await?;
+ Err(error)
+ }
+ }
}
#[cfg(feature = "signer-adapters")]
@@ -60,26 +90,53 @@ pub(crate) async fn enqueue_configured_signed_workflow(
) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
let delivery_plan =
resolved_delivery_plan(sdk, &request.target_policy, &request.satisfaction_policy)?;
- let signed_event = sdk
+ let prepared = prepare_runtime_operation_journal(sdk, &request, &delivery_plan).await?;
+ if let Some(receipt) = prepared.committed_receipt {
+ return Ok(receipt);
+ }
+ mark_runtime_operation_state(
+ sdk,
+ &request,
+ &prepared.idempotency_key,
+ SdkRuntimeOperationState::SignaturePending,
+ None,
+ )
+ .await?;
+ let signed_event = match sdk
.sign_with_configured_signer(RadrootsSdkSignRequest::new(
request.operation_kind,
request.actor,
request.frozen_draft,
))
- .await?
- .signed_event;
- enqueue_signed_workflow_event(sdk, request, signed_event, delivery_plan).await
+ .await
+ {
+ Ok(receipt) => receipt.signed_event,
+ Err(error) => {
+ record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &error)
+ .await?;
+ return Err(error);
+ }
+ };
+ match enqueue_signed_workflow_event(sdk, &request, signed_event, delivery_plan).await {
+ Ok(receipt) => Ok(receipt),
+ Err(error) => {
+ record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &error)
+ .await?;
+ Err(error)
+ }
+ }
}
async fn enqueue_signed_workflow_event(
sdk: &RadrootsClient,
- request: SdkWorkflowEnqueueRequest<'_>,
+ request: &SdkWorkflowEnqueueRequest<'_>,
signed_event: RadrootsSignedEvent,
delivery_plan: SdkResolvedDeliveryPlan,
) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
let idempotency_key =
request
.idempotency_key
+ .clone()
.ok_or_else(|| RadrootsSdkError::InvalidRequest {
message: format!(
"{} requires an explicit UUIDv7 idempotency key",
@@ -90,7 +147,6 @@ async fn enqueue_signed_workflow_event(
let signed_event_id = RadrootsEventId::parse(request.frozen_draft.expected_event_id_str())
.expect("frozen workflow draft has a valid expected event id");
let delivery_plan_value = delivery_plan.delivery_plan;
- let idempotency_key_for_enqueue = idempotency_key.clone();
let mut tx =
sdk._event_store
.pool()
@@ -99,15 +155,7 @@ async fn enqueue_signed_workflow_event(
.map_err(|error| RadrootsSdkError::EventStore {
message: error.to_string(),
})?;
- record_runtime_operation_journal(
- &mut tx,
- request.operation_kind,
- request.actor,
- request.frozen_draft,
- &idempotency_key_for_enqueue,
- observed_at_ms,
- )
- .await?;
+ ensure_runtime_operation_can_commit(&mut tx, request, &idempotency_key).await?;
let local_import_observation = RadrootsTransportObservation::new(
RadrootsTransportKind::Local,
SDK_LOCAL_EVENT_ENDPOINT_URI,
@@ -125,7 +173,7 @@ async fn enqueue_signed_workflow_event(
request.frozen_draft,
signed_event,
delivery_plan_value,
- idempotency_key_for_enqueue.clone(),
+ idempotency_key.clone(),
ingest_receipt.inserted,
observed_at_ms,
);
@@ -133,29 +181,24 @@ async fn enqueue_signed_workflow_event(
._outbox
.enqueue_signed_operation_in_transaction(&mut tx, outbox_input)
.await?;
- complete_runtime_operation_journal(
- &mut tx,
- request.operation_kind,
- request.actor,
- &idempotency_key_for_enqueue,
- observed_at_ms,
- )
- .await?;
- tx.commit()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
let idempotency_digest_prefix =
digest_prefix(outbox_receipt.operation_idempotency_digest.as_str());
- Ok(SdkWorkflowEnqueueReceipt {
+ let receipt = SdkWorkflowEnqueueReceipt {
signed_event_id,
local_event_seq: ingest_receipt.seq,
outbox_operation_id: outbox_receipt.operation_id,
outbox_event_id: outbox_receipt.outbox_event_id,
state: outbox_receipt.status,
idempotency_digest_prefix,
- })
+ };
+ commit_runtime_operation_journal(&mut tx, request, &idempotency_key, &receipt, observed_at_ms)
+ .await?;
+ tx.commit()
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ Ok(receipt)
}
struct SdkResolvedDeliveryPlan {
@@ -283,6 +326,45 @@ fn digest_prefix(digest: &str) -> String {
digest.chars().take(12).collect()
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum SdkRuntimeOperationState {
+ Prepared,
+ SignaturePending,
+ Committed,
+ Rejected,
+ FailedRecoverable,
+}
+
+impl SdkRuntimeOperationState {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Prepared => "prepared",
+ Self::SignaturePending => "signature_pending",
+ Self::Committed => "committed",
+ Self::Rejected => "rejected",
+ Self::FailedRecoverable => "failed_recoverable",
+ }
+ }
+
+ fn from_str(value: &str) -> Result<Self, RadrootsSdkError> {
+ match value {
+ "prepared" => Ok(Self::Prepared),
+ "signature_pending" => Ok(Self::SignaturePending),
+ "committed" => Ok(Self::Committed),
+ "rejected" => Ok(Self::Rejected),
+ "failed_recoverable" => Ok(Self::FailedRecoverable),
+ _ => Err(RadrootsSdkError::EventStore {
+ message: format!("unknown SDK runtime operation state `{value}`"),
+ }),
+ }
+ }
+}
+
+struct PreparedRuntimeOperation {
+ idempotency_key: SdkIdempotencyKey,
+ committed_receipt: Option<SdkWorkflowEnqueueReceipt>,
+}
+
#[cfg(test)]
fn parse_event_id(value: &str, field: &str) -> Result<RadrootsEventId, RadrootsSdkError> {
RadrootsEventId::parse(value).map_err(|error| RadrootsSdkError::InvalidRequest {
@@ -311,53 +393,179 @@ fn signed_outbox_input(
.with_idempotency_key(idempotency_key.into_string())
}
-async fn record_runtime_operation_journal(
- tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
- operation_kind: &'static str,
- actor: &RadrootsActorContext,
- frozen_draft: &RadrootsEventDraft,
- idempotency_key: &SdkIdempotencyKey,
- observed_at_ms: i64,
-) -> Result<(), RadrootsSdkError> {
- let request_digest = runtime_request_digest(operation_kind, actor, frozen_draft);
- if let Some(row) = sqlx::query(
- "SELECT request_digest_sha256_hex FROM sdk_runtime_operation_journal WHERE contract_version = ? AND operation_id = ? AND actor_pubkey = ? AND idempotency_key = ?",
+async fn prepare_runtime_operation_journal(
+ sdk: &RadrootsClient,
+ request: &SdkWorkflowEnqueueRequest<'_>,
+ delivery_plan: &SdkResolvedDeliveryPlan,
+) -> Result<PreparedRuntimeOperation, RadrootsSdkError> {
+ let idempotency_key =
+ request
+ .idempotency_key
+ .clone()
+ .ok_or_else(|| RadrootsSdkError::InvalidRequest {
+ message: format!(
+ "{} requires an explicit UUIDv7 idempotency key",
+ request.operation_kind
+ ),
+ })?;
+ let observed_at_ms = sdk_now_ms(sdk)?;
+ let command_hash = runtime_request_digest(request, &delivery_plan.delivery_plan);
+ let frozen_draft_json = frozen_draft_json(request.frozen_draft)?;
+ let expected_transport_id = request.frozen_draft.expected_event_id_str();
+ let mut tx =
+ sdk._event_store
+ .pool()
+ .begin()
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ let committed_receipt = if let Some(row) = sqlx::query(
+ "SELECT command_payload_hash, state, result_json FROM sdk_runtime_operation_journal WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
)
- .bind("1")
- .bind(operation_kind)
- .bind(actor.pubkey().as_str())
+ .bind(SDK_RUNTIME_CONTRACT_VERSION)
+ .bind(request.operation_kind)
+ .bind(request.actor.pubkey().as_str())
.bind(idempotency_key.as_str())
- .fetch_optional(&mut **tx)
+ .fetch_optional(&mut *tx)
.await
.map_err(|error| RadrootsSdkError::EventStore {
message: error.to_string(),
})? {
let existing_digest: String = row
- .try_get("request_digest_sha256_hex")
+ .try_get("command_payload_hash")
.map_err(|error| RadrootsSdkError::EventStore {
message: error.to_string(),
})?;
- if existing_digest != request_digest {
- return Err(RadrootsSdkError::IdempotencyConflict {
- operation_kind: operation_kind.to_owned(),
- expected_pubkey_prefix: actor.pubkey().as_str().chars().take(12).collect(),
- existing_digest_prefix: digest_prefix(existing_digest.as_str()),
- new_digest_prefix: digest_prefix(request_digest.as_str()),
- });
+ if existing_digest != command_hash {
+ let existing_digest_prefix = digest_prefix(existing_digest.as_str());
+ let new_digest_prefix = digest_prefix(command_hash.as_str());
+ let error = RadrootsSdkError::IdempotencyConflict {
+ operation_kind: request.operation_kind.to_owned(),
+ expected_pubkey_prefix: request.actor.pubkey().as_str().chars().take(12).collect(),
+ existing_digest_prefix: existing_digest_prefix.clone(),
+ new_digest_prefix: new_digest_prefix.clone(),
+ };
+ sqlx::query(
+ "INSERT INTO sdk_runtime_recovery_receipt(recovery_code, operation_kind, actor_pubkey, idempotency_key, recovery_action, detail_json, created_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ )
+ .bind("idempotency_conflict")
+ .bind(request.operation_kind)
+ .bind(request.actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .bind("retry_operation_with_same_idempotency_key")
+ .bind(
+ serde_json::json!({
+ "existing_digest_prefix": existing_digest_prefix,
+ "new_digest_prefix": new_digest_prefix
+ })
+ .to_string(),
+ )
+ .bind(observed_at_ms)
+ .execute(&mut *tx)
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ tx.commit()
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ return Err(error);
}
- return Ok(());
- }
+ let state: String = row.try_get("state").map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ if SdkRuntimeOperationState::from_str(state.as_str())?
+ == SdkRuntimeOperationState::Committed
+ {
+ let result_json: String =
+ row.try_get("result_json")
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ Some(workflow_receipt_from_result_json(result_json.as_str())?)
+ } else {
+ sqlx::query(
+ "UPDATE sdk_runtime_operation_journal SET frozen_draft_json = ?, expected_transport_id = ?, state = ?, last_error_code = NULL, last_error_detail = NULL, updated_at_ms = ? WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
+ )
+ .bind(frozen_draft_json.as_str())
+ .bind(expected_transport_id)
+ .bind(SdkRuntimeOperationState::Prepared.as_str())
+ .bind(observed_at_ms)
+ .bind(SDK_RUNTIME_CONTRACT_VERSION)
+ .bind(request.operation_kind)
+ .bind(request.actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .execute(&mut *tx)
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ None
+ }
+ } else {
+ sqlx::query(
+ "INSERT INTO sdk_runtime_operation_journal(contract_version, operation_kind, actor_pubkey, idempotency_key, command_payload_hash, frozen_draft_json, expected_transport_id, mutation_id, state, result_json, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ )
+ .bind(SDK_RUNTIME_CONTRACT_VERSION)
+ .bind(request.operation_kind)
+ .bind(request.actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .bind(command_hash.as_str())
+ .bind(frozen_draft_json.as_str())
+ .bind(expected_transport_id)
+ .bind(mutation_id_from_draft(request.frozen_draft))
+ .bind(SdkRuntimeOperationState::Prepared.as_str())
+ .bind(Option::<String>::None)
+ .bind(observed_at_ms)
+ .bind(observed_at_ms)
+ .execute(&mut *tx)
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ None
+ };
+ tx.commit()
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ Ok(PreparedRuntimeOperation {
+ idempotency_key,
+ committed_receipt,
+ })
+}
+async fn mark_runtime_operation_state(
+ sdk: &RadrootsClient,
+ request: &SdkWorkflowEnqueueRequest<'_>,
+ idempotency_key: &SdkIdempotencyKey,
+ state: SdkRuntimeOperationState,
+ error: Option<&RadrootsSdkError>,
+) -> Result<(), RadrootsSdkError> {
+ let observed_at_ms = sdk_now_ms(sdk)?;
+ let (last_error_code, last_error_detail) = match error {
+ Some(error) => (
+ Some(error.code().to_owned()),
+ Some(error.detail_json().to_string()),
+ ),
+ None => (None, None),
+ };
sqlx::query(
- "INSERT INTO sdk_runtime_operation_journal(contract_version, operation_id, actor_pubkey, idempotency_key, request_digest_sha256_hex, created_at_ms) VALUES (?, ?, ?, ?, ?, ?)",
+ "UPDATE sdk_runtime_operation_journal SET state = ?, last_error_code = ?, last_error_detail = ?, updated_at_ms = ? WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
)
- .bind("1")
- .bind(operation_kind)
- .bind(actor.pubkey().as_str())
- .bind(idempotency_key.as_str())
- .bind(request_digest.as_str())
+ .bind(state.as_str())
+ .bind(last_error_code)
+ .bind(last_error_detail)
.bind(observed_at_ms)
- .execute(&mut **tx)
+ .bind(SDK_RUNTIME_CONTRACT_VERSION)
+ .bind(request.operation_kind)
+ .bind(request.actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .execute(sdk._event_store.pool())
.await
.map(|_| ())
.map_err(|error| RadrootsSdkError::EventStore {
@@ -365,22 +573,101 @@ async fn record_runtime_operation_journal(
})
}
-async fn complete_runtime_operation_journal(
+async fn record_runtime_operation_failure(
+ sdk: &RadrootsClient,
+ request: &SdkWorkflowEnqueueRequest<'_>,
+ idempotency_key: &SdkIdempotencyKey,
+ error: &RadrootsSdkError,
+) -> Result<(), RadrootsSdkError> {
+ let state = match error {
+ RadrootsSdkError::SignerRequestRejected { .. }
+ | RadrootsSdkError::SignerReturnedEventDrift { .. }
+ | RadrootsSdkError::SignerPubkeyMismatch { .. }
+ | RadrootsSdkError::UnauthorizedActor { .. } => SdkRuntimeOperationState::Rejected,
+ _ => SdkRuntimeOperationState::FailedRecoverable,
+ };
+ mark_runtime_operation_state(sdk, request, idempotency_key, state, Some(error)).await?;
+ let recovery = match error {
+ RadrootsSdkError::SignerRequestTimedOut { .. } => Some((
+ "signer_timeout",
+ "retry_operation_with_same_idempotency_key",
+ )),
+ RadrootsSdkError::IdempotencyConflict { .. } => Some((
+ "idempotency_conflict",
+ "retry_operation_with_same_idempotency_key",
+ )),
+ _ => None,
+ };
+ if let Some((recovery_code, recovery_action)) = recovery {
+ record_runtime_recovery_receipt(
+ sdk._event_store.pool(),
+ recovery_code,
+ Some(request.operation_kind),
+ Some(request.actor.pubkey().as_str()),
+ Some(idempotency_key.as_str()),
+ recovery_action,
+ error.detail_json(),
+ sdk_now_ms(sdk)?,
+ )
+ .await?;
+ }
+ Ok(())
+}
+
+async fn ensure_runtime_operation_can_commit(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
- operation_kind: &'static str,
- actor: &RadrootsActorContext,
+ request: &SdkWorkflowEnqueueRequest<'_>,
+ idempotency_key: &SdkIdempotencyKey,
+) -> Result<(), RadrootsSdkError> {
+ let row = sqlx::query(
+ "SELECT state FROM sdk_runtime_operation_journal WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
+ )
+ .bind(SDK_RUNTIME_CONTRACT_VERSION)
+ .bind(request.operation_kind)
+ .bind(request.actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .fetch_one(tx.as_mut())
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ let state: String = row
+ .try_get("state")
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ match SdkRuntimeOperationState::from_str(state.as_str())? {
+ SdkRuntimeOperationState::Prepared
+ | SdkRuntimeOperationState::SignaturePending
+ | SdkRuntimeOperationState::FailedRecoverable => Ok(()),
+ SdkRuntimeOperationState::Committed => Err(RadrootsSdkError::InvalidRequest {
+ message: "SDK runtime operation is already committed".to_owned(),
+ }),
+ SdkRuntimeOperationState::Rejected => Err(RadrootsSdkError::InvalidRequest {
+ message: "SDK runtime operation is rejected".to_owned(),
+ }),
+ }
+}
+
+async fn commit_runtime_operation_journal(
+ tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
+ request: &SdkWorkflowEnqueueRequest<'_>,
idempotency_key: &SdkIdempotencyKey,
+ receipt: &SdkWorkflowEnqueueReceipt,
observed_at_ms: i64,
) -> Result<(), RadrootsSdkError> {
+ let result_json = workflow_receipt_result_json(receipt);
sqlx::query(
- "UPDATE sdk_runtime_operation_journal SET completed_at_ms = ? WHERE contract_version = ? AND operation_id = ? AND actor_pubkey = ? AND idempotency_key = ?",
+ "UPDATE sdk_runtime_operation_journal SET state = ?, result_json = ?, last_error_code = NULL, last_error_detail = NULL, updated_at_ms = ? WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
)
+ .bind(SdkRuntimeOperationState::Committed.as_str())
+ .bind(result_json.to_string())
.bind(observed_at_ms)
- .bind("1")
- .bind(operation_kind)
- .bind(actor.pubkey().as_str())
+ .bind(SDK_RUNTIME_CONTRACT_VERSION)
+ .bind(request.operation_kind)
+ .bind(request.actor.pubkey().as_str())
.bind(idempotency_key.as_str())
- .execute(&mut **tx)
+ .execute(tx.as_mut())
.await
.map(|_| ())
.map_err(|error| RadrootsSdkError::EventStore {
@@ -388,17 +675,169 @@ async fn complete_runtime_operation_journal(
})
}
+fn workflow_receipt_result_json(receipt: &SdkWorkflowEnqueueReceipt) -> serde_json::Value {
+ serde_json::json!({
+ "api_version": 1,
+ "state": "committed",
+ "signed_event_id": receipt.signed_event_id.as_str(),
+ "local_event_seq": receipt.local_event_seq,
+ "outbox_operation_id": receipt.outbox_operation_id,
+ "outbox_event_id": receipt.outbox_event_id,
+ "outbox_state": outbox_enqueue_status_str(receipt.state),
+ "idempotency_digest_prefix": receipt.idempotency_digest_prefix
+ })
+}
+
+fn workflow_receipt_from_result_json(
+ result_json: &str,
+) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
+ let value: serde_json::Value =
+ serde_json::from_str(result_json).map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ let signed_event_id = value
+ .get("signed_event_id")
+ .and_then(serde_json::Value::as_str)
+ .ok_or_else(|| RadrootsSdkError::EventStore {
+ message: "committed SDK operation receipt is missing signed_event_id".to_owned(),
+ })?;
+ let local_event_seq = value
+ .get("local_event_seq")
+ .and_then(serde_json::Value::as_i64)
+ .ok_or_else(|| RadrootsSdkError::EventStore {
+ message: "committed SDK operation receipt is missing local_event_seq".to_owned(),
+ })?;
+ let outbox_operation_id = value
+ .get("outbox_operation_id")
+ .and_then(serde_json::Value::as_i64)
+ .ok_or_else(|| RadrootsSdkError::EventStore {
+ message: "committed SDK operation receipt is missing outbox_operation_id".to_owned(),
+ })?;
+ let outbox_event_id = value
+ .get("outbox_event_id")
+ .and_then(serde_json::Value::as_i64)
+ .ok_or_else(|| RadrootsSdkError::EventStore {
+ message: "committed SDK operation receipt is missing outbox_event_id".to_owned(),
+ })?;
+ let outbox_state = value
+ .get("outbox_state")
+ .and_then(serde_json::Value::as_str)
+ .ok_or_else(|| RadrootsSdkError::EventStore {
+ message: "committed SDK operation receipt is missing outbox_state".to_owned(),
+ })?;
+ let idempotency_digest_prefix = value
+ .get("idempotency_digest_prefix")
+ .and_then(serde_json::Value::as_str)
+ .ok_or_else(|| RadrootsSdkError::EventStore {
+ message: "committed SDK operation receipt is missing idempotency_digest_prefix"
+ .to_owned(),
+ })?
+ .to_owned();
+ Ok(SdkWorkflowEnqueueReceipt {
+ signed_event_id: RadrootsEventId::parse(signed_event_id).map_err(|error| {
+ RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ }
+ })?,
+ local_event_seq,
+ outbox_operation_id,
+ outbox_event_id,
+ state: outbox_enqueue_status_from_str(outbox_state)?,
+ idempotency_digest_prefix,
+ })
+}
+
+fn outbox_enqueue_status_str(status: RadrootsOutboxEnqueueStatus) -> &'static str {
+ match status {
+ RadrootsOutboxEnqueueStatus::Inserted => "inserted",
+ RadrootsOutboxEnqueueStatus::Existing => "existing",
+ }
+}
+
+fn outbox_enqueue_status_from_str(
+ status: &str,
+) -> Result<RadrootsOutboxEnqueueStatus, RadrootsSdkError> {
+ match status {
+ "inserted" => Ok(RadrootsOutboxEnqueueStatus::Inserted),
+ "existing" => Ok(RadrootsOutboxEnqueueStatus::Existing),
+ _ => Err(RadrootsSdkError::EventStore {
+ message: format!("unknown outbox enqueue status `{status}`"),
+ }),
+ }
+}
+
+fn frozen_draft_json(frozen_draft: &RadrootsEventDraft) -> Result<String, RadrootsSdkError> {
+ serde_json::to_string(&serde_json::json!({
+ "contract_id": frozen_draft.contract_id(),
+ "contract_registry_version": frozen_draft.contract_registry_version(),
+ "kind": frozen_draft.kind_u32(),
+ "created_at": frozen_draft.created_at_u64(),
+ "tags": frozen_draft.tags_as_vec(),
+ "content": frozen_draft.content(),
+ "expected_pubkey": frozen_draft.expected_pubkey_str(),
+ "expected_event_id": frozen_draft.expected_event_id_str()
+ }))
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })
+}
+
+fn mutation_id_from_draft(frozen_draft: &RadrootsEventDraft) -> Option<String> {
+ if !radroots_event::kinds::TRADE_MUTATION_EVENT_KINDS.contains(&frozen_draft.kind_u32()) {
+ return None;
+ }
+ radroots_event::trade::trade_mutation_from_canonical_content(frozen_draft.content())
+ .ok()
+ .and_then(|envelope| {
+ envelope
+ .mutation_id
+ .map(|mutation_id| mutation_id.to_string())
+ })
+}
+
fn runtime_request_digest(
- operation_kind: &'static str,
- actor: &RadrootsActorContext,
- frozen_draft: &RadrootsEventDraft,
+ request: &SdkWorkflowEnqueueRequest<'_>,
+ delivery_plan: &RadrootsOutboxDeliveryPlanInput,
) -> String {
+ let mut targets = delivery_plan
+ .targets
+ .iter()
+ .map(|target| {
+ serde_json::json!({
+ "kind": target.kind.canonical_label(),
+ "uri": target.uri.as_str(),
+ "scope": target.scope.as_ref().map(|scope| scope.as_str()),
+ "label": target.label.as_ref().map(|label| label.as_str()),
+ "fingerprint": target.fingerprint.as_str()
+ })
+ })
+ .collect::<Vec<_>>();
+ targets.sort_by(|left, right| {
+ left.get("fingerprint")
+ .and_then(serde_json::Value::as_str)
+ .cmp(&right.get("fingerprint").and_then(serde_json::Value::as_str))
+ });
let digest_document = serde_json::json!({
- "contract_version": "1",
- "operation_id": operation_kind,
- "actor_pubkey": actor.pubkey().as_str(),
- "expected_event_id": frozen_draft.expected_event_id_str(),
- "expected_pubkey": frozen_draft.expected_pubkey_str(),
+ "contract_version": SDK_RUNTIME_CONTRACT_VERSION,
+ "operation_kind": request.operation_kind,
+ "actor_pubkey": request.actor.pubkey().as_str(),
+ "draft": {
+ "contract_id": request.frozen_draft.contract_id(),
+ "contract_registry_version": request.frozen_draft.contract_registry_version(),
+ "kind": request.frozen_draft.kind_u32(),
+ "created_at": request.frozen_draft.created_at_u64(),
+ "tags": request.frozen_draft.tags_as_vec(),
+ "content_sha256": hex::encode(Sha256::digest(request.frozen_draft.content().as_bytes())),
+ "expected_pubkey": request.frozen_draft.expected_pubkey_str(),
+ "expected_event_id": request.frozen_draft.expected_event_id_str()
+ },
+ "delivery_plan": {
+ "transport_profile_id": delivery_plan.transport_profile_id.as_str(),
+ "target_policy_version": delivery_plan.target_policy_version,
+ "satisfaction_policy": &delivery_plan.satisfaction_policy,
+ "reticulum_behavior": delivery_plan.reticulum_behavior.as_str(),
+ "targets": targets
+ }
});
let bytes =
serde_json::to_vec(&digest_document).expect("runtime journal digest document serializes");
diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs
@@ -695,7 +695,9 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation()
farmer_actor(),
farm(FARM_C_D_TAG, "North Farm"),
TargetPolicy::default_profile(),
- );
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000238")
+ .expect("idempotency key");
let error = sdk
.farms()
.enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(OTHER))
@@ -776,7 +778,7 @@ async fn farm_enqueue_publish_uses_explicit_idempotency_key_across_equivalent_ta
first_receipt.idempotency_digest_prefix,
second_receipt.idempotency_digest_prefix
);
- assert_eq!(second_receipt.state, SdkMutationState::AlreadyQueued);
+ assert_eq!(second_receipt.state, SdkMutationState::StoredAndQueued);
let paths = sdk.storage_paths().expect("paths");
let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs
@@ -582,7 +582,10 @@ async fn enqueue_prepared_publish_returns_structured_actor_errors() {
&non_seller_actor(),
prepared,
TargetPolicy::default_profile(),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000023e")
+ .expect("idempotency key"),
+ ),
&FixtureSigner::new(SELLER),
)
.await
@@ -608,7 +611,10 @@ async fn enqueue_prepared_publish_returns_sanitized_signer_errors() {
&actor,
prepared,
TargetPolicy::default_profile(),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000023f")
+ .expect("idempotency key"),
+ ),
&FixtureSigner::new(OTHER),
)
.await
@@ -680,7 +686,9 @@ async fn enqueue_publish_returns_sanitized_signer_errors() {
actor(),
listing(LISTING_C_D_TAG, "Coffee"),
TargetPolicy::default_profile(),
- );
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000023d")
+ .expect("idempotency key");
let error = sdk
.listings()
.enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(OTHER))
@@ -788,7 +796,7 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
.await
.expect("seed event store");
let conflicting_operation = sqlx::query(
- "INSERT INTO outbox_operations(operation_kind, expected_pubkey, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, 'queued', ?, ?)",
+ "INSERT INTO outbox_operations(operation_kind, expected_pubkey, semantic_scope, trade_id, mutation_id, canonical_payload_sha256, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES (?, ?, 'generic_event', NULL, NULL, NULL, ?, ?, 'queued', ?, ?)",
)
.bind(LISTING_PUBLISH_OPERATION_KIND)
.bind(SELLER)
@@ -847,13 +855,33 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
.expect("event lookup")
.is_none()
);
- let journal_count: i64 = sqlx::query("SELECT COUNT(*) FROM sdk_runtime_operation_journal")
+ let journal_row = sqlx::query(
+ "SELECT state, last_error_code FROM sdk_runtime_operation_journal WHERE operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
+ )
+ .bind(LISTING_PUBLISH_OPERATION_KIND)
+ .bind(SELLER)
+ .bind(idempotency_key)
+ .fetch_one(event_store.pool())
+ .await
+ .expect("journal row");
+ let journal_state: String = journal_row.try_get("state").expect("journal state");
+ let last_error_code: String = journal_row
+ .try_get("last_error_code")
+ .expect("journal error code");
+ assert_eq!(journal_state, "failed_recoverable");
+ assert_eq!(last_error_code, "idempotency_conflict");
+ let recovery_count: i64 = sqlx::query(
+ "SELECT COUNT(*) FROM sdk_runtime_recovery_receipt WHERE recovery_code = 'idempotency_conflict' AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
+ )
+ .bind(LISTING_PUBLISH_OPERATION_KIND)
+ .bind(SELLER)
+ .bind(idempotency_key)
.fetch_one(event_store.pool())
.await
- .expect("journal count")
+ .expect("recovery count")
.try_get(0)
- .expect("journal count value");
- assert_eq!(journal_count, 0);
+ .expect("recovery count value");
+ assert_eq!(recovery_count, 1);
let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
@@ -909,7 +937,7 @@ async fn enqueue_publish_uses_explicit_idempotency_key_across_equivalent_target_
first_receipt.idempotency_digest_prefix,
second_receipt.idempotency_digest_prefix
);
- assert_eq!(second_receipt.state, SdkMutationState::AlreadyQueued);
+ assert_eq!(second_receipt.state, SdkMutationState::StoredAndQueued);
let paths = sdk.storage_paths().expect("paths");
let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
diff --git a/crates/sdk/tests/market_runtime.rs b/crates/sdk/tests/market_runtime.rs
@@ -1,232 +0,0 @@
-#![cfg(feature = "runtime")]
-
-use radroots_authority::{
- RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity,
-};
-use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
- RadrootsCoreQuantityPrice, RadrootsCoreUnit,
-};
-use radroots_event::{
- contract::RadrootsActorRole,
- draft::{RadrootsEventDraft, RadrootsSignedEvent},
- farm::RadrootsFarmRef,
- ids::{RadrootsDTag, RadrootsInventoryBinId},
- listing::{
- RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
- RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingPublicLocation,
- RadrootsListingStatus,
- },
-};
-use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft,
-};
-use radroots_sdk::{
- ListingEnqueuePublishRequest, MarketSearchRequest, NostrProfile, NostrRelayUrlPolicy,
- RadrootsClient, RadrootsSdkError, RadrootsSdkTimestamp, SyncProjectionRefreshRequest,
- TargetPolicy, TransportProfile,
-};
-
-const SELLER_SECRET_KEY_HEX: &str =
- "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8";
-const SELLER_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
-const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
-const LISTING_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg";
-const RELAY: &str = "wss://relay.radroots.test";
-
-#[derive(Clone)]
-struct FixtureSigner {
- identity: RadrootsSignerIdentity,
- keys: RadrootsNostrKeys,
-}
-
-impl FixtureSigner {
- fn new(secret_key_hex: &str) -> Self {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
- let pubkey = keys.public_key().to_hex();
- Self {
- identity: RadrootsSignerIdentity::new(pubkey).expect("identity"),
- keys,
- }
- }
-}
-
-impl RadrootsEventSigner for FixtureSigner {
- fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
- self.identity.pubkey()
- }
-
- fn sign_frozen_draft(
- &self,
- draft: &RadrootsEventDraft,
- ) -> Result<RadrootsSignedEvent, RadrootsSignerError> {
- radroots_nostr_sign_frozen_draft(&self.keys, draft).map_err(|error| {
- RadrootsSignerError::SigningFailed {
- message: error.to_string(),
- }
- })
- }
-}
-
-fn seller_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER_PUBLIC_KEY_HEX, [RadrootsActorRole::Seller]).expect("actor")
-}
-
-fn decimal(raw: &str) -> RadrootsCoreDecimal {
- raw.parse().expect("decimal")
-}
-
-fn listing(title: &str) -> RadrootsListing {
- RadrootsListing {
- d_tag: RadrootsDTag::parse(LISTING_D_TAG).expect("d tag"),
- published_at: Some(1_700_000_000),
- farm: RadrootsFarmRef {
- pubkey: SELLER_PUBLIC_KEY_HEX.to_owned(),
- d_tag: FARM_D_TAG.to_owned(),
- },
- product: RadrootsListingProduct {
- key: "blueberries".to_owned(),
- title: title.to_owned(),
- category: "fruit".to_owned(),
- summary: Some("Fresh field berries".to_owned()),
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: RadrootsInventoryBinId::parse("pint").expect("bin id"),
- bins: vec![RadrootsListingBin {
- bin_id: RadrootsInventoryBinId::parse("pint").expect("bin id"),
- quantity: RadrootsCoreQuantity::new(decimal("1"), RadrootsCoreUnit::Each),
- price_per_canonical_unit: RadrootsCoreQuantityPrice {
- amount: RadrootsCoreMoney::new(decimal("6"), RadrootsCoreCurrency::USD),
- quantity: RadrootsCoreQuantity::new(decimal("1"), RadrootsCoreUnit::Each),
- },
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- }],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: Some(decimal("12")),
- availability: Some(RadrootsListingAvailability::Status {
- status: RadrootsListingStatus::Active,
- }),
- delivery_method: Some(RadrootsListingDeliveryMethod::Pickup),
- location: Some(RadrootsListingPublicLocation {
- primary: "Fernwood".to_owned(),
- city: Some("Victoria".to_owned()),
- region: Some("BC".to_owned()),
- country: Some("CA".to_owned()),
- geohash: "c2b2q".to_owned(),
- }),
- images: None,
- }
-}
-
-async fn directory_sdk() -> (tempfile::TempDir, RadrootsClient) {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let sdk = RadrootsClient::builder()
- .directory_storage(tempdir.path().join("sdk"))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .transport_profile(TransportProfile::nostr(
- NostrProfile::new([RELAY], NostrRelayUrlPolicy::Public).expect("Nostr profile"),
- ))
- .build()
- .await
- .expect("sdk");
- (tempdir, sdk)
-}
-
-#[tokio::test]
-async fn market_search_refreshes_local_projection_and_reads_fts() {
- let (_tempdir, sdk) = directory_sdk().await;
- let publish = ListingEnqueuePublishRequest::new(
- seller_actor(),
- listing("Blueberries"),
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("target relays")
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000240")
- .expect("idempotency key");
- let receipt = sdk
- .listings()
- .enqueue_publish_with_explicit_signer(publish, &FixtureSigner::new(SELLER_SECRET_KEY_HEX))
- .await
- .expect("publish");
-
- let search = sdk
- .market()
- .search(MarketSearchRequest::new("blueberries victoria").with_limit(10))
- .await
- .expect("search");
-
- assert_eq!(
- search.source,
- radroots_sdk::MarketSearchSource::LocalProjectionFts
- );
- assert_eq!(
- search.refresh.projection_id,
- "radroots.product_projection.v1"
- );
- assert_eq!(search.refresh.projection_version, 1);
- assert_eq!(search.refresh.refreshed_at_ms, 1_700_000_000_000);
- assert_eq!(search.refresh.scanned_events, 1);
- assert_eq!(search.refresh.listing_upserts, 1);
- assert_eq!(search.refresh.trade_upserts, 0);
- assert_eq!(search.refresh.validation_receipts, 0);
- assert_eq!(search.listings.len(), 1);
- assert_eq!(search.listings[0].listing_event_id, receipt.signed_event_id);
- assert_eq!(
- search.listings[0].seller_pubkey.as_str(),
- SELLER_PUBLIC_KEY_HEX
- );
- assert_eq!(search.listings[0].title, "Blueberries");
- assert_eq!(search.listings[0].product_type, "fruit");
- assert_eq!(
- search.listings[0].locality_city.as_deref(),
- Some("Victoria")
- );
- assert_eq!(search.listings[0].geohash5, "c2b2q");
-
- let second = sdk
- .market()
- .search(MarketSearchRequest::new("").with_limit(10))
- .await
- .expect("second search");
- assert_eq!(second.refresh.scanned_events, 0);
- assert_eq!(second.listings.len(), 1);
-}
-
-#[tokio::test]
-async fn projection_refresh_limits_are_request_errors() {
- let (_tempdir, sdk) = directory_sdk().await;
-
- let sync_error = sdk
- .sync()
- .refresh_projections(SyncProjectionRefreshRequest::new().with_limit(0))
- .await
- .expect_err("sync limit");
- assert!(matches!(
- sync_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
- assert_eq!(sync_error.code(), "invalid_request");
-
- let market_error = sdk
- .market()
- .search(MarketSearchRequest::new("berries").with_limit(0))
- .await
- .expect_err("market limit");
- assert!(matches!(
- market_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-}
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -1,4054 +0,0 @@
-#![cfg(feature = "runtime")]
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-use std::path::Path;
-#[cfg(feature = "transport-nostr-runtime")]
-use std::sync::{Arc, Mutex};
-use std::time::{Duration, Instant};
-
-#[cfg(feature = "transport-nostr-runtime")]
-use futures::future::BoxFuture;
-#[cfg(feature = "transport-nostr-runtime")]
-use nostr::JsonUtil;
-#[cfg(feature = "signer-adapters")]
-use radroots_authority::RadrootsActorContext;
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-use radroots_authority::RadrootsLocalEventSigner;
-use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
-};
-#[cfg(feature = "signer-adapters")]
-use radroots_event::contract::RadrootsActorRole;
-use radroots_event::ids::RadrootsPublicKey;
-use radroots_event::wire::RadrootsNip01EventWireParts;
-use radroots_event::{
- RadrootsEventEnvelope, RadrootsEventPtr,
- draft::RadrootsSignedEvent,
- ids::{RadrootsEventId, RadrootsListingAddress, RadrootsOrderId},
- kinds::{KIND_LISTING, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, KIND_POST},
- order::{
- RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem,
- RadrootsOrderEconomicLine, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment,
- RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
- },
-};
-use radroots_event_store::{RadrootsEventIngest, RadrootsEventStore};
-use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp, radroots_event_from_nostr,
- radroots_nostr_build_event,
-};
-#[cfg(feature = "signer-adapters")]
-use radroots_outbox::RadrootsOutbox;
-#[cfg(any(feature = "signer-adapters", feature = "transport-nostr-runtime"))]
-use radroots_sdk::{NostrProfile, NostrRelayUrlPolicy, TransportProfile};
-#[cfg(feature = "signer-adapters")]
-use radroots_sdk::{
- PrivacyPreflightConfirmation, PrivacyPreflightStatus, ProductSensitivityField, PublishMode,
- RadrootsSdkRecoveryAction, SatisfactionPolicy, SdkMutationState, TargetPolicy, TargetSet,
-};
-use radroots_sdk::{
- RadrootsClient, RadrootsSdkError, RadrootsSdkTimestamp, RadrootsTradeValidationTrustPolicy,
- RadrootsTradeValidationTrustState, SdkTradeStatusIssue, SdkTradeStatusIssueKind,
- SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT, TRADE_STATUS_MAX_LIMIT,
- TRADE_STATUS_WATCH_MAX_CAPACITY, TradeEvidenceIngestRequest, TradeRequestEvidenceIngestRequest,
- TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest, TradeStatusWatchCancelState,
- TradeStatusWatchRequest,
-};
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-use radroots_sdk::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
-#[cfg(feature = "signer-adapters")]
-use radroots_sdk::{
- TRADE_SUBMIT_OPERATION_KIND, TradeAcceptRequest, TradeCancelRequest, TradeDeclineRequest,
- TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest,
-};
-#[cfg(feature = "transport-nostr-runtime")]
-use radroots_sdk::{
- TradeEvidenceQueryBranchKind, TradeResyncNostrRelayOutcomeKind,
- TradeResyncNostrRelayTransportOutcomeKind, TradeResyncRequest, TradeSellerInboxRequest,
- TradeValidationReceiptInspectRequest, TradeValidationReceiptListRequest,
- TradeValidationReceiptVerifyRequest,
-};
-#[cfg(feature = "transport-nostr-runtime")]
-use radroots_trade::identity::RadrootsTradeLocator;
-use radroots_trade::order::RadrootsOrderIssue;
-use radroots_trade::validation_receipt::{
- RadrootsTradeValidationReceipt, RadrootsValidationReceiptProof,
- RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult,
- RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, RadrootsValidatorSetV1,
- validation_receipt_event_build, validation_receipt_public_values_hash_hex,
- validator_set_address_from_str,
-};
-#[cfg(feature = "transport-nostr-runtime")]
-use radroots_transport_nostr::{
- RadrootsMockRelayFetchAdapter, RadrootsRelayFetchAdapter, RadrootsRelayFetchItem,
- RadrootsRelayFetchRequest, RadrootsRelayTransportError,
-};
-use serde::Serialize;
-use serde::ser::{self, SerializeStruct};
-
-const BUYER_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
-const BUYER_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-const SELLER_SECRET_KEY_HEX: &str =
- "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8";
-const SELLER_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
-const SERVICE_SECRET_KEY_HEX: &str =
- "48314941f2c9c01ef99f531df7b1d59a8de23dbeb45a498e5aa5f671e921931f";
-const RELAY: &str = "wss://relay.radroots.test";
-
-fn signed_event_from_envelope(event: RadrootsEventEnvelope) -> RadrootsSignedEvent {
- let wire = event.to_nip01_wire();
- let raw_json = serde_json::to_string(&wire).expect("raw event json");
- RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
-}
-#[cfg(feature = "transport-nostr-runtime")]
-const RELAY_B: &str = "wss://relay-b.radroots.test";
-const PERF_TOTAL_LOCAL_EVENTS: i64 = 100_000;
-const PERF_TRADE_RELEVANT_EVENTS: i64 = 25_000;
-const PERF_ACTIVE_TRADES: usize = 1_000;
-const PERF_STATUS_P95_TARGET: Duration = Duration::from_millis(50);
-const STATUS_NOISE_NON_TRADE_EVENTS: i64 = 128;
-const STATUS_NOISE_TRADE_BACKGROUND_EVENTS: i64 = 64;
-
-#[derive(Clone, Copy)]
-enum FailingSerializeFailure {
- Start,
- Field(usize),
- End,
-}
-
-struct FailingStructSerializer {
- failure: FailingSerializeFailure,
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[derive(Clone, Default)]
-struct CapturingRelayFetchAdapter {
- filters_json: Arc<Mutex<Vec<String>>>,
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-impl CapturingRelayFetchAdapter {
- fn filters_json(&self) -> Vec<String> {
- self.filters_json
- .lock()
- .expect("captured filters lock")
- .clone()
- }
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-impl RadrootsRelayFetchAdapter for CapturingRelayFetchAdapter {
- fn fetch<'a>(
- &'a self,
- request: RadrootsRelayFetchRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayFetchItem>, RadrootsRelayTransportError>> {
- Box::pin(async move {
- let filters = request
- .filters()
- .iter()
- .map(JsonUtil::as_json)
- .collect::<Vec<_>>();
- *self.filters_json.lock().expect("captured filters lock") = filters;
- Ok(vec![relay_eose(RELAY)])
- })
- }
-}
-
-struct FailingSerializeStruct {
- field_index: usize,
- failure: FailingSerializeFailure,
-}
-
-#[derive(Debug)]
-struct FailingSerializeError;
-
-impl core::fmt::Display for FailingSerializeError {
- fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- formatter.write_str("intentional serializer failure")
- }
-}
-
-impl std::error::Error for FailingSerializeError {}
-
-impl ser::Error for FailingSerializeError {
- fn custom<T>(_message: T) -> Self
- where
- T: core::fmt::Display,
- {
- Self
- }
-}
-
-impl FailingStructSerializer {
- fn start() -> Self {
- Self {
- failure: FailingSerializeFailure::Start,
- }
- }
-
- fn field(field_index: usize) -> Self {
- Self {
- failure: FailingSerializeFailure::Field(field_index),
- }
- }
-
- fn end() -> Self {
- Self {
- failure: FailingSerializeFailure::End,
- }
- }
-}
-
-impl ser::Serializer for FailingStructSerializer {
- type Ok = ();
- type Error = FailingSerializeError;
- type SerializeSeq = ser::Impossible<(), FailingSerializeError>;
- type SerializeTuple = ser::Impossible<(), FailingSerializeError>;
- type SerializeTupleStruct = ser::Impossible<(), FailingSerializeError>;
- type SerializeTupleVariant = ser::Impossible<(), FailingSerializeError>;
- type SerializeMap = ser::Impossible<(), FailingSerializeError>;
- type SerializeStruct = FailingSerializeStruct;
- type SerializeStructVariant = ser::Impossible<(), FailingSerializeError>;
-
- fn serialize_bool(self, _value: bool) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i8(self, _value: i8) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i16(self, _value: i16) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i32(self, _value: i32) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i64(self, _value: i64) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u8(self, _value: u8) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u16(self, _value: u16) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u32(self, _value: u32) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u64(self, _value: u64) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_f32(self, _value: f32) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_f64(self, _value: f64) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_char(self, _value: char) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_str(self, _value: &str) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_some<T>(self, _value: &T) -> Result<Self::Ok, Self::Error>
- where
- T: ?Sized + Serialize,
- {
- Err(FailingSerializeError)
- }
-
- fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_unit_variant(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- ) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_newtype_struct<T>(
- self,
- _name: &'static str,
- _value: &T,
- ) -> Result<Self::Ok, Self::Error>
- where
- T: ?Sized + Serialize,
- {
- Err(FailingSerializeError)
- }
-
- fn serialize_newtype_variant<T>(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- _value: &T,
- ) -> Result<Self::Ok, Self::Error>
- where
- T: ?Sized + Serialize,
- {
- Err(FailingSerializeError)
- }
-
- fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_tuple_struct(
- self,
- _name: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeTupleStruct, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_tuple_variant(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeTupleVariant, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_struct(
- self,
- _name: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeStruct, Self::Error> {
- match self.failure {
- FailingSerializeFailure::Start => Err(FailingSerializeError),
- failure => Ok(FailingSerializeStruct {
- field_index: 0,
- failure,
- }),
- }
- }
-
- fn serialize_struct_variant(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeStructVariant, Self::Error> {
- Err(FailingSerializeError)
- }
-}
-
-impl SerializeStruct for FailingSerializeStruct {
- type Ok = ();
- type Error = FailingSerializeError;
-
- fn serialize_field<T>(&mut self, _key: &'static str, _value: &T) -> Result<(), Self::Error>
- where
- T: ?Sized + Serialize,
- {
- self.field_index += 1;
- match self.failure {
- FailingSerializeFailure::Field(field) if self.field_index == field => {
- Err(FailingSerializeError)
- }
- _ => Ok(()),
- }
- }
-
- fn end(self) -> Result<Self::Ok, Self::Error> {
- match self.failure {
- FailingSerializeFailure::End => Err(FailingSerializeError),
- _ => Ok(()),
- }
- }
-}
-
-fn assert_struct_serialize_error_paths<T>(value: &T, field_count: usize)
-where
- T: Serialize,
-{
- value
- .serialize(FailingStructSerializer::start())
- .expect_err("struct start failure");
- for field_index in 1..=field_count {
- value
- .serialize(FailingStructSerializer::field(field_index))
- .expect_err("struct field failure");
- }
- value
- .serialize(FailingStructSerializer::end())
- .expect_err("struct end failure");
-}
-
-async fn directory_sdk_and_store() -> (tempfile::TempDir, RadrootsClient, RadrootsEventStore) {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let sdk = RadrootsClient::builder()
- .directory_storage(tempdir.path().join("sdk"))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .build()
- .await
- .expect("sdk");
- let store = RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("event store");
- (tempdir, sdk, store)
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-async fn directory_sdk_and_store_with_relays(
- relays: &[&str],
-) -> (tempfile::TempDir, RadrootsClient, RadrootsEventStore) {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let mut builder = RadrootsClient::builder()
- .directory_storage(tempdir.path().join("sdk"))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000));
- if !relays.is_empty() {
- builder = builder.transport_profile(TransportProfile::nostr(
- NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public)
- .expect("Nostr profile"),
- ));
- }
- let sdk = builder.build().await.expect("sdk");
- let store = RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("event store");
- (tempdir, sdk, store)
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-async fn directory_sdk_with_signer(storage_root: &Path, secret_key_hex: &str) -> RadrootsClient {
- directory_sdk_with_signer_and_relays(storage_root, secret_key_hex, &[]).await
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-async fn directory_sdk_with_signer_and_relays(
- storage_root: &Path,
- secret_key_hex: &str,
- relays: &[&str],
-) -> RadrootsClient {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- let signer_keys = RadrootsNostrKeys::new(secret_key);
- let mut builder = RadrootsClient::builder()
- .directory_storage(storage_root)
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::from_event_signer(
- RadrootsLocalEventSigner::new(signer_keys).expect("local event signer"),
- )
- .expect("local signer"),
- ));
- if !relays.is_empty() {
- builder = builder.transport_profile(TransportProfile::nostr(
- NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public)
- .expect("Nostr profile"),
- ));
- }
- builder.build().await.expect("sdk")
-}
-
-fn order_id(raw: &str) -> RadrootsOrderId {
- RadrootsOrderId::parse(raw).expect("order id")
-}
-
-fn status_request(raw: &str) -> TradeStatusRequest {
- TradeStatusRequest::parse(raw).expect("order status request")
-}
-
-#[cfg(feature = "signer-adapters")]
-fn buyer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(BUYER_PUBLIC_KEY_HEX, [RadrootsActorRole::Buyer]).expect("actor")
-}
-
-#[cfg(feature = "signer-adapters")]
-fn seller_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER_PUBLIC_KEY_HEX, [RadrootsActorRole::Seller]).expect("actor")
-}
-
-fn listing_address() -> RadrootsListingAddress {
- RadrootsListingAddress::parse(format!(
- "{KIND_LISTING}:{SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("listing address")
-}
-
-fn listing_event_ptr() -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: deterministic_event_id("listing-event").into_string(),
- relays: Some(RELAY.to_owned()),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn explicit_trade_relays() -> TargetPolicy {
- TargetPolicy::explicit(
- TargetSet::nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("transport targets"),
- )
-}
-
-#[cfg(feature = "signer-adapters")]
-fn public_note_confirmation() -> PrivacyPreflightConfirmation {
- PrivacyPreflightConfirmation::new().confirm(ProductSensitivityField::PublicButSensitiveNotes)
-}
-
-#[cfg(feature = "signer-adapters")]
-fn expect_enqueued<Plan, Receipt>(outcome: TradeMutationOutcome<Plan, Receipt>) -> Receipt {
- match outcome {
- TradeMutationOutcome::Enqueued { receipt } => receipt,
- TradeMutationOutcome::DryRun { .. } => panic!("expected enqueue outcome"),
- TradeMutationOutcome::Published { .. } => panic!("expected enqueue outcome"),
- }
-}
-
-fn deterministic_event_id(raw: &str) -> RadrootsEventId {
- let mut bytes = [0u8; 32];
- for (index, byte) in raw.bytes().enumerate() {
- let primary = index % bytes.len();
- let secondary = (index * 7 + 13) % bytes.len();
- bytes[primary] = bytes[primary]
- .wrapping_add(byte)
- .wrapping_add((index as u8).wrapping_mul(31));
- bytes[secondary] ^= byte.rotate_left((index % 8) as u32);
- }
- let mut hex = String::with_capacity(64);
- for byte in bytes {
- use core::fmt::Write as _;
- write!(&mut hex, "{byte:02x}").expect("write hex");
- }
- RadrootsEventId::parse(hex).expect("event id")
-}
-
-fn validator_set_id() -> &'static str {
- "018f3d99-7d35-7c0c-8a0f-7f3b645abcde"
-}
-
-fn validator_set_event_id() -> RadrootsEventId {
- deterministic_event_id("validator-set-event")
-}
-
-fn validator_set_addr_raw(author_pubkey: &str) -> String {
- format!("30381:{author_pubkey}:{}", validator_set_id())
-}
-
-fn validator_set_policy_for_validator_pubkey(
- validator_pubkey: &str,
-) -> RadrootsTradeValidationTrustPolicy {
- let validator_set = RadrootsValidatorSetV1 {
- set_id: validator_set_id().to_owned(),
- validator_pubkey: RadrootsPublicKey::parse(validator_pubkey).expect("validator pubkey"),
- threshold: 1,
- valid_from: 1_700_000_000,
- valid_until: 1_800_000_000,
- protocol_contract_hash: hash32('7'),
- operator_name: "Radroots validation operator".to_owned(),
- operator_contact: None,
- };
- RadrootsTradeValidationTrustPolicy::production().with_validator_set(
- validator_set,
- validator_set_address_from_str(validator_set_addr_raw(validator_pubkey))
- .expect("validator set address"),
- validator_set_event_id().into_string(),
- )
-}
-
-fn decimal(raw: &str) -> RadrootsCoreDecimal {
- raw.parse().expect("decimal")
-}
-
-fn usd(raw: &str) -> RadrootsCoreMoney {
- RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD)
-}
-
-fn economics() -> RadrootsOrderEconomics {
- RadrootsOrderEconomics {
- quote_id: "quote-1".parse().expect("quote id"),
- quote_version: 1,
- pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
- currency: RadrootsCoreCurrency::USD,
- items: vec![RadrootsOrderEconomicItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- quantity_amount: decimal("1"),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: decimal("5"),
- unit_price_currency: RadrootsCoreCurrency::USD,
- line_subtotal: usd("10"),
- }],
- discounts: Vec::<RadrootsOrderEconomicLine>::new(),
- adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
- subtotal: usd("10"),
- discount_total: usd("0"),
- adjustment_total: usd("0"),
- total: usd("10"),
- }
-}
-
-fn order_request(raw_order_id: &str) -> RadrootsOrderRequest {
- RadrootsOrderRequest {
- order_id: order_id(raw_order_id),
- listing_addr: listing_address(),
- buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"),
- seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"),
- items: vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- economics: economics(),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-fn trade_propose_request(
- raw_order_id: &str,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
-) -> TradeProposeRequest {
- let order = order_request(raw_order_id);
- TradeProposeRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order,
- explicit_trade_relays(),
- publish_mode,
- satisfaction_policy,
- )
-}
-
-#[cfg(all(
- feature = "signer-adapters",
- feature = "local-signer",
- feature = "transport-nostr-runtime"
-))]
-#[tokio::test]
-async fn trade_product_clients_propose_inbox_accept_status_and_resync() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer_and_relays(
- storage_root.as_path(),
- BUYER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-facade-flow",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000204")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
-
- assert_eq!(
- propose_receipt.order_id.as_str(),
- "trade-product-facade-flow"
- );
- assert_eq!(
- propose_receipt
- .locator
- .root_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(propose_receipt.signed_event_id.as_str())
- );
- assert_eq!(
- propose_receipt.locator.listing_addr,
- Some(listing_address())
- );
- assert_eq!(
- propose_receipt
- .locator
- .seller_pubkey
- .as_ref()
- .map(RadrootsPublicKey::as_str),
- Some(SELLER_PUBLIC_KEY_HEX)
- );
-
- let seller_sdk = directory_sdk_with_signer_and_relays(
- storage_root.as_path(),
- SELLER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let inbox = seller_sdk
- .trades()
- .seller()
- .inbox(TradeSellerInboxRequest::new(seller_actor()))
- .await
- .expect("seller inbox");
- assert_eq!(inbox.seller_pubkey.as_str(), SELLER_PUBLIC_KEY_HEX);
- assert_eq!(inbox.statuses.len(), 1);
- assert_eq!(inbox.statuses[0].status, TradeStatusKind::Requested);
- assert_eq!(
- inbox.statuses[0]
- .root_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(propose_receipt.signed_event_id.as_str())
- );
-
- let accept_receipt = expect_enqueued(
- seller_sdk
- .trades()
- .seller()
- .accept_trade(
- TradeAcceptRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000205")
- .expect("accept idempotency"),
- )
- .await
- .expect("accept trade"),
- );
-
- assert_eq!(accept_receipt.order_id, propose_receipt.order_id);
- assert_eq!(accept_receipt.locator, propose_receipt.locator);
- assert_eq!(
- accept_receipt.request_event_id,
- propose_receipt.signed_event_id
- );
- let status = seller_sdk
- .trades()
- .status(TradeStatusRequest::new(propose_receipt.locator.clone()))
- .await
- .expect("facade status");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
- assert_eq!(
- status
- .decision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(accept_receipt.signed_event_id.as_str())
- );
-
- let resync_adapter = RadrootsMockRelayFetchAdapter::new(vec![relay_eose(RELAY)]);
- let resync = seller_sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(propose_receipt.locator),
- &resync_adapter,
- )
- .await
- .expect("facade resync");
- assert_eq!(resync.nostr_relay_urls, vec![RELAY.to_owned()]);
- assert_eq!(resync.evidence.eose_count, 1);
- assert_eq!(
- resync.status.status,
- TradeStatusKind::AgreedPendingValidation
- );
- assert_eq!(
- resync.status.last_event_id,
- Some(accept_receipt.signed_event_id)
- );
-}
-
-#[cfg(all(
- feature = "signer-adapters",
- feature = "local-signer",
- feature = "transport-nostr-runtime"
-))]
-#[tokio::test]
-async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let buyer_storage_root = tempdir.path().join("buyer-sdk");
- let seller_storage_root = tempdir.path().join("seller-sdk");
- let buyer_sdk = directory_sdk_with_signer_and_relays(
- buyer_storage_root.as_path(),
- BUYER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let seller_sdk = directory_sdk_with_signer_and_relays(
- seller_storage_root.as_path(),
- SELLER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let buyer_store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("buyer event store");
- let seller_store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("seller event store");
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-committed-resync",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000206")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
- assert_eq!(
- seller_store
- .status_summary()
- .await
- .expect("seller isolated before proposal import")
- .total_events,
- 0
- );
- let seller_proposal_resync_adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_event_item_from_store(&buyer_store, &propose_receipt.signed_event_id, RELAY, 4_000)
- .await,
- relay_eose(RELAY),
- ]);
- let seller_proposal_resync = seller_sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(propose_receipt.locator.clone()),
- &seller_proposal_resync_adapter,
- )
- .await
- .expect("seller proposal resync");
- assert_eq!(
- seller_proposal_resync.status.status,
- TradeStatusKind::Requested
- );
- assert_eq!(seller_proposal_resync.evidence.inserted_count, 1);
- let accept_receipt = expect_enqueued(
- seller_sdk
- .trades()
- .seller()
- .accept_trade(
- TradeAcceptRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000207")
- .expect("accept idempotency"),
- )
- .await
- .expect("accept trade"),
- );
- let receipt_raw_event = signed_raw_validation_receipt_event(
- "trade-product-committed-resync",
- &propose_receipt.listing_event_id,
- &propose_receipt.signed_event_id,
- &accept_receipt.signed_event_id,
- 33,
- );
- let receipt_event = radroots_event_from_nostr(&receipt_raw_event);
- let receipt_event_id =
- RadrootsEventId::parse(receipt_raw_event.id.to_hex().as_str()).expect("receipt id");
- let service_pubkey = public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX);
-
- seller_store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(receipt_event),
- 4_040,
- ))
- .await
- .expect("ingest validation receipt");
-
- let seller_resync = seller_sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(propose_receipt.locator.clone()),
- &RadrootsMockRelayFetchAdapter::new(vec![relay_eose(RELAY)]),
- )
- .await
- .expect("seller resync");
- assert_eq!(
- seller_resync.status.status,
- TradeStatusKind::AgreedPendingValidation
- );
- assert_eq!(
- seller_resync.status.rhi_receipt_event_id,
- Some(receipt_event_id.clone())
- );
- assert_eq!(
- seller_resync.status.last_event_id,
- Some(accept_receipt.signed_event_id.clone())
- );
- assert_eq!(
- seller_resync
- .status
- .validation_trust
- .as_ref()
- .map(|trust| trust.state),
- Some(RadrootsTradeValidationTrustState::Untrusted)
- );
-
- let trusted_local_policy = validator_set_policy_for_validator_pubkey(service_pubkey.as_str());
- let seller_trusted_local = seller_sdk
- .trades()
- .status(
- TradeStatusRequest::new(propose_receipt.locator.clone())
- .with_validation_trust_policy(trusted_local_policy),
- )
- .await
- .expect("seller trusted local status");
- assert_eq!(seller_trusted_local.status, TradeStatusKind::Committed);
- let seller_trust = seller_trusted_local
- .validation_trust
- .as_ref()
- .expect("seller validation trust");
- assert_eq!(
- seller_trust.state,
- RadrootsTradeValidationTrustState::ValidatorSetCommitted
- );
- assert!(seller_trust.production_committed);
- assert_eq!(
- seller_trust
- .receipt_author
- .as_ref()
- .map(|pubkey| pubkey.as_str()),
- Some(service_pubkey.as_str())
- );
-
- let buyer_committed_resync_adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_event_item_from_store(&seller_store, &accept_receipt.signed_event_id, RELAY, 4_100)
- .await,
- relay_raw_event_item(&receipt_raw_event, RELAY, 4_200),
- relay_eose(RELAY),
- ]);
- let buyer_resync = buyer_sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(propose_receipt.locator),
- &buyer_committed_resync_adapter,
- )
- .await
- .expect("buyer resync");
- assert_eq!(
- buyer_resync.status.status,
- TradeStatusKind::AgreedPendingValidation
- );
- assert_eq!(
- buyer_resync.status.rhi_receipt_event_id,
- Some(receipt_event_id)
- );
- assert_eq!(buyer_resync.evidence.inserted_count, 2);
-}
-
-#[tokio::test]
-async fn trade_status_trust_policy_requires_trusted_cryptographic_receipt_for_committed_confidence()
-{
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let order_id = "trade-status-trusted-crypto";
- let request_event = signed_order_request_event(order_id, 70);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let decision_event = signed_order_decision_event(order_id, &request_event_id, 71);
- let decision_event_id = RadrootsEventId::parse(decision_event.id_str()).expect("decision id");
- let listing_event_id = deterministic_event_id("listing-event");
- let receipt_raw_event = signed_raw_sp1_validation_receipt_event(
- order_id,
- &listing_event_id,
- &request_event_id,
- &decision_event_id,
- 72,
- );
- let receipt_event = radroots_event_from_nostr(&receipt_raw_event);
- let receipt_event_id =
- RadrootsEventId::parse(receipt_raw_event.id.to_hex()).expect("receipt id");
- for (event, observed_at_ms) in [
- (request_event, 7_000),
- (decision_event, 7_100),
- (receipt_event, 7_200),
- ] {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- observed_at_ms,
- ))
- .await
- .expect("ingest trade status trust event");
- }
-
- let default_status = sdk
- .trades()
- .status(status_request(order_id))
- .await
- .expect("default status");
- assert_eq!(
- default_status.status,
- TradeStatusKind::AgreedPendingValidation
- );
- assert_eq!(
- default_status
- .validation_trust
- .as_ref()
- .map(|trust| trust.state),
- Some(RadrootsTradeValidationTrustState::Untrusted)
- );
-
- let service_pubkey = public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX);
- let trusted_policy = validator_set_policy_for_validator_pubkey(service_pubkey.as_str());
- let trusted_status = sdk
- .trades()
- .status(status_request(order_id).with_validation_trust_policy(trusted_policy))
- .await
- .expect("trusted status");
-
- assert_eq!(trusted_status.status, TradeStatusKind::Committed);
- assert_eq!(
- trusted_status.rhi_receipt_event_id,
- Some(receipt_event_id.clone())
- );
- assert!(trusted_status.lifecycle_terminal);
- assert_eq!(
- trusted_status.next_action,
- TradeStatusNextActionKind::Terminal
- );
- let trust = trusted_status.validation_trust.expect("trusted decision");
- assert_eq!(
- trust.state,
- RadrootsTradeValidationTrustState::CryptographicCommitted
- );
- assert!(trust.production_committed);
- assert!(trust.cryptographic_proof_required);
- assert!(trust.cryptographic_proof_verified);
- assert_eq!(trust.proof_system.as_deref(), Some("sp1_core"));
- assert_eq!(
- trust.receipt_author.as_ref().map(|pubkey| pubkey.as_str()),
- Some(service_pubkey.as_str())
- );
-}
-
-#[cfg(all(
- feature = "signer-adapters",
- feature = "local-signer",
- feature = "transport-nostr-runtime"
-))]
-#[tokio::test]
-async fn trade_product_accept_resync_before_mutation_imports_relay_visible_request() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let buyer_storage_root = tempdir.path().join("buyer-sdk");
- let seller_storage_root = tempdir.path().join("seller-sdk");
- let buyer_sdk = directory_sdk_with_signer_and_relays(
- buyer_storage_root.as_path(),
- BUYER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let seller_sdk = directory_sdk_with_signer_and_relays(
- seller_storage_root.as_path(),
- SELLER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let buyer_store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("buyer event store");
- let seller_store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("seller event store");
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "01890f0e-6c00-7000-8000-000000000209",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000208")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
- assert_eq!(
- seller_store
- .status_summary()
- .await
- .expect("seller isolated before mutation")
- .total_events,
- 0
- );
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_event_item_from_store(&buyer_store, &propose_receipt.signed_event_id, RELAY, 4_300)
- .await,
- relay_eose(RELAY),
- ]);
- let accept_receipt = expect_enqueued(
- seller_sdk
- .trades()
- .seller()
- .accept_trade_with_fetch_adapter(
- TradeAcceptRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::ResyncBeforeMutation,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000209")
- .expect("accept idempotency"),
- &adapter,
- )
- .await
- .expect("accept trade"),
- );
- assert_eq!(
- accept_receipt.request_event_id,
- propose_receipt.signed_event_id
- );
- let status = seller_sdk
- .trades()
- .status(TradeStatusRequest::new(propose_receipt.locator))
- .await
- .expect("seller status after accept");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
- assert_eq!(
- status.decision_event_id,
- Some(accept_receipt.signed_event_id)
- );
- assert_eq!(
- seller_store
- .status_summary()
- .await
- .expect("seller imported request and decision")
- .total_events,
- 2
- );
-}
-
-#[cfg(all(
- feature = "signer-adapters",
- feature = "local-signer",
- feature = "transport-nostr-runtime"
-))]
-#[tokio::test]
-async fn trade_product_accept_local_only_does_not_fetch_nostr_evidence() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let buyer_storage_root = tempdir.path().join("buyer-sdk");
- let seller_storage_root = tempdir.path().join("seller-sdk");
- let buyer_sdk = directory_sdk_with_signer_and_relays(
- buyer_storage_root.as_path(),
- BUYER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let seller_sdk = directory_sdk_with_signer_and_relays(
- seller_storage_root.as_path(),
- SELLER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let seller_store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("seller event store");
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-local-only-no-fetch",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020a")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![relay_closed(RELAY, "must not fetch")]);
- let error = seller_sdk
- .trades()
- .seller()
- .accept_trade_with_fetch_adapter(
- TradeAcceptRequest::new(
- seller_actor(),
- propose_receipt.locator,
- vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- ),
- &adapter,
- )
- .await
- .expect_err("local-only accept without local evidence");
- let RadrootsSdkError::InvalidRequest { message } = error else {
- panic!("expected invalid request");
- };
- assert!(message.contains("trade.accept requires a locally projected trade"));
- assert_eq!(
- seller_store
- .status_summary()
- .await
- .expect("seller remains empty")
- .total_events,
- 0
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_accept_require_explicit_evidence_ingests_supplied_request() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let buyer_storage_root = tempdir.path().join("buyer-sdk");
- let seller_storage_root = tempdir.path().join("seller-sdk");
- let buyer_sdk =
- directory_sdk_with_signer(buyer_storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
- let seller_sdk =
- directory_sdk_with_signer(seller_storage_root.as_path(), SELLER_SECRET_KEY_HEX).await;
- let buyer_store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("buyer event store");
- let seller_store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("seller event store");
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "01890f0e-6c00-7000-8000-00000000020c",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020b")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
- let request_event = event_from_store(&buyer_store, &propose_receipt.signed_event_id).await;
- let accept_receipt = expect_enqueued(
- seller_sdk
- .trades()
- .seller()
- .accept_trade(
- TradeAcceptRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::require_explicit_evidence([
- TradeEvidenceIngestRequest::new(signed_event_from_envelope(request_event)),
- ]),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020c")
- .expect("accept idempotency"),
- )
- .await
- .expect("accept trade"),
- );
- assert_eq!(
- accept_receipt.request_event_id,
- propose_receipt.signed_event_id
- );
- let status = seller_sdk
- .trades()
- .status(TradeStatusRequest::new(propose_receipt.locator))
- .await
- .expect("seller explicit status after accept");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
- assert_eq!(
- seller_store
- .status_summary()
- .await
- .expect("seller imported request and decision")
- .total_events,
- 2
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_accept_require_explicit_evidence_rejects_empty_evidence() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let buyer_storage_root = tempdir.path().join("buyer-sdk");
- let seller_storage_root = tempdir.path().join("seller-sdk");
- let buyer_sdk =
- directory_sdk_with_signer(buyer_storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
- let seller_sdk =
- directory_sdk_with_signer(seller_storage_root.as_path(), SELLER_SECRET_KEY_HEX).await;
- let seller_store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("seller event store");
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-empty-explicit-accept",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020d")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
- let error = seller_sdk
- .trades()
- .seller()
- .accept_trade(TradeAcceptRequest::new(
- seller_actor(),
- propose_receipt.locator,
- vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::require_explicit_evidence(Vec::<TradeEvidenceIngestRequest>::new()),
- ))
- .await
- .expect_err("empty explicit evidence");
- let RadrootsSdkError::InvalidRequest { message } = error else {
- panic!("expected invalid request");
- };
- assert_eq!(message, "trade.accept requires explicit trade evidence");
- assert_eq!(
- seller_store
- .status_summary()
- .await
- .expect("seller remains empty")
- .total_events,
- 0
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_validation_receipts_fetch_from_relays_and_inspect_receipt() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let order_id = "trade-validation-receipts-sdk";
- let listing_event_id = deterministic_event_id("validation-receipt-listing");
- let root_event_id = deterministic_event_id("validation-receipt-request");
- let target_event_id = deterministic_event_id("validation-receipt-decision");
- let receipt_raw_event = signed_raw_validation_receipt_event(
- order_id,
- &listing_event_id,
- &root_event_id,
- &target_event_id,
- 33,
- );
- let receipt_event_id =
- RadrootsEventId::parse(receipt_raw_event.id.to_hex()).expect("receipt event id");
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&receipt_raw_event, RELAY, 4_000),
- relay_eose(RELAY),
- ]);
-
- let list = sdk
- .trades()
- .validation_receipts()
- .list_with_fetch_adapter(
- TradeValidationReceiptListRequest::parse(order_id).expect("list request"),
- &adapter,
- )
- .await
- .expect("validation receipt list");
-
- assert_eq!(list.nostr_relay_urls, vec![RELAY.to_owned()]);
- assert_eq!(list.receipts.len(), 1);
- assert!(list.invalid_receipts.is_empty());
- assert_eq!(list.nostr_evidence.out_of_filter_count, 0);
- assert_eq!(list.receipts[0].event.id_str(), receipt_event_id.as_str());
- assert_eq!(
- list.receipts[0].tags.root_event_id.as_str(),
- root_event_id.as_str()
- );
-
- let inspect = sdk
- .trades()
- .validation_receipts()
- .inspect_with_fetch_adapter(
- TradeValidationReceiptInspectRequest::new(receipt_event_id.clone()),
- &adapter,
- )
- .await
- .expect("validation receipt inspect");
-
- assert_eq!(inspect.receipt_event_id, receipt_event_id);
- assert!(inspect.invalid_receipt.is_none());
- assert_eq!(
- inspect
- .receipt
- .as_ref()
- .map(|receipt| receipt.tags.target_event_id.as_str()),
- Some(target_event_id.as_str())
- );
-
- let verify = sdk
- .trades()
- .validation_receipts()
- .verify_with_fetch_adapter(
- TradeValidationReceiptVerifyRequest::new(receipt_event_id.clone()),
- &adapter,
- )
- .await
- .expect("validation receipt verify");
- assert_eq!(
- verify
- .receipt
- .as_ref()
- .map(|receipt| receipt.tags.listing_event_id.as_str()),
- Some(listing_event_id.as_str())
- );
- assert!(
- store
- .get_event(receipt_event_id.as_str())
- .await
- .expect("stored receipt lookup")
- .is_some()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_validation_receipt_list_rejects_out_of_filter_order_receipts() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let requested_order_id = "validation-receipt-list-requested";
- let unrelated_order_id = "validation-receipt-list-unrelated";
- let unrelated_receipt = signed_raw_validation_receipt_event(
- unrelated_order_id,
- &deterministic_event_id("validation-receipt-list-unrelated-listing"),
- &deterministic_event_id("validation-receipt-list-unrelated-request"),
- &deterministic_event_id("validation-receipt-list-unrelated-decision"),
- 36,
- );
- let unrelated_receipt_id =
- RadrootsEventId::parse(unrelated_receipt.id.to_hex()).expect("unrelated receipt id");
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&unrelated_receipt, RELAY, 4_010),
- relay_eose(RELAY),
- ]);
-
- let list = sdk
- .trades()
- .validation_receipts()
- .list_with_fetch_adapter(
- TradeValidationReceiptListRequest::parse(requested_order_id).expect("list request"),
- &adapter,
- )
- .await
- .expect("validation receipt list");
-
- assert!(list.receipts.is_empty());
- assert!(list.invalid_receipts.is_empty());
- assert_eq!(list.nostr_evidence.inserted_count, 0);
- assert_eq!(list.nostr_evidence.out_of_filter_count, 1);
- assert!(list.nostr_evidence.events[0].out_of_filter);
- assert!(
- store
- .get_event(unrelated_receipt_id.as_str())
- .await
- .expect("unrelated receipt lookup")
- .is_none()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_validation_receipt_inspect_rejects_unrequested_relay_receipts() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let requested_receipt = signed_raw_validation_receipt_event(
- "validation-receipt-inspect-requested",
- &deterministic_event_id("validation-receipt-inspect-requested-listing"),
- &deterministic_event_id("validation-receipt-inspect-requested-request"),
- &deterministic_event_id("validation-receipt-inspect-requested-decision"),
- 37,
- );
- let requested_receipt_id =
- RadrootsEventId::parse(requested_receipt.id.to_hex()).expect("requested receipt id");
- let unrelated_receipt = signed_raw_validation_receipt_event(
- "validation-receipt-inspect-unrelated",
- &deterministic_event_id("validation-receipt-inspect-unrelated-listing"),
- &deterministic_event_id("validation-receipt-inspect-unrelated-request"),
- &deterministic_event_id("validation-receipt-inspect-unrelated-decision"),
- 38,
- );
- let unrelated_receipt_id =
- RadrootsEventId::parse(unrelated_receipt.id.to_hex()).expect("unrelated receipt id");
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&unrelated_receipt, RELAY, 4_020),
- relay_eose(RELAY),
- ]);
-
- let inspect = sdk
- .trades()
- .validation_receipts()
- .inspect_with_fetch_adapter(
- TradeValidationReceiptInspectRequest::new(requested_receipt_id.clone()),
- &adapter,
- )
- .await
- .expect("validation receipt inspect");
-
- assert_eq!(inspect.receipt_event_id, requested_receipt_id);
- assert!(inspect.receipt.is_none());
- assert!(inspect.invalid_receipt.is_none());
- assert_eq!(inspect.nostr_evidence.inserted_count, 0);
- assert_eq!(inspect.nostr_evidence.out_of_filter_count, 1);
- assert!(inspect.nostr_evidence.events[0].out_of_filter);
- assert!(
- store
- .get_event(unrelated_receipt_id.as_str())
- .await
- .expect("unrelated receipt lookup")
- .is_none()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_validation_receipt_inspect_skips_noise_before_exact_receipt_match() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let requested_receipt = signed_raw_validation_receipt_event(
- "validation-receipt-inspect-noise-requested",
- &deterministic_event_id("validation-receipt-inspect-noise-requested-listing"),
- &deterministic_event_id("validation-receipt-inspect-noise-requested-request"),
- &deterministic_event_id("validation-receipt-inspect-noise-requested-decision"),
- 39,
- );
- let requested_receipt_id =
- RadrootsEventId::parse(requested_receipt.id.to_hex()).expect("requested receipt id");
- let unrelated_receipt = signed_raw_validation_receipt_event(
- "validation-receipt-inspect-noise-unrelated",
- &deterministic_event_id("validation-receipt-inspect-noise-unrelated-listing"),
- &deterministic_event_id("validation-receipt-inspect-noise-unrelated-request"),
- &deterministic_event_id("validation-receipt-inspect-noise-unrelated-decision"),
- 40,
- );
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_malformed(RELAY),
- relay_raw_event_item(&unrelated_receipt, RELAY, 4_021),
- relay_raw_event_item(&requested_receipt, RELAY, 4_022),
- relay_eose(RELAY),
- ]);
-
- let inspect = sdk
- .trades()
- .validation_receipts()
- .inspect_with_fetch_adapter(
- TradeValidationReceiptInspectRequest::new(requested_receipt_id.clone()),
- &adapter,
- )
- .await
- .expect("validation receipt inspect");
-
- assert_eq!(inspect.receipt_event_id, requested_receipt_id);
- assert!(inspect.receipt.is_some());
- assert!(inspect.invalid_receipt.is_none());
- assert_eq!(inspect.nostr_evidence.inserted_count, 1);
- assert_eq!(inspect.nostr_evidence.malformed_count, 1);
- assert_eq!(inspect.nostr_evidence.out_of_filter_count, 1);
- assert_eq!(inspect.nostr_evidence.skipped_over_limit_count, 0);
- assert!(
- inspect
- .nostr_evidence
- .events
- .iter()
- .any(|event| event.malformed)
- );
- assert!(
- inspect
- .nostr_evidence
- .events
- .iter()
- .any(|event| event.out_of_filter)
- );
- assert!(
- store
- .get_event(requested_receipt_id.as_str())
- .await
- .expect("requested receipt lookup")
- .is_some()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_imports_nostr_evidence_into_empty_local_store() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let request_event = signed_raw_order_request_event("resync-empty-local-import", 41);
- let request_event_id =
- RadrootsEventId::parse(request_event.id.to_hex().as_str()).expect("event id");
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&request_event, RELAY, 5_000),
- relay_eose(RELAY),
- ]);
-
- let resync = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(order_id(
- "resync-empty-local-import",
- ))),
- &adapter,
- )
- .await
- .expect("resync");
-
- assert_eq!(resync.status.status, TradeStatusKind::Requested);
- assert_eq!(resync.evidence.inserted_count, 1);
- assert_eq!(resync.evidence.duplicate_count, 0);
- assert_eq!(resync.evidence.query_plan.branches.len(), 4);
- assert!(
- resync
- .evidence
- .query_plan
- .branches
- .iter()
- .any(|branch| branch.kind == TradeEvidenceQueryBranchKind::RequestRoots)
- );
- let request_branch = resync
- .evidence
- .branches
- .iter()
- .find(|branch| branch.branch == TradeEvidenceQueryBranchKind::RequestRoots)
- .expect("request root branch");
- assert_eq!(request_branch.accepted_count, 1);
- assert_eq!(request_branch.inserted_count, 1);
- assert!(!request_branch.empty_result);
- let lifecycle_branch = resync
- .evidence
- .branches
- .iter()
- .find(|branch| branch.branch == TradeEvidenceQueryBranchKind::LifecycleChain)
- .expect("lifecycle branch");
- assert!(lifecycle_branch.empty_result);
- assert_eq!(
- resync.evidence.events[0].event_id.as_deref(),
- Some(request_event_id.as_str())
- );
- assert!(
- store
- .get_event(request_event_id.as_str())
- .await
- .expect("stored event")
- .is_some()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_status_local_only_ignores_failing_fetch_adapter() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![relay_closed(RELAY, "must not fetch")]);
-
- let status = sdk
- .trades()
- .status_with_fetch_adapter(
- status_request("status-local-only-no-fetch")
- .with_source(SdkTradeStatusSource::LocalOnly),
- &adapter,
- )
- .await
- .expect("local-only status");
-
- assert_eq!(status.source, SdkTradeStatusSource::LocalOnly);
- assert_eq!(status.status, TradeStatusKind::Missing);
- assert!(status.online_evidence.is_none());
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_status_resync_then_local_fetches_evidence_before_status() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let request_event = signed_raw_order_request_event("status-resync-then-local", 46);
- let request_event_id =
- RadrootsEventId::parse(request_event.id.to_hex().as_str()).expect("request id");
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&request_event, RELAY, 5_250),
- relay_eose(RELAY),
- ]);
-
- let status = sdk
- .trades()
- .status_with_fetch_adapter(
- status_request("status-resync-then-local")
- .with_source(SdkTradeStatusSource::ResyncThenLocal),
- &adapter,
- )
- .await
- .expect("resync-then-local status");
-
- assert_eq!(status.source, SdkTradeStatusSource::ResyncThenLocal);
- assert_eq!(status.status, TradeStatusKind::Requested);
- assert_eq!(status.request_event_id, Some(request_event_id.clone()));
- let evidence = status.online_evidence.as_ref().expect("online evidence");
- assert_eq!(evidence.inserted_count, 1);
- let request_branch = evidence
- .branches
- .iter()
- .find(|branch| branch.branch == TradeEvidenceQueryBranchKind::RequestRoots)
- .expect("request branch");
- assert_eq!(request_branch.accepted_count, 1);
- assert_eq!(
- request_branch.events[0].event_id.as_deref(),
- Some(request_event_id.as_str())
- );
- assert!(
- store
- .get_event(request_event_id.as_str())
- .await
- .expect("stored event")
- .is_some()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_skips_noise_before_matching_trade_event() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let requested_order_id = order_id("resync-noise-requested");
- let request_event = signed_raw_order_request_event(requested_order_id.as_str(), 44);
- let request_event_id =
- RadrootsEventId::parse(request_event.id.to_hex()).expect("request event id");
- let unrelated_event = signed_raw_order_request_event("resync-noise-unrelated", 45);
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_malformed(RELAY),
- relay_raw_event_item(&unrelated_event, RELAY, 5_020),
- relay_raw_event_item(&request_event, RELAY, 5_021),
- relay_eose(RELAY),
- ]);
-
- let resync = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(requested_order_id)),
- &adapter,
- )
- .await
- .expect("resync");
-
- assert_eq!(resync.status.status, TradeStatusKind::Requested);
- assert_eq!(resync.evidence.inserted_count, 1);
- assert_eq!(resync.evidence.malformed_count, 1);
- assert_eq!(resync.evidence.out_of_filter_count, 1);
- assert_eq!(resync.evidence.skipped_over_limit_count, 0);
- assert!(
- store
- .get_event(request_event_id.as_str())
- .await
- .expect("stored event")
- .is_some()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_duplicate_replay_is_idempotent() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let request_event = signed_raw_order_request_event("resync-duplicate-replay", 42);
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&request_event, RELAY, 5_100),
- relay_eose(RELAY),
- ]);
- let locator = RadrootsTradeLocator::from_order_id(order_id("resync-duplicate-replay"));
-
- let first = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(TradeResyncRequest::new(locator.clone()), &adapter)
- .await
- .expect("first resync");
- let second = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(TradeResyncRequest::new(locator), &adapter)
- .await
- .expect("second resync");
-
- assert_eq!(first.evidence.inserted_count, 1);
- assert_eq!(second.evidence.inserted_count, 0);
- assert_eq!(second.evidence.duplicate_count, 1);
- assert_eq!(second.status.status, TradeStatusKind::Requested);
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_splits_lifecycle_branch_into_single_kind_filters() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let adapter = CapturingRelayFetchAdapter::default();
- sdk.trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(order_id(
- "resync-lifecycle-filter-shape",
- ))),
- &adapter,
- )
- .await
- .expect("resync");
-
- let filters = adapter.filters_json();
- assert_eq!(filters.len(), 4);
- for kind in [3422, 3423, 3432, 3440] {
- assert!(
- filters
- .iter()
- .any(|filter| filter.contains(format!("\"kinds\":[{kind}]").as_str())),
- "missing single-kind filter for {kind}: {filters:?}"
- );
- }
- assert!(
- !filters
- .iter()
- .any(|filter| filter.contains("\"kinds\":[3423,3424,3425,3432]"))
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_reports_malformed_evidence_without_poisoning_store() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let adapter =
- RadrootsMockRelayFetchAdapter::new(vec![relay_malformed(RELAY), relay_eose(RELAY)]);
-
- let resync = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(order_id(
- "resync-malformed-evidence",
- ))),
- &adapter,
- )
- .await
- .expect("resync");
-
- assert_eq!(resync.status.status, TradeStatusKind::Missing);
- assert_eq!(resync.evidence.malformed_count, 1);
- assert_eq!(resync.evidence.inserted_count, 0);
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("store summary")
- .total_events,
- 0
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_rejects_out_of_filter_evidence_without_poisoning_store() {
- let (_tempdir, sdk, store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let unrelated_event = signed_raw_order_request_event("resync-out-of-filter-unrelated", 43);
- let unrelated_event_id =
- RadrootsEventId::parse(unrelated_event.id.to_hex()).expect("unrelated event id");
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&unrelated_event, RELAY, 5_150),
- relay_eose(RELAY),
- ]);
-
- let resync = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(order_id(
- "resync-out-of-filter-requested",
- ))),
- &adapter,
- )
- .await
- .expect("resync");
-
- assert_eq!(resync.status.status, TradeStatusKind::Missing);
- assert_eq!(resync.evidence.inserted_count, 0);
- assert_eq!(resync.evidence.out_of_filter_count, 1);
- assert!(resync.evidence.events[0].out_of_filter);
- assert!(
- store
- .get_event(unrelated_event_id.as_str())
- .await
- .expect("unrelated event lookup")
- .is_none()
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_errors_on_total_relay_failure() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY, RELAY_B]).await;
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_closed(RELAY, "timeout: relay offline"),
- relay_closed(RELAY_B, "error: relay unavailable"),
- ]);
-
- let error = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(order_id(
- "resync-total-relay-failure",
- ))),
- &adapter,
- )
- .await
- .expect_err("total relay failure");
-
- assert_eq!(error.code(), "product_sync_transport_setup_failure");
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_resync_reports_partial_relay_failure() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY, RELAY_B]).await;
- let request_event = signed_raw_order_request_event("resync-partial-relay-failure", 43);
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_closed(RELAY_B, "timeout: relay unavailable"),
- relay_raw_event_item(&request_event, RELAY, 5_200),
- relay_eose(RELAY),
- ]);
-
- let resync = sdk
- .trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(RadrootsTradeLocator::from_order_id(order_id(
- "resync-partial-relay-failure",
- ))),
- &adapter,
- )
- .await
- .expect("partial failure resync");
-
- assert_eq!(resync.status.status, TradeStatusKind::Requested);
- assert_eq!(resync.evidence.closed_count, 1);
- assert_eq!(resync.evidence.eose_count, 1);
- assert_eq!(
- resync.evidence.nostr_relay_outcomes[0].outcome_kind,
- TradeResyncNostrRelayOutcomeKind::Closed
- );
- assert_eq!(
- resync.evidence.nostr_relay_outcomes[0].transport_outcome_kind,
- Some(TradeResyncNostrRelayTransportOutcomeKind::Timeout)
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-async fn relay_event_item_from_store(
- source: &RadrootsEventStore,
- event_id: &RadrootsEventId,
- relay_url: &str,
- observed_at_ms: i64,
-) -> RadrootsRelayFetchItem {
- let stored = source
- .get_event(event_id.as_str())
- .await
- .expect("source event lookup")
- .expect("source event");
- RadrootsRelayFetchItem::Event {
- relay_url: relay_url.to_owned(),
- raw_json: stored.raw_json,
- observed_at_ms,
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-async fn event_from_store(
- source: &RadrootsEventStore,
- event_id: &RadrootsEventId,
-) -> RadrootsEventEnvelope {
- let stored = source
- .get_event(event_id.as_str())
- .await
- .expect("source event lookup")
- .expect("source event");
- let event =
- serde_json::from_str::<nostr::Event>(stored.raw_json.as_str()).expect("stored raw event");
- radroots_event_from_nostr(&event)
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn relay_raw_event_item(
- event: &nostr::Event,
- relay_url: &str,
- observed_at_ms: i64,
-) -> RadrootsRelayFetchItem {
- RadrootsRelayFetchItem::Event {
- relay_url: relay_url.to_owned(),
- raw_json: event.as_json(),
- observed_at_ms,
- }
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn relay_eose(relay_url: &str) -> RadrootsRelayFetchItem {
- RadrootsRelayFetchItem::Eose {
- relay_url: relay_url.to_owned(),
- }
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn relay_closed(relay_url: &str, message: &str) -> RadrootsRelayFetchItem {
- RadrootsRelayFetchItem::Closed {
- relay_url: relay_url.to_owned(),
- message: message.to_owned(),
- }
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn relay_malformed(relay_url: &str) -> RadrootsRelayFetchItem {
- RadrootsRelayFetchItem::Event {
- relay_url: relay_url.to_owned(),
- raw_json: "{".to_owned(),
- observed_at_ms: 4_999,
- }
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_propose_idempotency_replays_same_payload_and_conflicts_different_payload() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
- let storage_paths = buyer_sdk.storage_paths().expect("storage paths");
- let store = RadrootsEventStore::open_file(&storage_paths.runtime_path)
- .await
- .expect("event store");
- let outbox = RadrootsOutbox::open_file(&storage_paths.runtime_path)
- .await
- .expect("outbox");
- let request = trade_propose_request(
- "trade-product-idempotent",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020e")
- .expect("idempotency");
-
- let first = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(request.clone())
- .await
- .expect("first proposal"),
- );
- let replay = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(request)
- .await
- .expect("replay proposal"),
- );
- assert_eq!(replay.state, SdkMutationState::AlreadyQueued);
- assert_eq!(replay.signed_event_id, first.signed_event_id);
- assert_eq!(replay.outbox_event_id, first.outbox_event_id);
- assert!(replay.workflow.idempotency.replayed_existing_operation);
- assert!(
- replay
- .workflow
- .idempotency
- .safe_to_retry_with_same_idempotency_key
- );
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store summary")
- .total_events,
- 1
- );
- assert_eq!(
- outbox
- .status_summary(i64::MAX)
- .await
- .expect("outbox summary")
- .total_events,
- 1
- );
-
- let conflict = buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-idempotent-conflict",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020e")
- .expect("conflict idempotency"),
- )
- .await
- .expect_err("different payload conflict");
-
- assert!(matches!(
- conflict,
- RadrootsSdkError::IdempotencyConflict {
- ref operation_kind,
- ..
- } if operation_kind == TRADE_SUBMIT_OPERATION_KIND
- ));
- assert_eq!(conflict.code(), "idempotency_conflict");
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store summary")
- .total_events,
- 1
- );
- assert_eq!(
- outbox
- .status_summary(i64::MAX)
- .await
- .expect("outbox summary")
- .total_events,
- 1
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_propose_requires_public_note_privacy_confirmation() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
-
- let missing_confirmation = buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-propose-public-note",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_public_note("please leave at the community table"),
- )
- .await
- .expect_err("missing public note confirmation");
-
- let RadrootsSdkError::PrivacyPreflight {
- operation,
- status,
- fields,
- } = &missing_confirmation
- else {
- panic!("expected privacy preflight error");
- };
- assert_eq!(operation, "trade.propose");
- assert_eq!(
- *status,
- PrivacyPreflightStatus::ExplicitConfirmationRequired
- );
- assert_eq!(fields, &[ProductSensitivityField::PublicButSensitiveNotes]);
- assert_eq!(missing_confirmation.code(), "privacy_preflight");
- assert_eq!(
- missing_confirmation.detail_json()["detail"]["fields"][0],
- "public_but_sensitive_notes"
- );
-
- let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("event store");
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_propose_publishes_public_note_after_confirmation() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
-
- let receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "01890f0e-6c00-7000-8000-00000000020f",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_public_note("please leave at the community table")
- .with_privacy_confirmation(public_note_confirmation())
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020f")
- .expect("proposal idempotency"),
- )
- .await
- .expect("confirmed proposal"),
- );
-
- assert_eq!(
- receipt.order_id.as_str(),
- "01890f0e-6c00-7000-8000-00000000020f"
- );
- let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("event store");
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_propose_blocks_sensitive_fulfillment_note_even_when_confirmed() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
-
- let forbidden = buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-propose-sensitive-note",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_public_note("pickup address is 123 Farm Lane")
- .with_privacy_confirmation(public_note_confirmation()),
- )
- .await
- .expect_err("forbidden public fulfillment details");
-
- let RadrootsSdkError::PrivacyPreflight {
- operation,
- status,
- fields,
- } = &forbidden
- else {
- panic!("expected privacy preflight error");
- };
- assert_eq!(operation, "trade.propose");
- assert_eq!(*status, PrivacyPreflightStatus::ForbiddenPublicFields);
- assert!(fields.contains(&ProductSensitivityField::SensitiveFulfillmentDetails));
- let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("event store");
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_decline_requires_public_reason_privacy_confirmation() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-privacy-decline",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000210")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
- let seller_sdk = directory_sdk_with_signer(storage_root.as_path(), SELLER_SECRET_KEY_HEX).await;
-
- let missing_confirmation = seller_sdk
- .trades()
- .seller()
- .decline_trade(TradeDeclineRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- "sold elsewhere",
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- ))
- .await
- .expect_err("missing public note confirmation");
-
- let RadrootsSdkError::PrivacyPreflight {
- operation,
- status,
- fields,
- } = &missing_confirmation
- else {
- panic!("expected privacy preflight error");
- };
- assert_eq!(operation, "trade.decline");
- assert_eq!(
- *status,
- PrivacyPreflightStatus::ExplicitConfirmationRequired
- );
- assert_eq!(fields, &[ProductSensitivityField::PublicButSensitiveNotes]);
- assert_eq!(missing_confirmation.code(), "privacy_preflight");
- assert_eq!(
- missing_confirmation.detail_json()["detail"]["fields"][0],
- "public_but_sensitive_notes"
- );
- let store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("event store");
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
-
- let decline_receipt = expect_enqueued(
- seller_sdk
- .trades()
- .seller()
- .decline_trade(
- TradeDeclineRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- "sold elsewhere",
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .with_privacy_confirmation(public_note_confirmation())
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000211")
- .expect("decline idempotency"),
- )
- .await
- .expect("confirmed decline"),
- );
- let status = seller_sdk
- .trades()
- .status(TradeStatusRequest::new(propose_receipt.locator))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::Declined);
- assert_eq!(
- status.decision_event_id,
- Some(decline_receipt.signed_event_id)
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_cancel_blocks_sensitive_fulfillment_reason_before_mutation() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-privacy-cancel",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000212")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
-
- let forbidden = buyer_sdk
- .trades()
- .buyer()
- .cancel_trade(
- TradeCancelRequest::new(
- buyer_actor(),
- propose_receipt.locator,
- "pickup address is 123 Farm Lane",
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .with_privacy_confirmation(public_note_confirmation()),
- )
- .await
- .expect_err("forbidden public fulfillment details");
-
- let RadrootsSdkError::PrivacyPreflight {
- operation,
- status,
- fields,
- } = &forbidden
- else {
- panic!("expected privacy preflight error");
- };
- assert_eq!(operation, "trade.cancel");
- assert_eq!(*status, PrivacyPreflightStatus::ForbiddenPublicFields);
- assert!(fields.contains(&ProductSensitivityField::SensitiveFulfillmentDetails));
- let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("event store");
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
-}
-
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_cancel_enqueues_with_locator_and_updates_status() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let storage_root = tempdir.path().join("sdk");
- let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "01890f0e-6c00-7000-8000-000000000214",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000213")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
-
- let cancellation = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .cancel_trade(
- TradeCancelRequest::new(
- buyer_actor(),
- propose_receipt.locator.clone(),
- "changed plan",
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .with_privacy_confirmation(public_note_confirmation())
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000214")
- .expect("cancel idempotency"),
- )
- .await
- .expect("cancel trade"),
- );
-
- assert_eq!(cancellation.locator, propose_receipt.locator);
- assert_eq!(cancellation.root_event_id, propose_receipt.signed_event_id);
- assert_eq!(
- cancellation.previous_event_id,
- propose_receipt.signed_event_id
- );
- let status = buyer_sdk
- .trades()
- .status(TradeStatusRequest::new(propose_receipt.locator))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::Cancelled);
- assert_eq!(
- status.cancellation_event_id,
- Some(cancellation.signed_event_id)
- );
- assert_eq!(status.next_action, TradeStatusNextActionKind::Terminal);
-}
-
-#[cfg(feature = "signer-adapters")]
-#[tokio::test]
-async fn trade_product_propose_dry_run_returns_plan_without_local_side_effects() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let outcome = sdk
- .trades()
- .buyer()
- .propose_trade(trade_propose_request(
- "trade-product-dry-run",
- PublishMode::DryRun,
- SatisfactionPolicy::NoWait,
- ))
- .await
- .expect("dry-run proposal");
- let plan = match outcome {
- TradeMutationOutcome::DryRun { plan } => plan,
- TradeMutationOutcome::Enqueued { .. } => panic!("expected dry-run outcome"),
- TradeMutationOutcome::Published { .. } => panic!("expected dry-run outcome"),
- };
-
- assert_eq!(plan.order_id.as_str(), "trade-product-dry-run");
- assert_eq!(plan.frozen_draft.kind_u32(), KIND_ORDER_REQUEST);
- assert_eq!(plan.expected_event_id, plan.workflow.expected_event_id);
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("outbox");
- assert!(
- outbox
- .claim_next_ready_event("worker", "claim", 2_000, 1_700_000_000_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-fn order_decision(raw_order_id: &str) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: order_id(raw_order_id),
- listing_addr: listing_address(),
- buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"),
- seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"),
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- },
- }
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn signed_raw_validation_receipt_event(
- raw_order_id: &str,
- listing_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- target_event_id: &RadrootsEventId,
- created_at: u32,
-) -> nostr::Event {
- signed_raw_event(
- SERVICE_SECRET_KEY_HEX,
- created_at,
- validation_receipt_wire_parts(
- raw_order_id,
- listing_event_id,
- root_event_id,
- target_event_id,
- ),
- )
-}
-
-fn signed_raw_sp1_validation_receipt_event(
- raw_order_id: &str,
- listing_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- target_event_id: &RadrootsEventId,
- created_at: u32,
-) -> nostr::Event {
- signed_raw_event(
- SERVICE_SECRET_KEY_HEX,
- created_at,
- validation_receipt_wire_parts_with_proof(
- raw_order_id,
- listing_event_id,
- root_event_id,
- target_event_id,
- RadrootsValidationReceiptProofSystem::Sp1Core,
- ),
- )
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn validation_receipt_wire_parts(
- raw_order_id: &str,
- listing_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- target_event_id: &RadrootsEventId,
-) -> RadrootsNip01EventWireParts {
- validation_receipt_wire_parts_with_proof(
- raw_order_id,
- listing_event_id,
- root_event_id,
- target_event_id,
- RadrootsValidationReceiptProofSystem::None,
- )
-}
-
-fn validation_receipt_wire_parts_with_proof(
- raw_order_id: &str,
- listing_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- target_event_id: &RadrootsEventId,
- proof_system: RadrootsValidationReceiptProofSystem,
-) -> RadrootsNip01EventWireParts {
- let proof = match proof_system {
- RadrootsValidationReceiptProofSystem::None => RadrootsValidationReceiptProof {
- inline_proof_base64: None,
- mode: None,
- program_hash: None,
- proof_reference: None,
- system: RadrootsValidationReceiptProofSystem::None,
- verifying_key_hash: None,
- },
- RadrootsValidationReceiptProofSystem::Sp1Core => RadrootsValidationReceiptProof {
- inline_proof_base64: Some("AQID".to_owned()),
- mode: Some("core".to_owned()),
- program_hash: Some(hash32('a')),
- proof_reference: None,
- system: RadrootsValidationReceiptProofSystem::Sp1Core,
- verifying_key_hash: Some(hash32('b')),
- },
- RadrootsValidationReceiptProofSystem::Sp1Compressed => RadrootsValidationReceiptProof {
- inline_proof_base64: Some("AQID".to_owned()),
- mode: Some("compressed".to_owned()),
- program_hash: Some(hash32('a')),
- proof_reference: None,
- system: RadrootsValidationReceiptProofSystem::Sp1Compressed,
- verifying_key_hash: Some(hash32('b')),
- },
- RadrootsValidationReceiptProofSystem::Sp1Groth16 => RadrootsValidationReceiptProof {
- inline_proof_base64: Some("AQID".to_owned()),
- mode: Some("groth16".to_owned()),
- program_hash: Some(hash32('a')),
- proof_reference: None,
- system: RadrootsValidationReceiptProofSystem::Sp1Groth16,
- verifying_key_hash: Some(hash32('b')),
- },
- RadrootsValidationReceiptProofSystem::Sp1Plonk => RadrootsValidationReceiptProof {
- inline_proof_base64: Some("AQID".to_owned()),
- mode: Some("plonk".to_owned()),
- program_hash: Some(hash32('a')),
- proof_reference: None,
- system: RadrootsValidationReceiptProofSystem::Sp1Plonk,
- verifying_key_hash: Some(hash32('b')),
- },
- };
- let receipt = RadrootsTradeValidationReceipt {
- changed_records_root: hash32('6'),
- domain: "radroots.receipt".to_owned(),
- error_bitmap: "0x00000000000000000000000000000000".to_owned(),
- event_set_root: hash32('c'),
- new_state_root: hash32('4'),
- previous_state_root: hash32('3'),
- proof,
- public_values_hash: validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#),
- receipt_type: RadrootsValidationReceiptType::TradeTransition,
- result: RadrootsValidationReceiptResult::Valid,
- statement: RadrootsValidationReceiptStatement {
- listing_event_id: listing_event_id.as_str().to_owned(),
- root_event_id: root_event_id.as_str().to_owned(),
- target_event_id: target_event_id.as_str().to_owned(),
- validator_set_addr: validator_set_address_from_str(validator_set_addr_raw(
- public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX).as_str(),
- ))
- .expect("validator set address"),
- validator_set_event_id: validator_set_event_id().into_string(),
- statement_type: RadrootsValidationReceiptType::TradeTransition,
- },
- version: 1,
- };
- validation_receipt_event_build(raw_order_id, &receipt).expect("receipt event")
-}
-
-fn public_key_hex_for_secret(secret_key_hex: &str) -> String {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- RadrootsNostrKeys::new(secret_key).public_key().to_hex()
-}
-
-async fn insert_perf_non_trade_events(store: &RadrootsEventStore, base: i64, count: i64) {
- let mut inserted = 0;
- while inserted < count {
- let batch = (count - inserted).min(1_000);
- sqlx::query(
- "WITH RECURSIVE seq(n) AS (SELECT 0 UNION ALL SELECT n + 1 FROM seq WHERE n + 1 < ?)
- INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms)
- SELECT lower(printf('%064x', ? + n)), ?, 1700000000 + n, 1, json_array(), '{}', ?, '{}', 'verified', 'unsupported_kind', NULL, NULL, 0, 1700000000000 + n, 1700000000000 + n FROM seq",
- )
- .bind(batch)
- .bind(base + inserted)
- .bind(SELLER_PUBLIC_KEY_HEX)
- .bind(perf_sig())
- .execute(store.pool())
- .await
- .expect("non-trade perf seed");
- inserted += batch;
- }
-}
-
-async fn insert_perf_trade_background_events(store: &RadrootsEventStore, base: i64, count: i64) {
- let mut inserted = 0;
- while inserted < count {
- let batch = (count - inserted).min(1_000);
- sqlx::query(
- "WITH RECURSIVE seq(n) AS (SELECT 0 UNION ALL SELECT n + 1 FROM seq WHERE n + 1 < ?)
- INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms)
- SELECT lower(printf('%064x', ? + n)), ?, 1700000000 + n, ?, json_array(json_array('d', 'perf-bg-' || printf('%06d', ? + n))), '{}', ?, '{}', 'verified', 'supported', 'radroots.order.request.v1', 'regular', 1, 1700000000000 + n, 1700000000000 + n FROM seq",
- )
- .bind(batch)
- .bind(base + inserted)
- .bind(BUYER_PUBLIC_KEY_HEX)
- .bind(i64::from(KIND_ORDER_REQUEST))
- .bind(base + inserted)
- .bind(perf_sig())
- .execute(store.pool())
- .await
- .expect("trade perf event seed");
- sqlx::query(
- "WITH RECURSIVE seq(n) AS (SELECT 0 UNION ALL SELECT n + 1 FROM seq WHERE n + 1 < ?)
- INSERT INTO event_envelope_tags(event_id, tag_index, tag_name, tag_value, tag_json, contract_semantic, contract_value_type, relay_indexed)
- SELECT lower(printf('%064x', ? + n)), 0, 'd', 'perf-bg-' || printf('%06d', ? + n), json_array('d', 'perf-bg-' || printf('%06d', ? + n)), NULL, NULL, 0 FROM seq",
- )
- .bind(batch)
- .bind(base + inserted)
- .bind(base + inserted)
- .bind(base + inserted)
- .execute(store.pool())
- .await
- .expect("trade perf tag seed");
- inserted += batch;
- }
-}
-
-async fn ingest_status_noise_events(
- store: &RadrootsEventStore,
- non_trade_count: i64,
- trade_count: i64,
-) {
- for index in 0..non_trade_count {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(signed_status_noise_post_event(
- index,
- 32_000 + index as u32,
- )),
- 1_700_200_000_000 + index,
- ))
- .await
- .expect("non-trade status noise ingest");
- }
-
- for index in 0..trade_count {
- let order_id = format!("status-noise-background-{index:03}");
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(signed_order_request_event(
- &order_id,
- 33_000 + index as u32,
- )),
- 1_700_200_100_000 + index,
- ))
- .await
- .expect("trade status noise ingest");
- }
-}
-
-fn perf_sig() -> String {
- "0".repeat(128)
-}
-
-fn hash32(ch: char) -> String {
- format!("0x{}", ch.to_string().repeat(64))
-}
-
-fn signed_event(
- secret_key_hex: &str,
- created_at: u32,
- parts: RadrootsNip01EventWireParts,
-) -> RadrootsEventEnvelope {
- let event = signed_raw_event(secret_key_hex, created_at, parts);
- radroots_event_from_nostr(&event)
-}
-
-fn signed_raw_event(
- secret_key_hex: &str,
- created_at: u32,
- parts: RadrootsNip01EventWireParts,
-) -> nostr::Event {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
- radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
- .expect("event builder")
- .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at)))
- .sign_with_keys(&keys)
- .expect("signed event")
-}
-
-fn signed_order_request_event(raw_order_id: &str, created_at: u32) -> RadrootsEventEnvelope {
- let draft = radroots_event_codec::order::order_request_event_build(
- &listing_event_ptr(),
- &order_request(raw_order_id),
- )
- .expect("request draft");
- signed_event(BUYER_SECRET_KEY_HEX, created_at, draft)
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn signed_raw_order_request_event(raw_order_id: &str, created_at: u32) -> nostr::Event {
- let draft = radroots_event_codec::order::order_request_event_build(
- &listing_event_ptr(),
- &order_request(raw_order_id),
- )
- .expect("request draft");
- signed_raw_event(BUYER_SECRET_KEY_HEX, created_at, draft)
-}
-
-fn signed_order_decision_event(
- raw_order_id: &str,
- root_event_id: &RadrootsEventId,
- created_at: u32,
-) -> RadrootsEventEnvelope {
- let draft = radroots_event_codec::order::order_decision_event_build(
- root_event_id,
- root_event_id,
- &order_decision(raw_order_id),
- )
- .expect("decision draft");
- signed_event(SELLER_SECRET_KEY_HEX, created_at, draft)
-}
-
-fn signed_status_noise_post_event(index: i64, created_at: u32) -> RadrootsEventEnvelope {
- signed_event(
- SELLER_SECRET_KEY_HEX,
- created_at,
- RadrootsNip01EventWireParts {
- kind: KIND_POST,
- content: format!("local status noise {index}"),
- tags: Vec::new(),
- },
- )
-}
-
-fn signed_non_order_event(created_at: u32) -> RadrootsEventEnvelope {
- signed_event(
- SELLER_SECRET_KEY_HEX,
- created_at,
- RadrootsNip01EventWireParts {
- kind: KIND_LISTING,
- content: "{}".to_owned(),
- tags: vec![vec!["d".to_owned(), "not-an-order".to_owned()]],
- },
- )
-}
-
-#[tokio::test]
-async fn order_evidence_ingest_stores_lifecycle_evidence_for_projection() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-evidence-ingest", 39);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let decision_event =
- signed_order_decision_event("order-evidence-ingest", &request_event_id, 40);
-
- let request_receipt = sdk
- .trades()
- .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
- request_event.clone(),
- )))
- .await
- .expect("request evidence");
- assert_eq!(request_receipt.order_id.as_str(), "order-evidence-ingest");
- assert_eq!(request_receipt.event_kind, KIND_ORDER_REQUEST);
- assert_eq!(request_receipt.local_event_seq, 1);
- assert!(request_receipt.inserted);
-
- let decision_receipt = sdk
- .trades()
- .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
- decision_event.clone(),
- )))
- .await
- .expect("decision evidence");
- assert_eq!(decision_receipt.order_id.as_str(), "order-evidence-ingest");
- assert_eq!(decision_receipt.event_kind, KIND_ORDER_DECISION);
- assert_eq!(decision_receipt.local_event_seq, 2);
- assert!(decision_receipt.inserted);
-
- let duplicate_receipt = sdk
- .trades()
- .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
- decision_event,
- )))
- .await
- .expect("duplicate decision evidence");
- assert_eq!(duplicate_receipt.local_event_seq, 2);
- assert!(!duplicate_receipt.inserted);
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 2
- );
-
- let status = sdk
- .trades()
- .status(status_request("order-evidence-ingest"))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
- assert_eq!(status.event_count, 2);
- assert_eq!(
- status
- .decision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(decision_receipt.event_id.as_str())
- );
-}
-
-#[tokio::test]
-async fn order_evidence_ingest_rejects_non_order_events() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let error = sdk
- .trades()
- .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
- signed_non_order_event(41),
- )))
- .await
- .expect_err("non order event");
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-#[tokio::test]
-async fn order_request_evidence_ingest_rejects_non_request_events() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let root_event_id = deterministic_event_id("non-request-root");
- let decision_event = signed_order_decision_event("non-request-root", &root_event_id, 40);
-
- let error = sdk
- .trades()
- .ingest_request_evidence(TradeRequestEvidenceIngestRequest::new(
- signed_event_from_envelope(decision_event),
- ))
- .await
- .expect_err("non request event");
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-#[tokio::test]
-async fn order_status_returns_not_found_for_missing_local_order() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let request = status_request("order-1");
-
- assert_eq!(request.limit, TRADE_STATUS_DEFAULT_LIMIT);
-
- let receipt = sdk.trades().status(request).await.expect("status");
-
- assert!(!receipt.found);
- assert_eq!(receipt.order_id.as_str(), "order-1");
- assert_eq!(receipt.source, SdkTradeStatusSource::LocalOnly);
- assert_eq!(receipt.event_count, 0);
- assert_eq!(receipt.limit_applied, TRADE_STATUS_DEFAULT_LIMIT);
- assert!(receipt.event_ids.is_empty());
- assert_eq!(receipt.status, TradeStatusKind::Missing);
- assert!(receipt.listing_addr.is_none());
- assert!(receipt.buyer_pubkey.is_none());
- assert!(receipt.seller_pubkey.is_none());
- assert!(receipt.economics.is_none());
- assert_eq!(receipt.next_action, TradeStatusNextActionKind::NoLocalOrder);
- assert_eq!(receipt.evidence.event_count, 0);
- assert_eq!(receipt.evidence.limit_applied, TRADE_STATUS_DEFAULT_LIMIT);
- assert!(!receipt.evidence.has_request);
- assert!(!receipt.evidence.has_issues);
- assert!(!receipt.eligibility.can_decide);
- assert!(!receipt.eligibility.can_cancel);
- assert!(receipt.issues.is_empty());
-}
-
-#[tokio::test]
-async fn order_status_query_uses_indexed_order_id_under_background_event_noise() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- ingest_status_noise_events(
- &store,
- STATUS_NOISE_NON_TRADE_EVENTS,
- STATUS_NOISE_TRADE_BACKGROUND_EVENTS,
- )
- .await;
-
- let request_event = signed_order_request_event("order-status-noise-active", 31_000);
- let request_event_id = request_event.id().clone();
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event),
- 1_700_200_000_000,
- ))
- .await
- .expect("active order ingest");
-
- let status = sdk
- .trades()
- .status(status_request("order-status-noise-active"))
- .await
- .expect("status");
- let summary = store.status_summary().await.expect("status summary");
-
- assert_eq!(
- summary.total_events,
- STATUS_NOISE_NON_TRADE_EVENTS + STATUS_NOISE_TRADE_BACKGROUND_EVENTS + 1
- );
- assert_eq!(status.status, TradeStatusKind::Requested);
- assert_eq!(status.event_count, 1);
- assert_eq!(status.limit_applied, TRADE_STATUS_DEFAULT_LIMIT);
- assert_eq!(status.event_ids, vec![request_event_id]);
- assert_eq!(
- status.next_action,
- TradeStatusNextActionKind::AwaitSellerDecision
- );
-}
-
-#[tokio::test]
-async fn order_status_rejects_invalid_limits_before_querying() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
-
- let zero = sdk
- .trades()
- .status(status_request("order-1").with_limit(0))
- .await
- .expect_err("zero limit");
- let too_large = sdk
- .trades()
- .status(status_request("order-1").with_limit(TRADE_STATUS_MAX_LIMIT + 1))
- .await
- .expect_err("too large");
-
- assert!(matches!(
- zero,
- RadrootsSdkError::TradeStatusLimitInvalid {
- limit: 0,
- min: 1,
- max: TRADE_STATUS_MAX_LIMIT
- }
- ));
- assert!(matches!(
- too_large,
- RadrootsSdkError::TradeStatusLimitInvalid {
- limit,
- min: 1,
- max: TRADE_STATUS_MAX_LIMIT
- } if limit == TRADE_STATUS_MAX_LIMIT + 1
- ));
-}
-
-#[test]
-fn order_status_parse_rejects_invalid_order_ids() {
- let error = TradeStatusRequest::parse("bad order id").expect_err("invalid order id");
-
- assert!(matches!(error, RadrootsSdkError::InvalidTradeId { .. }));
-}
-
-#[test]
-fn order_status_parse_accepts_root_specific_selectors() {
- let root_event_id = deterministic_event_id("order-status-root-selector");
- let request = TradeStatusRequest::parse(&format!("order-1@{}", root_event_id.as_str()))
- .expect("root-specific status request");
-
- assert_eq!(request.locator.order_id().as_str(), "order-1");
- assert_eq!(
- request
- .locator
- .root_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(root_event_id.as_str())
- );
- assert_eq!(
- TradeStatusRequest::locator_selector(&request.locator),
- format!("order-1@{}", root_event_id.as_str())
- );
-}
-
-#[test]
-fn order_status_parse_rejects_malformed_root_selectors() {
- for selector in ["order-1@", "@aaaaaaaa", "order-1@aaaaaaaa@bbbbbbbb"] {
- let error = TradeStatusRequest::parse(selector).expect_err("malformed root selector");
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- }
-}
-
-#[tokio::test]
-async fn order_status_contract_dtos_serialize_deterministically() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let request = status_request("order-1").with_limit(25);
- let request_json = serde_json::to_value(&request).expect("request json");
- assert_struct_serialize_error_paths(&request, 4);
-
- assert_eq!(
- request_json,
- serde_json::json!({
- "locator": {
- "trade_id": "order-1",
- "root_event_id": null,
- "listing_addr": null,
- "buyer_pubkey": null,
- "seller_pubkey": null
- },
- "limit": 25,
- "source": "local_only",
- "validation_trust_policy": {
- "validator_set": null,
- "validator_set_addr": null,
- "validator_set_event_id": null,
- "require_cryptographic_proof": false
- }
- })
- );
-
- let receipt = sdk.trades().status(request).await.expect("status");
- let receipt_json = serde_json::to_value(&receipt).expect("receipt json");
-
- assert_eq!(receipt_json["source"], "local_only");
- assert_eq!(receipt_json["status"], "missing");
- assert_eq!(receipt_json["locator"], request_json["locator"]);
- assert_eq!(receipt_json["order_id"], "order-1");
- assert_eq!(receipt_json["root_event_id"], serde_json::Value::Null);
- assert_eq!(receipt_json["ambiguity_candidates"], serde_json::json!([]));
- assert_eq!(receipt_json["listing_addr"], serde_json::Value::Null);
- assert_eq!(receipt_json["buyer_pubkey"], serde_json::Value::Null);
- assert_eq!(receipt_json["seller_pubkey"], serde_json::Value::Null);
- assert_eq!(
- receipt_json["rhi_receipt_event_id"],
- serde_json::Value::Null
- );
- assert_eq!(receipt_json["economics"], serde_json::Value::Null);
- assert_eq!(receipt_json["validation_trust"], serde_json::Value::Null);
- assert_eq!(receipt_json["online_evidence"], serde_json::Value::Null);
- assert_eq!(receipt_json["next_action"], "no_local_order");
- assert_eq!(receipt_json["evidence"]["event_count"], 0);
- assert_eq!(receipt_json["evidence"]["limit_applied"], 25);
- assert_eq!(receipt_json["evidence"]["has_request"], false);
- assert_eq!(receipt_json["evidence"]["has_validation_receipt"], false);
- assert_eq!(receipt_json["eligibility"]["can_decide"], false);
- assert_eq!(receipt_json["eligibility"]["can_cancel"], false);
-
- let issue = SdkTradeStatusIssue {
- kind: SdkTradeStatusIssueKind::DecisionPayloadInvalid,
- event_ids: vec![deterministic_event_id("issue-event")],
- };
- assert_eq!(issue.code(), "decision_payload_invalid");
- assert_struct_serialize_error_paths(&issue, 3);
- assert_eq!(
- serde_json::to_value(issue).expect("issue json"),
- serde_json::json!({
- "code": "decision_payload_invalid",
- "kind": "decision_payload_invalid",
- "event_ids": [deterministic_event_id("issue-event")]
- })
- );
-}
-
-#[test]
-fn order_status_issue_mapping_preserves_kind_codes_and_event_ids() {
- macro_rules! single_issue {
- ($variant:ident, $kind:ident, $code:literal) => {{
- let event_id = deterministic_event_id($code);
- (
- RadrootsOrderIssue::$variant {
- event_id: event_id.clone(),
- },
- SdkTradeStatusIssueKind::$kind,
- $code,
- vec![event_id],
- )
- }};
- }
-
- macro_rules! multi_issue {
- ($variant:ident, $kind:ident, $code:literal) => {{
- let event_ids = vec![
- deterministic_event_id(concat!($code, "-a")),
- deterministic_event_id(concat!($code, "-b")),
- ];
- (
- RadrootsOrderIssue::$variant {
- event_ids: event_ids.clone(),
- },
- SdkTradeStatusIssueKind::$kind,
- $code,
- event_ids,
- )
- }};
- }
-
- let cases = vec![
- (
- RadrootsOrderIssue::MissingRequest,
- SdkTradeStatusIssueKind::MissingRequest,
- "missing_request",
- Vec::new(),
- ),
- multi_issue!(MultipleRequests, MultipleRequests, "multiple_requests"),
- single_issue!(
- RequestPayloadInvalid,
- RequestPayloadInvalid,
- "request_payload_invalid"
- ),
- single_issue!(
- RequestOrderIdMismatch,
- RequestOrderIdMismatch,
- "request_order_id_mismatch"
- ),
- single_issue!(
- RequestAuthorMismatch,
- RequestAuthorMismatch,
- "request_author_mismatch"
- ),
- single_issue!(
- RequestListingAddressInvalid,
- RequestListingAddressInvalid,
- "request_listing_address_invalid"
- ),
- single_issue!(
- RequestSellerListingMismatch,
- RequestSellerListingMismatch,
- "request_seller_listing_mismatch"
- ),
- single_issue!(
- DecisionPayloadInvalid,
- DecisionPayloadInvalid,
- "decision_payload_invalid"
- ),
- single_issue!(
- DecisionOrderIdMismatch,
- DecisionOrderIdMismatch,
- "decision_order_id_mismatch"
- ),
- single_issue!(
- DecisionAuthorMismatch,
- DecisionAuthorMismatch,
- "decision_author_mismatch"
- ),
- single_issue!(
- DecisionCounterpartyMismatch,
- DecisionCounterpartyMismatch,
- "decision_counterparty_mismatch"
- ),
- single_issue!(
- DecisionBuyerMismatch,
- DecisionBuyerMismatch,
- "decision_buyer_mismatch"
- ),
- single_issue!(
- DecisionSellerMismatch,
- DecisionSellerMismatch,
- "decision_seller_mismatch"
- ),
- single_issue!(
- DecisionListingAddressInvalid,
- DecisionListingAddressInvalid,
- "decision_listing_address_invalid"
- ),
- single_issue!(
- DecisionListingMismatch,
- DecisionListingMismatch,
- "decision_listing_mismatch"
- ),
- single_issue!(
- DecisionRootMismatch,
- DecisionRootMismatch,
- "decision_root_mismatch"
- ),
- single_issue!(
- DecisionPreviousMismatch,
- DecisionPreviousMismatch,
- "decision_previous_mismatch"
- ),
- single_issue!(
- DecisionMissingInventoryCommitments,
- DecisionMissingInventoryCommitments,
- "decision_missing_inventory_commitments"
- ),
- single_issue!(
- DecisionInventoryCommitmentMismatch,
- DecisionInventoryCommitmentMismatch,
- "decision_inventory_commitment_mismatch"
- ),
- single_issue!(
- DecisionMissingReason,
- DecisionMissingReason,
- "decision_missing_reason"
- ),
- multi_issue!(
- ConflictingDecisions,
- ConflictingDecisions,
- "conflicting_decisions"
- ),
- single_issue!(
- CancellationWithoutCancellableOrder,
- CancellationWithoutCancellableOrder,
- "cancellation_without_cancellable_order"
- ),
- single_issue!(
- CancellationPayloadInvalid,
- CancellationPayloadInvalid,
- "cancellation_payload_invalid"
- ),
- single_issue!(
- CancellationOrderIdMismatch,
- CancellationOrderIdMismatch,
- "cancellation_order_id_mismatch"
- ),
- single_issue!(
- CancellationAuthorMismatch,
- CancellationAuthorMismatch,
- "cancellation_author_mismatch"
- ),
- single_issue!(
- CancellationCounterpartyMismatch,
- CancellationCounterpartyMismatch,
- "cancellation_counterparty_mismatch"
- ),
- single_issue!(
- CancellationBuyerMismatch,
- CancellationBuyerMismatch,
- "cancellation_buyer_mismatch"
- ),
- single_issue!(
- CancellationSellerMismatch,
- CancellationSellerMismatch,
- "cancellation_seller_mismatch"
- ),
- single_issue!(
- CancellationListingAddressInvalid,
- CancellationListingAddressInvalid,
- "cancellation_listing_address_invalid"
- ),
- single_issue!(
- CancellationListingMismatch,
- CancellationListingMismatch,
- "cancellation_listing_mismatch"
- ),
- single_issue!(
- CancellationRootMismatch,
- CancellationRootMismatch,
- "cancellation_root_mismatch"
- ),
- single_issue!(
- CancellationPreviousMismatch,
- CancellationPreviousMismatch,
- "cancellation_previous_mismatch"
- ),
- multi_issue!(ForkedLifecycle, ForkedLifecycle, "forked_lifecycle"),
- single_issue!(
- ValidationReceiptWithoutPendingAgreement,
- ValidationReceiptWithoutPendingAgreement,
- "validation_receipt_without_pending_agreement"
- ),
- single_issue!(
- ValidationReceiptOrderIdMismatch,
- ValidationReceiptOrderIdMismatch,
- "validation_receipt_order_id_mismatch"
- ),
- single_issue!(
- ValidationReceiptTypeMismatch,
- ValidationReceiptTypeMismatch,
- "validation_receipt_type_mismatch"
- ),
- single_issue!(
- ValidationReceiptRootMismatch,
- ValidationReceiptRootMismatch,
- "validation_receipt_root_mismatch"
- ),
- single_issue!(
- ValidationReceiptTargetMismatch,
- ValidationReceiptTargetMismatch,
- "validation_receipt_target_mismatch"
- ),
- single_issue!(
- ValidationReceiptListingMismatch,
- ValidationReceiptListingMismatch,
- "validation_receipt_listing_mismatch"
- ),
- multi_issue!(
- ConflictingValidationReceipts,
- ConflictingValidationReceipts,
- "conflicting_validation_receipts"
- ),
- {
- let event_id = deterministic_event_id("deterministic_validation_failure");
- (
- RadrootsOrderIssue::DeterministicValidationFailure {
- event_id: event_id.clone(),
- reason: "fixture validation failed".to_owned(),
- },
- SdkTradeStatusIssueKind::DeterministicValidationFailure,
- "deterministic_validation_failure",
- vec![event_id],
- )
- },
- {
- let expected_event_id = deterministic_event_id("stale_listing_event_expected");
- let current_event_id = deterministic_event_id("stale_listing_event_current");
- (
- RadrootsOrderIssue::StaleListingEvent {
- expected_event_id: expected_event_id.clone(),
- current_event_id: current_event_id.clone(),
- },
- SdkTradeStatusIssueKind::StaleListingEvent,
- "stale_listing_event",
- vec![expected_event_id, current_event_id],
- )
- },
- ];
-
- for (issue, expected_kind, expected_code, expected_event_ids) in cases {
- let sdk_issue = SdkTradeStatusIssue::from(issue);
-
- assert_eq!(sdk_issue.kind, expected_kind);
- assert_eq!(sdk_issue.code(), expected_code);
- assert_eq!(sdk_issue.event_ids, expected_event_ids);
- }
-}
-
-#[tokio::test]
-async fn order_status_projects_local_request_and_decision_events() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-1", 20);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let decision_event = signed_order_decision_event("order-1", &request_event_id, 21);
-
- for (event, observed_at_ms) in [
- (request_event.clone(), 2_000),
- (decision_event.clone(), 2_100),
- ] {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- observed_at_ms,
- ))
- .await
- .expect("ingest");
- }
-
- let receipt = sdk
- .trades()
- .status(status_request("order-1").with_limit(1_000))
- .await
- .expect("status");
-
- assert!(receipt.found);
- assert_eq!(receipt.order_id.as_str(), "order-1");
- assert_eq!(receipt.source, SdkTradeStatusSource::LocalOnly);
- assert_eq!(receipt.event_count, 2);
- assert_eq!(receipt.limit_applied, 1_000);
- assert_eq!(
- receipt
- .event_ids
- .iter()
- .map(RadrootsEventId::as_str)
- .collect::<Vec<_>>(),
- vec![request_event.id_str(), decision_event.id_str()]
- );
- assert_eq!(receipt.status, TradeStatusKind::AgreedPendingValidation);
- assert_eq!(
- receipt
- .request_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(request_event.id_str())
- );
- assert_eq!(
- receipt
- .decision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(decision_event.id_str())
- );
- assert_eq!(
- receipt.last_event_id.as_ref().map(RadrootsEventId::as_str),
- Some(decision_event.id_str())
- );
- assert_eq!(receipt.listing_addr, Some(listing_address()));
- assert_eq!(
- receipt.buyer_pubkey.as_ref().map(ToString::to_string),
- Some(BUYER_PUBLIC_KEY_HEX.to_owned())
- );
- assert_eq!(
- receipt.seller_pubkey.as_ref().map(ToString::to_string),
- Some(SELLER_PUBLIC_KEY_HEX.to_owned())
- );
- assert_eq!(receipt.economics, Some(economics()));
- assert!(receipt.issues.is_empty());
- assert!(!receipt.lifecycle_terminal);
- assert_eq!(
- receipt.next_action,
- TradeStatusNextActionKind::AwaitValidation
- );
- assert_eq!(receipt.evidence.event_count, 2);
- assert!(receipt.evidence.has_request);
- assert!(receipt.evidence.has_decision);
- assert!(receipt.evidence.has_agreement);
- assert!(!receipt.evidence.has_issues);
- assert!(!receipt.eligibility.can_decide);
- assert!(!receipt.eligibility.can_cancel);
-}
-
-#[tokio::test]
-async fn order_status_reports_limited_local_results() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-1", 25);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let decision_event = signed_order_decision_event("order-1", &request_event_id, 26);
-
- for (event, observed_at_ms) in [(request_event.clone(), 2_500), (decision_event, 2_600)] {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- observed_at_ms,
- ))
- .await
- .expect("ingest");
- }
-
- let receipt = sdk
- .trades()
- .status(status_request("order-1").with_limit(1))
- .await
- .expect("status");
-
- assert!(receipt.found);
- assert_eq!(receipt.status, TradeStatusKind::Requested);
- assert_eq!(receipt.event_count, 1);
- assert_eq!(receipt.limit_applied, 1);
- assert_eq!(
- receipt
- .event_ids
- .iter()
- .map(RadrootsEventId::as_str)
- .collect::<Vec<_>>(),
- vec![request_event.id_str()]
- );
- assert_eq!(
- receipt
- .request_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(request_event.id_str())
- );
- assert!(receipt.decision_event_id.is_none());
- assert_eq!(
- receipt.last_event_id.as_ref().map(RadrootsEventId::as_str),
- Some(request_event.id_str())
- );
- assert!(receipt.issues.is_empty());
-}
-
-#[tokio::test]
-async fn order_status_reports_root_ambiguity_for_reused_trade_ids() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let first_request_event = signed_order_request_event("order-1", 27);
- let second_request_event = signed_order_request_event("order-1", 28);
-
- for (event, observed_at_ms) in [
- (first_request_event.clone(), 2_700),
- (second_request_event.clone(), 2_800),
- ] {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- observed_at_ms,
- ))
- .await
- .expect("ingest");
- }
-
- let receipt = sdk
- .trades()
- .status(status_request("order-1"))
- .await
- .expect("status");
-
- assert!(!receipt.found);
- assert_eq!(receipt.status, TradeStatusKind::Ambiguous);
- assert_eq!(receipt.event_count, 2);
- assert_eq!(
- receipt
- .event_ids
- .iter()
- .map(RadrootsEventId::as_str)
- .collect::<Vec<_>>(),
- vec![first_request_event.id_str(), second_request_event.id_str()]
- );
- assert!(receipt.issues.is_empty());
- let candidate_roots = receipt
- .ambiguity_candidates
- .iter()
- .map(|candidate| {
- candidate
- .locator
- .root_event_id
- .as_ref()
- .map(RadrootsEventId::as_str)
- .expect("root event id")
- })
- .collect::<Vec<_>>();
- assert_eq!(
- candidate_roots,
- vec![first_request_event.id_str(), second_request_event.id_str()]
- );
- assert_eq!(
- receipt
- .ambiguity_candidates
- .iter()
- .map(|candidate| TradeStatusRequest::locator_selector(&candidate.locator))
- .collect::<Vec<_>>(),
- vec![
- format!("order-1@{}", first_request_event.id_str()),
- format!("order-1@{}", second_request_event.id_str())
- ]
- );
-
- let root_specific = sdk
- .trades()
- .status(
- TradeStatusRequest::parse(&format!("order-1@{}", second_request_event.id_str()))
- .expect("root status request"),
- )
- .await
- .expect("root-specific status");
-
- assert!(root_specific.found);
- assert_eq!(root_specific.status, TradeStatusKind::Requested);
- assert_eq!(
- root_specific
- .request_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(second_request_event.id_str())
- );
- assert!(root_specific.ambiguity_candidates.is_empty());
-}
-
-#[cfg(feature = "signer-adapters")]
-#[tokio::test]
-async fn trade_product_mutation_returns_structured_ambiguity() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let first_request_event = signed_order_request_event("order-1", 27);
- let second_request_event = signed_order_request_event("order-1", 28);
-
- for (event, observed_at_ms) in [
- (first_request_event.clone(), 2_700),
- (second_request_event.clone(), 2_800),
- ] {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- observed_at_ms,
- ))
- .await
- .expect("ingest");
- }
-
- let error = sdk
- .trades()
- .seller()
- .accept_trade(TradeAcceptRequest::new(
- seller_actor(),
- status_request("order-1").locator,
- vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 1,
- }],
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- ))
- .await
- .expect_err("ambiguous product mutation");
-
- let RadrootsSdkError::TradeAmbiguous {
- operation,
- locator,
- candidates,
- } = &error
- else {
- panic!("expected structured trade ambiguity error");
- };
- assert_eq!(operation, "trade.accept");
- assert_eq!(locator.order_id().as_str(), "order-1");
- assert_eq!(candidates.len(), 2);
- assert_eq!(
- candidates
- .iter()
- .map(|candidate| {
- candidate
- .root_event_id
- .as_ref()
- .map(RadrootsEventId::as_str)
- .expect("root event id")
- })
- .collect::<Vec<_>>(),
- vec![first_request_event.id_str(), second_request_event.id_str()]
- );
- assert_eq!(
- error.recovery_actions(),
- vec![RadrootsSdkRecoveryAction::SelectTradeRoot]
- );
- let detail = error.detail_json();
- assert_eq!(detail["code"], "trade_ambiguous");
- assert_eq!(detail["detail"]["operation"], "trade.accept");
- assert_eq!(
- detail["detail"]["candidates"]
- .as_array()
- .expect("candidates")
- .len(),
- 2
- );
-}
-
-#[tokio::test]
-async fn order_status_maps_malformed_local_data_to_sanitized_error() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-1", 30);
- let raw_event_json = serde_json::to_string(&request_event).expect("raw event json");
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 3_000,
- ))
- .await
- .expect("ingest");
- sqlx::query("UPDATE event_envelopes SET tags_json = '[' WHERE event_id = ?")
- .bind(request_event.id_str())
- .execute(store.pool())
- .await
- .expect("corrupt tags");
-
- let error = sdk
- .trades()
- .status(status_request("order-1"))
- .await
- .expect_err("projection error");
- let message = error.to_string();
-
- assert!(matches!(error, RadrootsSdkError::Projection { .. }));
- assert!(message.contains("contains invalid tags_json"));
- assert!(!message.contains(raw_event_json.as_str()));
- assert!(!message.contains(request_event.sig_str()));
- assert!(!message.contains("\"tags\""));
- assert!(!message.contains("\"content\""));
-}
-
-#[tokio::test]
-async fn trade_status_watch_emits_finite_refresh_window() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let order_id = "watch-finite-refresh-window";
- let request_event = signed_order_request_event(order_id, 820);
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event),
- 1_700_400_000_000,
- ))
- .await
- .expect("request ingest");
-
- let mut watch = sdk
- .trades()
- .watch(
- TradeStatusWatchRequest::new(status_request(order_id))
- .with_capacity(2)
- .with_refresh_interval_ms(1)
- .with_refresh_limit(2),
- )
- .await
- .expect("watch");
-
- let first = watch.next().await.expect("first").expect("first update");
- let second = watch.next().await.expect("second").expect("second update");
- let closed = watch.next().await.expect("closed");
- let cancel = watch.cancel().await;
-
- assert_eq!(watch.capacity(), 2);
- assert_eq!(first.sequence, 1);
- assert_eq!(second.sequence, 2);
- assert_eq!(first.status.status, TradeStatusKind::Requested);
- assert_eq!(second.status.status, TradeStatusKind::Requested);
- assert!(closed.is_none());
- assert_eq!(cancel.state, TradeStatusWatchCancelState::AlreadyFinished);
-}
-
-#[tokio::test]
-async fn trade_status_watch_backpressures_slow_consumer_with_bounded_buffer() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let mut watch = sdk
- .trades()
- .watch(
- TradeStatusWatchRequest::parse("watch-slow-consumer")
- .expect("watch request")
- .with_capacity(1)
- .with_refresh_interval_ms(1)
- .with_refresh_limit(50),
- )
- .await
- .expect("watch");
-
- tokio::time::sleep(Duration::from_millis(25)).await;
- let buffered_len = watch.buffered_len();
- let cancel = watch.cancel().await;
- let post_cancel = watch.next().await.expect("post cancel");
-
- assert_eq!(buffered_len, 1);
- assert_eq!(cancel.state, TradeStatusWatchCancelState::Cancelled);
- assert_eq!(cancel.buffered_updates_dropped, 1);
- assert!(post_cancel.is_none());
-}
-
-#[tokio::test]
-async fn trade_status_watch_cancel_drains_buffer_and_closes_stream() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let mut watch = sdk
- .trades()
- .watch(
- TradeStatusWatchRequest::parse("watch-cancel-close")
- .expect("watch request")
- .with_capacity(4)
- .with_refresh_interval_ms(1),
- )
- .await
- .expect("watch");
- let first = watch.next().await.expect("first").expect("first update");
-
- let cancel = watch.cancel().await;
- tokio::time::sleep(Duration::from_millis(5)).await;
- let post_cancel = watch.next().await.expect("post cancel");
-
- assert_eq!(first.sequence, 1);
- assert_eq!(first.status.status, TradeStatusKind::Missing);
- assert_eq!(cancel.state, TradeStatusWatchCancelState::Cancelled);
- assert!(post_cancel.is_none());
-}
-
-#[tokio::test]
-async fn trade_status_watch_closes_after_producer_error() {
- let sdk = RadrootsClient::builder().build().await.expect("sdk");
- let mut watch = sdk
- .trades()
- .watch(
- TradeStatusWatchRequest::new(
- TradeStatusRequest::parse("watch-producer-error")
- .expect("status request")
- .with_source(SdkTradeStatusSource::ResyncThenLocal),
- )
- .with_refresh_interval_ms(1)
- .with_refresh_limit(2)
- .with_capacity(2),
- )
- .await
- .expect("watch");
-
- let error = watch.next().await.expect_err("producer error");
- let closed = watch.next().await.expect("closed");
-
- assert!(!error.to_string().is_empty());
- assert!(closed.is_none());
-}
-
-#[tokio::test]
-async fn trade_status_watch_rejects_unbounded_capacity() {
- let sdk = RadrootsClient::builder().build().await.expect("sdk");
- let result = sdk
- .trades()
- .watch(
- TradeStatusWatchRequest::parse("watch-invalid-capacity")
- .expect("watch request")
- .with_capacity(TRADE_STATUS_WATCH_MAX_CAPACITY + 1),
- )
- .await;
- let Err(error) = result else {
- panic!("expected capacity error");
- };
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert!(error.to_string().contains("capacity"));
-}
-
-#[tokio::test]
-#[ignore = "manual expensive release-gate lane for the 100k local-event status target"]
-async fn manual_local_status_perf_gate_measures_100k_events() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let background_non_trade_events = PERF_TOTAL_LOCAL_EVENTS - PERF_TRADE_RELEVANT_EVENTS;
- let background_trade_events = PERF_TRADE_RELEVANT_EVENTS - PERF_ACTIVE_TRADES as i64;
- insert_perf_non_trade_events(&store, 10_000_000, background_non_trade_events).await;
- insert_perf_trade_background_events(&store, 20_000_000, background_trade_events).await;
-
- let mut active_order_ids = Vec::with_capacity(PERF_ACTIVE_TRADES);
- for index in 0..PERF_ACTIVE_TRADES {
- let order_id = format!("perf-active-{index:04}");
- let event = signed_order_request_event(&order_id, 20_000 + index as u32);
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- 1_700_100_000_000 + index as i64,
- ))
- .await
- .expect("active trade ingest");
- active_order_ids.push(order_id);
- }
-
- let summary = store.status_summary().await.expect("status summary");
- assert_eq!(summary.total_events, PERF_TOTAL_LOCAL_EVENTS);
-
- let mut durations = Vec::with_capacity(active_order_ids.len());
- for order_id in &active_order_ids {
- let started = Instant::now();
- let status = sdk
- .trades()
- .status(status_request(order_id))
- .await
- .expect("status");
- durations.push(started.elapsed());
- assert_eq!(status.status, TradeStatusKind::Requested);
- assert_eq!(status.event_count, 1);
- }
-
- durations.sort_unstable();
- let p95 = durations[(durations.len() * 95 / 100).saturating_sub(1)];
- let cargo_target_dir =
- std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "<unset>".to_owned());
- println!(
- "manual local status performance gate p95_us={} target_us={} total_local_events={PERF_TOTAL_LOCAL_EVENTS} trade_relevant_events={PERF_TRADE_RELEVANT_EVENTS} active_trades={PERF_ACTIVE_TRADES} os={} arch={} cargo_target_dir={cargo_target_dir}",
- p95.as_micros(),
- PERF_STATUS_P95_TARGET.as_micros(),
- std::env::consts::OS,
- std::env::consts::ARCH
- );
- assert!(
- p95 <= PERF_STATUS_P95_TARGET,
- "local status p95 {}us exceeded target {}us for {PERF_TOTAL_LOCAL_EVENTS} local events, {PERF_TRADE_RELEVANT_EVENTS} trade-relevant events, and {PERF_ACTIVE_TRADES} active trades",
- p95.as_micros(),
- PERF_STATUS_P95_TARGET.as_micros()
- );
-}
diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs
@@ -181,7 +181,11 @@ async fn sdk_directory_storage_creates_deterministic_sqlite_files() {
.iter()
.any(|name| name == "listing_projection")
);
- assert!(runtime_tables.iter().any(|name| name == "trade_projection"));
+ assert!(
+ runtime_tables
+ .iter()
+ .any(|name| name == "sdk_runtime_trade_projection_checkpoint")
+ );
assert!(
runtime_tables
.iter()
@@ -200,15 +204,15 @@ async fn sdk_directory_storage_creates_deterministic_sqlite_files() {
assert!(!runtime_tables.iter().any(|name| name == "nostr_event"));
assert!(!runtime_tables.iter().any(|name| name == "nostr_event_tag"));
assert_eq!(
- sqlite_trade_projection_primary_key(&paths.runtime_path).await,
- vec!["order_id", "root_event_id", "projection_version"]
+ sqlite_runtime_projection_checkpoint_primary_key(&paths.runtime_path).await,
+ vec!["projection_name"]
);
assert!(!runtime_tables.iter().any(|name| name == "outbox_operation"));
let private_tables = sqlite_table_names(&paths.private_path).await;
assert!(
private_tables
.iter()
- .any(|name| name == "sdk_private_farm_location")
+ .any(|name| name == "private_farm_location")
);
let studio_tables = sqlite_table_names(&paths.studio_path).await;
assert!(studio_tables.iter().any(|name| name == "sdk_studio_state"));
@@ -1008,14 +1012,14 @@ async fn sqlite_table_names(path: &Path) -> Vec<String> {
names
}
-async fn sqlite_trade_projection_primary_key(path: &Path) -> Vec<String> {
+async fn sqlite_runtime_projection_checkpoint_primary_key(path: &Path) -> Vec<String> {
let options = SqliteConnectOptions::new().filename(path).read_only(true);
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(options)
.await
.expect("open sqlite for trade projection inspection");
- let rows = sqlx::query("PRAGMA table_info(trade_projection)")
+ let rows = sqlx::query("PRAGMA table_info(sdk_runtime_trade_projection_checkpoint)")
.fetch_all(&pool)
.await
.expect("trade projection table info");
@@ -1089,7 +1093,8 @@ fn sdk_examples_stay_on_product_api_boundary() {
assert!(local_enqueue.contains("enqueue_prepared_publish"));
assert!(!local_enqueue.contains("enqueue_prepared_publish_with_explicit_signer"));
assert!(local_enqueue.contains("push_outbox_with_transport"));
- assert!(local_enqueue.contains("TradeStatusRequest"));
+ assert!(!local_enqueue.contains("TradeStatusRequest"));
+ assert!(!local_enqueue.contains(".trades()"));
let myc_setup = include_str!("../examples/sdk_v1_myc_nip46_signer_setup.rs");
assert!(myc_setup.contains("RadrootsSdkMycNip46Signer"));
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -3,2809 +3,91 @@ use std::{
path::{Path, PathBuf},
};
-struct ForbiddenSdkConcept {
- pattern: &'static str,
- reason: &'static str,
+fn manifest_dir() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
-const FORBIDDEN_SDK_SOURCE_CONCEPTS: &[ForbiddenSdkConcept] = &[
- ForbiddenSdkConcept {
- pattern: "radroots_studio_app",
- reason: "SDK source must not depend on the app crate family",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_cli",
- reason: "SDK source must not depend on the CLI crate family",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsApp",
- reason: "SDK source must not import app product concepts",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsCli",
- reason: "SDK source must not import CLI product concepts",
- },
- ForbiddenSdkConcept {
- pattern: "AppSdk",
- reason: "app SDK adapters belong outside the SDK crate",
- },
- ForbiddenSdkConcept {
- pattern: "DesktopApp",
- reason: "desktop app runtime concepts belong outside the SDK crate",
- },
- ForbiddenSdkConcept {
- pattern: "domains/radroots/studio_apps",
- reason: "SDK source must remain standalone and path-agnostic",
- },
-];
-
-const FORBIDDEN_SDK_README_CONCEPTS: &[ForbiddenSdkConcept] = &[
- ForbiddenSdkConcept {
- pattern: "RadrootsSdk::builder()",
- reason: "SDK docs must describe RadrootsClient as the product runtime entrypoint",
- },
- ForbiddenSdkConcept {
- pattern: "sdk.orders()",
- reason: "SDK docs must describe the current trade product clients",
- },
- ForbiddenSdkConcept {
- pattern: "OrderStatusRequest",
- reason: "SDK docs must describe TradeStatusRequest as the status request DTO",
- },
- ForbiddenSdkConcept {
- pattern: "protocol::",
- reason: "SDK docs must not advertise a public protocol workflow bypass",
- },
- ForbiddenSdkConcept {
- pattern: "sdk.trade_buyer()",
- reason: "SDK docs must use grouped trade product handles",
- },
- ForbiddenSdkConcept {
- pattern: "sdk.trade_seller()",
- reason: "SDK docs must use grouped trade product handles",
- },
- ForbiddenSdkConcept {
- pattern: "sdk.trade_status()",
- reason: "SDK docs must use sdk.trades().status(...) as the only product status entrypoint",
- },
- ForbiddenSdkConcept {
- pattern: "sdk.trade_resync()",
- reason: "SDK docs must use grouped trade product handles",
- },
- ForbiddenSdkConcept {
- pattern: "sdk.trade_validation()",
- reason: "SDK docs must use sdk.trades().validation_receipts() for validation receipt inspection",
- },
- ForbiddenSdkConcept {
- pattern: "sdk.dvm()",
- reason: "SDK docs must not advertise the retired DVM client surface",
- },
- ForbiddenSdkConcept {
- pattern: "`relay-client` is retained",
- reason: "SDK docs must describe canonical feature behavior instead of retained surfaces",
- },
- ForbiddenSdkConcept {
- pattern: "direct relay publish callers",
- reason: "SDK docs must describe adapter behavior rather than caller compatibility posture",
- },
- ForbiddenSdkConcept {
- pattern: "compatibility",
- reason: "SDK docs must not frame target-state runtime surfaces as compatibility policy",
- },
- ForbiddenSdkConcept {
- pattern: "legacy",
- reason: "SDK docs must not frame target-state runtime surfaces as legacy policy",
- },
- ForbiddenSdkConcept {
- pattern: "preserve serde",
- reason: "SDK docs must describe serialized-field stability without serde-compat wording",
- },
- ForbiddenSdkConcept {
- pattern: "Nostr event timestamp",
- reason: "SDK docs must describe event-envelope timestamps without generic Nostr wording",
- },
-];
-
-const FORBIDDEN_FOUNDATION_HARDENING_RETIRED_CONCEPTS: &[ForbiddenSdkConcept] = &[
- ForbiddenSdkConcept {
- pattern: "WireEventParts",
- reason: "event construction must use current Radroots NIP-01 wire part names",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsFrozenEventDraft",
- reason: "event draft surfaces must use the current RadrootsEventDraft name",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsNostrEvent",
- reason: "product-level event surfaces must use protocol-neutral event names",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsNostrEventRef",
- reason: "product-level event references must use RadrootsEventRef",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsNostrEventPtr",
- reason: "product-level event pointers must use RadrootsEventPtr",
- },
- ForbiddenSdkConcept {
- pattern: "pub type SignedEvent",
- reason: "SDK signing adapters must not expose compatibility aliases for signed events",
- },
- ForbiddenSdkConcept {
- pattern: "pub type NostrEventBuilder",
- reason: "SDK signing adapters must not expose compatibility aliases for event builders",
- },
- ForbiddenSdkConcept {
- pattern: "SignedNostrEvent",
- reason: "generic signed-event surfaces must use product-neutral signed-event names",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsEventIndexIndexCheckpoint",
- reason: "event-index checkpoint names must not duplicate the index noun",
- },
- ForbiddenSdkConcept {
- pattern: "RadrootsEventsIndexed",
- reason: "event-indexed APIs must use the singular event-index crate family",
- },
- ForbiddenSdkConcept {
- pattern: "RADROOTS_EVENTS_VERSION",
- reason: "event contract version constants must use the current singular event namespace",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_events",
- reason: "SDK crate and package surfaces must use the current singular event crate names",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_events_codec",
- reason: "SDK event codec surfaces must use the current singular event-codec name",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_events_indexed",
- reason: "SDK event index surfaces must use the current singular event-index name",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_local_events",
- reason: "SDK local event storage must not reintroduce retired local-events names",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_local_store",
- reason: "SDK runtime storage must not reintroduce retired local-store names",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_types",
- reason: "SDK shared type surfaces must use current crate ownership instead of retired types crates",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_types_bindings",
- reason: "SDK generated bindings must not reintroduce retired types-binding names",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_nostr_ndb",
- reason: "SDK Nostr database surfaces must not reintroduce retired ndb names",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_replica_db",
- reason: "SDK replica surfaces must use current replica-store ownership",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_replica_db_schema",
- reason: "SDK replica schema surfaces must use current replica-schema ownership",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_sp1_guest_trade",
- reason: "SDK trade SP1 surfaces must use the current trade_sp1 crate names",
- },
- ForbiddenSdkConcept {
- pattern: "radroots_sp1_host_trade",
- reason: "SDK trade SP1 surfaces must use the current trade_sp1 crate names",
- },
-];
-
-const FORBIDDEN_FOUNDATION_HARDENING_DOC_CONCEPTS: &[ForbiddenSdkConcept] = &[
- ForbiddenSdkConcept {
- pattern: "Nostr event timestamp",
- reason: "SDK docs must describe event-envelope timestamps without generic Nostr wording",
- },
- ForbiddenSdkConcept {
- pattern: "Forwarded satisfies Delivered",
- reason: "SDK docs must not imply forwarded evidence is strict delivery",
- },
- ForbiddenSdkConcept {
- pattern: "StoredByGateway satisfies Delivered",
- reason: "SDK docs must not imply gateway storage evidence is strict delivery",
- },
- ForbiddenSdkConcept {
- pattern: "Seen satisfies Delivered",
- reason: "SDK docs must not imply seen evidence is strict delivery",
- },
-];
-
-const FORBIDDEN_SDK_ROOT_TRADE_ALIAS_NAMES: &[&str] = &[
- "trade_buyer",
- "trade_seller",
- "trade_status",
- "trade_resync",
- "trade_validation",
-];
-
-const FORBIDDEN_DAEMON_PUBLISH_PROXY_IDENTIFIERS: &[&str] = &[
- concat!("\"radrootsd", "_", "pro", "xy\""),
- concat!("radrootsd.publish", "_", "pro", "xy.v1"),
- concat!("radroots_publish", "_", "pro", "xy_protocol"),
- concat!("publish", "_", "pro", "xy_protocol"),
- "publish.relays.resolve",
-];
-
-const REQUIRED_SDK_README_CONCEPTS: &[&str] = &[
- "RadrootsClient::builder()",
- "sdk.trades()",
- "TradeStatusRequest",
- "sdk.trades().buyer()",
- "sdk.trades().seller()",
- "sdk.trades().status(...)",
- "sdk.trades().resync()",
- "sdk.trades().validation_receipts()",
- "event-envelope timestamp",
-];
-
-const REQUIRED_TRADE_RUNTIME_EXPORTS: &[&str] = &[
- "TRADE_STATUS_DEFAULT_LIMIT",
- "TRADE_STATUS_MAX_LIMIT",
- "TradeEvidenceBranchReceipt",
- "TradeEvidenceIngestReceipt",
- "TradeEvidenceIngestRequest",
- "TradeEvidenceQueryBranch",
- "TradeEvidenceQueryBranchKind",
- "TradeEvidenceQueryPlan",
- "TradeEvidenceNostrRelayFilter",
- "TradeEvidenceNostrRelayTagFilter",
- "TradeRequestEvidenceIngestReceipt",
- "TradeRequestEvidenceIngestRequest",
- "TradeResyncEventImportReceipt",
- "TradeResyncEvidenceReceipt",
- "TradeResyncNostrRelayOutcomeKind",
- "TradeResyncNostrRelayOutcomeReceipt",
- "TradeResyncNostrRelayTransportOutcomeKind",
- "TradeResyncReceipt",
- "TradeResyncRequest",
- "TradeSellerInboxReceipt",
- "TradeSellerInboxRequest",
- "TradeStatusAmbiguityCandidate",
- "TradeStatusEligibility",
- "TradeStatusEvidenceSummary",
- "TradeStatusKind",
- "TradeStatusNextActionKind",
- "TradeStatusReceipt",
- "TradeStatusRequest",
- "TradeValidationReceiptEvent",
- "TradeValidationReceiptInspectReceipt",
- "TradeValidationReceiptInspectRequest",
- "TradeValidationReceiptInvalidCandidate",
- "TradeValidationReceiptListReceipt",
- "TradeValidationReceiptListRequest",
- "TradeValidationReceiptNostrEvidenceReceipt",
- "TradeValidationReceiptNostrRelayOutcomeKind",
- "TradeValidationReceiptNostrRelayOutcomeReceipt",
- "TradeValidationReceiptNostrRelayTransportOutcomeKind",
- "TradeValidationReceiptTags",
- "TradeValidationReceiptVerifyRequest",
- "TradeValidationTrustDecision",
- "SdkTradeStatusIssue",
- "SdkTradeStatusIssueKind",
- "SdkTradeStatusSource",
-];
-
-const REQUIRED_TRADE_SIGNER_EXPORTS: &[&str] = &[
- "TRADE_CANCELLATION_OPERATION_KIND",
- "TRADE_DECISION_OPERATION_KIND",
- "TRADE_SUBMIT_OPERATION_KIND",
- "TradeAcceptRequest",
- "TradeCancelRequest",
- "TradeCancellationPlan",
- "TradeCancellationReceipt",
- "TradeDecisionPlan",
- "TradeDecisionReceipt",
- "TradeDeclineRequest",
- "TradeEvidenceMode",
- "TradeMutationOutcome",
- "TradeProposeRequest",
- "TradeSubmitPlan",
- "TradeSubmitReceipt",
- "TradeWorkflowEnqueueReceipt",
- "TradeWorkflowIdempotencyReceipt",
- "TradeWorkflowKind",
- "TradeWorkflowPlan",
- "TradeWorkflowRetryAdvice",
-];
-
-const REQUIRED_IDENTITY_MODEL_EXPORTS: &[&str] = &[
- "DEFAULT_IDENTITY_PATH",
- "IdentityError",
- "RADROOTS_USERNAME_MAX_LEN",
- "RADROOTS_USERNAME_MIN_LEN",
- "RADROOTS_USERNAME_REGEX",
- "RadrootsIdentity",
- "RadrootsIdentityFile",
- "RadrootsIdentityId",
- "RadrootsIdentityProfile",
- "RadrootsIdentityPublic",
- "RadrootsIdentitySecretKeyFormat",
- "radroots_username_is_valid",
- "radroots_username_normalize",
-];
-
-const REQUIRED_IDENTITY_STORAGE_EXPORTS: &[&str] = &[
- "RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT",
- "RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX",
- "RadrootsEncryptedIdentityFile",
- "encrypted_identity_wrapping_key_path",
- "load_encrypted_identity",
- "load_encrypted_identity_with_key_slot",
- "load_identity_profile",
- "rotate_encrypted_identity",
- "rotate_encrypted_identity_with_key_slot",
- "store_encrypted_identity",
- "store_encrypted_identity_with_key_slot",
- "store_identity_profile",
-];
-
-const REQUIRED_TRADE_POLICY_EXPORTS: &[&str] = &[
- "SatisfactionPolicy",
- "PublishMode",
- "TargetPolicy",
- "SdkTradeIdempotencyRecord",
- "PrivacyPreflightConfirmation",
- "PrivacyPreflightReceipt",
- "PrivacyPreflightStatus",
- "ProductSensitivityField",
- "RadrootsTradeValidationTrustPolicy",
- "RadrootsTradeValidationTrustState",
- "SDK_TRADE_PROJECTION_CACHE_VERSION",
- "SdkTradeProjectionCache",
- "SdkTradeProjectionCacheKey",
- "SdkTradeProjectionCacheRecord",
-];
-
-const REQUIRED_TRADES_CLIENT_METHODS: &[&str] = &[
- "pub async fn ingest_evidence(",
- "pub async fn ingest_request_evidence(",
- "pub async fn status(",
-];
-
-const FORBIDDEN_TRADES_CLIENT_PUBLIC_METHODS: &[&str] = &[
- "pub fn prepare_submit(",
- "pub async fn enqueue_submit(",
- "pub async fn enqueue_prepared_submit(",
- "pub async fn enqueue_submit_with_explicit_signer(",
- "pub async fn enqueue_prepared_submit_with_explicit_signer(",
- "pub fn prepare_decision(",
- "pub async fn enqueue_decision(",
- "pub async fn enqueue_prepared_decision(",
- "pub async fn enqueue_decision_with_explicit_signer(",
- "pub async fn enqueue_prepared_decision_with_explicit_signer(",
- "pub fn prepare_revision_proposal(",
- "pub async fn enqueue_revision_proposal(",
- "pub async fn enqueue_prepared_revision_proposal(",
- "pub async fn enqueue_revision_proposal_with_explicit_signer(",
- "pub async fn enqueue_prepared_revision_proposal_with_explicit_signer(",
- "pub fn prepare_revision_decision(",
- "pub async fn enqueue_revision_decision(",
- "pub async fn enqueue_prepared_revision_decision(",
- "pub async fn enqueue_revision_decision_with_explicit_signer(",
- "pub async fn enqueue_prepared_revision_decision_with_explicit_signer(",
- "pub fn prepare_cancellation(",
- "pub async fn enqueue_cancellation(",
- "pub async fn enqueue_prepared_cancellation(",
- "pub async fn enqueue_cancellation_with_explicit_signer(",
- "pub async fn enqueue_prepared_cancellation_with_explicit_signer(",
-];
-
-const REQUIRED_TRADE_BUYER_CLIENT_METHODS: &[&str] =
- &["pub async fn propose_trade(", "pub async fn cancel_trade("];
-
-const REQUIRED_TRADE_SELLER_CLIENT_METHODS: &[&str] = &[
- "pub async fn inbox(",
- "pub async fn accept_trade(",
- "pub async fn decline_trade(",
-];
-
-const REQUIRED_TRADE_RESYNC_CLIENT_METHODS: &[&str] = &["pub async fn resync("];
-
-const REQUIRED_TRADE_VALIDATION_RECEIPTS_CLIENT_METHODS: &[&str] = &[
- "pub async fn list(",
- "pub async fn inspect(",
- "pub async fn verify(",
-];
-
-const FORBIDDEN_PRODUCT_CLIENT_HANDLES: &[&str] = &[
- "DvmClient",
- "TradeStatusClient",
- "TradeValidationClient",
- "pub struct TradeStatusClient",
- "pub struct TradeValidationClient",
-];
-
-const FORBIDDEN_PRODUCT_CLIENT_METHODS: &[&str] = &[
- "pub fn status_client(",
- "pub fn validation(",
- "pub fn root(&self) -> &'client RadrootsClient",
-];
-
-const FORBIDDEN_ORDER_RUNTIME_PUBLIC_EXPORTS: &[&str] = &[
- "CheckoutClient",
- "EscrowClient",
- "InvoiceClient",
- concat!("Order", "FulfillmentStatusKind"),
- concat!("Order", "FulfillmentUpdateEnqueueRequest"),
- concat!("Order", "FulfillmentUpdatePlan"),
- concat!("Order", "FulfillmentUpdatePrepareRequest"),
- concat!("Order", "FulfillmentUpdateReceipt"),
- concat!("Order", "Payment", "HandoffKind"),
- concat!("Order", "PaymentRecordEnqueueRequest"),
- concat!("Order", "PaymentRecordPrepareRequest"),
- concat!("Order", "Payment", "Record", "Receipt"),
- concat!("Order", "Payment", "StateKind"),
- concat!("Order", "ReceiptRecordEnqueueRequest"),
- concat!("Order", "ReceiptRecordPlan"),
- concat!("Order", "ReceiptRecordPrepareRequest"),
- concat!("Order", "ReceiptRecord", "Receipt"),
- concat!("Order", "SettlementDecisionEnqueueRequest"),
- concat!("Order", "SettlementDecisionPrepareRequest"),
- concat!("Order", "SettlementDecisionReceipt"),
- concat!("Order", "Settlement", "StateKind"),
- concat!("Payment", "Client"),
- concat!("Payments", "Client"),
- "RefundClient",
- "WalletClient",
- "ORDER_FULFILLMENT_UPDATE_OPERATION_KIND",
- "ORDER_PAYMENT_RECORD_OPERATION_KIND",
- "ORDER_RECEIPT_RECORD_OPERATION_KIND",
- "ORDER_SETTLEMENT_DECISION_OPERATION_KIND",
-];
-
-const FORBIDDEN_REMOVED_ORDER_PRODUCT_EXPORTS: &[&str] = &[
- "ORDER_CANCELLATION_OPERATION_KIND",
- "ORDER_DECISION_OPERATION_KIND",
- "ORDER_REVISION_DECISION_OPERATION_KIND",
- "ORDER_REVISION_PROPOSAL_OPERATION_KIND",
- "ORDER_STATUS_DEFAULT_LIMIT",
- "ORDER_STATUS_MAX_LIMIT",
- "ORDER_SUBMIT_OPERATION_KIND",
- "OrderCancellationEnqueueRequest",
- "OrderCancellationPlan",
- "OrderCancellationPrepareRequest",
- "OrderCancellationReceipt",
- "OrderDecisionEnqueueRequest",
- "OrderDecisionPlan",
- "OrderDecisionPrepareRequest",
- "OrderDecisionReceipt",
- "OrderEvidenceIngestReceipt",
- "OrderEvidenceIngestRequest",
- "OrderRequestEvidenceIngestReceipt",
- "OrderRequestEvidenceIngestRequest",
- "OrderRevisionDecisionEnqueueRequest",
- "OrderRevisionDecisionPlan",
- "OrderRevisionDecisionPrepareRequest",
- "OrderRevisionDecisionReceipt",
- "OrderRevisionProposalEnqueueRequest",
- "OrderRevisionProposalPlan",
- "OrderRevisionProposalPrepareRequest",
- "OrderRevisionProposalReceipt",
- "OrderStatusEligibility",
- "OrderStatusEvidenceSummary",
- "OrderStatusReceipt",
- "OrderStatusRequest",
- "OrderSubmitEnqueueRequest",
- "OrderSubmitPlan",
- "OrderSubmitPrepareRequest",
- "OrderSubmitReceipt",
- "OrderWorkflowEnqueueReceipt",
- "OrderWorkflowIdempotencyReceipt",
- "OrderWorkflowKind",
- "OrderWorkflowPlan",
- "OrderWorkflowRetryAdvice",
- "SdkDvmInventoryBinWitness",
- "SdkOrderStatusIssue",
- "SdkOrderStatusIssueKind",
- "SdkOrderStatusSource",
- "TradeProtocolClient",
-];
-
-const FORBIDDEN_TRADE_LOW_LEVEL_REQUEST_EXPORTS: &[&str] = &[
- "TradeCancellationEnqueueRequest",
- "TradeCancellationPrepareRequest",
- "TradeDecisionEnqueueRequest",
- "TradeDecisionPrepareRequest",
- "TradeRevisionDecisionEnqueueRequest",
- "TradeRevisionDecisionPrepareRequest",
- "TradeRevisionProposalEnqueueRequest",
- "TradeRevisionProposalPrepareRequest",
- "TradeSubmitEnqueueRequest",
- "TradeSubmitPrepareRequest",
-];
-
-const FORBIDDEN_ORDER_RUNTIME_METHODS: &[&str] = &[
- "accept_settlement",
- "checkout",
- "enqueue_fulfillment",
- "enqueue_payment",
- "enqueue_receipt_record",
- "enqueue_settlement",
- "escrow",
- "fulfillment",
- "invoice",
- "payment_provider",
- "prepare_fulfillment",
- "prepare_payment",
- "prepare_receipt_record",
- "prepare_settlement",
- "receipt_record",
- "record_payment",
- "refund",
- "reject_settlement",
- "settle_payment",
- "wallet",
-];
-
-#[test]
-fn sdk_sources_do_not_import_app_or_cli_concepts() {
- for path in rust_source_files(Path::new(env!("CARGO_MANIFEST_DIR")).join("src").as_path()) {
- let source = read_source(path.as_path());
- for concept in FORBIDDEN_SDK_SOURCE_CONCEPTS {
- assert!(
- !contains_forbidden_concept(&source, concept.pattern),
- "{} contains forbidden SDK source concept `{}`: {}",
- path.display(),
- concept.pattern,
- concept.reason
- );
- }
- }
-}
-
-#[test]
-fn sdk_sources_reject_production_dead_code_suppressions() {
- let offenders = rust_source_files(Path::new(env!("CARGO_MANIFEST_DIR")).join("src").as_path())
- .into_iter()
- .filter_map(|path| {
- let source = read_source(path.as_path());
- source
- .contains("allow(dead_code)")
- .then(|| path.display().to_string())
- })
- .collect::<Vec<_>>();
-
- assert!(
- offenders.is_empty(),
- "SDK sources contain production dead-code suppressions: {offenders:?}"
- );
-}
-
-#[test]
-fn sdk_manifest_does_not_depend_on_app_or_cli_crates() {
- let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
- let manifest = read_source(manifest_path.as_path());
-
- for crate_name in ["radroots_studio_app", "radroots_cli"] {
- assert!(
- !manifest.contains(crate_name),
- "SDK manifest contains forbidden downstream crate dependency `{crate_name}`"
- );
- }
-}
-
-#[test]
-fn sdk_transport_nostr_features_do_not_retain_relay_named_aliases() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let manifest = read_source(manifest_dir.join("Cargo.toml").as_path());
-
- for required in [
- "transport-nostr-client = [",
- "transport-nostr-runtime = [",
- "\"transport-nostr-runtime\",",
- "\"transport-nostr-client\",",
- ] {
- assert!(
- manifest.contains(required),
- "Cargo.toml must retain canonical SDK transport feature witness `{required}`"
- );
- }
- for forbidden in ["relay-client", "relay-runtime"] {
- assert!(
- !manifest.contains(forbidden),
- "Cargo.toml must not retain removed SDK transport feature `{forbidden}`"
- );
- }
-
- let lib_source = read_source(manifest_dir.join("src/lib.rs").as_path());
- assert!(
- lib_source.contains("feature = \"transport-nostr-client\""),
- "src/lib.rs must gate adapters on transport-nostr-client"
- );
- assert!(
- !lib_source.contains("relay-client") && !lib_source.contains("relay-runtime"),
- "src/lib.rs must not retain removed transport feature names"
- );
-
- let adapters_mod_source = read_source(manifest_dir.join("src/adapters/mod.rs").as_path());
- assert!(
- adapters_mod_source.contains("pub mod nostr;"),
- "src/adapters/mod.rs must expose the Nostr adapter module by transport kind"
- );
- assert!(
- !adapters_mod_source.contains("pub mod relay;"),
- "src/adapters/mod.rs must not retain a relay-named public adapter module"
- );
- assert!(
- !manifest_dir.join("src/adapters/relay.rs").exists(),
- "src/adapters/relay.rs must not remain as a relay-named public adapter module"
- );
-
- for relative_path in [
- "src/sync_runtime.rs",
- "src/orders_runtime.rs",
- "examples/runtime_local.rs",
- "README",
- ] {
- let source = read_source(manifest_dir.join(relative_path).as_path());
- for forbidden in ["relay-client", "relay-runtime"] {
- assert!(
- !source.contains(forbidden),
- "{relative_path} must not retain removed SDK transport feature `{forbidden}`"
- );
- }
- }
-}
-
-#[test]
-fn sdk_transport_public_api_rejects_removed_relay_aliases_and_neutral_target_constructor() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let lib_source = read_source(manifest_dir.join("src/lib.rs").as_path());
- let nostr_adapter = read_source(manifest_dir.join("src/adapters/nostr.rs").as_path());
- let transport_source = read_source(manifest_dir.join("src/transport.rs").as_path());
- let target_set_impl = source_between(
- transport_source.as_str(),
- "impl TargetSet {",
- "\n}\n\nimpl serde::Serialize for TargetSet",
- );
-
- for required in [
- "RadrootsNostrClient",
- "RadrootsNostrClientOptions",
- "RadrootsNostrError",
- "RadrootsNostrEventId",
- "RadrootsNostrOutput",
- "RadrootsTransportDeliveryReceipt",
- "RadrootsTransportDeliveryTargetStatus",
- "RadrootsTransportKind",
- "RadrootsTransportOutcome",
- "RadrootsTransportSatisfactionClass",
- "RadrootsTransportTargetReceipt",
- "pub fn nostr_relays",
- "pub fn transport_targets",
- ] {
- assert!(
- nostr_adapter.contains(required)
- || transport_source.contains(required)
- || lib_source.contains(required),
- "SDK public transport API must retain explicit target-state witness `{required}`"
- );
- }
-
- for forbidden in [
- "pub type RelayClient",
- "pub type RelayClientOptions",
- "pub type RelayError",
- "pub type RelayEventId",
- "pub type RelayOutput",
- "RelayClient::",
- "RelayOutput<",
- "pub fn new<I, S>",
- ] {
- assert!(
- !nostr_adapter.contains(forbidden) && !target_set_impl.contains(forbidden),
- "SDK transport public API must not retain removed alias or constructor `{forbidden}`"
- );
- }
-
- for forbidden in [
- "RadrootsTransportDeliveryReceipt as TransportDeliveryReceipt",
- "RadrootsTransportDeliveryTargetStatus as TransportDeliveryTargetStatus",
- "RadrootsTransportKind as TransportKind",
- "RadrootsTransportOutcome as TransportOutcome",
- "RadrootsTransportSatisfactionClass as TransportSatisfactionClass",
- "RadrootsTransportTargetReceipt as TransportTargetReceipt",
- ] {
- assert!(
- !transport_source.contains(forbidden) && !lib_source.contains(forbidden),
- "SDK transport public API must not retain removed transport alias `{forbidden}`"
- );
- }
-
- for relative_root in ["src", "tests", "examples"] {
- let root = manifest_dir.join(relative_root);
- if !root.exists() {
- continue;
- }
- for path in rust_source_files(root.as_path()) {
- if path.file_name().and_then(|file_name| file_name.to_str())
- == Some("source_boundary.rs")
- {
- continue;
- }
- let source = read_source(path.as_path());
- let relative_path = relative_manifest_path(manifest_dir, path.as_path());
- assert!(
- !contains_sdk_target_set_new(source.as_str()),
- "{relative_path} must use TargetSet::nostr_relays or TargetSet::transport_targets explicitly"
- );
- for forbidden in [
- "RelayClient",
- "RelayClientOptions",
- "RelayError",
- "RelayEventId",
- "RelayOutput",
- ] {
- assert!(
- !source.contains(forbidden),
- "{relative_path} must not retain removed SDK Nostr adapter alias `{forbidden}`"
- );
- }
- }
- }
-}
-
-fn contains_sdk_target_set_new(source: &str) -> bool {
- source.match_indices("TargetSet::new(").any(|(index, _)| {
- source[..index]
- .chars()
- .next_back()
- .is_none_or(|character| !is_rust_identifier_character(character))
- })
-}
-
-#[test]
-fn sdk_radrootsd_execution_required_targets_stay_fingerprint_exact() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let sync_runtime = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
-
- for required in [
- "let required_remaining_targets =\n radrootsd_required_remaining_targets(&plan.satisfaction_policy, &active_targets)?;",
- "required_remaining_targets.as_deref()",
- "fn radrootsd_required_remaining_targets",
- "RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets }",
- "target.endpoint_fingerprint == *required",
- "target.status.counts_as_transport_satisfaction(*class)",
- "target.status.is_ready_for_attempt()",
- ] {
- assert!(
- sync_runtime.contains(required),
- "SDK radrootsd publish must retain exact required-target witness `{required}`"
- );
- }
-
- let required_targets_arm = source_between(
- sync_runtime.as_str(),
- "RadrootsTransportSatisfactionPolicy::RequiredTargets { .. } => {\n TransportPublishDeliveryPolicy::required_targets(",
- " RadrootsTransportSatisfactionPolicy::Quorum { .. } => {",
- );
- for forbidden in [
- "TransportPublishDeliveryPolicy::All",
- "TransportPublishDeliveryPolicy::Any",
- "TransportPublishDeliveryPolicy::Quorum",
- ] {
- assert!(
- !required_targets_arm.contains(forbidden),
- "SDK RequiredTargets radrootsd arm must not lower to count policy `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn sdk_readme_documents_current_public_product_surface() {
- let readme_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("README");
- let readme = read_source(readme_path.as_path());
-
- for concept in FORBIDDEN_SDK_README_CONCEPTS {
- assert!(
- !readme.contains(concept.pattern),
- "README contains forbidden SDK public API concept `{}`: {}",
- concept.pattern,
- concept.reason
- );
- }
-
- for concept in REQUIRED_SDK_README_CONCEPTS {
- assert!(
- readme.contains(concept),
- "README must document current SDK public API concept `{concept}`"
- );
- }
-}
-
-#[test]
-fn sdk_foundation_hardening_surfaces_reject_retired_names_and_ambiguous_docs() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let repo_root = manifest_dir
- .parent()
- .and_then(Path::parent)
- .expect("SDK repo root");
- let mut findings = Vec::new();
-
- for path in sdk_foundation_hardening_guard_files(repo_root) {
- let source = read_source(path.as_path());
- let relative_path = relative_manifest_path(repo_root, path.as_path());
-
- for concept in FORBIDDEN_FOUNDATION_HARDENING_RETIRED_CONCEPTS {
- if foundation_hardening_retired_concept_allowed(relative_path.as_str(), concept.pattern)
- {
- continue;
- }
- if contains_forbidden_concept(source.as_str(), concept.pattern) {
- findings.push(format!(
- "{} contains retired Foundation Hardening concept `{}`: {}",
- relative_path, concept.pattern, concept.reason
- ));
- }
- }
-
- if is_doc_surface(path.as_path()) {
- for concept in FORBIDDEN_FOUNDATION_HARDENING_DOC_CONCEPTS {
- if source.contains(concept.pattern) {
- findings.push(format!(
- "{} contains ambiguous Foundation Hardening wording `{}`: {}",
- relative_path, concept.pattern, concept.reason
- ));
- }
- }
- }
- }
-
- assert!(
- findings.is_empty(),
- "SDK Foundation Hardening V1 source-boundary violations:\n{}",
- findings.join("\n")
- );
-}
-
-#[test]
-fn sdk_sources_do_not_reintroduce_root_trade_alias_surface() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
-
- for path in sdk_root_alias_guard_files(manifest_dir) {
- let source = read_source(path.as_path());
- let relative_path = relative_manifest_path(manifest_dir, path.as_path());
- let findings = root_trade_alias_findings(relative_path.as_str(), source.as_str());
- assert!(
- findings.is_empty(),
- "SDK root trade aliases are forbidden:\n{}",
- findings.join("\n")
- );
- }
-}
-
-#[test]
-fn radroots_client_does_not_expose_root_trade_alias_methods() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/runtime.rs")
- .as_path(),
- );
- let client_impl = impl_block(source.as_str(), "impl RadrootsClient {");
- let findings = root_trade_alias_findings("src/runtime.rs RadrootsClient impl", client_impl);
-
- assert!(
- findings.is_empty(),
- "RadrootsClient must expose grouped product handles instead of root trade aliases:\n{}",
- findings.join("\n")
- );
-}
-
-#[test]
-fn root_trade_alias_scanner_preserves_internal_status_identifiers() {
- let allowed_source = r#"
- let code = "trade_status_limit_invalid";
- let error = RadrootsSdkError::trade_status_limit_invalid(0, 1, 100);
- async fn dvm_validation_receipt_ingest_commits_pending_trade_status() {}
- "#;
- assert!(
- root_trade_alias_findings("allowed_fixture.rs", allowed_source).is_empty(),
- "internal status identifiers must remain allowed when they are not root alias calls or definitions"
- );
-
- let forbidden_source = r#"
- impl RadrootsClient {
- pub async fn trade_status(&self) {}
- }
- async fn workflow(sdk: RadrootsClient) {
- sdk.trade_resync (request).await;
- RadrootsClient::trade_buyer(&sdk);
- }
- "#;
- let findings = root_trade_alias_findings("forbidden_fixture.rs", forbidden_source);
-
- for alias in ["trade_status", "trade_resync", "trade_buyer"] {
- assert!(
- findings.iter().any(|finding| finding.contains(alias)),
- "root trade alias scanner must reject `{alias}`"
- );
- }
-}
-
-#[test]
-fn farm_runtime_stays_on_product_runtime_boundary() {
- product_runtime_file_stays_on_boundary("src/farms_runtime.rs");
-}
-
-#[test]
-fn order_runtime_stays_on_product_runtime_boundary() {
- product_runtime_file_stays_on_boundary("src/orders_runtime.rs");
-}
-
-#[test]
-fn migrated_runtime_tests_stay_on_product_runtime_boundary() {
- for file in ["tests/farms_runtime.rs", "tests/orders_runtime.rs"] {
- product_runtime_file_stays_on_boundary(file);
- }
-}
-
-#[test]
-fn private_protocol_helper_modules_are_runtime_gated_by_lib() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let lib_source = read_source(manifest_dir.join("src/lib.rs").as_path());
-
- for module in ["farm", "order"] {
- let module_gate = format!("#[cfg(feature = \"runtime\")]\nmod {module};");
- assert!(
- lib_source.contains(module_gate.as_str()),
- "src/lib.rs must keep private protocol helper module `{module}` behind runtime"
- );
- }
-
- for relative_path in ["src/farm.rs", "src/order.rs"] {
- let source = read_source(manifest_dir.join(relative_path).as_path());
- assert!(
- !source.contains("feature = \"runtime\""),
- "{relative_path} must rely on the lib.rs module-level runtime gate"
- );
- assert!(
- !source.contains("feature = \"serde_json\""),
- "{relative_path} must rely on the runtime feature's serde_json contract instead of duplicating it"
- );
- }
-
- let order_source = read_source(manifest_dir.join("src/order.rs").as_path());
- for helper in [
- "build_order_request_draft",
- "build_order_decision_draft",
- "build_order_cancellation_draft",
- ] {
- let helper_gate = format!("#[cfg(feature = \"signer-adapters\")]\npub fn {helper}(");
- assert!(
- order_source.contains(helper_gate.as_str()),
- "src/order.rs must keep `{helper}` available only for signer adapters"
- );
- }
-}
-
-#[test]
-fn default_status_noise_test_uses_production_ingest_not_perf_sql_ballast() {
- let orders_runtime_tests = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("tests/orders_runtime.rs")
- .as_path(),
- );
- let default_status_noise_test = function_source(
- orders_runtime_tests.as_str(),
- "order_status_query_uses_indexed_order_id_under_background_event_noise",
- );
- let manual_perf_test = function_source(
- orders_runtime_tests.as_str(),
- "manual_local_status_perf_gate_measures_100k_events",
- );
-
- assert!(
- default_status_noise_test.contains("ingest_status_noise_events("),
- "default status noise test must use production-equivalent event-store ingest"
- );
- for forbidden in [
- "insert_perf_non_trade_events(",
- "insert_perf_trade_background_events(",
- "sqlx::query(",
- ] {
- assert!(
- !default_status_noise_test.contains(forbidden),
- "default status noise test must not use manual SQL ballast helper `{forbidden}`"
- );
- }
- for required in [
- "insert_perf_non_trade_events(",
- "insert_perf_trade_background_events(",
- ] {
- assert!(
- manual_perf_test.contains(required),
- "manual performance gate must retain explicit SQL ballast helper `{required}`"
- );
- }
-}
-
-#[test]
-fn agreement_order_runtime_excludes_post_agreement_surfaces() {
- let lib_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/lib.rs")
- .as_path(),
- );
- let order_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/orders_runtime.rs")
- .as_path(),
- );
-
- for forbidden in FORBIDDEN_ORDER_RUNTIME_PUBLIC_EXPORTS {
- assert!(
- !lib_source.contains(forbidden),
- "src/lib.rs must not expose unsupported order runtime surface `{forbidden}`"
- );
- }
-
- for forbidden in FORBIDDEN_ORDER_RUNTIME_METHODS {
- assert!(
- !order_source.contains(forbidden),
- "src/orders_runtime.rs must not expose unsupported order runtime method or capability `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn order_runtime_public_exports_are_explicit() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/lib.rs")
- .as_path(),
- );
-
- assert!(
- source.contains("mod orders_runtime;"),
- "src/lib.rs must keep orders_runtime as an internal implementation module"
- );
- assert!(
- source.contains("pub use crate::orders_runtime::{"),
- "src/lib.rs must explicitly re-export approved order runtime types"
- );
- assert!(
- !source.contains("pub mod orders_runtime;"),
- "src/lib.rs must not expose the orders_runtime module path"
- );
- assert!(
- !source.contains("pub use crate::orders_runtime::*;"),
- "src/lib.rs must not wildcard-export the order runtime"
- );
-
- let runtime_exports = export_block(
- source.as_str(),
- "#[cfg(feature = \"runtime\")]\npub use crate::orders_runtime::{",
- );
- let signer_exports = export_block(
- source.as_str(),
- "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\npub use crate::orders_runtime::{",
- );
-
- for export in REQUIRED_TRADE_RUNTIME_EXPORTS {
- assert!(
- runtime_exports.contains(export),
- "src/lib.rs must explicitly expose trade SDK runtime export `{export}`"
- );
- }
-
- for export in REQUIRED_TRADE_SIGNER_EXPORTS {
- assert!(
- signer_exports.contains(export),
- "src/lib.rs must expose trade SDK signer export `{export}` behind runtime plus signer-adapters"
- );
- assert!(
- !runtime_exports.contains(export),
- "src/lib.rs runtime-only export block must not expose signer workflow export `{export}`"
- );
- }
-
- for export in REQUIRED_TRADE_POLICY_EXPORTS {
- assert!(
- source.contains(export),
- "src/lib.rs must explicitly expose trade policy export `{export}`"
- );
- }
-
- for forbidden in FORBIDDEN_REMOVED_ORDER_PRODUCT_EXPORTS {
- assert!(
- !source.contains(forbidden),
- "src/lib.rs must not expose removed order SDK product export `{forbidden}`"
- );
- }
-
- for forbidden in FORBIDDEN_TRADE_LOW_LEVEL_REQUEST_EXPORTS {
- assert!(
- !source.contains(forbidden),
- "src/lib.rs must not expose low-level trade mutation request export `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn trade_resync_and_validation_receipts_use_nostr_scoped_public_evidence_names() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let orders_source = read_source(manifest_dir.join("src/orders_runtime.rs").as_path());
- let lib_source = read_source(manifest_dir.join("src/lib.rs").as_path());
-
- for required in [
- "pub nostr_relay_urls: Vec<String>,",
- "pub nostr_evidence: TradeValidationReceiptNostrEvidenceReceipt,",
- "pub nostr_relay_outcomes: Vec<TradeResyncNostrRelayOutcomeReceipt>,",
- "pub nostr_relay_outcomes: Vec<TradeValidationReceiptNostrRelayOutcomeReceipt>,",
- "pub nostr_relay_url: String,",
- "TradeResyncNostrRelayOutcomeReceipt",
- "TradeResyncNostrRelayOutcomeKind",
- "TradeResyncNostrRelayTransportOutcomeKind",
- "TradeValidationReceiptNostrEvidenceReceipt",
- "TradeValidationReceiptNostrRelayOutcomeReceipt",
- "TradeValidationReceiptNostrRelayOutcomeKind",
- "TradeValidationReceiptNostrRelayTransportOutcomeKind",
- ] {
- assert!(
- orders_source.contains(required) || lib_source.contains(required),
- "SDK trade evidence source must retain Nostr-scoped public witness `{required}`"
- );
- }
-
- for forbidden in [
- "TradeResyncRelayOutcomeReceipt",
- "TradeResyncRelayOutcomeKind",
- "TradeResyncRelayTransportOutcomeKind",
- "TradeValidationReceiptRelayEvidenceReceipt",
- "TradeValidationReceiptRelayOutcomeReceipt",
- "TradeValidationReceiptRelayOutcomeKind",
- "TradeValidationReceiptRelayTransportOutcomeKind",
- "pub relay_targets:",
- "pub relay_evidence:",
- "pub relay_url:",
- "pub relays:",
- "relay_transport",
- ] {
- assert!(
- !orders_source.contains(forbidden) && !lib_source.contains(forbidden),
- "SDK trade evidence public source must not retain generic relay-shaped name `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn order_runtime_rejects_retired_status_source_names() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/orders_runtime.rs")
- .as_path(),
- );
-
- for retired in ["LocalEventStore", "local_event_store"] {
- assert!(
- !source.contains(retired),
- "src/orders_runtime.rs must not expose retired trade status source `{retired}`"
- );
- }
-}
-
-#[test]
-fn order_runtime_mutation_requests_require_evidence_mode() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/orders_runtime.rs")
- .as_path(),
- );
-
- assert!(
- source.contains("pub enum TradeEvidenceMode"),
- "src/orders_runtime.rs must define explicit trade mutation evidence modes"
- );
- for variant in [
- "LocalOnly",
- "ResyncBeforeMutation",
- "RequireExplicitEvidence",
- ] {
- assert!(
- source.contains(variant),
- "TradeEvidenceMode must retain `{variant}`"
- );
- }
- for request in [
- "TradeAcceptRequest",
- "TradeDeclineRequest",
- "TradeCancelRequest",
- ] {
- let struct_block = struct_block(source.as_str(), request);
- assert!(
- struct_block.contains("pub evidence_mode: TradeEvidenceMode"),
- "{request} must carry explicit evidence_mode"
- );
- }
- for constructor in [
- "impl TradeAcceptRequest",
- "impl TradeDeclineRequest",
- "impl TradeCancelRequest",
- ] {
- let impl_source = impl_block(source.as_str(), constructor);
- assert!(
- impl_source.contains("evidence_mode: TradeEvidenceMode"),
- "{constructor}::new must require explicit evidence mode"
- );
- }
-}
-
-#[test]
-fn retired_dvm_runtime_public_exports_are_absent() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let source = read_source(manifest_dir.join("src/lib.rs").as_path());
-
- assert!(
- !manifest_dir.join("src/dvm_runtime.rs").exists(),
- "src/dvm_runtime.rs must not exist after V1 validation receipt consolidation"
- );
- for forbidden in [
- "mod dvm_runtime;",
- "pub mod dvm_runtime;",
- "pub use crate::dvm_runtime",
- "DVM_TRADE_TRANSITION_PROOF_REQUEST_CONTRACT_ID",
- "DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND",
- "DvmProofMode",
- "DvmTradeTransitionProof",
- "DvmValidationReceipt",
- "RadrootsTradeInventoryBinWitnessDto",
- ] {
- assert!(
- !source.contains(forbidden),
- "src/lib.rs must not expose retired DVM runtime surface `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn local_sdk_signer_uses_event_signer_capability_boundary() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let signer_source = read_source(manifest_dir.join("src/signer_provider.rs").as_path());
- let lib_source = read_source(manifest_dir.join("src/lib.rs").as_path());
-
- for required in [
- "pub type RadrootsSdkLocalSignerCapability",
- "dyn RadrootsEventSigner + Send + Sync",
- "Arc<RadrootsSdkLocalSignerCapability>",
- "pub fn from_event_signer",
- "pub fn from_shared_event_signer",
- ] {
- assert!(
- signer_source.contains(required),
- "signer provider must expose the protected local signer capability boundary `{required}`"
- );
- }
-
- assert!(
- lib_source.contains("RadrootsSdkLocalSignerCapability"),
- "SDK root exports the local signer capability type for downstream protected signer wiring"
- );
- for forbidden in [
- "RadrootsSdkLocalKeySigner::new",
- "pub fn new(keys",
- "RadrootsLocalEventSigner",
- ] {
- assert!(
- !signer_source.contains(forbidden),
- "signer provider must not expose or couple to retired raw local signer constructor `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn private_store_schema_is_v1_encrypted_private_authority_only() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let source = read_source(manifest_dir.join("src/private_store.rs").as_path());
-
- for required in [
- "private_metadata",
- "wrapped_profile_key",
- "wrapped_signing_secret",
- "private_farm_location",
- "trade_private_thread",
- "buyer_contact_private",
- "cursor_hmac_key",
- "nip46_session_private",
- "key_rotation_progress",
- "RadrootsProtectedStoreEnvelope",
- "RadrootsProtectedFileKeySource",
- "credential_backend",
- ] {
- assert!(
- source.contains(required),
- "private store source must contain V1 encrypted private authority contract `{required}`"
- );
- }
-
- assert!(
- !source.contains("CREATE TABLE IF NOT EXISTS sdk_private_farm_location"),
- "retired plaintext SDK private farm-location table must not be created"
- );
- assert!(
- source.matches("sdk_private_farm_location").count() == 1
- && source.contains("reject_pre_v1_private_store"),
- "the retired private table name may appear only in the fail-closed pre-V1 rejection check"
- );
- let migration_start = source
- .find("const PRIVATE_STORE_MIGRATION_UP")
- .expect("private store migration must exist");
- let migration_end = source[migration_start..]
- .find("\"#;")
- .map(|offset| migration_start + offset)
- .expect("private store migration must close");
- let migration = &source[migration_start..migration_end];
- for forbidden_plaintext_column in [
- "label TEXT",
- "latitude REAL",
- "longitude REAL",
- "locality_city TEXT",
- "locality_region TEXT",
- "locality_country TEXT",
- "geohash5 TEXT",
- "geonames_id",
- ] {
- assert!(
- !migration.contains(forbidden_plaintext_column),
- "private farm-location migration must not expose plaintext column `{forbidden_plaintext_column}`"
- );
- }
-}
-
-#[test]
-fn identity_public_surface_is_an_explicit_feature_module() {
- let lib_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/lib.rs")
- .as_path(),
- );
- let identity_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/identity.rs")
- .as_path(),
- );
-
- assert!(
- lib_source
- .lines()
- .any(|line| line.trim() == "pub mod identity;"),
- "src/lib.rs must expose identity as an explicit feature-gated public module"
- );
- assert!(
- lib_source
- .lines()
- .all(|line| line.trim() != "mod identity;"),
- "src/lib.rs must not hide identity exports in a private module"
- );
- assert!(
- !lib_source.contains("pub use crate::identity::{"),
- "src/lib.rs must not flatten identity exports into root aliases"
- );
-
- for export in REQUIRED_IDENTITY_MODEL_EXPORTS {
- assert!(
- identity_source.contains(export),
- "src/identity.rs must expose identity model export `{export}`"
- );
- }
-
- for export in REQUIRED_IDENTITY_STORAGE_EXPORTS {
- assert!(
- identity_source.contains(export),
- "src/identity.rs must expose identity storage export `{export}`"
- );
- }
-}
-
-#[test]
-fn orders_client_surface_is_inventory_guarded() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/orders_runtime.rs")
- .as_path(),
- );
-
- assert!(
- source.contains("impl<'sdk> TradesClient<'sdk> {"),
- "src/orders_runtime.rs must own TradesClient runtime methods"
- );
-
- for method in REQUIRED_TRADES_CLIENT_METHODS {
- assert!(
- source.contains(method),
- "TradesClient must expose inventory-guarded method `{method}`"
- );
- }
-
- for method in FORBIDDEN_TRADES_CLIENT_PUBLIC_METHODS {
- assert!(
- !source.contains(method),
- "TradesClient must not expose low-level trade mutation method `{method}`"
- );
- }
-}
-
-#[test]
-fn trade_product_facade_methods_are_inventory_guarded() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/orders_runtime.rs")
- .as_path(),
- );
-
- for method in REQUIRED_TRADE_BUYER_CLIENT_METHODS {
- assert!(
- source.contains(method),
- "TradeBuyerClient must expose product workflow method `{method}`"
- );
- }
-
- for method in REQUIRED_TRADE_SELLER_CLIENT_METHODS {
- assert!(
- source.contains(method),
- "TradeSellerClient must expose product workflow method `{method}`"
- );
- }
-
- for method in REQUIRED_TRADE_RESYNC_CLIENT_METHODS {
- assert!(
- source.contains(method),
- "TradeResyncClient must expose product workflow method `{method}`"
- );
- }
-
- for method in REQUIRED_TRADE_VALIDATION_RECEIPTS_CLIENT_METHODS {
- assert!(
- source.contains(method),
- "TradeValidationReceiptsClient must expose product workflow method `{method}`"
- );
- }
-}
-
-#[test]
-fn trade_product_facade_feature_gates_are_explicit() {
- let orders_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/orders_runtime.rs")
- .as_path(),
- );
- let product_clients_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/product_clients.rs")
- .as_path(),
- );
-
- assert!(
- product_clients_source.contains(
- "#[cfg(feature = \"signer-adapters\")]\n pub fn buyer(&self) -> TradeBuyerClient<'client> {"
- ),
- "TradesClient::buyer must be gated by signer-adapters inside the runtime TradesClient impl"
- );
- assert!(
- product_clients_source.contains(
- "pub fn validation_receipts(&self) -> TradeValidationReceiptsClient<'client>"
- ),
- "TradesClient must expose validation receipts through the grouped trade product facade"
- );
- assert!(
- product_clients_source.contains(
- "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\n#[derive(Clone, Copy)]\npub struct TradeBuyerClient<'client>"
- ),
- "TradeBuyerClient must be gated by runtime plus signer-adapters"
- );
- assert!(
- product_clients_source.contains(
- "#[cfg(feature = \"runtime\")]\n#[derive(Clone, Copy)]\npub struct TradeSellerClient<'client>"
- ),
- "TradeSellerClient must remain runtime-visible for seller inbox reads"
- );
- assert!(
- orders_source.contains(
- "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\nimpl<'sdk> TradeBuyerClient<'sdk> {"
- ),
- "TradeBuyerClient product mutation impl must require runtime plus signer-adapters"
- );
- assert!(
- orders_source
- .contains("#[cfg(feature = \"runtime\")]\nimpl<'sdk> TradeSellerClient<'sdk> {"),
- "TradeSellerClient impl must remain runtime-visible for seller inbox"
- );
-
- let seller_impl = impl_block(
- orders_source.as_str(),
- "impl<'sdk> TradeSellerClient<'sdk> {",
- );
- assert!(
- seller_impl.contains("pub async fn inbox("),
- "TradeSellerClient must expose seller inbox in the runtime impl"
- );
- for method in ["accept_trade", "decline_trade"] {
- let gated_method =
- format!("#[cfg(feature = \"signer-adapters\")]\n pub async fn {method}(");
- assert!(
- seller_impl.contains(gated_method.as_str()),
- "TradeSellerClient::{method} must be gated by signer-adapters"
- );
- }
-}
-
-#[test]
-fn trade_propose_request_stays_product_shaped() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/orders_runtime.rs")
- .as_path(),
- );
- let request = struct_block(source.as_str(), "TradeProposeRequest");
-
- assert!(
- !request.contains("RadrootsOrderRequest"),
- "TradeProposeRequest must not expose protocol-shaped order request input"
- );
- 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>",
- ] {
- assert!(
- request.contains(required_field),
- "TradeProposeRequest must expose product field `{required_field}`"
- );
- }
-}
-
-#[test]
-fn retired_dvm_client_surface_is_absent() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let lib_source = read_source(manifest_dir.join("src/lib.rs").as_path());
- let clients_source = read_source(manifest_dir.join("src/product_clients.rs").as_path());
-
- assert!(
- !manifest_dir.join("src/dvm_runtime.rs").exists(),
- "src/dvm_runtime.rs must not exist after V1 validation receipt consolidation"
- );
- for forbidden in [
- "DvmClient",
- "prepare_trade_transition_proof_request",
- "enqueue_trade_transition_proof_request",
- "ingest_validation_receipt",
- ] {
- assert!(
- !lib_source.contains(forbidden) && !clients_source.contains(forbidden),
- "SDK product client surface must not expose retired DVM client concept `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn product_clients_remain_thin_sdk_handles() {
- let lib_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/lib.rs")
- .as_path(),
- );
- let clients_source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/product_clients.rs")
- .as_path(),
- );
-
- assert!(
- lib_source.contains("mod product_clients;"),
- "src/lib.rs must keep product_clients internal"
- );
- assert!(
- lib_source.contains("pub use crate::product_clients::{"),
- "src/lib.rs must explicitly export product client handles"
- );
- assert!(
- !lib_source.contains("pub mod product_clients;"),
- "src/lib.rs must not expose the product_clients module path"
- );
-
- for client in [
- "FarmsClient",
- "ListingsClient",
- "MarketClient",
- "SyncClient",
- "TradeResyncClient",
- "TradeSellerClient",
- "TradeValidationReceiptsClient",
- "TradesClient",
- ] {
- assert!(
- lib_source.contains(client),
- "src/lib.rs must export product client handle `{client}`"
- );
- assert!(
- clients_source.contains(format!("pub struct {client}<'client>").as_str()),
- "product_clients.rs must define thin handle `{client}`"
- );
- }
-
- let runtime_product_exports = export_block(
- lib_source.as_str(),
- "#[cfg(feature = \"runtime\")]\npub use crate::product_clients::{",
- );
- assert!(
- !runtime_product_exports.contains("TradeBuyerClient"),
- "src/lib.rs runtime-only product client export block must not expose TradeBuyerClient"
- );
- assert!(
- lib_source.contains(
- "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\npub use crate::product_clients::TradeBuyerClient;"
- ),
- "src/lib.rs must export TradeBuyerClient only behind runtime plus signer-adapters"
- );
- assert!(
- clients_source.contains("pub struct TradeBuyerClient<'client>"),
- "product_clients.rs must define thin signer-gated handle `TradeBuyerClient`"
- );
-
- for forbidden in FORBIDDEN_PRODUCT_CLIENT_HANDLES {
- assert!(
- !lib_source.contains(forbidden),
- "src/lib.rs must not export removed product client handle `{forbidden}`"
- );
- assert!(
- !clients_source.contains(forbidden),
- "product_clients.rs must not define removed product client handle `{forbidden}`"
- );
- }
-
- for forbidden in FORBIDDEN_PRODUCT_CLIENT_METHODS {
- assert!(
- !clients_source.contains(forbidden),
- "product_clients.rs must not expose removed product client method `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn removed_client_and_config_modules_are_absent() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
-
- for relative_path in ["src/client.rs", "src/config.rs"] {
- let path = manifest_dir.join(relative_path);
- assert!(
- !path.exists(),
- "{relative_path} must not exist after SDK runtime surface closure"
- );
- }
-}
-
-#[test]
-fn removed_trade_client_root_export_is_absent() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/lib.rs")
- .as_path(),
- );
-
- assert!(
- !source.contains("TradeClient"),
- "src/lib.rs must not re-export the removed TradeClient facade"
- );
-}
-
-#[test]
-fn removed_client_config_modules_are_not_public() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/lib.rs")
- .as_path(),
- );
-
- for forbidden in [
- "pub mod client;",
- "pub mod config;",
- "pub use crate::client",
- "pub use crate::config",
- "RadrootsSdkClient",
- "RadrootsSdkConfig",
- "SdkTransportMode",
- "ProfileClient",
- "FarmClient",
- "ListingClient",
- "SdkPublishReceipt",
- "SdkTransportReceipt",
- ] {
- assert!(
- !source.contains(forbidden),
- "src/lib.rs must not expose removed SDK client/config concept `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn sdk_public_api_does_not_export_protocol_workflow_bypass() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let source = read_source(manifest_dir.join("src/lib.rs").as_path());
-
- for forbidden in ["pub mod protocol;", "pub use crate::protocol"] {
- assert!(
- !source.contains(forbidden),
- "src/lib.rs must not expose protocol workflow bypass `{forbidden}`"
- );
- }
-
- assert!(
- !manifest_dir.join("src/protocol").exists(),
- "src/protocol must not remain as a public protocol re-export surface"
- );
+fn read_source(path: &Path) -> String {
+ fs::read_to_string(path).unwrap_or_else(|error| panic!("read {}: {error}", path.display()))
}
-#[test]
-fn sdk_radrootsd_execution_surfaces_reject_removed_daemon_publish_identifiers() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
-
- for relative_path in [
- "Cargo.toml",
- "src/lib.rs",
- "src/adapters/radrootsd.rs",
- "src/sync_runtime.rs",
- "src/transport.rs",
- ] {
- let source = read_source(manifest_dir.join(relative_path).as_path());
- for forbidden in FORBIDDEN_DAEMON_PUBLISH_PROXY_IDENTIFIERS {
- assert!(
- !contains_forbidden_concept(source.as_str(), forbidden),
- "{relative_path} must not reintroduce removed daemon publish identifier `{forbidden}`"
- );
- }
- }
-
- let transport_source = read_source(manifest_dir.join("src/transport.rs").as_path());
- assert!(
- !transport_source.contains(concat!("RadrootsTransportKind::", "Pro", "xy")),
- "src/transport.rs must not model radrootsd execution as a transport kind"
- );
-
- let sync_runtime_source = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
- assert!(
- !sync_runtime_source.contains(concat!("RadrootsTransportKind::", "Pro", "xy")),
- "src/sync_runtime.rs must not identify radrootsd execution through a transport kind"
- );
- assert!(
- sync_runtime_source.contains("radrootsd execution explicit targets are Nostr-only"),
- "src/sync_runtime.rs must reject non-Nostr radrootsd explicit targets before behavior is lost"
- );
- assert!(
- sync_runtime_source.contains("active_delivery_plan_id(claimed"),
- "src/sync_runtime.rs must derive radrootsd publish behavior from the claimed active delivery plan"
- );
- for required in [
- "let mut completed_target_ids = std::collections::BTreeSet::new();",
- "let mut matched_outcomes = Vec::new();",
- "matched multiple ready delivery targets",
- "matched delivery target",
- "more than once",
- ] {
- assert!(
- sync_runtime_source.contains(required),
- "src/sync_runtime.rs must retain radrootsd completion uniqueness witness `{required}`"
- );
- }
- assert!(
- !sync_runtime_source.contains("TransportPublishReticulumBehavior::RejectDeliveryAttempts"),
- "src/sync_runtime.rs must not rewrite Reticulum radrootsd outbox targets to reject attempts"
- );
- let sync_runtime_unit_source = read_source(
- manifest_dir
- .join("tests/unit/sync_runtime_tests.rs")
- .as_path(),
- );
- for required in [
- "claimed_uningested_radrootsd_event",
- "assert_no_transport_publish_request",
- "assert!(!stored_before.event_store_ingested)",
- "assert!(!stored.event_store_ingested)",
- "with_timeout(Duration::from_millis(50))",
- "radrootsd_delivery_policy_rejects_non_accepted_satisfaction_before_daemon_publish",
- "radrootsd_outbox_target_conversion_preserves_nostr_scope_and_label",
- "radrootsd_completion_matches_duplicate_endpoint_targets_by_scope",
- "push_radrootsd_event_receipt_preserves_daemon_target_metadata",
- "radrootsd_completion_rejects_duplicate_daemon_outcome_before_local_mutation",
- ] {
- assert!(
- sync_runtime_unit_source.contains(required),
- "tests/unit/sync_runtime_tests.rs must retain radrootsd local-validation ordering proof `{required}`"
- );
- }
-
- let adapter_source = read_source(manifest_dir.join("src/adapters/radrootsd.rs").as_path());
- assert!(
- !adapter_source.contains("impl RadrootsRelayPublishAdapter for RadrootsdPublishAdapter"),
- "src/adapters/radrootsd.rs must not implement relay publish traits for RadrootsdPublishAdapter"
- );
- assert!(
- !adapter_source.contains("radrootsd_relay_receipt_from_response"),
- "src/adapters/radrootsd.rs must not convert typed transport publish jobs into relay receipts"
- );
- assert!(
- !adapter_source.contains("TransportPublishReticulumBehavior::RejectDeliveryAttempts"),
- "src/adapters/radrootsd.rs must not rewrite Reticulum relay targets to reject attempts"
- );
-
- for required in [
- "TransportPublishOutcomeKind::DeferredUntilImplemented",
- "mark_delivery_target_deferred_until_implemented",
- "TransportPublishOutcomeKind::DeferredUntilImplemented",
- "mark_delivery_target_deferred_until_implemented",
- "PushOutboxEventState::DeferredUntilImplemented",
- "PushOutboxEventState::DeferredUntilImplemented",
- "PushOutboxTargetOutcomeKind::DeferredUntilImplemented",
- "PushOutboxTargetOutcomeKind::DeferredUntilImplemented",
- "reject_non_accepted_radrootsd_satisfaction",
- "class != RadrootsTransportSatisfactionClass::Accepted",
- "target.target_scope.as_ref()",
- "outcome.target_scope.as_deref()",
- "target_scope: outcome.target_scope",
- "target_label: outcome.target_label",
- ] {
- assert!(
- sync_runtime_source.contains(required),
- "src/sync_runtime.rs must preserve radrootsd deferred outcome witness `{required}`"
- );
- }
-
- let receipt_source = source_between(
- sync_runtime_source.as_str(),
- "fn push_radrootsd_event_receipt",
- "fn push_radrootsd_target_receipt",
- );
- assert!(
- receipt_source.contains("push_receipt_event_id("),
- "push_radrootsd_event_receipt must convert daemon event ids through the typed receipt helper"
- );
- for forbidden in [".expect(", ".unwrap(", "panic!("] {
- assert!(
- !receipt_source.contains(forbidden),
- "push_radrootsd_event_receipt must not use production panic path `{forbidden}`"
- );
- }
-
- let radrootsd_target_receipt_source = source_between(
- sync_runtime_source.as_str(),
- "fn push_radrootsd_target_receipt",
- "fn push_radrootsd_target_outcome_kind",
- );
- for forbidden in ["target_scope: None", "target_label: None"] {
- assert!(
- !radrootsd_target_receipt_source.contains(forbidden),
- "push_radrootsd_target_receipt must not hard-code daemon metadata field `{forbidden}`"
- );
- }
+fn active_source_files() -> Vec<PathBuf> {
+ let src = manifest_dir().join("src");
+ fs::read_dir(src)
+ .expect("src entries")
+ .map(|entry| entry.expect("src entry").path())
+ .filter(|path| path.extension().is_some_and(|extension| extension == "rs"))
+ .collect()
}
#[test]
-fn sdk_direct_nostr_push_receipts_preserve_outbox_target_metadata() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let sync_runtime_source = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
-
- for required in [
- "RadrootsOutboxPublishReceipt",
- "RadrootsOutboxPublishTargetReceipt",
- "push_event_final_state(&publish)",
- ".target_receipts",
- "target_scope: target.target_scope",
- "target_label: target.target_label",
- ] {
- assert!(
- sync_runtime_source.contains(required),
- "src/sync_runtime.rs must preserve direct Nostr target-aware receipt witness `{required}`"
- );
- }
+fn retired_order_runtime_roots_are_not_active() {
+ let manifest = manifest_dir();
+ let lib = read_source(&manifest.join("src/lib.rs"));
- let direct_event_receipt_source = source_between(
- sync_runtime_source.as_str(),
- "fn push_event_receipt",
- "fn push_receipt_event_id",
- );
- assert!(
- !direct_event_receipt_source.contains(".relays"),
- "push_event_receipt must not derive SDK direct publish targets from URL-level relay receipts"
- );
-
- let direct_target_receipt_source = source_between(
- sync_runtime_source.as_str(),
- "fn push_target_receipt",
- "#[cfg(all(test",
- );
for forbidden in [
- "target_scope: None",
- "target_label: None",
- "RadrootsRelayPublishRelayReceipt",
+ "mod orders_runtime;",
+ "mod market_runtime;",
+ "mod trade_storage;",
+ "mod order;",
+ "pub use crate::orders_runtime",
+ "pub use crate::market_runtime",
+ "pub use crate::trade_storage",
] {
assert!(
- !direct_target_receipt_source.contains(forbidden),
- "push_target_receipt must not hard-code or relay-derive direct Nostr metadata token `{forbidden}`"
+ !lib.contains(forbidden),
+ "src/lib.rs must not activate retired runtime root `{forbidden}`"
);
}
- let sync_runtime_test_source =
- read_source(manifest_dir.join("tests/sync_runtime.rs").as_path());
- for required in [
- "push_outbox_with_transport_preserves_scoped_duplicate_target_metadata",
- "push_outbox_adapter_transport_failure_preserves_scoped_target_metadata",
- "target.target_scope.as_deref() == Some(\"farm.a\")",
- "target.target_scope.as_deref() == Some(\"farm.b\")",
+ for retired in [
+ "src/orders_runtime.rs",
+ "src/market_runtime.rs",
+ "src/trade_storage.rs",
+ "src/order.rs",
+ "tests/orders_runtime.rs",
+ "tests/market_runtime.rs",
+ "tests/trade_public_api.rs",
+ "tests/trade_product_publish_runtime.rs",
] {
assert!(
- sync_runtime_test_source.contains(required),
- "tests/sync_runtime.rs must retain SDK direct Nostr metadata coverage `{required}`"
+ !manifest.join(retired).exists(),
+ "retired SDK order runtime file must be removed: {retired}"
);
}
}
#[test]
-fn sdk_public_outcome_label_contracts_are_explicit() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let sync_runtime_source = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
- let orders_runtime_source = read_source(manifest_dir.join("src/orders_runtime.rs").as_path());
-
- for (source, enum_name, required_labels) in [
- (
- sync_runtime_source.as_str(),
- "PushOutboxTargetOutcomeKind",
- &[
- r#"Self::Accepted => "accepted""#,
- r#"Self::DuplicateAccepted => "duplicate_accepted""#,
- r#"Self::Blocked => "blocked""#,
- r#"Self::RateLimited => "rate_limited""#,
- r#"Self::Invalid => "invalid""#,
- r#"Self::PowRequired => "pow_required""#,
- r#"Self::Restricted => "restricted""#,
- r#"Self::AuthRequired => "auth_required""#,
- r#"Self::Muted => "muted""#,
- r#"Self::Unsupported => "unsupported""#,
- r#"Self::PaymentRequired => "payment_required""#,
- r#"Self::Error => "error""#,
- r#"Self::Timeout => "timeout""#,
- r#"Self::ConnectionFailed => "connection_failed""#,
- r#"Self::TargetUriRejected => "target_uri_rejected""#,
- r#"Self::SkippedAlreadyAccepted => "skipped_already_accepted""#,
- r#"Self::DeferredUntilImplemented => "deferred_until_implemented""#,
- r#"Self::Unknown => "unknown""#,
- ][..],
- ),
- (
- sync_runtime_source.as_str(),
- "PushOutboxTransportOutcomeKind",
- &[
- r#"Self::Accepted => "accepted""#,
- r#"Self::DuplicateAccepted => "duplicate_accepted""#,
- r#"Self::Delivered => "delivered""#,
- r#"Self::Forwarded => "forwarded""#,
- r#"Self::StoredByGateway => "stored_by_gateway""#,
- r#"Self::Seen => "seen""#,
- r#"Self::DeferredUntilImplemented => "deferred_until_implemented""#,
- r#"Self::Rejected => "rejected""#,
- r#"Self::RouteUnavailable => "route_unavailable""#,
- r#"Self::PayloadTooLarge => "payload_too_large""#,
- r#"Self::PolicyDenied => "policy_denied""#,
- r#"Self::Timeout => "timeout""#,
- r#"Self::ConnectionFailed => "connection_failed""#,
- r#"Self::TransportUnavailable => "transport_unavailable""#,
- ][..],
- ),
- (
- orders_runtime_source.as_str(),
- "TradeResyncNostrRelayOutcomeKind",
- &[
- r#"Self::Eose => "eose""#,
- r#"Self::Closed => "closed""#,
- r#"Self::Notice => "notice""#,
- ][..],
- ),
- (
- orders_runtime_source.as_str(),
- "TradeResyncNostrRelayTransportOutcomeKind",
- &[
- r#"Self::Accepted => "accepted""#,
- r#"Self::DuplicateAccepted => "duplicate_accepted""#,
- r#"Self::Blocked => "blocked""#,
- r#"Self::RateLimited => "rate_limited""#,
- r#"Self::Invalid => "invalid""#,
- r#"Self::PowRequired => "pow_required""#,
- r#"Self::Restricted => "restricted""#,
- r#"Self::AuthRequired => "auth_required""#,
- r#"Self::Muted => "muted""#,
- r#"Self::Unsupported => "unsupported""#,
- r#"Self::PaymentRequired => "payment_required""#,
- r#"Self::Error => "error""#,
- r#"Self::Timeout => "timeout""#,
- r#"Self::ConnectionFailed => "connection_failed""#,
- r#"Self::RelayUrlRejected => "relay_url_rejected""#,
- r#"Self::SkippedAlreadyAccepted => "skipped_already_accepted""#,
- r#"Self::Unknown => "unknown""#,
- ][..],
- ),
- (
- orders_runtime_source.as_str(),
- "TradeValidationReceiptNostrRelayOutcomeKind",
- &[
- r#"Self::Eose => "eose""#,
- r#"Self::Closed => "closed""#,
- r#"Self::Notice => "notice""#,
- ][..],
- ),
- (
- orders_runtime_source.as_str(),
- "TradeValidationReceiptNostrRelayTransportOutcomeKind",
- &[
- r#"Self::Accepted => "accepted""#,
- r#"Self::DuplicateAccepted => "duplicate_accepted""#,
- r#"Self::Blocked => "blocked""#,
- r#"Self::RateLimited => "rate_limited""#,
- r#"Self::Invalid => "invalid""#,
- r#"Self::PowRequired => "pow_required""#,
- r#"Self::Restricted => "restricted""#,
- r#"Self::AuthRequired => "auth_required""#,
- r#"Self::Muted => "muted""#,
- r#"Self::Unsupported => "unsupported""#,
- r#"Self::PaymentRequired => "payment_required""#,
- r#"Self::Error => "error""#,
- r#"Self::Timeout => "timeout""#,
- r#"Self::ConnectionFailed => "connection_failed""#,
- r#"Self::RelayUrlRejected => "relay_url_rejected""#,
- r#"Self::SkippedAlreadyAccepted => "skipped_already_accepted""#,
- r#"Self::Unknown => "unknown""#,
- ][..],
- ),
- ] {
- let marker = format!("impl {enum_name} {{");
- let block = impl_block(source, marker.as_str());
- assert!(
- block.contains("pub fn as_str(self) -> &'static str"),
- "{enum_name} must expose an SDK-owned public label helper"
- );
- for required in required_labels {
+fn active_sources_do_not_import_retired_trade_modules() {
+ for path in active_source_files() {
+ let source = read_source(&path);
+ for forbidden in ["radroots_trade::order", "radroots_trade::projection"] {
assert!(
- block.contains(required),
- "{enum_name} label helper must explicitly cover `{required}`"
- );
- }
- for forbidden in ["serde_json::to_value", "panic!(", "_ =>"] {
- assert!(
- !block.contains(forbidden),
- "{enum_name} label helper must not contain forbidden conversion token `{forbidden}`"
+ !source.contains(forbidden),
+ "{} must not import retired trade module `{forbidden}`",
+ path.display()
);
}
}
}
#[test]
-fn sdk_transport_policy_sources_reject_configured_profile_and_radrootsd_relay_bridge() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- for relative_path in [
- "src",
- "tests",
- "examples",
- "packages/event-bindings/src",
- "packages/event-codec-js/src",
- ] {
- let root = manifest_dir.join(relative_path);
- if !root.exists() {
- continue;
- }
- for path in rust_source_files(root.as_path()) {
- if path.file_name().and_then(|file_name| file_name.to_str())
- == Some("source_boundary.rs")
- {
- continue;
- }
- let source = read_source(path.as_path());
- for forbidden in [
- "UseConfiguredProfile",
- "use_configured_profile",
- "configured_profile()",
- "UseTransportProfile",
- "use_transport_profile",
- "impl RadrootsRelayPublishAdapter for RadrootsdPublishAdapter",
- "radrootsd_relay_receipt_from_response",
- ] {
- assert!(
- !source.contains(forbidden),
- "{} must not reintroduce removed transport policy or relay bridge surface `{forbidden}`",
- path.display()
- );
- }
- }
- }
-}
-
-#[test]
-fn sdk_transport_sources_reject_digest_only_and_top_level_relay_adapter_dispatch() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- for relative_path in ["src", "tests", "examples"] {
- let root = manifest_dir.join(relative_path);
- if !root.exists() {
- continue;
- }
- for path in rust_source_files(root.as_path()) {
- if path.file_name().and_then(|file_name| file_name.to_str())
- == Some("source_boundary.rs")
- {
- continue;
- }
- let source = read_source(path.as_path());
- let relative_path = relative_manifest_path(manifest_dir, path.as_path());
- for forbidden in ["payload_digest", "DigestOnly", "push_outbox_with_adapter"] {
- assert!(
- !source.contains(forbidden),
- "{relative_path} must not retain digest-only or adapter-owned transport dispatch `{forbidden}`"
- );
- }
- }
- }
-
- let sync_runtime = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
- for required in [
- "RadrootsNostrTransport::new(adapter)",
- "self.push_outbox_with_transport(&transport, request).await",
- "pub async fn push_outbox_with_transport<T>",
- "T: radroots_transport::RadrootsTransport + ?Sized",
- "publish_claimed_outbox_event_with_transport(",
- ] {
- assert!(
- sync_runtime.contains(required),
- "src/sync_runtime.rs must retain generic transport dispatch witness `{required}`"
- );
- }
- assert!(
- !sync_runtime.contains("RadrootsRelayPublishAdapter"),
- "src/sync_runtime.rs must not expose relay adapter dispatch as the top-level SDK push path"
- );
-}
-
-#[test]
-fn sdk_sync_status_sources_reject_retired_relay_shaped_generic_fields() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let sync_runtime = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
- let status_summary = source_between(
- sync_runtime.as_str(),
- "pub struct SyncTransportStatusSummary",
- "\n}\n\n#[cfg(feature = \"runtime\")]\nimpl SyncTransportStatusSummary",
- );
-
- for required in [
- "pub configured_transport_target_count: usize,",
- "pub configured_transport_targets: Vec<SyncTransportTargetSummary>,",
- "pub transport_statuses: Vec<SyncTransportStatusSummary>,",
- ".configured_transport_targets()?",
- ".transport_statuses()",
- "pub target_scope: Option<String>,",
- "pub target_label: Option<String>,",
- "target.scope.as_ref()",
- "target.label.as_ref()",
- "pub transport: String,",
- "pub configured: bool,",
- "pub implementation: String,",
- "pub usable_for_delivery: bool,",
- "pub capabilities: SyncTransportOperationCapabilitiesSummary,",
- "pub struct SyncTransportOperationCapabilitiesSummary",
- "pub deliver: bool,",
- "pub fetch: bool,",
- "status.capabilities.deliver",
- "status.capabilities.fetch",
- "pub message: String,",
- ] {
- assert!(
- sync_runtime.contains(required),
- "SDK sync status must retain transport-neutral field witness `{required}`"
- );
- }
-
- for required in [
- "pub transport: String,",
- "pub configured: bool,",
- "pub implementation: String,",
- "pub usable_for_delivery: bool,",
- "pub capabilities: SyncTransportOperationCapabilitiesSummary,",
- "pub message: String,",
- ] {
- assert!(
- status_summary.contains(required),
- "SDK sync status summary must retain canonical status field witness `{required}`"
- );
- }
-
- let operation_capabilities_summary = source_between(
- sync_runtime.as_str(),
- "pub struct SyncTransportOperationCapabilitiesSummary",
- "\n}\n\n#[cfg(feature = \"runtime\")]\nimpl SyncTransportStatusSummary",
- );
- for required in ["pub deliver: bool,", "pub fetch: bool,"] {
- assert!(
- operation_capabilities_summary.contains(required),
- "SDK sync transport capabilities must retain operation field witness `{required}`"
- );
- }
-
- for forbidden in [
- concat!("configured_nostr", "_relay", "_count"),
- concat!("configured_nostr", "_relays"),
- concat!("target", "_relays"),
- concat!("connected", "_relays"),
- concat!("acknowledged", "_relays"),
- concat!("failed", "_relays"),
- concat!("relay", "_count"),
- ] {
- assert!(
- !sync_runtime.contains(forbidden),
- "SDK sync status source must not expose retired relay-shaped generic field `{forbidden}`"
- );
- }
-
- for forbidden in [
- concat!("pub transport", "_kind: String,"),
- concat!("pub implementation", "_state: String,"),
- "pub readiness: String,",
- concat!("pub publish", "_usable: bool,"),
- concat!("pub fetch", "_usable: bool,"),
- concat!("pub redacted", "_message: Option<String>,"),
- ] {
- assert!(
- !status_summary.contains(forbidden),
- "SDK sync status summary must not expose retired status field `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn sdk_transport_sources_expose_full_satisfaction_policy_without_legacy_aliases() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let transport_source = read_source(manifest_dir.join("src/transport.rs").as_path());
- let workflow_runtime = read_source(manifest_dir.join("src/workflow_runtime.rs").as_path());
-
- for required in [
- "pub enum SatisfactionPolicy",
- "AnyAccepted",
- "AllAccepted",
- "QuorumAccepted",
- "AnyDelivered",
- "AllDelivered",
- "QuorumDelivered",
- "RequiredAcceptedTargets",
- "RequiredDeliveredTargets",
- "pub fn quorum_accepted(",
- "pub fn quorum_delivered(",
- "pub fn required_accepted_targets",
- "pub fn required_delivered_targets",
- "RadrootsTransportSatisfactionPolicy::required_targets",
- "RadrootsTransportSatisfactionClass::Accepted",
- "RadrootsTransportSatisfactionClass::Delivered",
- ] {
- assert!(
- transport_source.contains(required),
- "src/transport.rs must retain full SDK satisfaction policy witness `{required}`"
- );
- }
-
- for forbidden in [
- "AtLeastOneTarget",
- "AllTargets",
- "pub fn at_least",
- concat!("at_least", "_one_target"),
- concat!("all", "_targets"),
- ] {
- assert!(
- !transport_source.contains(forbidden) && !workflow_runtime.contains(forbidden),
- "SDK transport/workflow sources must not retain legacy satisfaction alias `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn sdk_workflow_runtime_records_local_import_observations() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let workflow_runtime = read_source(manifest_dir.join("src/workflow_runtime.rs").as_path());
-
- for required in [
- "SDK_LOCAL_EVENT_ENDPOINT_URI",
- "RadrootsTransportKind::Local",
- "RadrootsTransportObservationType::LocalImport",
- ".with_observation(local_import_observation)",
- ] {
- assert!(
- workflow_runtime.contains(required),
- "SDK workflow runtime must retain local import observation witness `{required}`"
- );
- }
-}
-
-#[test]
-fn sdk_transport_sources_keep_reticulum_push_boundary() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
-
- for relative_path in [
- "src/transport.rs",
- "src/sync_runtime.rs",
- "src/error.rs",
- "src/adapters/radrootsd.rs",
- ] {
- let source = read_source(manifest_dir.join(relative_path).as_path());
- if let Some(line) = removed_reticulum_endpoint_lines(source.as_str())
- .into_iter()
- .next()
- {
- panic!("{relative_path}:{line} contains removed Reticulum endpoint");
- }
- }
-
- let transport_source = read_source(manifest_dir.join("src/transport.rs").as_path());
- assert!(
- transport_source.contains("RADROOTS_RETICULUM_ENDPOINT_URI"),
- "src/transport.rs must consume the shared Reticulum endpoint constant"
- );
- assert!(
- !transport_source.contains("const RETICULUM_PREVIEW_ENDPOINT_URI"),
- "src/transport.rs must not duplicate the Reticulum endpoint constant"
- );
- assert!(
- !transport_source.contains("reticulum:local"),
- "src/transport.rs must not duplicate the Reticulum endpoint literal"
- );
-
- let sync_runtime = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
- for required in [
- "TransportProfile::Reticulum { .. }",
- "reticulum_push_receipt",
- "reticulum_event_receipt",
- "push_reported_event",
- "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE",
- "ReticulumTryNowRequest",
- "try_reticulum_now",
- "\"sync.try_reticulum_now\"",
- "RadrootsSdkError::ReticulumTransportUnavailable",
- "pub target_scope: Option<String>,",
- "pub target_label: Option<String>,",
- "pub transport_outcome_kind: Option<PushOutboxTransportOutcomeKind>,",
- "PushOutboxTransportOutcomeKind::TransportUnavailable",
- ] {
- assert!(
- sync_runtime.contains(required),
- "src/sync_runtime.rs must retain Reticulum push boundary `{required}`"
- );
- }
- assert!(
- !sync_runtime.contains("push_outbox_has_no_reticulum_work"),
- "src/sync_runtime.rs must not revive the Reticulum ready-work error probe"
- );
- assert!(
- !sync_runtime.contains("RadrootsSdkError::reticulum_transport_unavailable(\n"),
- "src/sync_runtime.rs must not return Reticulum deferred until implemented errors from push_outbox"
- );
-
- let error_source = read_source(manifest_dir.join("src/error.rs").as_path());
- for required in [
- "reticulum_transport_unavailable",
- "reticulum_transport_deferred",
- "Reticulum endpoint",
- ] {
- assert!(
- error_source.contains(required),
- "src/error.rs must retain Reticulum error witness `{required}`"
- );
- }
-}
-
-#[test]
-fn sdk_feature_matrix_keeps_reticulum_runtime_owned_without_alias() {
- let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
- let manifest_source = read_source(manifest_dir.join("Cargo.toml").as_path());
- let features_source = source_between(manifest_source.as_str(), "[features]", "[dependencies]");
- let runtime_source = source_between(features_source, "runtime = [", "local-signer = [");
- let nostr_runtime_source = source_between(
- features_source,
- "transport-nostr-runtime = [",
- "local-runtime = [",
- );
-
- for required in [
- "\"dep:radroots_transport_reticulum\"",
- "\"dep:radroots_transport\"",
- "\"dep:radroots_outbox\"",
- ] {
- assert!(
- runtime_source.contains(required),
- "SDK runtime feature must retain Reticulum matrix witness `{required}`"
- );
- }
- for required in [
- "\"runtime\"",
- "\"dep:radroots_nostr\"",
- "\"radroots_nostr/client\"",
- "\"radroots_transport_nostr/client\"",
- ] {
- assert!(
- nostr_runtime_source.contains(required),
- "SDK Nostr runtime feature must retain real delivery matrix witness `{required}`"
- );
- }
-
- for forbidden in [
- concat!("transport-reticulum-", "pre", "view"),
- "radroots_transport_reticulum/client",
- "reticulum-runtime",
- "dep:rns",
- "dep:rnsd",
- "dep:reticulum",
- "dep:pyo3",
- "package = \"reticulum\"",
- "package = \"pyo3\"",
- ] {
- assert!(
- !manifest_source.contains(forbidden),
- "SDK feature matrix must not introduce Reticulum alias or real runtime dependency `{forbidden}`"
- );
- }
-
- let xtask_check_source = read_source(
- manifest_dir
- .join("../../tools/xtask/src/check.rs")
- .as_path(),
- );
- for required in [
- "check_sdk_workspace_reticulum_dependency_boundaries",
- "check_manifest_reticulum_dependency_boundaries",
- "check_cargo_lock_reticulum_package_names",
- "check_cargo_metadata_reticulum_package_names",
- "package = \"reticulum\"",
- "package = \"pyo3\"",
- "dto_bindgen_backend_python",
- ] {
- assert!(
- xtask_check_source.contains(required),
- "SDK xtask Reticulum release gate must retain package-graph bypass witness `{required}`"
- );
- }
-
- let sync_runtime_source = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
- for required in [
- "#[cfg(not(feature = \"radrootsd-execution\"))]",
- "self.sdk.radrootsd_execution_profile().is_some()",
- "required_feature: \"radrootsd-execution\"",
- ] {
- assert!(
- sync_runtime_source.contains(required),
- "SDK runtime-only push_outbox must retain radrootsd execution feature gate witness `{required}`"
- );
- }
-}
-
-fn product_runtime_file_stays_on_boundary(relative_path: &str) {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join(relative_path)
- .as_path(),
- );
-
- for forbidden in [
- "RadrootsSdkClient",
- "SdkTransportMode",
- "SdkPublishReceipt",
- "radrootsd",
- "publish_with_identity",
- "publish_parts_via_relay",
- "publish_listing_via_radrootsd",
- "publish_order_request_via_radrootsd",
- "publish_farm_via_radrootsd",
- ] {
- assert!(
- !source.contains(forbidden),
- "{relative_path} must not use removed SDK client or transport concept `{forbidden}`"
- );
- }
-}
-
-fn sdk_root_alias_guard_files(manifest_dir: &Path) -> Vec<PathBuf> {
- let mut paths = vec![manifest_dir.join("README")];
-
- for relative_root in ["src", "examples", "tests"] {
- let root = manifest_dir.join(relative_root);
- if root.exists() {
- paths.extend(rust_source_files(root.as_path()));
- }
- }
-
- paths.retain(|path| {
- path.file_name().and_then(|file_name| file_name.to_str()) != Some("source_boundary.rs")
- });
- paths.sort();
- paths
-}
-
-fn sdk_foundation_hardening_guard_files(repo_root: &Path) -> Vec<PathBuf> {
- let mut paths = Vec::new();
-
- for path in [
- repo_root.join("Cargo.toml"),
- repo_root.join("package.json"),
- repo_root.join("README"),
- repo_root.join("README.md"),
- ] {
- if path.exists() {
- paths.push(path);
- }
- }
-
- for relative_root in ["crates", "packages"] {
- let root = repo_root.join(relative_root);
- if root.exists() {
- collect_sdk_foundation_hardening_guard_files(root.as_path(), &mut paths);
- }
- }
-
- paths.sort();
- paths
-}
-
-fn collect_sdk_foundation_hardening_guard_files(root: &Path, paths: &mut Vec<PathBuf>) {
- for entry in fs::read_dir(root)
- .unwrap_or_else(|error| panic!("failed to read {}: {error}", root.display()))
- {
- let path = entry.expect("SDK guard entry").path();
- if path.file_name().and_then(|file_name| file_name.to_str()) == Some("source_boundary.rs") {
- continue;
- }
-
- if path.is_dir() {
- if matches!(
- path.file_name().and_then(|file_name| file_name.to_str()),
- Some("target") | Some("node_modules") | Some("dist")
- ) {
- continue;
- }
- collect_sdk_foundation_hardening_guard_files(path.as_path(), paths);
- continue;
- }
-
- let is_guarded_extension = matches!(
- path.extension().and_then(|extension| extension.to_str()),
- Some("rs") | Some("ts") | Some("json") | Some("toml") | Some("md")
- );
- if is_guarded_extension
- || matches!(
- path.file_name().and_then(|file_name| file_name.to_str()),
- Some("README") | Some("README.md")
- )
- {
- paths.push(path);
- }
- }
-}
-
-fn foundation_hardening_retired_concept_allowed(relative_path: &str, pattern: &str) -> bool {
- pattern == "RadrootsNostrEvent" && sdk_nostr_protocol_context(relative_path)
-}
-
-fn sdk_nostr_protocol_context(relative_path: &str) -> bool {
- matches!(
- relative_path,
- "crates/sdk/src/adapters/nostr.rs"
- | "crates/sdk/src/adapters/signing.rs"
- | "crates/sdk/src/signer_provider.rs"
- | "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs"
- | "crates/sdk/tests/unit/signer_provider_tests.rs"
- )
-}
-
-fn read_source(path: &Path) -> String {
- fs::read_to_string(path)
- .unwrap_or_else(|error| panic!("failed to read source {}: {error}", path.display()))
-}
-
-fn contains_forbidden_concept(source: &str, pattern: &str) -> bool {
- if !pattern.chars().all(is_rust_identifier_character) {
- return source.contains(pattern);
- }
-
- source.match_indices(pattern).any(|(index, _)| {
- let before = source[..index].chars().next_back();
- let after = source[index + pattern.len()..].chars().next();
- before.is_none_or(|character| !is_rust_identifier_character(character))
- && after.is_none_or(|character| !is_rust_identifier_character(character))
- })
-}
-
-fn is_doc_surface(path: &Path) -> bool {
- matches!(
- path.file_name().and_then(|file_name| file_name.to_str()),
- Some("README") | Some("README.md")
- ) || matches!(
- path.extension().and_then(|extension| extension.to_str()),
- Some("md")
- )
-}
-
-fn removed_reticulum_endpoint_lines(source: &str) -> Vec<usize> {
- let removed_endpoint = ["reticulum:", "pre", "view"].concat();
- source
- .match_indices(removed_endpoint.as_str())
- .filter_map(|(index, _)| {
- let after = source[index + removed_endpoint.len()..].chars().next();
- (after != Some('-')).then(|| line_number(source, index))
- })
- .collect()
-}
-
-fn root_trade_alias_findings(relative_path: &str, source: &str) -> Vec<String> {
- let mut findings = Vec::new();
-
- for alias in FORBIDDEN_SDK_ROOT_TRADE_ALIAS_NAMES {
- for (index, _) in source.match_indices(alias) {
- let before = source[..index].chars().next_back();
- let after_index = index + alias.len();
- let after = source[after_index..].chars().next();
-
- if before.is_some_and(is_rust_identifier_character)
- || after.is_some_and(is_rust_identifier_character)
- {
- continue;
- }
-
- if source[after_index..]
- .chars()
- .find(|character| !character.is_whitespace())
- != Some('(')
- {
- continue;
- }
-
- let prefix = source[..index].trim_end();
- let reason = if prefix.ends_with('.') {
- "root client method call"
- } else if prefix.ends_with("::") {
- "root client associated function call"
- } else if prefix_ends_with_keyword(prefix, "fn") {
- "function or method definition"
- } else {
- continue;
- };
-
- findings.push(format!(
- "{relative_path}:{} uses `{alias}` as a {reason}",
- line_number(source, index)
- ));
- }
- }
-
- findings
-}
-
-fn prefix_ends_with_keyword(source: &str, keyword: &str) -> bool {
- source.ends_with(keyword)
- && source[..source.len() - keyword.len()]
- .chars()
- .next_back()
- .is_none_or(|character| !is_rust_identifier_character(character))
-}
-
-fn line_number(source: &str, index: usize) -> usize {
- source[..index]
- .bytes()
- .filter(|byte| *byte == b'\n')
- .count()
- + 1
-}
-
-fn relative_manifest_path(manifest_dir: &Path, path: &Path) -> String {
- path.strip_prefix(manifest_dir)
- .unwrap_or(path)
- .display()
- .to_string()
-}
-
-fn function_source<'source>(source: &'source str, function_name: &str) -> &'source str {
- let signature = format!("async fn {function_name}");
- let start = source
- .find(signature.as_str())
- .unwrap_or_else(|| panic!("failed to find test function `{function_name}`"));
- let source_after_start = &source[start..];
- let end = source_after_start
- .find("\n#[tokio::test]")
- .unwrap_or(source_after_start.len());
- &source_after_start[..end]
-}
-
-fn source_between<'source>(
- source: &'source str,
- start_marker: &str,
- end_marker: &str,
-) -> &'source str {
- let start = source
- .find(start_marker)
- .unwrap_or_else(|| panic!("failed to find source marker `{start_marker}`"));
- let source_after_start = &source[start..];
- let end = source_after_start
- .find(end_marker)
- .unwrap_or_else(|| panic!("failed to find source marker `{end_marker}`"));
- &source_after_start[..end]
-}
-
-fn export_block<'source>(source: &'source str, marker: &str) -> &'source str {
- let start = source
- .find(marker)
- .unwrap_or_else(|| panic!("failed to find export block marker `{marker}`"));
- let source_after_start = &source[start..];
- let end = source_after_start
- .find("\n};")
- .unwrap_or_else(|| panic!("failed to find end of export block `{marker}`"));
- &source_after_start[..end]
-}
-
-fn impl_block<'source>(source: &'source str, marker: &str) -> &'source str {
- let start = source
- .find(marker)
- .unwrap_or_else(|| panic!("failed to find impl block marker `{marker}`"));
- let source_after_start = &source[start..];
- let end = source_after_start
- .find("\n}\n\n#[cfg(")
- .unwrap_or(source_after_start.len());
- &source_after_start[..end]
-}
-
-fn struct_block<'source>(source: &'source str, marker: &str) -> &'source str {
- let signature = format!("pub struct {marker}");
- let start = source
- .find(signature.as_str())
- .unwrap_or_else(|| panic!("failed to find struct block marker `{marker}`"));
- let source_after_start = &source[start..];
- let end = source_after_start
- .find("\n}\n\n#[cfg(")
- .unwrap_or(source_after_start.len());
- &source_after_start[..end]
-}
-
-fn is_rust_identifier_character(character: char) -> bool {
- character == '_' || character.is_ascii_alphanumeric()
-}
-
-fn rust_source_files(root: &Path) -> Vec<PathBuf> {
- let mut paths = Vec::new();
- collect_rust_source_files(root, &mut paths);
- paths.sort();
- paths
-}
-
-fn collect_rust_source_files(root: &Path, paths: &mut Vec<PathBuf>) {
- let entries = fs::read_dir(root).unwrap_or_else(|error| {
- panic!(
- "failed to read source directory {}: {error}",
- root.display()
- )
- });
-
- for entry in entries {
- let entry = entry.unwrap_or_else(|error| {
- panic!(
- "failed to inspect source directory {}: {error}",
- root.display()
- )
- });
- let path = entry.path();
-
- if path.is_dir() {
- collect_rust_source_files(path.as_path(), paths);
- continue;
- }
-
- if path.extension().and_then(|extension| extension.to_str()) == Some("rs") {
- paths.push(path);
+fn active_sources_do_not_describe_compatibility_paths() {
+ for path in active_source_files() {
+ let source = read_source(&path).to_lowercase();
+ for forbidden in [
+ "compatibility",
+ "legacy",
+ "shim",
+ "dual-read",
+ "dual-write",
+ "fallback adapter",
+ ] {
+ assert!(
+ !source.contains(forbidden),
+ "{} must not describe `{forbidden}` behavior",
+ path.display()
+ );
}
}
}
diff --git a/crates/sdk/tests/trade_product_publish_runtime.rs b/crates/sdk/tests/trade_product_publish_runtime.rs
@@ -1,297 +0,0 @@
-#![cfg(all(
- feature = "runtime",
- feature = "signer-adapters",
- feature = "local-signer",
- feature = "radrootsd-execution"
-))]
-
-use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner};
-use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
-};
-use radroots_event::{
- RadrootsEventPtr,
- contract::RadrootsActorRole,
- ids::RadrootsListingAddress,
- kinds::KIND_LISTING,
- order::{
- RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics,
- RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
- },
-};
-use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey};
-use radroots_sdk::{
- NostrRelayUrlPolicy, PublishMode, PushOutboxTargetOutcomeKind, RadrootsClient,
- RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp,
- RadrootsdExecutionProfile, SatisfactionPolicy, TargetPolicy, TargetSet, TradeMutationOutcome,
- TradeProposeRequest,
-};
-use std::{
- io::{Read, Write},
- net::{TcpListener, TcpStream},
- thread::JoinHandle,
-};
-
-const BUYER_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
-const BUYER_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-const SELLER_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
-const RELAY: &str = "wss://relay.radroots.test";
-
-struct RecordedTransportPublishRequest {
- body: String,
-}
-
-fn spawn_trade_transport_publish_server() -> (String, JoinHandle<RecordedTransportPublishRequest>) {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind transport publish server");
- let endpoint = format!("http://{}/rpc", listener.local_addr().expect("addr"));
- let handle = std::thread::spawn(move || {
- let (mut stream, _) = listener.accept().expect("accept");
- let body = read_transport_publish_request_body(&mut stream);
- write_transport_publish_accept_response(&mut stream, body.as_str());
- RecordedTransportPublishRequest { body }
- });
- (endpoint, handle)
-}
-
-fn read_transport_publish_request_body(stream: &mut TcpStream) -> String {
- let mut request = Vec::new();
- let mut buffer = [0u8; 1024];
- loop {
- let read = stream.read(&mut buffer).expect("read request");
- if read == 0 {
- break;
- }
- request.extend_from_slice(&buffer[..read]);
- if request.windows(4).any(|window| window == b"\r\n\r\n") {
- let headers_end = request
- .windows(4)
- .position(|window| window == b"\r\n\r\n")
- .expect("headers end")
- + 4;
- let header_text = String::from_utf8_lossy(&request[..headers_end]);
- let content_length = header_text
- .lines()
- .find_map(|line| {
- let (name, value) = line.split_once(':')?;
- name.eq_ignore_ascii_case("content-length")
- .then(|| value.trim().parse::<usize>().expect("content length"))
- })
- .unwrap_or(0);
- while request.len() < headers_end + content_length {
- let read = stream.read(&mut buffer).expect("read body");
- if read == 0 {
- break;
- }
- request.extend_from_slice(&buffer[..read]);
- }
- break;
- }
- }
- let request_text = String::from_utf8_lossy(&request);
- let (_, body) = request_text.split_once("\r\n\r\n").expect("request body");
- body.to_owned()
-}
-
-fn write_transport_publish_accept_response(stream: &mut TcpStream, body: &str) {
- let body_json: serde_json::Value = serde_json::from_str(body).expect("body json");
- let raw_event_json = body_json["params"]["raw_event_json"]
- .as_str()
- .expect("raw event json");
- let event: serde_json::Value = serde_json::from_str(raw_event_json).expect("event json");
- let response_body = serde_json::json!({
- "jsonrpc": "2.0",
- "id": body_json["id"],
- "result": {
- "deduplicated": false,
- "job": {
- "job_id": "trade-product-publish-job",
- "status": "delivery_satisfied",
- "terminal": true,
- "delivery_satisfied": true,
- "event_id": event["id"],
- "pubkey": event["pubkey"],
- "event_kind": event["kind"],
- "target_policy": body_json["params"]["target_policy"],
- "delivery_policy": body_json["params"]["delivery_policy"],
- "target_count": 1,
- "acknowledged_count": 1,
- "retryable_count": 0,
- "terminal_count": 0,
- "requested_at_ms": 1700000000000i64,
- "completed_at_ms": 1700000000100i64,
- "targets": [{
- "transport_kind": "nostr",
- "endpoint_uri": RELAY,
- "source": "request",
- "attempted": true,
- "outcome_kind": "accepted",
- "message": "accepted"
- }]
- }
- }
- })
- .to_string();
- let response = format!(
- "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
- response_body.len(),
- response_body
- );
- stream
- .write_all(response.as_bytes())
- .expect("write response");
-}
-
-fn buyer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(BUYER_PUBLIC_KEY_HEX, [RadrootsActorRole::Buyer]).expect("actor")
-}
-
-fn listing_address() -> RadrootsListingAddress {
- RadrootsListingAddress::parse(format!(
- "{KIND_LISTING}:{SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("listing address")
-}
-
-fn listing_event_ptr() -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: "6ccf12d1e56c21065d239bc3d46c0000cd000095d20000d9000073cd00009600".to_owned(),
- relays: Some(RELAY.to_owned()),
- }
-}
-
-fn decimal(raw: &str) -> RadrootsCoreDecimal {
- raw.parse().expect("decimal")
-}
-
-fn usd(raw: &str) -> RadrootsCoreMoney {
- RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD)
-}
-
-fn economics() -> RadrootsOrderEconomics {
- RadrootsOrderEconomics {
- quote_id: "quote-1".parse().expect("quote id"),
- quote_version: 1,
- pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
- currency: RadrootsCoreCurrency::USD,
- items: vec![RadrootsOrderEconomicItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- quantity_amount: decimal("1"),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: decimal("5"),
- unit_price_currency: RadrootsCoreCurrency::USD,
- line_subtotal: usd("10"),
- }],
- discounts: Vec::<RadrootsOrderEconomicLine>::new(),
- adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
- subtotal: usd("10"),
- discount_total: usd("0"),
- adjustment_total: usd("0"),
- total: usd("10"),
- }
-}
-
-fn order_request(raw_order_id: &str) -> RadrootsOrderRequest {
- RadrootsOrderRequest {
- order_id: raw_order_id.parse().expect("order id"),
- listing_addr: listing_address(),
- buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"),
- seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"),
- items: vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- economics: economics(),
- }
-}
-
-fn trade_propose_request(
- raw_order_id: &str,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
-) -> TradeProposeRequest {
- let order = order_request(raw_order_id);
- TradeProposeRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order,
- explicit_trade_relays(),
- publish_mode,
- satisfaction_policy,
- )
-}
-
-fn explicit_trade_relays() -> TargetPolicy {
- TargetPolicy::explicit(
- TargetSet::nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("target relays"),
- )
-}
-
-#[tokio::test]
-async fn trade_product_propose_enqueue_and_publish_uses_ack_policy() {
- let (endpoint, handle) = spawn_trade_transport_publish_server();
- let tempdir = tempfile::tempdir().expect("tempdir");
- let secret_key = RadrootsNostrSecretKey::from_hex(BUYER_SECRET_KEY_HEX).expect("secret key");
- let signer_keys = RadrootsNostrKeys::new(secret_key);
- let sdk = RadrootsClient::builder()
- .directory_storage(tempdir.path().join("sdk"))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::from_event_signer(
- RadrootsLocalEventSigner::new(signer_keys).expect("local event signer"),
- )
- .expect("local signer"),
- ))
- .radrootsd_execution_profile(RadrootsdExecutionProfile::new(endpoint))
- .build()
- .await
- .expect("sdk");
-
- let outcome = sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "01890f0e-6c00-7000-8000-000000000223",
- PublishMode::EnqueueAndPublish,
- SatisfactionPolicy::AnyAccepted,
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000223")
- .expect("idempotency"),
- )
- .await
- .expect("publish proposal");
- let (receipt, publish) = match outcome {
- TradeMutationOutcome::Published { receipt, publish } => (receipt, publish),
- TradeMutationOutcome::DryRun { .. } => panic!("expected published outcome"),
- TradeMutationOutcome::Enqueued { .. } => panic!("expected published outcome"),
- };
-
- assert_eq!(
- receipt.order_id.as_str(),
- "01890f0e-6c00-7000-8000-000000000223"
- );
- assert_eq!(publish.attempted_events, 1);
- assert_eq!(publish.published_events, 1);
- assert_eq!(publish.events.len(), 1);
- assert_eq!(publish.events[0].outbox_event_id, receipt.outbox_event_id);
- assert_eq!(publish.events[0].quorum, 1);
- assert!(publish.events[0].quorum_met);
- assert_eq!(
- publish.events[0].targets[0].outcome_kind,
- PushOutboxTargetOutcomeKind::Accepted
- );
-
- let recorded = handle.join().expect("transport publish request");
- let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body");
- assert_eq!(body["method"], "transport.publish.event");
- assert_eq!(body["params"]["delivery_policy"]["mode"], "any");
- assert_eq!(body["params"]["target_policy"]["kind"], "explicit_targets");
- assert_eq!(
- body["params"]["target_policy"]["targets"][0]["endpoint_uri"],
- RELAY
- );
-}
diff --git a/crates/sdk/tests/trade_public_api.rs b/crates/sdk/tests/trade_public_api.rs
@@ -1,64 +0,0 @@
-#![cfg(feature = "runtime")]
-
-use radroots_authority::RadrootsActorContext;
-use radroots_event::contract::RadrootsActorRole;
-use radroots_sdk::{
- RadrootsClient, TradeResyncRequest, TradeSellerInboxRequest, TradeStatusKind,
- TradeStatusRequest, TradeValidationReceiptListRequest,
-};
-
-const SELLER_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
-
-#[tokio::test]
-async fn grouped_trade_surface_is_the_public_product_entrypoint() {
- let sdk = RadrootsClient::builder().build().await.expect("sdk");
- let trades = sdk.trades();
-
- let _seller = trades.seller();
- let _resync = trades.resync();
- let validation_receipts = trades.validation_receipts();
-
- let status = trades
- .status(TradeStatusRequest::parse("trade-public-api-order").expect("status request"))
- .await
- .expect("status");
-
- assert_eq!(status.status, TradeStatusKind::Missing);
-
- let resync_error = trades
- .resync()
- .resync(TradeResyncRequest::new(status.locator))
- .await
- .expect_err("resync requires configured relays");
-
- #[cfg(feature = "transport-nostr-runtime")]
- assert_eq!(resync_error.code(), "empty_transport_targets");
- #[cfg(not(feature = "transport-nostr-runtime"))]
- assert_eq!(resync_error.code(), "product_sync_unsupported");
-
- let validation_receipt_error = validation_receipts
- .list(
- TradeValidationReceiptListRequest::parse("trade-public-api-order")
- .expect("validation receipt list request"),
- )
- .await
- .expect_err("validation receipt list requires configured relays");
-
- #[cfg(feature = "transport-nostr-runtime")]
- assert_eq!(validation_receipt_error.code(), "empty_transport_targets");
- #[cfg(not(feature = "transport-nostr-runtime"))]
- assert_eq!(validation_receipt_error.code(), "product_sync_unsupported");
-
- let seller_actor =
- RadrootsActorContext::test(SELLER_PUBLIC_KEY_HEX, [RadrootsActorRole::Seller])
- .expect("seller actor");
- let inbox = trades
- .seller()
- .inbox(TradeSellerInboxRequest::new(seller_actor))
- .await
- .expect("seller inbox");
-
- assert_eq!(inbox.seller_pubkey.as_str(), SELLER_PUBLIC_KEY_HEX);
- assert!(inbox.statuses.is_empty());
-}
diff --git a/crates/sdk/tests/unit/error_tests.rs b/crates/sdk/tests/unit/error_tests.rs
@@ -295,8 +295,15 @@ fn sdk_error_contract_methods_cover_representative_classes_and_details() {
existing_digest_prefix: "existing".to_owned(),
new_digest_prefix: "new".to_owned(),
},
- RadrootsSdkError::trade_status_limit_invalid(0, 1, 100),
- RadrootsSdkError::invalid_trade_id("bad order", "bad id"),
+ RadrootsSdkError::TradeStatusLimitInvalid {
+ limit: 0,
+ min: 1,
+ max: 100,
+ },
+ RadrootsSdkError::InvalidTradeId {
+ value: "bad order".to_owned(),
+ message: "bad id".to_owned(),
+ },
RadrootsSdkError::PrivacyPreflight {
operation: "trade.cancel".to_owned(),
status: PrivacyPreflightStatus::ExplicitConfirmationRequired,
diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs
@@ -670,7 +670,10 @@ async fn farm_configured_enqueue_reports_prepare_and_signer_errors() {
plan,
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237")
+ .expect("idempotency"),
+ ),
)
.await,
Err(RadrootsSdkError::SignerUnavailable { .. })
diff --git a/crates/sdk/tests/unit/listings_runtime_tests.rs b/crates/sdk/tests/unit/listings_runtime_tests.rs
@@ -369,7 +369,10 @@ async fn listing_configured_enqueue_reports_missing_signer_after_prepare() {
plan,
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000233")
+ .expect("idempotency"),
+ ),
)
.await,
Err(RadrootsSdkError::SignerUnavailable { .. })
diff --git a/crates/sdk/tests/unit/market_runtime_tests.rs b/crates/sdk/tests/unit/market_runtime_tests.rs
@@ -1,112 +0,0 @@
-use super::*;
-
-const SELLER_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
-
-fn projection_row() -> RadrootsListingProjectionRow {
- RadrootsListingProjectionRow {
- listing_addr: RadrootsListingAddress::parse(format!(
- "30402:{SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("listing address"),
- listing_event_id: "a".repeat(64),
- seller_pubkey: SELLER_PUBLIC_KEY_HEX.to_owned(),
- title: "Blueberries".to_owned(),
- description: "Fresh field berries".to_owned(),
- product_type: "fruit".to_owned(),
- price_amount: "6".to_owned(),
- price_currency: "USD".to_owned(),
- inventory_available: "12".to_owned(),
- delivery_method: "pickup".to_owned(),
- locality_primary: "Fernwood".to_owned(),
- locality_city: Some("Victoria".to_owned()),
- locality_region: Some("BC".to_owned()),
- locality_country: Some("CA".to_owned()),
- geohash5: "c2b2q".to_owned(),
- updated_at_ms: 1_700_000_000_000,
- }
-}
-
-fn projection_error_message(error: RadrootsSdkError) -> String {
- match error {
- RadrootsSdkError::Projection { message } => message,
- other => panic!("expected projection error, got {other:?}"),
- }
-}
-
-#[test]
-fn market_search_request_builders_preserve_refresh_contract() {
- let request = MarketSearchRequest::new("berries")
- .with_limit(7)
- .with_projection_refresh(SyncProjectionRefreshRequest::new().with_limit(3));
-
- assert_eq!(request.query, "berries");
- assert_eq!(request.limit, 7);
- assert_eq!(request.projection_refresh.limit, 3);
-}
-
-#[test]
-fn listing_projection_row_conversion_validates_stored_identity_columns() {
- let row = projection_row();
- let search_row =
- MarketListingSearchRow::try_from_projection_row(row.clone()).expect("search row");
- assert_eq!(search_row.listing_addr, row.listing_addr);
- assert_eq!(search_row.listing_event_id.as_str(), row.listing_event_id);
- assert_eq!(search_row.seller_pubkey.as_str(), row.seller_pubkey);
- assert_eq!(search_row.title, "Blueberries");
-
- let mut invalid_event_id = row.clone();
- invalid_event_id.listing_event_id = "not-an-event-id".to_owned();
- assert!(
- projection_error_message(
- MarketListingSearchRow::try_from_projection_row(invalid_event_id).unwrap_err()
- )
- .contains("projection event id")
- );
-
- let mut invalid_seller = row;
- invalid_seller.seller_pubkey = "not-a-pubkey".to_owned();
- assert!(
- projection_error_message(
- MarketListingSearchRow::try_from_projection_row(invalid_seller).unwrap_err()
- )
- .contains("projection seller pubkey")
- );
-}
-
-#[test]
-fn market_search_receipt_serializes_absent_optional_locality_fields() {
- let mut row = projection_row();
- row.locality_city = None;
- row.locality_region = None;
- row.locality_country = None;
- let search_row = MarketListingSearchRow::try_from_projection_row(row).expect("search row");
- let receipt = MarketSearchReceipt {
- source: MarketSearchSource::LocalProjectionFts,
- refresh: SyncProjectionRefreshReceipt::default(),
- listings: vec![search_row],
- };
-
- let value = serde_json::to_value(receipt).expect("receipt json");
-
- assert_eq!(value["source"], "local_projection_fts");
- assert!(value["listings"][0]["locality_city"].is_null());
- assert!(value["listings"][0]["locality_region"].is_null());
- assert!(value["listings"][0]["locality_country"].is_null());
-}
-
-#[tokio::test]
-async fn market_search_reports_projection_refresh_errors_before_querying_rows() {
- let sdk = crate::RadrootsClient::builder()
- .clock(crate::RadrootsSdkClock::BeforeUnixEpoch)
- .build()
- .await
- .expect("sdk");
-
- assert!(matches!(
- sdk.market()
- .search(MarketSearchRequest::new("berries"))
- .await,
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
-}
diff --git a/crates/sdk/tests/unit/orders_runtime_tests.rs b/crates/sdk/tests/unit/orders_runtime_tests.rs
@@ -1,199 +0,0 @@
-use super::*;
-
-#[test]
-#[cfg(feature = "transport-nostr-runtime")]
-fn validation_receipt_limit_rejects_out_of_range_values() {
- assert!(validate_validation_receipt_limit(1).is_ok());
- assert!(validate_validation_receipt_limit(TRADE_STATUS_MAX_LIMIT).is_ok());
-
- let zero = validate_validation_receipt_limit(0).expect_err("zero limit");
- assert_eq!(
- zero.to_string(),
- format!("sdk order status limit invalid: limit=0, min=1, max={TRADE_STATUS_MAX_LIMIT}")
- );
-
- let over_max =
- validate_validation_receipt_limit(TRADE_STATUS_MAX_LIMIT + 1).expect_err("over max");
- assert_eq!(
- over_max.to_string(),
- format!(
- "sdk order status limit invalid: limit={}, min=1, max={TRADE_STATUS_MAX_LIMIT}",
- TRADE_STATUS_MAX_LIMIT + 1
- )
- );
-}
-
-#[test]
-fn relay_outcome_labels_cover_current_variants() {
- for (kind, label) in [
- (TradeResyncNostrRelayOutcomeKind::Eose, "eose"),
- (TradeResyncNostrRelayOutcomeKind::Closed, "closed"),
- (TradeResyncNostrRelayOutcomeKind::Notice, "notice"),
- ] {
- assert_eq!(kind.as_str(), label);
- }
-
- for (kind, label) in [
- (TradeValidationReceiptNostrRelayOutcomeKind::Eose, "eose"),
- (
- TradeValidationReceiptNostrRelayOutcomeKind::Closed,
- "closed",
- ),
- (
- TradeValidationReceiptNostrRelayOutcomeKind::Notice,
- "notice",
- ),
- ] {
- assert_eq!(kind.as_str(), label);
- }
-
- for (kind, label) in [
- (
- TradeResyncNostrRelayTransportOutcomeKind::Accepted,
- "accepted",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::DuplicateAccepted,
- "duplicate_accepted",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::Blocked,
- "blocked",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::RateLimited,
- "rate_limited",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::Invalid,
- "invalid",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::PowRequired,
- "pow_required",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::Restricted,
- "restricted",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::AuthRequired,
- "auth_required",
- ),
- (TradeResyncNostrRelayTransportOutcomeKind::Muted, "muted"),
- (
- TradeResyncNostrRelayTransportOutcomeKind::Unsupported,
- "unsupported",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::PaymentRequired,
- "payment_required",
- ),
- (TradeResyncNostrRelayTransportOutcomeKind::Error, "error"),
- (
- TradeResyncNostrRelayTransportOutcomeKind::Timeout,
- "timeout",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::ConnectionFailed,
- "connection_failed",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::RelayUrlRejected,
- "relay_url_rejected",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::SkippedAlreadyAccepted,
- "skipped_already_accepted",
- ),
- (
- TradeResyncNostrRelayTransportOutcomeKind::Unknown,
- "unknown",
- ),
- ] {
- assert_eq!(kind.as_str(), label);
- }
-
- for (kind, label) in [
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Accepted,
- "accepted",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::DuplicateAccepted,
- "duplicate_accepted",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Blocked,
- "blocked",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::RateLimited,
- "rate_limited",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Invalid,
- "invalid",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::PowRequired,
- "pow_required",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Restricted,
- "restricted",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::AuthRequired,
- "auth_required",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Muted,
- "muted",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Unsupported,
- "unsupported",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::PaymentRequired,
- "payment_required",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Error,
- "error",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Timeout,
- "timeout",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::ConnectionFailed,
- "connection_failed",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::RelayUrlRejected,
- "relay_url_rejected",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::SkippedAlreadyAccepted,
- "skipped_already_accepted",
- ),
- (
- TradeValidationReceiptNostrRelayTransportOutcomeKind::Unknown,
- "unknown",
- ),
- ] {
- assert_eq!(kind.as_str(), label);
- }
-}
-
-#[test]
-fn camel_to_snake_converts_debug_case_labels() {
- assert_eq!(camel_to_snake("AwaitValidation"), "await_validation");
- assert_eq!(
- camel_to_snake("InspectEvidenceIssues"),
- "inspect_evidence_issues"
- );
- assert_eq!(camel_to_snake("Terminal"), "terminal");
-}
diff --git a/crates/sdk/tests/unit/runtime_tests.rs b/crates/sdk/tests/unit/runtime_tests.rs
@@ -299,7 +299,7 @@ fn transport_profile_defaults_are_explicit() {
#[tokio::test]
async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures() {
- let memory = open_storage(&RadrootsSdkStorageConfig::Memory)
+ let memory = open_storage(&RadrootsSdkStorageConfig::Memory, 0)
.await
.expect("memory storage");
assert!(memory.paths.is_none());
@@ -320,7 +320,7 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
let tempdir = tempfile::tempdir().expect("tempdir");
let directory = tempdir.path().join("sdk");
- let directory_storage = open_storage(&RadrootsSdkStorageConfig::Directory(directory))
+ let directory_storage = open_storage(&RadrootsSdkStorageConfig::Directory(directory), 0)
.await
.expect("directory storage");
let directory_paths = directory_storage.paths.expect("directory paths");
@@ -344,24 +344,148 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
let file_path = tempdir.path().join("not-directory");
fs::write(&file_path, b"file").expect("file");
- assert!(!io_message(open_directory_storage(&file_path).await).is_empty());
+ assert!(!io_message(open_directory_storage(&file_path, 0).await).is_empty());
let event_store_directory = tempdir.path().join("event-store-directory");
fs::create_dir(&event_store_directory).expect("event store dir");
fs::create_dir(event_store_directory.join(RUNTIME_SQLITE_FILE))
.expect("event store file slot dir");
- assert_event_store_error(open_directory_storage(&event_store_directory).await);
+ assert_event_store_error(open_directory_storage(&event_store_directory, 0).await);
let studio_directory = tempdir.path().join("studio-directory");
fs::create_dir(&studio_directory).expect("studio dir");
fs::create_dir(studio_directory.join(STUDIO_SQLITE_FILE)).expect("studio file slot dir");
- assert_studio_store_error(open_directory_storage(&studio_directory).await);
+ assert_studio_store_error(open_directory_storage(&studio_directory, 0).await);
let private_store_directory = tempdir.path().join("private-store-directory");
fs::create_dir(&private_store_directory).expect("private store dir");
fs::create_dir(private_store_directory.join(PRIVATE_SQLITE_FILE))
.expect("private store file slot dir");
- assert_private_store_error(open_directory_storage(&private_store_directory).await);
+ assert_private_store_error(open_directory_storage(&private_store_directory, 0).await);
+}
+
+#[tokio::test]
+async fn runtime_schema_refuses_newer_profiles_before_migration() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let profile = tempdir.path().join("sdk");
+ fs::create_dir(&profile).expect("profile");
+ let runtime_path = profile.join(RUNTIME_SQLITE_FILE);
+ let pool = open_runtime_file_pool(&runtime_path)
+ .await
+ .expect("runtime pool");
+ sqlx::query("PRAGMA user_version = 99")
+ .execute(&pool)
+ .await
+ .expect("user version");
+ pool.close().await;
+
+ let unsupported = assert_unsupported_profile_error(open_directory_storage(&profile, 10).await);
+
+ assert_eq!(unsupported, runtime_path);
+}
+
+#[tokio::test]
+async fn runtime_startup_recovery_updates_journal_reservations_projections_and_outbox() {
+ let storage = open_storage(&RadrootsSdkStorageConfig::Memory, 0)
+ .await
+ .expect("memory storage");
+ let pool = storage.event_store.pool();
+ sqlx::query(
+ "INSERT INTO sdk_runtime_operation_journal(contract_version, operation_kind, actor_pubkey, idempotency_key, command_payload_hash, frozen_draft_json, expected_transport_id, state, created_at_ms, updated_at_ms) VALUES ('1', 'trade.proposal.v1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '019b0000-0000-7000-8000-000000000001', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', '{}', 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'signature_pending', 1, 1)",
+ )
+ .execute(pool)
+ .await
+ .expect("operation journal");
+ sqlx::query(
+ "INSERT INTO sdk_seller_inventory_reservation(reservation_id, farm_id, candidate_id, authority_id, inventory_epoch, assertion_commitment, state, lease_until_ms, created_at_ms, updated_at_ms) VALUES ('reservation-1', 'farm-1', 'candidate-1', 'seller-1', 1, 'commitment-1', 'prepared', 5, 1, 1)",
+ )
+ .execute(pool)
+ .await
+ .expect("reservation");
+ sqlx::query(
+ "INSERT INTO sdk_seller_inventory_reservation_line(reservation_id, farm_id, candidate_id, line_id, bin_id, quantity_mantissa, quantity_scale, unit_code) VALUES ('reservation-1', 'farm-1', 'candidate-1', 'line-1', 'bin-1', '1', 0, 'lb')",
+ )
+ .execute(pool)
+ .await
+ .expect("reservation line");
+ sqlx::query(
+ "INSERT INTO sdk_seller_inventory_reservation(reservation_id, farm_id, candidate_id, authority_id, inventory_epoch, assertion_commitment, state, lease_until_ms, created_at_ms, updated_at_ms) VALUES ('reservation-2', 'farm-1', 'candidate-1', 'seller-1', 1, 'commitment-2', 'prepared', 50, 1, 1)",
+ )
+ .execute(pool)
+ .await
+ .expect("second reservation");
+ assert!(
+ sqlx::query(
+ "INSERT INTO sdk_seller_inventory_reservation_line(reservation_id, farm_id, candidate_id, line_id, bin_id, quantity_mantissa, quantity_scale, unit_code) VALUES ('reservation-2', 'farm-1', 'candidate-1', 'line-1', 'bin-1', '1', 0, 'lb')",
+ )
+ .execute(pool)
+ .await
+ .is_err()
+ );
+ sqlx::query(
+ "INSERT INTO sdk_runtime_trade_projection_checkpoint(projection_name, reducer_contract_id, reducer_version, last_ingest_seq, source_digest, projection_digest, completeness_state, updated_at_ms) VALUES ('trade_projection', 'radroots.trade.reducer.v1', 1, 7, 'source', 'projection', 'rebuilding', 1)",
+ )
+ .execute(pool)
+ .await
+ .expect("projection checkpoint");
+ let operation_id = sqlx::query(
+ "INSERT INTO outbox_operations(operation_kind, expected_pubkey, semantic_scope, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES ('listing.publish.v1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'generic_event', '019b0000-0000-7000-8000-000000000002', 'digest', 'queued', 1, 1)",
+ )
+ .execute(pool)
+ .await
+ .expect("outbox operation")
+ .last_insert_rowid();
+ sqlx::query(
+ "INSERT INTO outbox_event(operation_id, event_id, expected_pubkey, draft_json, state, attempt_count, claim_token, claim_owner, claim_expires_at_ms, next_attempt_after_ms, event_store_ingested, event_store_inserted, created_at_ms, updated_at_ms) VALUES (?, 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '{}', 'publishing', 1, 'claim-1', 'worker-1', 5, 1, 0, 0, 1, 1)",
+ )
+ .bind(operation_id)
+ .execute(pool)
+ .await
+ .expect("outbox event");
+
+ recover_sdk_runtime_state(pool, 10).await.expect("recover");
+
+ let operation_state: String = sqlx::query_scalar(
+ "SELECT state FROM sdk_runtime_operation_journal WHERE operation_kind = 'trade.proposal.v1'",
+ )
+ .fetch_one(pool)
+ .await
+ .expect("operation state");
+ let reservation_state: String = sqlx::query_scalar(
+ "SELECT state FROM sdk_seller_inventory_reservation WHERE reservation_id = 'reservation-1'",
+ )
+ .fetch_one(pool)
+ .await
+ .expect("reservation state");
+ let projection_state: String = sqlx::query_scalar(
+ "SELECT completeness_state FROM sdk_runtime_trade_projection_checkpoint WHERE projection_name = 'trade_projection'",
+ )
+ .fetch_one(pool)
+ .await
+ .expect("projection state");
+ let outbox_state: String =
+ sqlx::query_scalar("SELECT state FROM outbox_event WHERE outbox_event_id = 1")
+ .fetch_one(pool)
+ .await
+ .expect("outbox state");
+ let outbox_claim: Option<String> =
+ sqlx::query_scalar("SELECT claim_token FROM outbox_event WHERE outbox_event_id = 1")
+ .fetch_one(pool)
+ .await
+ .expect("outbox claim");
+ let receipts: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM sdk_runtime_recovery_receipt WHERE recovery_code IN ('signer_timeout','reservation_expiry','projection_stale','relay_failure')",
+ )
+ .fetch_one(pool)
+ .await
+ .expect("recovery receipts");
+
+ assert_eq!(operation_state, "failed_recoverable");
+ assert_eq!(reservation_state, "expired");
+ assert_eq!(projection_state, "stale");
+ assert_eq!(outbox_state, "publish_retryable");
+ assert!(outbox_claim.is_none());
+ assert_eq!(receipts, 4);
}
#[tokio::test]
@@ -603,53 +727,6 @@ async fn storage_status_inspection_is_read_only_and_never_creates_missing_profil
}
#[tokio::test]
-async fn quarantine_reset_moves_pre_v1_artifacts_before_materializing_v1_stores() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let profile = tempdir.path().join("profile");
- let quarantine = tempdir.path().join("quarantine");
- fs::create_dir(&profile).expect("profile");
- fs::write(profile.join("event_store.sqlite"), b"old event store").expect("old event store");
- fs::write(profile.join("event_store.sqlite-wal"), b"old event wal").expect("old event wal");
- fs::write(profile.join("outbox.sqlite"), b"old outbox").expect("old outbox");
-
- let unsupported = assert_unsupported_profile_error(
- RadrootsClient::builder()
- .directory_storage(&profile)
- .build()
- .await,
- );
- assert_eq!(unsupported, profile.join("event_store.sqlite"));
-
- let receipt = RadrootsClient::quarantine_reset_storage(QuarantineResetRequest::new(
- &profile,
- &quarantine,
- ))
- .await
- .expect("quarantine reset");
-
- assert_eq!(receipt.profile, profile);
- assert_eq!(receipt.quarantine, quarantine);
- assert_eq!(receipt.quarantined_paths.len(), 3);
- assert!(receipt.quarantine.join("event_store.sqlite").exists());
- assert!(receipt.quarantine.join("event_store.sqlite-wal").exists());
- assert!(receipt.quarantine.join("outbox.sqlite").exists());
- assert!(!receipt.profile.join("event_store.sqlite").exists());
- assert!(!receipt.profile.join("outbox.sqlite").exists());
- assert!(receipt.reset_paths.runtime_path.exists());
- assert!(receipt.reset_paths.private_path.exists());
- assert!(receipt.reset_paths.studio_path.exists());
-
- let status =
- RadrootsClient::inspect_storage_status(&receipt.profile, StorageStatusRequest::new())
- .await
- .expect("read-only status after reset");
- assert_eq!(status.event_store.total_events, 0);
- assert_eq!(status.outbox.total_events, 0);
- assert_eq!(status.private_store.farm_private_locations, 0);
- assert_eq!(status.studio_store.studio_state_records, 0);
-}
-
-#[tokio::test]
async fn storage_status_integrity_and_backup_map_closed_pool_errors() {
let event_store_closed = RadrootsClient::builder().build().await.expect("sdk");
event_store_closed._event_store.pool().close().await;
@@ -1364,7 +1441,7 @@ fn restore_staging_helpers_cover_new_and_existing_destination_installs() {
#[cfg(unix)]
#[tokio::test]
async fn sqlite_backup_errors_cover_invalid_paths_and_execute_failures() {
- let storage = open_storage(&RadrootsSdkStorageConfig::Memory)
+ let storage = open_storage(&RadrootsSdkStorageConfig::Memory, 0)
.await
.expect("memory storage");
@@ -1409,7 +1486,7 @@ async fn sqlite_backup_errors_cover_invalid_paths_and_execute_failures() {
.await,
);
- let studio_closed_storage = open_storage(&RadrootsSdkStorageConfig::Memory)
+ let studio_closed_storage = open_storage(&RadrootsSdkStorageConfig::Memory, 0)
.await
.expect("studio closed storage");
studio_closed_storage.studio_store.pool().close().await;
diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs
@@ -1413,14 +1413,14 @@ async fn radrootsd_completion_updates_outbox_for_success_retryable_and_terminal_
(
"radrootsd-complete-deferred",
PushOutboxEventState::DeferredUntilImplemented,
- PushOutboxEventState::DeferredUntilImplemented,
+ PushOutboxEventState::FailedTerminal,
RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented,
TransportPublishOutcomeKind::DeferredUntilImplemented,
),
(
"radrootsd-complete-deferred-until-implemented",
PushOutboxEventState::DeferredUntilImplemented,
- PushOutboxEventState::DeferredUntilImplemented,
+ PushOutboxEventState::FailedTerminal,
RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented,
TransportPublishOutcomeKind::DeferredUntilImplemented,
),
diff --git a/crates/sdk/tests/unit/workflow_runtime_tests.rs b/crates/sdk/tests/unit/workflow_runtime_tests.rs
@@ -157,10 +157,25 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() {
"wss://relay.example.com"
);
assert!(input.event_store_inserted);
+ let frozen = frozen_draft_json(&draft).expect("frozen draft json");
+ assert!(frozen.contains("\"expected_event_id\""));
+ let receipt = SdkWorkflowEnqueueReceipt {
+ signed_event_id: RadrootsEventId::parse(draft.expected_event_id_str()).expect("event id"),
+ local_event_seq: 1,
+ outbox_operation_id: 2,
+ outbox_event_id: 3,
+ state: radroots_outbox::RadrootsOutboxEnqueueStatus::Inserted,
+ idempotency_digest_prefix: "abcdef123456".to_owned(),
+ };
+ let receipt_json = workflow_receipt_result_json(&receipt);
+ let decoded =
+ workflow_receipt_from_result_json(receipt_json.to_string().as_str()).expect("receipt");
+ assert_eq!(decoded.outbox_event_id, receipt.outbox_event_id);
+ assert_eq!(outbox_enqueue_status_str(decoded.state), "inserted");
}
#[tokio::test]
-async fn default_operation_idempotency_ignores_target_policy() {
+async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_command_hash() {
let sdk = crate::RadrootsClient::builder()
.fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
1_700_000_012,
@@ -189,7 +204,7 @@ async fn default_operation_idempotency_ignores_target_policy() {
operation_kind: "workflow.test.v1",
actor: &actor,
frozen_draft: &draft,
- target_policy: first_target_policy,
+ target_policy: first_target_policy.clone(),
satisfaction_policy: SatisfactionPolicy::AllAccepted,
idempotency_key: Some(workflow_idempotency_key(0x240)),
},
@@ -197,7 +212,21 @@ async fn default_operation_idempotency_ignores_target_policy() {
)
.await
.expect("first enqueue");
- let second = enqueue_signed_workflow(
+ let replay = enqueue_signed_workflow(
+ &sdk,
+ SdkWorkflowEnqueueRequest {
+ operation_kind: "workflow.test.v1",
+ actor: &actor,
+ frozen_draft: &draft,
+ target_policy: first_target_policy.clone(),
+ satisfaction_policy: SatisfactionPolicy::AllAccepted,
+ idempotency_key: Some(workflow_idempotency_key(0x240)),
+ },
+ &signer,
+ )
+ .await
+ .expect("replay enqueue");
+ let conflict = enqueue_signed_workflow(
&sdk,
SdkWorkflowEnqueueRequest {
operation_kind: "workflow.test.v1",
@@ -210,32 +239,32 @@ async fn default_operation_idempotency_ignores_target_policy() {
&signer,
)
.await
- .expect("second enqueue");
+ .expect_err("conflicting command hash");
assert_eq!(
first.state,
radroots_outbox::RadrootsOutboxEnqueueStatus::Inserted
);
assert_eq!(
- second.state,
+ replay.state,
radroots_outbox::RadrootsOutboxEnqueueStatus::Inserted
);
- assert_eq!(first.outbox_operation_id, second.outbox_operation_id);
- assert_eq!(first.outbox_event_id, second.outbox_event_id);
+ assert_eq!(first.outbox_operation_id, replay.outbox_operation_id);
+ assert_eq!(first.outbox_event_id, replay.outbox_event_id);
assert_eq!(
first.idempotency_digest_prefix,
- second.idempotency_digest_prefix
+ replay.idempotency_digest_prefix
);
+ assert!(matches!(
+ conflict,
+ RadrootsSdkError::IdempotencyConflict { .. }
+ ));
let plans = sdk
._outbox
.delivery_plans(first.outbox_event_id)
.await
.expect("delivery plans");
- assert_eq!(plans.len(), 2);
- assert_ne!(
- plans[0].delivery_plan_idempotency_digest,
- plans[1].delivery_plan_idempotency_digest
- );
+ assert_eq!(plans.len(), 1);
let targets = sdk
._outbox
.delivery_targets(first.outbox_event_id)
@@ -247,11 +276,17 @@ async fn default_operation_idempotency_ignores_target_policy() {
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
target_uris,
- std::collections::BTreeSet::from([
- "wss://relay-a.example.com",
- "wss://relay-b.example.com",
- ])
+ std::collections::BTreeSet::from(["wss://relay-a.example.com"])
);
+ let recovery_count: i64 = sqlx::query(
+ "SELECT COUNT(*) FROM sdk_runtime_recovery_receipt WHERE recovery_code = 'idempotency_conflict'",
+ )
+ .fetch_one(sdk._event_store.pool())
+ .await
+ .expect("recovery count")
+ .try_get(0)
+ .expect("recovery count value");
+ assert_eq!(recovery_count, 1);
}
#[tokio::test]
diff --git a/crates/trade_bindings/src/dto.rs b/crates/trade_bindings/src/dto.rs
@@ -3,10 +3,7 @@ use dto_bindgen_core::{
RustTypeId, SourceSpan, TargetFieldNames, TargetOverride, TypeDef, TypeRef, VariantDef,
VariantShape, WireFieldNames,
};
-use radroots_trade::{
- listing::model::RadrootsTradeListingTotal, order::RadrootsOrderWorkflowProjection,
- workflow::RadrootsTradeWorkflowState,
-};
+use radroots_trade::listing::model::RadrootsTradeListingTotal;
pub fn dto_roots() -> Vec<RootDescriptor> {
let mut roots = radroots_trade::dto::dto_roots()
@@ -25,17 +22,9 @@ pub fn dto_roots() -> Vec<RootDescriptor> {
RootDescriptor::new::<RadrootsTradeListingSort>(),
RootDescriptor::new::<RadrootsTradeListingSortField>(),
RootDescriptor::new::<RadrootsTradeMarketplaceListingSummary>(),
- RootDescriptor::new::<RadrootsTradeMarketplaceOrderSummary>(),
RootDescriptor::new::<RadrootsTradeModerationFlag>(),
RootDescriptor::new::<RadrootsTradeModerationSeverity>(),
RootDescriptor::new::<RadrootsTradeModerationStatus>(),
- RootDescriptor::new::<RadrootsTradeOrderBackofficeOverlay>(),
- RootDescriptor::new::<RadrootsTradeOrderBackofficeQuery>(),
- RootDescriptor::new::<RadrootsTradeOrderBackofficeView>(),
- RootDescriptor::new::<RadrootsTradeOrderFacets>(),
- RootDescriptor::new::<RadrootsTradeOrderQuery>(),
- RootDescriptor::new::<RadrootsTradeOrderSort>(),
- RootDescriptor::new::<RadrootsTradeOrderSortField>(),
RootDescriptor::new::<RadrootsTradeReviewPriority>(),
RootDescriptor::new::<RadrootsTradeReviewQueueEntry>(),
RootDescriptor::new::<RadrootsTradeReviewStatus>(),
@@ -81,7 +70,6 @@ imported_ts_type!(
imported_ts_type!(RadrootsListingProductImport, "RadrootsListingProduct");
imported_ts_type!(RadrootsPlotRefImport, "RadrootsPlotRef");
imported_ts_type!(RadrootsResourceAreaRefImport, "RadrootsResourceAreaRef");
-imported_ts_type!(RadrootsOrderEventTypeImport, "RadrootsOrderEventType");
#[derive(dto_bindgen::Dto)]
pub struct RadrootsTradeFacetCount {
@@ -262,19 +250,6 @@ pub struct RadrootsTradeMarketplaceListingSummary {
}
#[derive(dto_bindgen::Dto)]
-pub struct RadrootsTradeMarketplaceOrderSummary {
- pub order_id: String,
- pub listing_addr: String,
- pub buyer_pubkey: String,
- pub seller_pubkey: String,
- pub status: RadrootsTradeWorkflowState,
- pub last_message_type: RadrootsOrderEventTypeImport,
- pub item_count: u32,
- pub total_bin_count: u32,
- pub last_reason: Option<String>,
-}
-
-#[derive(dto_bindgen::Dto)]
pub struct RadrootsTradeModerationFlag {
pub code: String,
pub severity: RadrootsTradeModerationSeverity,
@@ -304,69 +279,6 @@ pub enum RadrootsTradeModerationStatus {
}
#[derive(dto_bindgen::Dto)]
-pub struct RadrootsTradeOrderBackofficeOverlay {
- pub order_id: String,
- pub review_queue: Option<RadrootsTradeReviewQueueEntry>,
- pub moderation_flags: Vec<RadrootsTradeModerationFlag>,
-}
-
-#[derive(dto_bindgen::Dto)]
-pub struct RadrootsTradeOrderBackofficeQuery {
- pub order: RadrootsTradeOrderQuery,
- pub requires_review: Option<bool>,
- pub has_open_moderation_flags: Option<bool>,
-}
-
-#[derive(dto_bindgen::Dto)]
-pub struct RadrootsTradeOrderBackofficeView {
- pub order: RadrootsOrderWorkflowProjection,
- pub marketplace: RadrootsTradeMarketplaceOrderSummary,
- pub overlay: Option<RadrootsTradeOrderBackofficeOverlay>,
- pub requires_review: bool,
- pub open_moderation_flag_count: u32,
-}
-
-#[derive(dto_bindgen::Dto)]
-pub struct RadrootsTradeOrderFacets {
- pub buyer_pubkeys: Vec<RadrootsTradeFacetCount>,
- pub seller_pubkeys: Vec<RadrootsTradeFacetCount>,
- pub listing_addrs: Vec<RadrootsTradeFacetCount>,
- pub statuses: Vec<RadrootsTradeFacetCount>,
-}
-
-#[derive(dto_bindgen::Dto)]
-pub struct RadrootsTradeOrderQuery {
- pub listing_addr: Option<String>,
- pub buyer_pubkey: Option<String>,
- pub seller_pubkey: Option<String>,
- pub status: Option<RadrootsTradeWorkflowState>,
-}
-
-#[derive(dto_bindgen::Dto)]
-pub struct RadrootsTradeOrderSort {
- pub field: RadrootsTradeOrderSortField,
- pub direction: RadrootsTradeSortDirection,
-}
-
-#[derive(dto_bindgen::Dto)]
-pub enum RadrootsTradeOrderSortField {
- #[serde(rename = "order_id")]
- OrderId,
- #[serde(rename = "listing_addr")]
- ListingAddr,
- #[serde(rename = "buyer_pubkey")]
- BuyerPubkey,
- #[serde(rename = "seller_pubkey")]
- SellerPubkey,
- #[serde(rename = "status")]
- Status,
- #[serde(rename = "last_message_type")]
- LastMessageType,
- #[serde(rename = "total_bin_count")]
- TotalBinCount,
-}
-
-#[derive(dto_bindgen::Dto)]
pub enum RadrootsTradeReviewPriority {
#[serde(rename = "low")]
Low,
diff --git a/crates/trade_bindings/src/lib.rs b/crates/trade_bindings/src/lib.rs
@@ -39,11 +39,12 @@ pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[
event_import("RadrootsListingProduct"),
event_import("RadrootsListingPublicLocation"),
event_import("RadrootsListingStatus"),
- event_import("RadrootsOrderEconomics"),
- event_import("RadrootsOrderEventType"),
- event_import("RadrootsOrderInventoryCommitment"),
- source_support("RadrootsOrderIssue"),
local_shape("RadrootsTradeFacetCount"),
+ source_root("RadrootsTradeAgreementStateV1"),
+ source_root("RadrootsTradeAttestationStateV1"),
+ source_root("RadrootsTradeConflictStateV1"),
+ source_root("RadrootsTradeEvidenceStateV1"),
+ source_root("RadrootsTradeFulfillmentStateV1"),
source_root("RadrootsTradeListing"),
local_shape("RadrootsTradeListingBackofficeOverlay"),
local_shape("RadrootsTradeListingBackofficeQuery"),
@@ -58,23 +59,17 @@ pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[
source_root("RadrootsTradeListingSubtotal"),
source_root("RadrootsTradeListingTotal"),
local_shape("RadrootsTradeMarketplaceListingSummary"),
- local_shape("RadrootsTradeMarketplaceOrderSummary"),
local_shape("RadrootsTradeModerationFlag"),
local_shape("RadrootsTradeModerationSeverity"),
local_shape("RadrootsTradeModerationStatus"),
- local_shape("RadrootsTradeOrderBackofficeOverlay"),
- local_shape("RadrootsTradeOrderBackofficeQuery"),
- local_shape("RadrootsTradeOrderBackofficeView"),
- local_shape("RadrootsTradeOrderFacets"),
- local_shape("RadrootsTradeOrderQuery"),
- local_shape("RadrootsTradeOrderSort"),
- local_shape("RadrootsTradeOrderSortField"),
- source_root("RadrootsOrderWorkflowProjection"),
+ source_root("RadrootsTradeNegotiationStateV1"),
+ source_root("RadrootsTradePaymentStateV1"),
+ source_root("RadrootsTradePrivateTermsStateV1"),
+ source_root("RadrootsTradeProjectionV1"),
local_shape("RadrootsTradeReviewPriority"),
local_shape("RadrootsTradeReviewQueueEntry"),
local_shape("RadrootsTradeReviewStatus"),
local_shape("RadrootsTradeSortDirection"),
- source_root("RadrootsTradeWorkflowState"),
];
pub const TRADE_LARGE_INTEGER_POLICIES: &[TradeLargeIntegerPolicyEntry] = &[
@@ -92,12 +87,6 @@ pub const TRADE_LARGE_INTEGER_POLICIES: &[TradeLargeIntegerPolicyEntry] = &[
"RadrootsTradeMarketplaceListingSummary",
"terminal_order_count",
),
- json_number_safe_count("RadrootsTradeMarketplaceOrderSummary", "item_count"),
- json_number_safe_count("RadrootsTradeMarketplaceOrderSummary", "total_bin_count"),
- json_number_safe_count(
- "RadrootsTradeOrderBackofficeView",
- "open_moderation_flag_count",
- ),
];
const fn source_root(export_name: &'static str) -> TradeTypeInventoryEntry {
@@ -107,13 +96,6 @@ const fn source_root(export_name: &'static str) -> TradeTypeInventoryEntry {
}
}
-const fn source_support(export_name: &'static str) -> TradeTypeInventoryEntry {
- TradeTypeInventoryEntry {
- export_name,
- disposition: TradeTypeDisposition::SourceTradeSupport,
- }
-}
-
const fn event_import(export_name: &'static str) -> TradeTypeInventoryEntry {
TradeTypeInventoryEntry {
export_name,
@@ -174,11 +156,12 @@ mod tests {
"RadrootsListingProduct",
"RadrootsListingPublicLocation",
"RadrootsListingStatus",
- "RadrootsOrderEconomics",
- "RadrootsOrderEventType",
- "RadrootsOrderInventoryCommitment",
- "RadrootsOrderIssue",
"RadrootsTradeFacetCount",
+ "RadrootsTradeAgreementStateV1",
+ "RadrootsTradeAttestationStateV1",
+ "RadrootsTradeConflictStateV1",
+ "RadrootsTradeEvidenceStateV1",
+ "RadrootsTradeFulfillmentStateV1",
"RadrootsTradeListing",
"RadrootsTradeListingBackofficeOverlay",
"RadrootsTradeListingBackofficeQuery",
@@ -193,23 +176,17 @@ mod tests {
"RadrootsTradeListingSubtotal",
"RadrootsTradeListingTotal",
"RadrootsTradeMarketplaceListingSummary",
- "RadrootsTradeMarketplaceOrderSummary",
"RadrootsTradeModerationFlag",
"RadrootsTradeModerationSeverity",
"RadrootsTradeModerationStatus",
- "RadrootsTradeOrderBackofficeOverlay",
- "RadrootsTradeOrderBackofficeQuery",
- "RadrootsTradeOrderBackofficeView",
- "RadrootsTradeOrderFacets",
- "RadrootsTradeOrderQuery",
- "RadrootsTradeOrderSort",
- "RadrootsTradeOrderSortField",
- "RadrootsOrderWorkflowProjection",
+ "RadrootsTradeNegotiationStateV1",
+ "RadrootsTradePaymentStateV1",
+ "RadrootsTradePrivateTermsStateV1",
+ "RadrootsTradeProjectionV1",
"RadrootsTradeReviewPriority",
"RadrootsTradeReviewQueueEntry",
"RadrootsTradeReviewStatus",
- "RadrootsTradeSortDirection",
- "RadrootsTradeWorkflowState"
+ "RadrootsTradeSortDirection"
]
);
}
@@ -225,9 +202,6 @@ mod tests {
"RadrootsListingProduct",
"RadrootsListingPublicLocation",
"RadrootsListingStatus",
- "RadrootsOrderEconomics",
- "RadrootsOrderEventType",
- "RadrootsOrderInventoryCommitment",
] {
assert_eq!(
disposition(export_name),
@@ -247,20 +221,28 @@ mod tests {
assert_eq!(
source_roots,
[
+ "RadrootsTradeAgreementStateV1",
+ "RadrootsTradeAttestationStateV1",
+ "RadrootsTradeConflictStateV1",
+ "RadrootsTradeEvidenceStateV1",
+ "RadrootsTradeFulfillmentStateV1",
"RadrootsTradeListing",
"RadrootsTradeListingSubtotal",
"RadrootsTradeListingTotal",
- "RadrootsOrderWorkflowProjection",
- "RadrootsTradeWorkflowState"
+ "RadrootsTradeNegotiationStateV1",
+ "RadrootsTradePaymentStateV1",
+ "RadrootsTradePrivateTermsStateV1",
+ "RadrootsTradeProjectionV1"
]
);
}
#[test]
fn trade_source_support_types_are_marked_for_source_registry() {
- assert_eq!(
- disposition("RadrootsOrderIssue"),
- TradeTypeDisposition::SourceTradeSupport
+ assert!(
+ TRADE_TYPE_INVENTORY
+ .iter()
+ .all(|entry| entry.disposition != TradeTypeDisposition::SourceTradeSupport)
);
}
@@ -314,21 +296,6 @@ mod tests {
"terminal_order_count",
super::TradeLargeIntegerPolicy::JsonNumberSafeCount
),
- (
- "RadrootsTradeMarketplaceOrderSummary",
- "item_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeMarketplaceOrderSummary",
- "total_bin_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeOrderBackofficeView",
- "open_moderation_flag_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
]
);
}
diff --git a/tools/sdk_xtask_import/src/dto_roots.rs b/tools/sdk_xtask_import/src/dto_roots.rs
@@ -145,16 +145,6 @@ const TRADE_EXTERNAL_OVERRIDES: &[DtoExternalOverride] = &[
event_override("RadrootsEventPtr"),
event_override("RadrootsPlotRef"),
event_override("RadrootsResourceAreaRef"),
- event_override("RadrootsOrderEconomicActor"),
- event_override("RadrootsOrderEconomicEffect"),
- event_override("RadrootsOrderEconomicItem"),
- event_override("RadrootsOrderEconomicLine"),
- event_override("RadrootsOrderEconomicLineKind"),
- event_override("RadrootsOrderEconomics"),
- event_override("RadrootsOrderEventType"),
- event_override("RadrootsOrderInventoryCommitment"),
- event_override("RadrootsOrderItem"),
- event_override("RadrootsOrderPricingBasis"),
];
const TRADE_REQUIRED_EXTERNAL_PACKAGE_IMPORTS: &[&str] =
@@ -682,9 +672,15 @@ mod tests {
"RadrootsEventIndexCheckpoint",
];
const TRADE_TYPE_INVENTORY: &[&str] = &[
- "RadrootsOrderIssue",
- "RadrootsOrderWorkflowProjection",
+ "RadrootsTradeAgreementClaimV1",
+ "RadrootsTradeAgreementStateV1",
+ "RadrootsTradeAttestationRecordV1",
+ "RadrootsTradeAttestationResultV1",
+ "RadrootsTradeAttestationStateV1",
+ "RadrootsTradeConflictStateV1",
+ "RadrootsTradeEvidenceStateV1",
"RadrootsTradeFacetCount",
+ "RadrootsTradeFulfillmentStateV1",
"RadrootsTradeListing",
"RadrootsTradeListingBackofficeOverlay",
"RadrootsTradeListingBackofficeQuery",
@@ -699,22 +695,18 @@ mod tests {
"RadrootsTradeListingSubtotal",
"RadrootsTradeListingTotal",
"RadrootsTradeMarketplaceListingSummary",
- "RadrootsTradeMarketplaceOrderSummary",
"RadrootsTradeModerationFlag",
"RadrootsTradeModerationSeverity",
"RadrootsTradeModerationStatus",
- "RadrootsTradeOrderBackofficeOverlay",
- "RadrootsTradeOrderBackofficeQuery",
- "RadrootsTradeOrderBackofficeView",
- "RadrootsTradeOrderFacets",
- "RadrootsTradeOrderQuery",
- "RadrootsTradeOrderSort",
- "RadrootsTradeOrderSortField",
+ "RadrootsTradeNegotiationStateV1",
+ "RadrootsTradePaymentStateV1",
+ "RadrootsTradePrivateTermsStateV1",
+ "RadrootsTradeProjectionV1",
+ "RadrootsTradeReducerIssueV1",
"RadrootsTradeReviewPriority",
"RadrootsTradeReviewQueueEntry",
"RadrootsTradeReviewStatus",
"RadrootsTradeSortDirection",
- "RadrootsTradeWorkflowState",
];
#[test]
@@ -993,17 +985,17 @@ mod tests {
assert!(
imports
.get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsOrderEventType"))
+ .is_some_and(|names| names.contains("RadrootsFarmRef"))
);
assert!(
imports
.get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsOrderEconomics"))
+ .is_some_and(|names| names.contains("RadrootsListing"))
);
assert!(
imports
.get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsOrderInventoryCommitment"))
+ .is_some_and(|names| names.contains("RadrootsListingBin"))
);
for duplicate in [
@@ -1015,27 +1007,19 @@ mod tests {
"export type RadrootsCommercialMessageType = ",
"export type RadrootsOrderStatus = ",
"export type RadrootsTradeOrderWorkflowMessage = ",
+ "export type RadrootsOrderWorkflowProjection = ",
+ "export type RadrootsTradeMarketplaceOrderSummary = ",
+ "export type RadrootsTradeOrderQuery = ",
] {
assert!(!TRADE_BINDINGS_TYPES_TS.contains(duplicate));
}
- let marketplace_order_summary = type_declaration(
- TRADE_BINDINGS_TYPES_TS,
- "RadrootsTradeMarketplaceOrderSummary",
- );
- assert!(marketplace_order_summary.contains("status: RadrootsTradeWorkflowState"));
- assert!(marketplace_order_summary.contains("last_message_type: RadrootsOrderEventType"));
- assert!(!marketplace_order_summary.contains("has_requested_discounts"));
-
- let order_query = type_declaration(TRADE_BINDINGS_TYPES_TS, "RadrootsTradeOrderQuery");
- assert!(order_query.contains("status?: RadrootsTradeWorkflowState | null"));
-
- let workflow_projection =
- type_declaration(TRADE_BINDINGS_TYPES_TS, "RadrootsOrderWorkflowProjection");
- assert!(workflow_projection.contains("status: RadrootsTradeWorkflowState"));
- assert!(workflow_projection.contains("request_event_id?: string | null"));
- assert!(workflow_projection.contains("last_event_id?: string | null"));
- assert!(workflow_projection.contains("listing_addr?: string | null"));
+ let projection = type_declaration(TRADE_BINDINGS_TYPES_TS, "RadrootsTradeProjectionV1");
+ assert!(projection.contains("agreement_state: RadrootsTradeAgreementStateV1"));
+ assert!(projection.contains("negotiation_state: RadrootsTradeNegotiationStateV1"));
+ assert!(projection.contains("agreement_claims: Array<RadrootsTradeAgreementClaimV1>"));
+ assert!(projection.contains("issues: Array<RadrootsTradeReducerIssueV1>"));
+ assert!(projection.contains("projection_digest: string"));
for stale_counter in [
"root_event_id",
"last_message_type",
@@ -1051,7 +1035,7 @@ mod tests {
"discount_accept_count",
"discount_decline_count",
] {
- assert!(!workflow_projection.contains(stale_counter));
+ assert!(!projection.contains(stale_counter));
}
}