commit bffc3d0fe3ef8c541edacfa42daa3ab81e477641
parent 89b28cff0055c0bd7e92a021d077edff9214db38
Author: triesap <tyson@radroots.org>
Date: Wed, 15 Jul 2026 11:52:34 +0000
runtime: consolidate sdk storage transactions
Move SDK local runtime effects onto the V1 runtime/private/studio SQLite layout and require explicit UUIDv7 idempotency for mutations. Route event-store ingest, operation journal state, and outbox enqueue through one runtime transaction; add read-only inspection, pre-V1 fail-closed detection, and explicit quarantine reset. Remove retired DVM, revision, partial-local-mutation, and disabled legacy test surfaces while updating downstream tests, README, examples, and generated bindings.
Diffstat:
47 files changed, 2790 insertions(+), 13865 deletions(-)
diff --git a/crates/event_bindings/src/model.rs b/crates/event_bindings/src/model.rs
@@ -113,57 +113,14 @@ const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[
("KIND_APP_DATA", kinds::KIND_APP_DATA),
("KIND_LISTING", kinds::KIND_LISTING),
("KIND_APPLICATION_HANDLER", kinds::KIND_APPLICATION_HANDLER),
- (
- "KIND_TRADE_LISTING_VALIDATION_REQUEST",
- kinds::KIND_TRADE_LISTING_VALIDATION_REQUEST,
- ),
- (
- "KIND_TRADE_LISTING_VALIDATION_RESULT",
- kinds::KIND_TRADE_LISTING_VALIDATION_RESULT,
- ),
- (
- "KIND_TRADE_TRANSITION_PROOF_REQUEST",
- kinds::KIND_TRADE_TRANSITION_PROOF_REQUEST,
- ),
- (
- "KIND_TRADE_TRANSITION_PROOF_RESULT",
- kinds::KIND_TRADE_TRANSITION_PROOF_RESULT,
- ),
("KIND_ORDER_REQUEST", kinds::KIND_ORDER_REQUEST),
("KIND_ORDER_DECISION", kinds::KIND_ORDER_DECISION),
- (
- "KIND_ORDER_REVISION_PROPOSAL",
- kinds::KIND_ORDER_REVISION_PROPOSAL,
- ),
- (
- "KIND_ORDER_REVISION_DECISION",
- kinds::KIND_ORDER_REVISION_DECISION,
- ),
("KIND_ORDER_CANCELLATION", kinds::KIND_ORDER_CANCELLATION),
- ("KIND_TRADE_QUESTION", kinds::KIND_TRADE_QUESTION),
- ("KIND_TRADE_ANSWER", kinds::KIND_TRADE_ANSWER),
- (
- "KIND_TRADE_DISCOUNT_REQUEST",
- kinds::KIND_TRADE_DISCOUNT_REQUEST,
- ),
- (
- "KIND_TRADE_DISCOUNT_OFFER",
- kinds::KIND_TRADE_DISCOUNT_OFFER,
- ),
- (
- "KIND_TRADE_DISCOUNT_ACCEPT",
- kinds::KIND_TRADE_DISCOUNT_ACCEPT,
- ),
(
"KIND_TRADE_FORBIDDEN_3431",
kinds::KIND_TRADE_FORBIDDEN_3431,
),
(
- "KIND_TRADE_FULFILLMENT_UPDATE",
- kinds::KIND_TRADE_FULFILLMENT_UPDATE,
- ),
- ("KIND_TRADE_RECEIPT", kinds::KIND_TRADE_RECEIPT),
- (
"KIND_TRADE_VALIDATION_RECEIPT",
kinds::KIND_TRADE_VALIDATION_RECEIPT,
),
diff --git a/crates/sdk/README b/crates/sdk/README
@@ -7,9 +7,9 @@ The SDK v1 product runtime is centered on `RadrootsClient::builder()`,
`RadrootsClient::builder()` defaults to memory storage, the system clock, the `LocalOnly` transport
profile, and no production network publishing. Directory storage is opt-in and creates
-`event_store.sqlite` and `outbox.sqlite` in the selected directory. Configured Nostr relay URLs live
-inside `TransportProfile::Nostr` or the Nostr side of `TransportProfile::Hybrid`, and product
-enqueue requests choose the active profile through `TargetPolicy::default_profile()`.
+`runtime.sqlite`, `private.sqlite`, and `studio.sqlite` in the selected directory. Configured Nostr
+relay URLs live inside `TransportProfile::Nostr` or the Nostr side of `TransportProfile::Hybrid`,
+and product enqueue requests choose the active profile through `TargetPolicy::default_profile()`.
When `signer-adapters` is enabled, `RadrootsClient::builder()` accepts a configured
`RadrootsSdkSignerProvider`. The production signing modes are `local_key` and `myc_nip46`. Product
@@ -23,13 +23,9 @@ that prepared plan, ingests it into the local event store, queues signed outbox
typed `ListingEnqueueReceipt`. `sdk.listings().enqueue_publish(...)` is the convenience path that
prepares once and delegates to `enqueue_prepared_publish(...)`. Farm and trade write methods follow
the same configured signer pattern. The enqueue path uses typed transport target and idempotency
-inputs; omitted idempotency keys are derived deterministically. `SatisfactionPolicy::NoWait` records
-first-class no-wait delivery plans, so enqueue-only product workflows can complete without accepted
-or delivered transport targets and without fabricated target delivery receipts.
-
-Explicit signer injection remains available under `*_with_explicit_signer` method names for
-controlled adapter-level tests and advanced integration checks. Those methods are not the primary
-product API.
+inputs; write requests require explicit UUIDv7 idempotency keys. `SatisfactionPolicy::NoWait`
+records first-class no-wait delivery plans, so enqueue-only product workflows can complete without
+accepted or delivered transport targets and without fabricated target delivery receipts.
Event `created_at` and local observation time are separate contracts. The event timestamp remains
the authored event-envelope timestamp, while event-store and outbox mutation timestamps use the SDK
@@ -47,19 +43,17 @@ transport-level substrate checks. `radrootsd-proxy` adds daemon-resolved publish
`publish.event`.
`sdk.trades()` exposes the local trade runtime surface. With `runtime`, callers can ingest local trade
-evidence, ingest DVM validation receipts through `sdk.dvm()`, read `sdk.trades().status(...)`, resync
-through `sdk.trades().resync()`, inspect validation receipts through
-`sdk.trades().validation_receipts()`, and read seller inbox projections through
-`sdk.trades().seller()`.
+evidence, read `sdk.trades().status(...)`, resync through `sdk.trades().resync()`, inspect
+validation receipts through `sdk.trades().validation_receipts()`, and read seller inbox projections
+through `sdk.trades().seller()`.
`sdk.trades().status(...)` accepts `TradeStatusRequest` and reads local projections; it returns typed
source, event count, limit state, event IDs, ambiguity candidates, eligibility, next-action, and
reducer issue data. Status is not a network fetch.
Configured-signer trade writes compile when both `runtime` and `signer-adapters` are enabled. Buyer
-write workflows use `sdk.trades().buyer()` for propose, cancel, and revision-decision actions.
-Seller write workflows use `sdk.trades().seller()` for accept, decline, and revision-proposal
-actions. Runtime-only builds intentionally expose no buyer write handle and no trade mutation request
-DTOs. DVM proof requests and validation receipt ingestion use `sdk.dvm()`.
+write workflows use `sdk.trades().buyer()` for propose and cancel actions. Seller write workflows
+use `sdk.trades().seller()` for accept and decline actions. Runtime-only builds intentionally expose
+no buyer write handle and no trade mutation request DTOs.
The `local-runtime` feature is the curated feature bundle for local product runtime consumers. It
enables `std`, `serde`, `serde_json`, `runtime`, `signer-adapters`, `transport-nostr-runtime`, and
@@ -77,9 +71,9 @@ Relay URL policy is explicit. Public relay URLs must use `wss://`. Local develop
URLs are accepted only under `NostrRelayUrlPolicy::Localhost` and only for `localhost`, `127.0.0.1`,
or `[::1]`. Non-local insecure `ws://` targets, including private LAN addresses, are rejected.
-Low-level event-contract, wire-codec, reducer, and DVM contract ownership lives in the shared
-Radroots libraries. The SDK exposes product APIs and selected adapter boundaries; it does not expose
-a public workflow-bypass namespace for product callers.
+Low-level event-contract, wire-codec, reducer, and validation receipt contract ownership lives in
+the shared Radroots libraries. The SDK exposes product APIs and selected adapter boundaries; it does
+not expose a public workflow-bypass namespace for product callers.
## Examples
diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs
@@ -30,7 +30,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listing = sample_listing(seller.as_str());
let prepare_request = ListingPreparePublishRequest::new(actor.clone(), listing);
let target_policy = TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public)?;
- let idempotency_key = SdkIdempotencyKey::new("example-1")?;
+ let idempotency_key = SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000202")?;
let prepared = sdk.listings().prepare_publish(prepare_request)?;
let enqueue = sdk
diff --git a/crates/sdk/examples/sdk_v1_listing_prepare.rs b/crates/sdk/examples/sdk_v1_listing_prepare.rs
@@ -23,7 +23,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let plan = sdk.listings().prepare_publish(request)?;
println!("public listing: {}", plan.public_listing_addr.as_str());
- println!("draft listing: {}", plan.draft_listing_addr.as_str());
println!("expected event: {}", plan.expected_event_id.as_str());
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
@@ -43,7 +43,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
&actor,
prepared,
target_policy,
- Some(SdkIdempotencyKey::new("sdk-v1-local-example")?),
+ Some(SdkIdempotencyKey::new(
+ "01890f0e-6c00-7000-8000-000000000201",
+ )?),
)
.await?;
let adapter = RadrootsMockRelayPublishAdapter::new();
diff --git a/crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs b/crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs
@@ -42,7 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Arc::new(ExampleNip46Transport),
)?;
let sdk = RadrootsClient::builder()
- .signer_provider(RadrootsSdkSignerProvider::MycNip46(signer))
+ .signer_provider(RadrootsSdkSignerProvider::MycNip46(Box::new(signer)))
.build()
.await?;
let status = sdk.signer_status().expect("configured signer status");
diff --git a/crates/sdk/src/dvm_runtime.rs b/crates/sdk/src/dvm_runtime.rs
@@ -1,742 +0,0 @@
-#[cfg(feature = "signer-adapters")]
-use crate::workflow_runtime::enqueue_configured_signed_workflow;
-#[cfg(feature = "runtime")]
-use crate::{
- DvmClient, NostrRelayUrlPolicy, RadrootsSdkError, RadrootsSdkTimestamp, SatisfactionPolicy,
- SdkIdempotencyKey, SdkMutationState, SyncProjectionRefreshReceipt,
- SyncProjectionRefreshRequest, TargetPolicy,
- runtime::sdk_now_ms,
- sync_runtime::refresh_product_projections_for_sdk,
- workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow},
-};
-#[cfg(feature = "runtime")]
-use radroots_authority::{RadrootsActorContext, RadrootsEventSigner, authorize_actor_for_draft};
-use radroots_event::wire::RadrootsNip01EventWireParts;
-#[cfg(feature = "runtime")]
-use radroots_event::{
- draft::{RadrootsEventDraft, RadrootsSignedEvent},
- ids::{RadrootsEventId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey},
- kinds::KIND_TRADE_TRANSITION_PROOF_REQUEST,
-};
-use radroots_event_codec::wire::canonicalize_tags;
-#[cfg(feature = "runtime")]
-use radroots_event_store::RadrootsEventIngest;
-#[cfg(feature = "runtime")]
-use radroots_trade::dvm::RadrootsTradeInventoryBinWitnessDto;
-#[cfg(feature = "runtime")]
-use radroots_trade::validation_receipt::{
- RadrootsTradeCommitmentConfidence, RadrootsTradeValidationAuthority,
- RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProofSystem,
- RadrootsValidationReceiptResult, RadrootsValidationReceiptType,
- verify_validation_receipt_event,
-};
-
-#[cfg(feature = "runtime")]
-const RADROOTS_TRADE_TRANSITION_WITNESS_VERSION: u32 = 1;
-#[cfg(feature = "runtime")]
-const RADROOTS_TRADE_TRANSITION_PROTOCOL_VERSION: &str = "radroots.trade.v1";
-#[cfg(feature = "runtime")]
-const RADROOTS_TRADE_TRANSITION_REDUCER_PROGRAM_HASH: &str =
- "0x3d8f7f463904d71f2d0d14b1551450756697e51c7b658e10c6d5c20a7bc61f08";
-#[cfg(feature = "runtime")]
-const RADROOTS_TRADE_TRANSITION_PROOF_TARGET: &str = "trade.order_acceptance.v1";
-
-#[cfg(feature = "runtime")]
-pub const DVM_TRADE_TRANSITION_PROOF_REQUEST_CONTRACT_ID: &str =
- "radroots.trade.transition_proof.request.v1";
-#[cfg(feature = "runtime")]
-pub const DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND: &str =
- "dvm.trade_transition_proof.request.v1";
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum DvmProofMode {
- #[default]
- None,
- Core,
- Compressed,
- Groth16,
- Plonk,
-}
-
-#[cfg(feature = "runtime")]
-impl DvmProofMode {
- pub const fn as_str(self) -> &'static str {
- match self {
- Self::None => "none",
- Self::Core => "core",
- Self::Compressed => "compressed",
- Self::Groth16 => "groth16",
- Self::Plonk => "plonk",
- }
- }
-
- pub const fn proof_system(self) -> RadrootsValidationReceiptProofSystem {
- match self {
- Self::None => RadrootsValidationReceiptProofSystem::None,
- Self::Core => RadrootsValidationReceiptProofSystem::Sp1Core,
- Self::Compressed => RadrootsValidationReceiptProofSystem::Sp1Compressed,
- Self::Groth16 => RadrootsValidationReceiptProofSystem::Sp1Groth16,
- Self::Plonk => RadrootsValidationReceiptProofSystem::Sp1Plonk,
- }
- }
-
- const fn requires_sp1_identity(self) -> bool {
- !matches!(self, Self::None)
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct DvmTradeTransitionProofPrepareRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub worker_pubkey: RadrootsPublicKey,
- pub listing_addr: RadrootsListingAddress,
- pub listing_event_id: RadrootsEventId,
- pub request_event_id: RadrootsEventId,
- pub decision_event_id: RadrootsEventId,
- pub inventory_bins: Vec<RadrootsTradeInventoryBinWitnessDto>,
- pub inventory_sequence: u128,
- pub previous_state_root: Option<String>,
- pub proof_mode: DvmProofMode,
- pub sp1_program_hash: Option<String>,
- pub sp1_verifying_key_hash: Option<String>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-impl DvmTradeTransitionProofPrepareRequest {
- pub fn new(
- actor: RadrootsActorContext,
- worker_pubkey: RadrootsPublicKey,
- listing_addr: RadrootsListingAddress,
- listing_event_id: RadrootsEventId,
- request_event_id: RadrootsEventId,
- decision_event_id: RadrootsEventId,
- inventory_bins: Vec<RadrootsTradeInventoryBinWitnessDto>,
- ) -> Self {
- Self {
- actor,
- worker_pubkey,
- listing_addr,
- listing_event_id,
- request_event_id,
- decision_event_id,
- inventory_bins,
- inventory_sequence: 0,
- previous_state_root: None,
- proof_mode: DvmProofMode::None,
- sp1_program_hash: None,
- sp1_verifying_key_hash: None,
- created_at: None,
- }
- }
-
- pub fn with_inventory_sequence(mut self, inventory_sequence: u128) -> Self {
- self.inventory_sequence = inventory_sequence;
- self
- }
-
- pub fn with_previous_state_root(mut self, previous_state_root: impl Into<String>) -> Self {
- self.previous_state_root = Some(previous_state_root.into());
- self
- }
-
- pub fn with_proof_mode(mut self, proof_mode: DvmProofMode) -> Self {
- self.proof_mode = proof_mode;
- self
- }
-
- pub fn with_sp1_identity(
- mut self,
- program_hash: impl Into<String>,
- verifying_key_hash: impl Into<String>,
- ) -> Self {
- self.sp1_program_hash = Some(program_hash.into());
- self.sp1_verifying_key_hash = Some(verifying_key_hash.into());
- 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 DvmTradeTransitionProofEnqueueRequest {
- #[serde(flatten)]
- pub prepare: DvmTradeTransitionProofPrepareRequest,
- pub target_policy: TargetPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
-}
-
-#[cfg(feature = "runtime")]
-impl DvmTradeTransitionProofEnqueueRequest {
- pub fn new(
- actor: RadrootsActorContext,
- worker_pubkey: RadrootsPublicKey,
- listing_addr: RadrootsListingAddress,
- listing_event_id: RadrootsEventId,
- request_event_id: RadrootsEventId,
- decision_event_id: RadrootsEventId,
- inventory_bins: Vec<RadrootsTradeInventoryBinWitnessDto>,
- target_policy: TargetPolicy,
- ) -> Self {
- Self::from_prepare(
- DvmTradeTransitionProofPrepareRequest::new(
- actor,
- worker_pubkey,
- listing_addr,
- listing_event_id,
- request_event_id,
- decision_event_id,
- inventory_bins,
- ),
- target_policy,
- )
- }
-
- pub fn from_prepare(
- prepare: DvmTradeTransitionProofPrepareRequest,
- target_policy: TargetPolicy,
- ) -> Self {
- Self {
- prepare,
- target_policy,
- idempotency_key: None,
- }
- }
-
- pub fn try_with_nostr_targets<I, S>(
- mut self,
- target_policy: I,
- policy: NostrRelayUrlPolicy,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.target_policy = TargetPolicy::try_nostr_relays(target_policy, policy)?;
- Ok(self)
- }
-
- pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key);
- 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_inventory_sequence(mut self, inventory_sequence: u128) -> Self {
- self.prepare.inventory_sequence = inventory_sequence;
- self
- }
-
- pub fn with_previous_state_root(mut self, previous_state_root: impl Into<String>) -> Self {
- self.prepare.previous_state_root = Some(previous_state_root.into());
- self
- }
-
- pub fn with_proof_mode(mut self, proof_mode: DvmProofMode) -> Self {
- self.prepare.proof_mode = proof_mode;
- self
- }
-
- pub fn with_sp1_identity(
- mut self,
- program_hash: impl Into<String>,
- verifying_key_hash: impl Into<String>,
- ) -> Self {
- self.prepare.sp1_program_hash = Some(program_hash.into());
- self.prepare.sp1_verifying_key_hash = Some(verifying_key_hash.into());
- self
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.prepare.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct DvmTradeTransitionProofRequestPayload {
- pub witness_version: u32,
- pub proof_target: String,
- pub listing_event_id: String,
- pub request_event_id: String,
- pub decision_event_id: String,
- pub inventory_bins: Vec<RadrootsTradeInventoryBinWitnessDto>,
- pub inventory_sequence: u128,
- pub previous_state_root: Option<String>,
- pub proof_mode: DvmProofMode,
- pub reducer_program_hash: String,
- pub radroots_protocol_version: String,
- pub sp1_program_hash: Option<String>,
- pub sp1_verifying_key_hash: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct DvmTradeTransitionProofPlan {
- pub worker_pubkey: RadrootsPublicKey,
- pub listing_addr: RadrootsListingAddress,
- pub listing_event_id: RadrootsEventId,
- pub request_event_id: RadrootsEventId,
- pub decision_event_id: RadrootsEventId,
- pub proof_mode: DvmProofMode,
- pub expected_receipt_proof_system: RadrootsValidationReceiptProofSystem,
- pub expected_event_id: RadrootsEventId,
- pub frozen_draft: RadrootsEventDraft,
- pub payload: DvmTradeTransitionProofRequestPayload,
- pub created_at: RadrootsSdkTimestamp,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct DvmTradeTransitionProofReceipt {
- pub worker_pubkey: RadrootsPublicKey,
- pub listing_addr: RadrootsListingAddress,
- pub listing_event_id: RadrootsEventId,
- pub request_event_id: RadrootsEventId,
- pub decision_event_id: RadrootsEventId,
- pub expected_event_id: RadrootsEventId,
- pub signed_event_id: RadrootsEventId,
- pub proof_mode: DvmProofMode,
- pub expected_receipt_proof_system: RadrootsValidationReceiptProofSystem,
- 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 DvmValidationReceiptIngestRequest {
- pub event: RadrootsSignedEvent,
- pub observed_at: Option<RadrootsSdkTimestamp>,
- pub expected_order_id: Option<RadrootsOrderId>,
- pub expected_listing_event_id: Option<RadrootsEventId>,
- pub expected_root_event_id: Option<RadrootsEventId>,
- pub expected_target_event_id: Option<RadrootsEventId>,
- pub projection_refresh: SyncProjectionRefreshRequest,
-}
-
-#[cfg(feature = "runtime")]
-impl DvmValidationReceiptIngestRequest {
- pub fn new(event: RadrootsSignedEvent) -> Self {
- Self {
- event,
- observed_at: None,
- expected_order_id: None,
- expected_listing_event_id: None,
- expected_root_event_id: None,
- expected_target_event_id: None,
- projection_refresh: SyncProjectionRefreshRequest::new(),
- }
- }
-
- pub fn with_observed_at(mut self, observed_at: RadrootsSdkTimestamp) -> Self {
- self.observed_at = Some(observed_at);
- self
- }
-
- pub fn with_expected_order_id(mut self, order_id: RadrootsOrderId) -> Self {
- self.expected_order_id = Some(order_id);
- self
- }
-
- pub fn with_expected_listing_event_id(mut self, listing_event_id: RadrootsEventId) -> Self {
- self.expected_listing_event_id = Some(listing_event_id);
- self
- }
-
- pub fn with_expected_root_event_id(mut self, root_event_id: RadrootsEventId) -> Self {
- self.expected_root_event_id = Some(root_event_id);
- self
- }
-
- pub fn with_expected_target_event_id(mut self, target_event_id: RadrootsEventId) -> Self {
- self.expected_target_event_id = Some(target_event_id);
- 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 DvmValidationReceiptIngestReceipt {
- pub receipt_event_id: RadrootsEventId,
- pub order_id: RadrootsOrderId,
- pub listing_event_id: RadrootsEventId,
- pub root_event_id: RadrootsEventId,
- pub target_event_id: RadrootsEventId,
- pub receipt_type: RadrootsValidationReceiptType,
- pub result: RadrootsValidationReceiptResult,
- pub proof_system: RadrootsValidationReceiptProofSystem,
- pub validation_authority: Option<RadrootsTradeValidationAuthority>,
- pub commitment_confidence: RadrootsTradeCommitmentConfidence,
- pub local_event_seq: i64,
- pub inserted: bool,
- pub refresh: SyncProjectionRefreshReceipt,
-}
-
-#[cfg(feature = "runtime")]
-impl<'sdk> DvmClient<'sdk> {
- pub fn prepare_trade_transition_proof_request(
- &self,
- request: DvmTradeTransitionProofPrepareRequest,
- ) -> Result<DvmTradeTransitionProofPlan, RadrootsSdkError> {
- let created_at = match request.created_at {
- Some(created_at) => created_at,
- None => self.sdk.now()?,
- };
- dvm_trade_transition_proof_plan(request, created_at)
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn enqueue_trade_transition_proof_request(
- &self,
- request: DvmTradeTransitionProofEnqueueRequest,
- ) -> Result<DvmTradeTransitionProofReceipt, RadrootsSdkError> {
- let actor = request.prepare.actor.clone();
- let target_policy = request.target_policy.clone();
- let idempotency_key = request.idempotency_key.clone();
- let plan = self.prepare_trade_transition_proof_request(request.prepare)?;
- let enqueue = enqueue_configured_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND,
- actor: &actor,
- frozen_draft: &plan.frozen_draft,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key,
- },
- )
- .await?;
- Ok(dvm_trade_transition_proof_receipt(plan, enqueue))
- }
-
- pub async fn enqueue_trade_transition_proof_request_with_explicit_signer(
- &self,
- request: DvmTradeTransitionProofEnqueueRequest,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<DvmTradeTransitionProofReceipt, RadrootsSdkError> {
- let actor = request.prepare.actor.clone();
- let target_policy = request.target_policy.clone();
- let idempotency_key = request.idempotency_key.clone();
- let plan = self.prepare_trade_transition_proof_request(request.prepare)?;
- let enqueue = enqueue_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND,
- actor: &actor,
- frozen_draft: &plan.frozen_draft,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key,
- },
- signer,
- )
- .await?;
- Ok(dvm_trade_transition_proof_receipt(plan, enqueue))
- }
-
- pub async fn ingest_validation_receipt(
- &self,
- request: DvmValidationReceiptIngestRequest,
- ) -> Result<DvmValidationReceiptIngestReceipt, RadrootsSdkError> {
- let verified = verify_validation_receipt_event(
- request.event.envelope(),
- RadrootsValidationReceiptExpectedBinding {
- order_id: request
- .expected_order_id
- .as_ref()
- .map(RadrootsOrderId::as_str),
- listing_event_id: request
- .expected_listing_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- root_event_id: request
- .expected_root_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- target_event_id: request
- .expected_target_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- ..RadrootsValidationReceiptExpectedBinding::default()
- },
- )
- .map_err(validation_receipt_sdk_error)?;
- let receipt_event_id = parse_event_id(request.event.id_str(), "receipt event id")?;
- let order_id =
- RadrootsOrderId::parse(verified.tags.order_id.as_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("validation receipt order id is invalid: {error}"),
- }
- })?;
- let listing_event_id = RadrootsEventId::parse(verified.tags.listing_event_id.as_str())
- .expect("verified validation receipt listing event id is valid");
- let root_event_id = RadrootsEventId::parse(verified.tags.root_event_id.as_str())
- .expect("verified validation receipt root event id is valid");
- let target_event_id = RadrootsEventId::parse(verified.tags.target_event_id.as_str())
- .expect("verified validation receipt target event id is valid");
- let observed_at_ms = match request.observed_at {
- Some(observed_at) => sdk_timestamp_ms(observed_at)?,
- None => sdk_now_ms(self.sdk)?,
- };
- let ingest = self
- .sdk
- ._event_store
- .ingest_event(RadrootsEventIngest::new(request.event, observed_at_ms))
- .await?;
- let refresh =
- refresh_product_projections_for_sdk(self.sdk, request.projection_refresh).await?;
- Ok(DvmValidationReceiptIngestReceipt {
- receipt_event_id,
- order_id,
- listing_event_id,
- root_event_id,
- target_event_id,
- receipt_type: verified.receipt.receipt_type,
- result: verified.receipt.result,
- proof_system: verified.receipt.proof.system,
- validation_authority: None,
- commitment_confidence: validation_receipt_ingest_confidence(verified.receipt.result),
- local_event_seq: ingest.seq,
- inserted: ingest.inserted,
- refresh,
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-fn validation_receipt_ingest_confidence(
- result: RadrootsValidationReceiptResult,
-) -> RadrootsTradeCommitmentConfidence {
- match result {
- RadrootsValidationReceiptResult::Valid => RadrootsTradeCommitmentConfidence::LocalOnly,
- RadrootsValidationReceiptResult::Invalid => RadrootsTradeCommitmentConfidence::Invalid,
- }
-}
-
-#[cfg(feature = "runtime")]
-fn dvm_trade_transition_proof_plan(
- request: DvmTradeTransitionProofPrepareRequest,
- created_at: RadrootsSdkTimestamp,
-) -> Result<DvmTradeTransitionProofPlan, RadrootsSdkError> {
- validate_inventory_bins(&request.inventory_bins)?;
- validate_sp1_identity(&request)?;
- let payload = DvmTradeTransitionProofRequestPayload {
- witness_version: RADROOTS_TRADE_TRANSITION_WITNESS_VERSION,
- proof_target: RADROOTS_TRADE_TRANSITION_PROOF_TARGET.to_owned(),
- listing_event_id: request.listing_event_id.as_str().to_owned(),
- request_event_id: request.request_event_id.as_str().to_owned(),
- decision_event_id: request.decision_event_id.as_str().to_owned(),
- inventory_bins: request.inventory_bins.clone(),
- inventory_sequence: request.inventory_sequence,
- previous_state_root: request.previous_state_root.clone(),
- proof_mode: request.proof_mode,
- reducer_program_hash: RADROOTS_TRADE_TRANSITION_REDUCER_PROGRAM_HASH.to_owned(),
- radroots_protocol_version: RADROOTS_TRADE_TRANSITION_PROTOCOL_VERSION.to_owned(),
- sp1_program_hash: request.sp1_program_hash.clone(),
- sp1_verifying_key_hash: request.sp1_verifying_key_hash.clone(),
- };
- let content = serde_json::to_string(&payload).expect("DVM proof request payload serializes");
- let mut tags = vec![
- vec!["a".to_owned(), request.listing_addr.as_str().to_owned()],
- vec!["p".to_owned(), request.worker_pubkey.as_str().to_owned()],
- vec![
- "i".to_owned(),
- request.decision_event_id.as_str().to_owned(),
- "event".to_owned(),
- "radroots:order_decision_event".to_owned(),
- ],
- ];
- canonicalize_tags(&mut tags);
- let parts = RadrootsNip01EventWireParts {
- kind: KIND_TRADE_TRANSITION_PROOF_REQUEST,
- content,
- tags,
- };
- let frozen_draft = RadrootsEventDraft::new(
- DVM_TRADE_TRANSITION_PROOF_REQUEST_CONTRACT_ID,
- parts.kind,
- u64::from(created_at.try_into_nostr_created_at()?),
- parts.tags,
- parts.content,
- request.actor.pubkey().as_str(),
- )
- .expect("DVM proof request draft is valid");
- authorize_actor_for_draft(&request.actor, &frozen_draft)?;
- let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id_str())
- .expect("frozen DVM proof request draft produces a valid event id");
- Ok(DvmTradeTransitionProofPlan {
- worker_pubkey: request.worker_pubkey,
- listing_addr: request.listing_addr,
- listing_event_id: request.listing_event_id,
- request_event_id: request.request_event_id,
- decision_event_id: request.decision_event_id,
- proof_mode: request.proof_mode,
- expected_receipt_proof_system: request.proof_mode.proof_system(),
- expected_event_id,
- frozen_draft,
- payload,
- created_at,
- })
-}
-
-#[cfg(feature = "runtime")]
-fn validate_inventory_bins(
- inventory_bins: &[RadrootsTradeInventoryBinWitnessDto],
-) -> Result<(), RadrootsSdkError> {
- if inventory_bins.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "DVM proof request inventory bins cannot be empty".to_owned(),
- });
- }
- for bin in inventory_bins {
- if bin.bin_id.as_str().trim().is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "DVM proof request inventory bin id cannot be empty".to_owned(),
- });
- }
- if bin.previous_reserved > bin.listing_capacity {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "DVM proof request inventory bin `{}` previous_reserved exceeds listing_capacity",
- bin.bin_id.as_str()
- ),
- });
- }
- }
- Ok(())
-}
-
-#[cfg(feature = "runtime")]
-fn validate_sp1_identity(
- request: &DvmTradeTransitionProofPrepareRequest,
-) -> Result<(), RadrootsSdkError> {
- validate_optional_hash32(&request.previous_state_root, "previous_state_root")?;
- validate_optional_hash32(&request.sp1_program_hash, "sp1_program_hash")?;
- validate_optional_hash32(&request.sp1_verifying_key_hash, "sp1_verifying_key_hash")?;
- let has_sp1_identity =
- request.sp1_program_hash.is_some() || request.sp1_verifying_key_hash.is_some();
- if request.proof_mode == DvmProofMode::None && has_sp1_identity {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "DVM proof mode none cannot include SP1 identity hashes".to_owned(),
- });
- }
- if request.proof_mode.requires_sp1_identity()
- && (request.sp1_program_hash.is_none() || request.sp1_verifying_key_hash.is_none())
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "DVM proof mode {} requires SP1 program and verifying key hashes",
- request.proof_mode.as_str()
- ),
- });
- }
- Ok(())
-}
-
-#[cfg(feature = "runtime")]
-fn validate_optional_hash32(
- value: &Option<String>,
- field: &'static str,
-) -> Result<(), RadrootsSdkError> {
- if let Some(value) = value {
- let hash = value.as_str();
- if hash.len() != 66 || !hash.starts_with("0x") || !is_lower_hex(&hash[2..]) {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!("DVM proof request {field} must be 0x-prefixed lowercase hex32"),
- });
- }
- }
- Ok(())
-}
-
-#[cfg(feature = "runtime")]
-fn is_lower_hex(value: &str) -> bool {
- value
- .bytes()
- .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
-}
-
-#[cfg(feature = "runtime")]
-fn dvm_trade_transition_proof_receipt(
- plan: DvmTradeTransitionProofPlan,
- enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> DvmTradeTransitionProofReceipt {
- DvmTradeTransitionProofReceipt {
- worker_pubkey: plan.worker_pubkey,
- listing_addr: plan.listing_addr,
- listing_event_id: plan.listing_event_id,
- request_event_id: plan.request_event_id,
- decision_event_id: plan.decision_event_id,
- expected_event_id: plan.expected_event_id,
- signed_event_id: enqueue.signed_event_id,
- proof_mode: plan.proof_mode,
- expected_receipt_proof_system: plan.expected_receipt_proof_system,
- 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 = "runtime")]
-fn parse_event_id(value: &str, field: &'static str) -> Result<RadrootsEventId, RadrootsSdkError> {
- RadrootsEventId::parse(value).map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("{field} is invalid: {error}"),
- })
-}
-
-#[cfg(feature = "runtime")]
-fn validation_receipt_sdk_error(
- error: radroots_trade::validation_receipt::RadrootsValidationReceiptError,
-) -> RadrootsSdkError {
- RadrootsSdkError::InvalidRequest {
- message: format!("validation receipt event is invalid: {error}"),
- }
-}
-
-#[cfg(feature = "runtime")]
-fn sdk_timestamp_ms(timestamp: RadrootsSdkTimestamp) -> Result<i64, RadrootsSdkError> {
- let seconds = i64::try_from(timestamp.unix_seconds()).map_err(|_| {
- RadrootsSdkError::TimestampOutOfRange {
- value: timestamp.unix_seconds(),
- }
- })?;
- seconds
- .checked_mul(1_000)
- .ok_or(RadrootsSdkError::TimestampOutOfRange {
- value: timestamp.unix_seconds(),
- })
-}
-
-#[cfg(all(test, feature = "runtime"))]
-#[path = "../tests/unit/dvm_runtime_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs
@@ -18,7 +18,6 @@ pub enum RadrootsSdkErrorClass {
Authorization,
Clock,
Configuration,
- LocalMutation,
Request,
Storage,
Transport,
@@ -30,7 +29,6 @@ pub enum RadrootsSdkErrorClass {
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RadrootsSdkRecoveryAction {
- RetryOutboxEnqueue,
InspectLocalStores,
InspectGeoNamesAsset,
RetryOperationWithSameIdempotencyKey,
@@ -60,27 +58,6 @@ pub enum RadrootsSdkGeoNamesErrorKind {
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum RadrootsSdkPartialLocalMutationFailure {
- OutboxEnqueue,
- OutboxIdempotencyConflict,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct RadrootsSdkPartialLocalMutationError {
- pub event_id: Option<String>,
- pub operation_kind: String,
- pub idempotency_digest_prefix: Option<String>,
- pub stored: bool,
- pub queued: bool,
- pub recovery: RadrootsSdkRecoveryAction,
- pub failure: RadrootsSdkPartialLocalMutationFailure,
-}
-
-#[cfg(feature = "runtime")]
#[derive(Debug)]
#[non_exhaustive]
pub enum RadrootsSdkError {
@@ -156,7 +133,7 @@ pub enum RadrootsSdkError {
},
TradeAmbiguous {
operation: String,
- locator: RadrootsTradeLocator,
+ locator: Box<RadrootsTradeLocator>,
candidates: Vec<RadrootsTradeLocator>,
},
PrivacyPreflight {
@@ -185,7 +162,11 @@ pub enum RadrootsSdkError {
InvalidRequest {
message: String,
},
- ListingDraft {
+ UnsupportedProfileSchema {
+ path: PathBuf,
+ message: String,
+ },
+ ListingEdit {
message: String,
},
ListingMutation {
@@ -197,6 +178,9 @@ pub enum RadrootsSdkError {
PrivateStore {
message: String,
},
+ StudioStore {
+ message: String,
+ },
GeoNames {
kind: RadrootsSdkGeoNamesErrorKind,
message: String,
@@ -207,7 +191,6 @@ pub enum RadrootsSdkError {
Projection {
message: String,
},
- PartialLocalMutation(RadrootsSdkPartialLocalMutationError),
}
#[cfg(feature = "runtime")]
@@ -247,10 +230,12 @@ impl RadrootsSdkError {
Self::Authority { .. } => "authority",
Self::EventStore { .. } => "event_store",
Self::InvalidRequest { .. } => "invalid_request",
- Self::ListingDraft { .. } => "listing_draft",
+ Self::UnsupportedProfileSchema { .. } => "unsupported_profile_schema",
+ Self::ListingEdit { .. } => "listing_edit",
Self::ListingMutation { .. } => "listing_mutation",
Self::Outbox { .. } => "outbox",
Self::PrivateStore { .. } => "private_store",
+ Self::StudioStore { .. } => "studio_store",
Self::GeoNames { kind, .. } => match kind {
RadrootsSdkGeoNamesErrorKind::Configuration => "geonames_configuration",
RadrootsSdkGeoNamesErrorKind::Download => "geonames_download",
@@ -261,7 +246,6 @@ impl RadrootsSdkError {
},
Self::Transport { .. } => "transport",
Self::Projection { .. } => "projection",
- Self::PartialLocalMutation(_) => "partial_local_mutation",
}
}
@@ -271,6 +255,7 @@ impl RadrootsSdkError {
| Self::EventStore { .. }
| Self::Outbox { .. }
| Self::PrivateStore { .. }
+ | Self::StudioStore { .. }
| Self::Projection { .. } => RadrootsSdkErrorClass::Storage,
Self::GeoNames { kind, .. } => match kind {
RadrootsSdkGeoNamesErrorKind::Configuration => RadrootsSdkErrorClass::Configuration,
@@ -300,7 +285,8 @@ impl RadrootsSdkError {
| Self::SignerProtocol { .. }
| Self::SignerAuthChallengePending { .. }
| Self::InvalidRequest { .. }
- | Self::ListingDraft { .. }
+ | Self::UnsupportedProfileSchema { .. }
+ | Self::ListingEdit { .. }
| Self::ListingMutation { .. } => RadrootsSdkErrorClass::Request,
Self::ProductSyncUnsupported { .. }
| Self::ReticulumPreviewTransportUnavailable { .. } => {
@@ -310,7 +296,6 @@ impl RadrootsSdkError {
| Self::Transport { .. }
| Self::SignerRequestTimedOut { .. }
| Self::SignerTransport { .. } => RadrootsSdkErrorClass::Transport,
- Self::PartialLocalMutation(_) => RadrootsSdkErrorClass::LocalMutation,
}
}
@@ -322,6 +307,7 @@ impl RadrootsSdkError {
| Self::EventStore { .. }
| Self::Outbox { .. }
| Self::PrivateStore { .. }
+ | Self::StudioStore { .. }
| Self::GeoNames {
kind: RadrootsSdkGeoNamesErrorKind::Cache
| RadrootsSdkGeoNamesErrorKind::Download,
@@ -331,7 +317,6 @@ impl RadrootsSdkError {
| Self::SignerRequestTimedOut { .. }
| Self::SignerTransport { .. }
| Self::Projection { .. }
- | Self::PartialLocalMutation(_)
)
}
@@ -341,6 +326,7 @@ impl RadrootsSdkError {
| Self::EventStore { .. }
| Self::Outbox { .. }
| Self::PrivateStore { .. }
+ | Self::StudioStore { .. }
| Self::Projection { .. } => vec![RadrootsSdkRecoveryAction::InspectLocalStores],
Self::GeoNames { kind, .. } => match kind {
RadrootsSdkGeoNamesErrorKind::Configuration => {
@@ -372,6 +358,9 @@ impl RadrootsSdkError {
}
Self::TradeAmbiguous { .. } => vec![RadrootsSdkRecoveryAction::SelectTradeRoot],
Self::PrivacyPreflight { .. } => vec![RadrootsSdkRecoveryAction::FixRequest],
+ Self::UnsupportedProfileSchema { .. } => {
+ vec![RadrootsSdkRecoveryAction::InspectLocalStores]
+ }
Self::ProductSyncUnsupported { .. } => {
vec![RadrootsSdkRecoveryAction::EnableRequiredFeature]
}
@@ -387,14 +376,13 @@ impl RadrootsSdkError {
Self::SignerAuthChallengePending { .. } => {
vec![RadrootsSdkRecoveryAction::CompleteSignerAuthentication]
}
- Self::PartialLocalMutation(error) => vec![error.recovery],
Self::ClockBeforeUnixEpoch
| Self::TimestampOutOfRange { .. }
| Self::TradeStatusLimitInvalid { .. }
| Self::InvalidTradeId { .. }
| Self::SignerProtocol { .. }
| Self::InvalidRequest { .. }
- | Self::ListingDraft { .. }
+ | Self::ListingEdit { .. }
| Self::ListingMutation { .. } => vec![RadrootsSdkRecoveryAction::FixRequest],
}
}
@@ -488,14 +476,17 @@ impl RadrootsSdkError {
| Self::Authority { message }
| Self::EventStore { message }
| Self::InvalidRequest { message }
- | Self::ListingDraft { message }
+ | Self::ListingEdit { message }
| Self::ListingMutation { message }
| Self::Outbox { message }
| Self::PrivateStore { message }
+ | Self::StudioStore { message }
| Self::Transport { message }
| Self::Projection { message } => json!({ "message": message }),
+ Self::UnsupportedProfileSchema { path, message } => {
+ json!({ "path": path.display().to_string(), "message": message })
+ }
Self::GeoNames { kind, message } => json!({ "kind": kind, "message": message }),
- Self::PartialLocalMutation(error) => json!(error),
};
json!({
"code": self.code(),
@@ -507,42 +498,6 @@ impl RadrootsSdkError {
})
}
- pub fn partial_local_mutation(error: RadrootsSdkPartialLocalMutationError) -> Self {
- Self::PartialLocalMutation(error)
- }
-
- pub fn partial_outbox_enqueue_mutation(
- event_id: impl Into<String>,
- operation_kind: impl Into<String>,
- idempotency_digest_prefix: impl Into<String>,
- ) -> Self {
- Self::PartialLocalMutation(RadrootsSdkPartialLocalMutationError {
- event_id: Some(event_id.into()),
- operation_kind: operation_kind.into(),
- idempotency_digest_prefix: Some(idempotency_digest_prefix.into()),
- stored: true,
- queued: false,
- recovery: RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey,
- failure: RadrootsSdkPartialLocalMutationFailure::OutboxEnqueue,
- })
- }
-
- pub fn partial_outbox_idempotency_conflict_mutation(
- event_id: impl Into<String>,
- operation_kind: impl Into<String>,
- idempotency_digest_prefix: impl Into<String>,
- ) -> Self {
- Self::PartialLocalMutation(RadrootsSdkPartialLocalMutationError {
- event_id: Some(event_id.into()),
- operation_kind: operation_kind.into(),
- idempotency_digest_prefix: Some(idempotency_digest_prefix.into()),
- stored: true,
- queued: false,
- recovery: RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey,
- failure: RadrootsSdkPartialLocalMutationFailure::OutboxIdempotencyConflict,
- })
- }
-
pub(crate) fn empty_transport_targets(operation: impl Into<String>) -> Self {
Self::EmptyTransportTargets {
operation: operation.into(),
@@ -700,12 +655,18 @@ impl fmt::Display for RadrootsSdkError {
Self::Authority { message } => write!(f, "sdk authority error: {message}"),
Self::EventStore { message } => write!(f, "sdk event store error: {message}"),
Self::InvalidRequest { message } => write!(f, "sdk invalid request: {message}"),
- Self::ListingDraft { message } => write!(f, "sdk listing draft error: {message}"),
+ Self::UnsupportedProfileSchema { path, message } => write!(
+ f,
+ "sdk unsupported profile schema at `{}`: {message}",
+ path.display()
+ ),
+ Self::ListingEdit { message } => write!(f, "sdk listing edit error: {message}"),
Self::ListingMutation { message } => {
write!(f, "sdk listing mutation error: {message}")
}
Self::Outbox { message } => write!(f, "sdk outbox error: {message}"),
Self::PrivateStore { message } => write!(f, "sdk private store error: {message}"),
+ Self::StudioStore { message } => write!(f, "sdk studio store error: {message}"),
Self::GeoNames { kind, message } => {
write!(f, "sdk GeoNames {kind:?} error: {message}")
}
@@ -713,20 +674,6 @@ impl fmt::Display for RadrootsSdkError {
write!(f, "sdk transport error: {message}")
}
Self::Projection { message } => write!(f, "sdk projection error: {message}"),
- Self::PartialLocalMutation(error) => write!(
- f,
- "sdk local mutation partially completed: event_id={}, operation_kind={}, idempotency_digest_prefix={}, stored={}, queued={}, failure={:?}, recovery={:?}",
- error.event_id.as_deref().unwrap_or("<unknown>"),
- error.operation_kind,
- error
- .idempotency_digest_prefix
- .as_deref()
- .unwrap_or("<none>"),
- error.stored,
- error.queued,
- error.failure,
- error.recovery
- ),
}
}
}
@@ -818,16 +765,16 @@ impl From<radroots_geocoder::GeocoderError> for RadrootsSdkError {
}
#[cfg(feature = "runtime")]
-impl From<radroots_trade::listing::RadrootsListingDraftError> for RadrootsSdkError {
- fn from(error: radroots_trade::listing::RadrootsListingDraftError) -> Self {
+impl From<radroots_trade::listing::RadrootsListingEditError> for RadrootsSdkError {
+ fn from(error: radroots_trade::listing::RadrootsListingEditError) -> Self {
match error {
- radroots_trade::listing::RadrootsListingDraftError::ActorRoleUnsatisfied {
+ radroots_trade::listing::RadrootsListingEditError::ActorRoleUnsatisfied {
required_role,
} => Self::UnauthorizedActor {
operation: "listing.prepare_publish".to_owned(),
reason: format!("missing role {required_role:?}"),
},
- error => Self::ListingDraft {
+ error => Self::ListingEdit {
message: error.to_string(),
},
}
diff --git a/crates/sdk/src/farms_runtime.rs b/crates/sdk/src/farms_runtime.rs
@@ -103,7 +103,7 @@ impl FarmEnqueuePublishRequest {
}
pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key.into());
+ self.idempotency_key = Some(idempotency_key);
self
}
@@ -1017,6 +1017,6 @@ fn farm_private_location_candidate(
}
}
-#[cfg(all(test, feature = "runtime"))]
+#[cfg(all(test, feature = "runtime", feature = "signer-adapters"))]
#[path = "../tests/unit/farms_runtime_tests.rs"]
mod tests;
diff --git a/crates/sdk/src/idempotency.rs b/crates/sdk/src/idempotency.rs
@@ -2,7 +2,6 @@ use crate::RadrootsSdkError;
use core::fmt;
use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey};
use serde::ser::SerializeStruct;
-use sha2::{Digest, Sha256};
pub const SDK_IDEMPOTENCY_KEY_MAX_LEN: usize = 256;
@@ -30,6 +29,9 @@ impl SdkIdempotencyKey {
"idempotency key must not contain control characters",
));
}
+ if !is_uuid_v7(value) {
+ return Err(invalid_request("idempotency key must be a UUIDv7"));
+ }
Ok(Self(value.to_owned()))
}
@@ -40,22 +42,6 @@ impl SdkIdempotencyKey {
pub fn into_string(self) -> String {
self.0
}
-
- pub(crate) fn derive(
- operation_kind: &'static str,
- expected_event_id: &str,
- expected_pubkey: &str,
- ) -> Self {
- let input = SdkIdempotencyDerivationInput {
- operation_kind,
- expected_event_id,
- expected_pubkey,
- };
- let bytes = serde_json::to_vec(&input).expect("idempotency derivation input serializes");
- let digest = hex::encode(Sha256::digest(bytes));
- Self::new(format!("{operation_kind}:{digest}"))
- .expect("derived idempotency key satisfies SDK validation")
- }
}
impl fmt::Debug for SdkIdempotencyKey {
@@ -79,13 +65,6 @@ impl serde::Serialize for SdkIdempotencyKey {
}
}
-#[derive(serde::Serialize)]
-struct SdkIdempotencyDerivationInput<'a> {
- operation_kind: &'static str,
- expected_event_id: &'a str,
- expected_pubkey: &'a str,
-}
-
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct SdkTradeIdempotencyRecord {
pub idempotency_key: SdkIdempotencyKey,
@@ -118,6 +97,21 @@ fn invalid_request(message: impl Into<String>) -> RadrootsSdkError {
}
}
+fn is_uuid_v7(value: &str) -> bool {
+ let bytes = value.as_bytes();
+ bytes.len() == 36
+ && bytes[8] == b'-'
+ && bytes[13] == b'-'
+ && bytes[18] == b'-'
+ && bytes[23] == b'-'
+ && bytes[14] == b'7'
+ && matches!(bytes[19], b'8' | b'9' | b'a' | b'b')
+ && bytes
+ .iter()
+ .enumerate()
+ .all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit())
+}
+
#[cfg(test)]
#[path = "../tests/unit/idempotency_tests.rs"]
mod tests;
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -14,13 +14,14 @@ mod actor_json;
))]
pub mod adapters;
#[cfg(feature = "runtime")]
-mod dvm_runtime;
-#[cfg(feature = "runtime")]
mod error;
#[cfg(feature = "runtime")]
mod farm;
#[cfg(feature = "runtime")]
mod farms_runtime;
+#[cfg(test)]
+#[path = "../tests/support/fixture_signer.rs"]
+pub(crate) mod fixture_signer;
#[cfg(feature = "runtime")]
mod geonames;
#[cfg(feature = "runtime")]
@@ -45,9 +46,14 @@ mod private_store;
mod product_clients;
#[cfg(feature = "runtime")]
mod runtime;
+#[cfg(test)]
+#[path = "../tests/support/serializer_failure.rs"]
+pub(crate) mod serializer_failure;
#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
mod signer_provider;
#[cfg(feature = "runtime")]
+mod studio_store;
+#[cfg(feature = "runtime")]
mod sync_runtime;
#[cfg(feature = "runtime")]
mod trade_storage;
@@ -59,18 +65,8 @@ mod workflow_runtime;
pub use radroots_runtime_contract_v1 as runtime_contract_v1;
#[cfg(feature = "runtime")]
-pub use crate::dvm_runtime::{
- DVM_TRADE_TRANSITION_PROOF_REQUEST_CONTRACT_ID,
- DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND, DvmProofMode,
- DvmTradeTransitionProofEnqueueRequest, DvmTradeTransitionProofPlan,
- DvmTradeTransitionProofPrepareRequest, DvmTradeTransitionProofReceipt,
- DvmTradeTransitionProofRequestPayload, DvmValidationReceiptIngestReceipt,
- DvmValidationReceiptIngestRequest,
-};
-#[cfg(feature = "runtime")]
pub use crate::error::{
RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind,
- RadrootsSdkPartialLocalMutationError, RadrootsSdkPartialLocalMutationFailure,
RadrootsSdkRecoveryAction,
};
#[cfg(feature = "runtime")]
@@ -166,20 +162,16 @@ pub use crate::orders_runtime::{
TradeValidationReceiptNostrEvidenceReceipt, TradeValidationReceiptNostrRelayOutcomeKind,
TradeValidationReceiptNostrRelayOutcomeReceipt,
TradeValidationReceiptNostrRelayTransportOutcomeKind, TradeValidationReceiptTags,
- TradeValidationReceiptVerifyRequest, TradeValidationReceiptWorkerEvidence,
- TradeValidationReceiptWorkerEvidenceSelection, TradeValidationTrustDecision,
+ TradeValidationReceiptVerifyRequest, TradeValidationTrustDecision,
};
#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
pub use crate::orders_runtime::{
- TRADE_CANCELLATION_OPERATION_KIND, TRADE_DECISION_OPERATION_KIND,
- TRADE_REVISION_DECISION_OPERATION_KIND, TRADE_REVISION_PROPOSAL_OPERATION_KIND,
- TRADE_SUBMIT_OPERATION_KIND, TradeAcceptRequest, TradeCancelRequest, TradeCancellationPlan,
- TradeCancellationReceipt, TradeDecisionPlan, TradeDecisionReceipt, TradeDeclineRequest,
- TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest, TradeRevisionDecisionPlan,
- TradeRevisionDecisionReceipt, TradeRevisionDecisionRequest, TradeRevisionProposalPlan,
- TradeRevisionProposalReceipt, TradeRevisionProposalRequest, TradeSubmitPlan,
- TradeSubmitReceipt, TradeWorkflowEnqueueReceipt, TradeWorkflowIdempotencyReceipt,
- TradeWorkflowKind, TradeWorkflowPlan, TradeWorkflowRetryAdvice,
+ 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::{
@@ -190,18 +182,19 @@ pub use crate::privacy::{
pub use crate::product_clients::TradeBuyerClient;
#[cfg(feature = "runtime")]
pub use crate::product_clients::{
- DvmClient, FarmsClient, GeoNamesClient, ListingsClient, MarketClient, SyncClient,
- TradeResyncClient, TradeSellerClient, TradeValidationReceiptsClient, TradesClient,
+ FarmsClient, GeoNamesClient, ListingsClient, MarketClient, SyncClient, TradeResyncClient,
+ TradeSellerClient, TradeValidationReceiptsClient, TradesClient,
};
#[cfg(feature = "runtime")]
pub use crate::runtime::{
- 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, StorageCheckpointReceipt,
- StorageCheckpointRequest, StorageStatusReceipt, StorageStatusRequest,
+ BackupReceipt, BackupRequest, IntegrityReceipt, IntegrityRequest, QuarantineResetReceipt,
+ QuarantineResetRequest, 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,
};
#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
pub use crate::signer_provider::{
@@ -241,8 +234,6 @@ pub use crate::transport::{
TargetSet, TransportProfile, TransportReceipt,
};
#[cfg(feature = "runtime")]
-pub use radroots_trade::dvm::RadrootsTradeInventoryBinWitnessDto;
-#[cfg(feature = "runtime")]
pub use radroots_trade::validation_receipt::{
RadrootsTradeCommitmentConfidence, RadrootsTradeValidationAuthority,
RadrootsTradeValidationTrustPolicy, RadrootsTradeValidationTrustState,
diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs
@@ -18,8 +18,8 @@ use radroots_event::{
use radroots_outbox::RadrootsOutboxEnqueueStatus;
#[cfg(feature = "runtime")]
use radroots_trade::listing::{
- RadrootsCanonicalListingDraft, RadrootsListingDraftDocumentV1, RadrootsListingMutation,
- build_listing_mutation_draft, canonicalize_listing_draft,
+ RadrootsCanonicalListingEdit, RadrootsListingEditDocumentV1, RadrootsListingMutation,
+ build_listing_mutation_draft, canonicalize_listing_edit,
};
#[cfg(feature = "runtime")]
pub const LISTING_PUBLISH_OPERATION_KIND: &str = "listing.publish.v1";
@@ -30,7 +30,7 @@ pub const LISTING_PUBLISH_OPERATION_KIND: &str = "listing.publish.v1";
pub struct ListingPreparePublishRequest {
#[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
pub actor: RadrootsActorContext,
- pub document: RadrootsListingDraftDocumentV1,
+ pub document: RadrootsListingEditDocumentV1,
pub created_at: Option<RadrootsSdkTimestamp>,
}
@@ -39,14 +39,14 @@ impl ListingPreparePublishRequest {
pub fn new(actor: RadrootsActorContext, listing: RadrootsListing) -> Self {
Self {
actor,
- document: RadrootsListingDraftDocumentV1::new(listing),
+ document: RadrootsListingEditDocumentV1::new(listing),
created_at: None,
}
}
pub fn from_document(
actor: RadrootsActorContext,
- document: RadrootsListingDraftDocumentV1,
+ document: RadrootsListingEditDocumentV1,
) -> Self {
Self {
actor,
@@ -67,7 +67,7 @@ impl ListingPreparePublishRequest {
pub struct ListingEnqueuePublishRequest {
#[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
pub actor: RadrootsActorContext,
- pub document: RadrootsListingDraftDocumentV1,
+ pub document: RadrootsListingEditDocumentV1,
pub target_policy: TargetPolicy,
pub idempotency_key: Option<SdkIdempotencyKey>,
pub created_at: Option<RadrootsSdkTimestamp>,
@@ -82,14 +82,14 @@ impl ListingEnqueuePublishRequest {
) -> Self {
Self::from_document(
actor,
- RadrootsListingDraftDocumentV1::new(listing),
+ RadrootsListingEditDocumentV1::new(listing),
target_policy,
)
}
pub fn from_document(
actor: RadrootsActorContext,
- document: RadrootsListingDraftDocumentV1,
+ document: RadrootsListingEditDocumentV1,
target_policy: TargetPolicy,
) -> Self {
Self {
@@ -115,7 +115,7 @@ impl ListingEnqueuePublishRequest {
}
pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key.into());
+ self.idempotency_key = Some(idempotency_key);
self
}
@@ -137,7 +137,6 @@ impl ListingEnqueuePublishRequest {
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct ListingPublishPlan {
pub public_listing_addr: RadrootsListingAddress,
- pub draft_listing_addr: RadrootsListingAddress,
pub expected_event_id: RadrootsEventId,
pub frozen_draft: RadrootsEventDraft,
pub created_at: RadrootsSdkTimestamp,
@@ -166,7 +165,6 @@ impl From<RadrootsOutboxEnqueueStatus> for SdkMutationState {
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct ListingEnqueueReceipt {
pub public_listing_addr: RadrootsListingAddress,
- pub draft_listing_addr: RadrootsListingAddress,
pub expected_event_id: RadrootsEventId,
pub signed_event_id: RadrootsEventId,
pub local_event_seq: i64,
@@ -301,7 +299,6 @@ fn listing_enqueue_receipt(
) -> ListingEnqueueReceipt {
ListingEnqueueReceipt {
public_listing_addr: plan.public_listing_addr,
- draft_listing_addr: plan.draft_listing_addr,
expected_event_id: plan.expected_event_id,
signed_event_id: enqueue.signed_event_id,
local_event_seq: enqueue.local_event_seq,
@@ -313,36 +310,34 @@ fn listing_enqueue_receipt(
}
#[cfg(feature = "runtime")]
-fn canonical_listing_draft(
+fn canonical_listing_edit(
actor: &RadrootsActorContext,
- document: RadrootsListingDraftDocumentV1,
-) -> Result<RadrootsCanonicalListingDraft, RadrootsSdkError> {
- canonicalize_listing_draft(actor, document).map_err(Into::into)
+ document: RadrootsListingEditDocumentV1,
+) -> Result<RadrootsCanonicalListingEdit, RadrootsSdkError> {
+ canonicalize_listing_edit(actor, document).map_err(Into::into)
}
#[cfg(feature = "runtime")]
fn listing_publish_plan(
actor: &RadrootsActorContext,
- document: RadrootsListingDraftDocumentV1,
+ document: RadrootsListingEditDocumentV1,
created_at: RadrootsSdkTimestamp,
) -> Result<ListingPublishPlan, RadrootsSdkError> {
let created_at_nostr = created_at.try_into_nostr_created_at()?;
- let canonical = canonical_listing_draft(actor, document)?;
+ let canonical = canonical_listing_edit(actor, document)?;
let public_listing_addr = canonical.public_listing_addr().clone();
- let draft_listing_addr = canonical.draft_listing_addr().clone();
let mutation = RadrootsListingMutation::publish(canonical);
let frozen_draft = build_listing_mutation_draft(&mutation, u64::from(created_at_nostr))?;
let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id_str())
- .expect("frozen listing draft produces a valid event id");
+ .expect("frozen listing edit produces a valid event id");
Ok(ListingPublishPlan {
public_listing_addr,
- draft_listing_addr,
expected_event_id,
frozen_draft,
created_at,
})
}
-#[cfg(all(test, feature = "runtime"))]
+#[cfg(all(test, feature = "runtime", feature = "signer-adapters"))]
#[path = "../tests/unit/listings_runtime_tests.rs"]
mod tests;
diff --git a/crates/sdk/src/order.rs b/crates/sdk/src/order.rs
@@ -1,80 +1,54 @@
pub use radroots_event::order::*;
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
pub use radroots_event_codec::error::EventEncodeError;
pub use radroots_event_codec::order::RadrootsOrderEnvelopeParseError;
use radroots_event::RadrootsEventEnvelope;
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
use radroots_event::wire::RadrootsNip01EventWireParts;
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
use radroots_event::{RadrootsEventPtr, ids::RadrootsEventId, tags::TAG_E};
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Debug, Clone)]
pub struct RadrootsOrderRequestDraft {
parts: RadrootsNip01EventWireParts,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Debug, Clone)]
pub struct RadrootsOrderDecisionDraft {
parts: RadrootsNip01EventWireParts,
}
-#[cfg(any(feature = "signer-adapters", test))]
-#[derive(Debug, Clone)]
-pub struct RadrootsOrderRevisionProposalDraft {
- parts: RadrootsNip01EventWireParts,
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-#[derive(Debug, Clone)]
-pub struct RadrootsOrderRevisionDecisionDraft {
- parts: RadrootsNip01EventWireParts,
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Debug, Clone)]
pub struct RadrootsOrderCancellationDraft {
parts: RadrootsNip01EventWireParts,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
impl RadrootsOrderRequestDraft {
pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
self.parts
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
impl RadrootsOrderDecisionDraft {
pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
self.parts
}
}
-#[cfg(any(feature = "signer-adapters", test))]
-impl RadrootsOrderRevisionProposalDraft {
- pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
- self.parts
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-impl RadrootsOrderRevisionDecisionDraft {
- pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
- self.parts
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
impl RadrootsOrderCancellationDraft {
pub fn into_wire_parts(self) -> RadrootsNip01EventWireParts {
self.parts
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn with_contract_root_event_tag(
mut parts: RadrootsNip01EventWireParts,
root_event_id: &RadrootsEventId,
@@ -85,7 +59,7 @@ fn with_contract_root_event_tag(
parts
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
pub fn build_order_request_draft(
listing_event: &RadrootsEventPtr,
payload: &RadrootsOrderRequest,
@@ -95,7 +69,7 @@ pub fn build_order_request_draft(
})
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
pub fn build_order_decision_draft(
root_event_id: &RadrootsEventId,
prev_event_id: &RadrootsEventId,
@@ -113,43 +87,7 @@ pub fn build_order_decision_draft(
})
}
-#[cfg(any(feature = "signer-adapters", test))]
-pub fn build_order_revision_proposal_draft(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderRevisionProposal,
-) -> Result<RadrootsOrderRevisionProposalDraft, EventEncodeError> {
- Ok(RadrootsOrderRevisionProposalDraft {
- parts: with_contract_root_event_tag(
- radroots_event_codec::order::order_revision_proposal_event_build(
- root_event_id,
- prev_event_id,
- payload,
- )?,
- root_event_id,
- ),
- })
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-pub fn build_order_revision_decision_draft(
- root_event_id: &RadrootsEventId,
- prev_event_id: &RadrootsEventId,
- payload: &RadrootsOrderRevisionDecision,
-) -> Result<RadrootsOrderRevisionDecisionDraft, EventEncodeError> {
- Ok(RadrootsOrderRevisionDecisionDraft {
- parts: with_contract_root_event_tag(
- radroots_event_codec::order::order_revision_decision_event_build(
- root_event_id,
- prev_event_id,
- payload,
- )?,
- root_event_id,
- ),
- })
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
pub fn build_order_cancellation_draft(
root_event_id: &RadrootsEventId,
prev_event_id: &RadrootsEventId,
diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs
@@ -8,9 +8,7 @@ use crate::sync_runtime::SyncProjectionRefreshReceipt;
use crate::sync_runtime::{SyncProjectionRefreshRequest, refresh_product_projections_for_sdk};
#[cfg(feature = "signer-adapters")]
use crate::workflow_runtime::enqueue_configured_signed_workflow;
-#[cfg(all(feature = "runtime", test))]
-use crate::{NostrRelayUrlPolicy, workflow_runtime::enqueue_signed_workflow};
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
use crate::{
PrivacyPreflightConfirmation, PrivacyPreflightReceipt, ProductSensitivityField, PublishMode,
PushOutboxReceipt, PushOutboxRequest, RadrootsSdkRecoveryAction, SatisfactionPolicy,
@@ -23,9 +21,7 @@ use crate::{
};
#[cfg(feature = "runtime")]
use radroots_authority::RadrootsActorContext;
-#[cfg(all(feature = "runtime", test))]
-use radroots_authority::RadrootsEventSigner;
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
use radroots_event::wire::RadrootsNip01EventWireParts;
#[cfg(feature = "runtime")]
use radroots_event::{
@@ -35,20 +31,17 @@ use radroots_event::{
ids::RadrootsEventId,
kinds::{
KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST,
- KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL,
- KIND_TRADE_TRANSITION_PROOF_RESULT, KIND_TRADE_VALIDATION_RECEIPT,
+ KIND_TRADE_VALIDATION_RECEIPT,
},
tags::TAG_P,
};
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
use radroots_event::{
RadrootsEventPtr,
draft::RadrootsEventDraft,
- ids::RadrootsOrderRevisionId,
order::{
RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderRequest,
- RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal,
},
};
#[cfg(any(feature = "runtime", feature = "signer-adapters", test))]
@@ -64,7 +57,6 @@ use radroots_event::{
#[cfg(feature = "runtime")]
use radroots_event_codec::order::{
order_cancellation_from_event, order_decision_from_event, order_request_from_event,
- order_revision_decision_from_event, order_revision_proposal_from_event,
};
#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
use radroots_event_store::RadrootsStoredEventTag;
@@ -76,12 +68,10 @@ use radroots_nostr::prelude::{
radroots_nostr_filter_tag,
};
#[cfg(feature = "runtime")]
-use radroots_trade::dvm::RADROOTS_DVM_TAG_VALIDATION_RECEIPT;
-#[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(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
use radroots_trade::order::{
RadrootsOrderCanonicalizationError, RadrootsOrderProjectionQueryResult,
canonicalize_order_decision_for_signer, canonicalize_order_request_for_signer,
@@ -113,14 +103,11 @@ use radroots_transport_nostr::{
RadrootsRelayFetchRequest, RadrootsRelayOutcomeKind, fetch_and_ingest_relay_events,
};
#[cfg(feature = "runtime")]
-use serde::Deserialize;
-#[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::{
- collections::{BTreeMap, BTreeSet},
- time::Duration,
-};
+use std::time::Duration;
#[cfg(feature = "runtime")]
use tokio::{
sync::{mpsc, oneshot},
@@ -140,48 +127,36 @@ pub const TRADE_STATUS_WATCH_MAX_CAPACITY: usize = 128;
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(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
pub const TRADE_SUBMIT_OPERATION_KIND: &str = "trade.submit.v1";
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
pub const TRADE_DECISION_OPERATION_KIND: &str = "trade.decision.v1";
-#[cfg(any(feature = "signer-adapters", test))]
-pub const TRADE_REVISION_PROPOSAL_OPERATION_KIND: &str = "trade.revision.proposal.v1";
-#[cfg(any(feature = "signer-adapters", test))]
-pub const TRADE_REVISION_DECISION_OPERATION_KIND: &str = "trade.revision.decision.v1";
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
pub const TRADE_CANCELLATION_OPERATION_KIND: &str = "trade.cancellation.v1";
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
const TRADE_SUBMIT_CONTRACT_ID: &str = "radroots.order.request.v1";
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
const TRADE_DECISION_CONTRACT_ID: &str = "radroots.order.decision.v1";
-#[cfg(any(feature = "signer-adapters", test))]
-const TRADE_REVISION_PROPOSAL_CONTRACT_ID: &str = "radroots.order.revision_proposal.v1";
-#[cfg(any(feature = "signer-adapters", test))]
-const TRADE_REVISION_DECISION_CONTRACT_ID: &str = "radroots.order.revision_decision.v1";
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
const TRADE_CANCELLATION_CONTRACT_ID: &str = "radroots.order.cancellation.v1";
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TradeWorkflowKind {
Submit,
Decision,
- RevisionProposal,
- RevisionDecision,
Cancellation,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[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::RevisionProposal => TRADE_REVISION_PROPOSAL_OPERATION_KIND,
- Self::RevisionDecision => TRADE_REVISION_DECISION_OPERATION_KIND,
Self::Cancellation => TRADE_CANCELLATION_OPERATION_KIND,
}
}
@@ -190,14 +165,12 @@ impl TradeWorkflowKind {
match self {
Self::Submit => TRADE_SUBMIT_CONTRACT_ID,
Self::Decision => TRADE_DECISION_CONTRACT_ID,
- Self::RevisionProposal => TRADE_REVISION_PROPOSAL_CONTRACT_ID,
- Self::RevisionDecision => TRADE_REVISION_DECISION_CONTRACT_ID,
Self::Cancellation => TRADE_CANCELLATION_CONTRACT_ID,
}
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeWorkflowPlan {
pub kind: TradeWorkflowKind,
@@ -207,7 +180,7 @@ pub struct TradeWorkflowPlan {
pub created_at: RadrootsSdkTimestamp,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeWorkflowEnqueueReceipt {
pub kind: TradeWorkflowKind,
@@ -223,7 +196,7 @@ pub struct TradeWorkflowEnqueueReceipt {
pub retry: TradeWorkflowRetryAdvice,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeWorkflowIdempotencyReceipt {
pub digest_prefix: Option<String>,
@@ -231,7 +204,7 @@ pub struct TradeWorkflowIdempotencyReceipt {
pub safe_to_retry_with_same_idempotency_key: bool,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeWorkflowRetryAdvice {
pub retryable_after_error: bool,
@@ -239,7 +212,7 @@ pub struct TradeWorkflowRetryAdvice {
pub recovery_actions: Vec<RadrootsSdkRecoveryAction>,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
pub struct TradeSubmitPrepareRequest {
@@ -250,100 +223,7 @@ pub struct TradeSubmitPrepareRequest {
pub created_at: Option<RadrootsSdkTimestamp>,
}
-#[cfg(any(feature = "signer-adapters", test))]
-#[cfg(test)]
-impl TradeSubmitPrepareRequest {
- pub fn new(
- actor: RadrootsActorContext,
- listing_event: RadrootsEventPtr,
- order: RadrootsOrderRequest,
- ) -> Self {
- Self {
- actor,
- listing_event,
- order,
- created_at: None,
- }
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[cfg(test)]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeSubmitEnqueueRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub listing_event: RadrootsEventPtr,
- pub order: RadrootsOrderRequest,
- pub target_policy: TargetPolicy,
- pub publish_mode: PublishMode,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-#[cfg(test)]
-impl TradeSubmitEnqueueRequest {
- 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,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key: None,
- created_at: None,
- }
- }
-
- pub fn try_with_nostr_targets<I, S>(
- mut self,
- target_policy: I,
- policy: NostrRelayUrlPolicy,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.target_policy = TargetPolicy::try_nostr_relays(target_policy, policy)?;
- Ok(self)
- }
-
- pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key.into());
- 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(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeSubmitPlan {
pub workflow: TradeWorkflowPlan,
@@ -357,7 +237,7 @@ pub struct TradeSubmitPlan {
pub created_at: RadrootsSdkTimestamp,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeSubmitReceipt {
pub workflow: TradeWorkflowEnqueueReceipt,
@@ -445,7 +325,7 @@ pub struct TradeEvidenceIngestReceipt {
pub inserted: bool,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
pub struct TradeDecisionPrepareRequest {
@@ -456,100 +336,7 @@ pub struct TradeDecisionPrepareRequest {
pub created_at: Option<RadrootsSdkTimestamp>,
}
-#[cfg(any(feature = "signer-adapters", test))]
-#[cfg(test)]
-impl TradeDecisionPrepareRequest {
- pub fn new(
- actor: RadrootsActorContext,
- request_event: RadrootsEventPtr,
- decision: RadrootsOrderDecision,
- ) -> Self {
- Self {
- actor,
- request_event,
- decision,
- created_at: None,
- }
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[cfg(test)]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeDecisionEnqueueRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub request_event: RadrootsEventPtr,
- pub decision: RadrootsOrderDecision,
- pub target_policy: TargetPolicy,
- pub publish_mode: PublishMode,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-#[cfg(test)]
-impl TradeDecisionEnqueueRequest {
- pub fn new(
- actor: RadrootsActorContext,
- request_event: RadrootsEventPtr,
- decision: RadrootsOrderDecision,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- ) -> Self {
- Self {
- actor,
- request_event,
- decision,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key: None,
- created_at: None,
- }
- }
-
- pub fn try_with_nostr_targets<I, S>(
- mut self,
- target_policy: I,
- policy: NostrRelayUrlPolicy,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.target_policy = TargetPolicy::try_nostr_relays(target_policy, policy)?;
- Ok(self)
- }
-
- pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key.into());
- 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(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeDecisionPlan {
pub workflow: TradeWorkflowPlan,
@@ -563,7 +350,7 @@ pub struct TradeDecisionPlan {
pub created_at: RadrootsSdkTimestamp,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeDecisionReceipt {
pub workflow: TradeWorkflowEnqueueReceipt,
@@ -582,99 +369,156 @@ pub struct TradeDecisionReceipt {
pub idempotency_digest_prefix: Option<String>,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
-pub struct TradeRevisionProposalPrepareRequest {
+pub struct TradeCancellationPrepareRequest {
#[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
pub actor: RadrootsActorContext,
pub root_event: RadrootsEventPtr,
pub previous_event: RadrootsEventPtr,
- pub proposal: RadrootsOrderRevisionProposal,
+ pub cancellation: RadrootsOrderCancellation,
pub created_at: Option<RadrootsSdkTimestamp>,
}
-#[cfg(any(feature = "signer-adapters", test))]
-#[cfg(test)]
-impl TradeRevisionProposalPrepareRequest {
- pub fn new(
- actor: RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- proposal: RadrootsOrderRevisionProposal,
- ) -> Self {
- Self {
- actor,
- root_event,
- previous_event,
- proposal,
- created_at: None,
- }
- }
+#[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,
+}
- 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, 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 = "runtime")]
-#[cfg(test)]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeRevisionProposalEnqueueRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub root_event: RadrootsEventPtr,
- pub previous_event: RadrootsEventPtr,
- pub proposal: RadrootsOrderRevisionProposal,
- pub target_policy: TargetPolicy,
- pub publish_mode: PublishMode,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
+#[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 = "runtime")]
-#[cfg(test)]
-impl TradeRevisionProposalEnqueueRequest {
- pub fn new(
+#[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,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- proposal: RadrootsOrderRevisionProposal,
+ listing_event: RadrootsEventPtr,
+ order: RadrootsOrderRequest,
target_policy: TargetPolicy,
publish_mode: PublishMode,
satisfaction_policy: SatisfactionPolicy,
) -> Self {
Self {
actor,
- root_event,
- previous_event,
- proposal,
+ 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 try_with_nostr_targets<I, S>(
- mut self,
- target_policy: I,
- policy: NostrRelayUrlPolicy,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.target_policy = TargetPolicy::try_nostr_relays(target_policy, policy)?;
- Ok(self)
+ 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_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key.into());
+ pub fn with_privacy_confirmation(
+ mut self,
+ privacy_confirmation: PrivacyPreflightConfirmation,
+ ) -> Self {
+ self.privacy_confirmation = privacy_confirmation;
self
}
@@ -692,134 +536,53 @@ impl TradeRevisionProposalEnqueueRequest {
}
}
-#[cfg(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeRevisionProposalPlan {
- 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(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeRevisionProposalReceipt {
- 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(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeRevisionDecisionPrepareRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub root_event: RadrootsEventPtr,
- pub previous_event: RadrootsEventPtr,
- pub decision: RadrootsOrderRevisionDecision,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-#[cfg(test)]
-impl TradeRevisionDecisionPrepareRequest {
- pub fn new(
- actor: RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- decision: RadrootsOrderRevisionDecision,
- ) -> Self {
- Self {
- actor,
- root_event,
- previous_event,
- decision,
- created_at: None,
- }
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[cfg(test)]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
-pub struct TradeRevisionDecisionEnqueueRequest {
+pub struct TradeAcceptRequest {
#[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
pub actor: RadrootsActorContext,
- pub root_event: RadrootsEventPtr,
- pub previous_event: RadrootsEventPtr,
- pub decision: RadrootsOrderRevisionDecision,
+ 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 = "runtime")]
-#[cfg(test)]
-impl TradeRevisionDecisionEnqueueRequest {
+#[cfg(feature = "signer-adapters")]
+impl TradeAcceptRequest {
pub fn new(
actor: RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- decision: RadrootsOrderRevisionDecision,
+ locator: RadrootsTradeLocator,
+ inventory_commitments: Vec<RadrootsOrderInventoryCommitment>,
target_policy: TargetPolicy,
publish_mode: PublishMode,
satisfaction_policy: SatisfactionPolicy,
+ evidence_mode: TradeEvidenceMode,
) -> Self {
Self {
actor,
- root_event,
- previous_event,
- decision,
+ locator,
+ inventory_commitments,
target_policy,
publish_mode,
satisfaction_policy,
+ evidence_mode,
+ privacy_confirmation: PrivacyPreflightConfirmation::new(),
idempotency_key: None,
created_at: None,
}
}
- pub fn try_with_nostr_targets<I, S>(
+ pub fn with_privacy_confirmation(
mut self,
- target_policy: I,
- policy: NostrRelayUrlPolicy,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.target_policy = TargetPolicy::try_nostr_relays(target_policy, policy)?;
- Ok(self)
- }
-
- pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key.into());
+ privacy_confirmation: PrivacyPreflightConfirmation,
+ ) -> Self {
+ self.privacy_confirmation = privacy_confirmation;
self
}
@@ -837,134 +600,53 @@ impl TradeRevisionDecisionEnqueueRequest {
}
}
-#[cfg(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeRevisionDecisionPlan {
- 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(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeRevisionDecisionReceipt {
- 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(any(feature = "signer-adapters", test))]
-#[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(any(feature = "signer-adapters", test))]
-#[cfg(test)]
-impl TradeCancellationPrepareRequest {
- pub fn new(
- actor: RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- cancellation: RadrootsOrderCancellation,
- ) -> Self {
- Self {
- actor,
- root_event,
- previous_event,
- cancellation,
- created_at: None,
- }
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[cfg(test)]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
-pub struct TradeCancellationEnqueueRequest {
+pub struct TradeDeclineRequest {
#[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
pub actor: RadrootsActorContext,
- pub root_event: RadrootsEventPtr,
- pub previous_event: RadrootsEventPtr,
- pub cancellation: RadrootsOrderCancellation,
+ 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 = "runtime")]
-#[cfg(test)]
-impl TradeCancellationEnqueueRequest {
+#[cfg(feature = "signer-adapters")]
+impl TradeDeclineRequest {
pub fn new(
actor: RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- cancellation: RadrootsOrderCancellation,
+ locator: RadrootsTradeLocator,
+ reason: impl Into<String>,
target_policy: TargetPolicy,
publish_mode: PublishMode,
satisfaction_policy: SatisfactionPolicy,
+ evidence_mode: TradeEvidenceMode,
) -> Self {
Self {
actor,
- root_event,
- previous_event,
- cancellation,
+ locator,
+ reason: reason.into(),
target_policy,
publish_mode,
satisfaction_policy,
+ evidence_mode,
+ privacy_confirmation: PrivacyPreflightConfirmation::new(),
idempotency_key: None,
created_at: None,
}
}
- pub fn try_with_nostr_targets<I, S>(
+ pub fn with_privacy_confirmation(
mut self,
- target_policy: I,
- policy: NostrRelayUrlPolicy,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.target_policy = TargetPolicy::try_nostr_relays(target_policy, policy)?;
- Ok(self)
- }
-
- pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key.into());
+ privacy_confirmation: PrivacyPreflightConfirmation,
+ ) -> Self {
+ self.privacy_confirmation = privacy_confirmation;
self
}
@@ -982,143 +664,48 @@ impl TradeCancellationEnqueueRequest {
}
}
-#[cfg(any(feature = "signer-adapters", test))]
-#[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(any(feature = "signer-adapters", test))]
-#[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(any(feature = "signer-adapters", test))]
-#[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(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum TradeEvidenceMode {
- LocalOnly,
- ResyncBeforeMutation,
- RequireExplicitEvidence {
- evidence: Vec<TradeEvidenceIngestRequest>,
- },
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-impl TradeEvidenceMode {
- pub fn require_explicit_evidence(
- evidence: impl IntoIterator<Item = TradeEvidenceIngestRequest>,
- ) -> Self {
- Self::RequireExplicitEvidence {
- evidence: evidence.into_iter().collect(),
- }
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
-pub struct TradeProposeRequest {
+pub struct TradeCancelRequest {
#[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 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(any(feature = "signer-adapters", test))]
-impl TradeProposeRequest {
+#[cfg(feature = "signer-adapters")]
+impl TradeCancelRequest {
pub fn new(
actor: RadrootsActorContext,
- listing_event: RadrootsEventPtr,
- order_id: RadrootsOrderId,
- listing_addr: RadrootsListingAddress,
- seller_pubkey: RadrootsPublicKey,
- items: Vec<RadrootsOrderItem>,
- economics: RadrootsOrderEconomics,
+ locator: RadrootsTradeLocator,
+ reason: impl Into<String>,
target_policy: TargetPolicy,
publish_mode: PublishMode,
satisfaction_policy: SatisfactionPolicy,
+ evidence_mode: TradeEvidenceMode,
) -> Self {
Self {
actor,
- listing_event,
- order_id,
- listing_addr,
- seller_pubkey,
- items,
- economics,
- public_note: None,
+ 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_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,
@@ -1141,423 +728,91 @@ impl TradeProposeRequest {
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "runtime")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
-pub struct TradeAcceptRequest {
+pub struct TradeSellerInboxRequest {
#[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>,
+ pub limit: u32,
}
-#[cfg(any(feature = "signer-adapters", test))]
-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 {
+#[cfg(feature = "runtime")]
+impl TradeSellerInboxRequest {
+ pub fn new(actor: RadrootsActorContext) -> Self {
Self {
actor,
- locator,
- inventory_commitments,
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation: PrivacyPreflightConfirmation::new(),
- idempotency_key: None,
- created_at: None,
+ limit: TRADE_STATUS_DEFAULT_LIMIT,
}
}
- pub fn with_privacy_confirmation(
- mut self,
- privacy_confirmation: PrivacyPreflightConfirmation,
- ) -> Self {
- self.privacy_confirmation = privacy_confirmation;
+ pub fn with_limit(mut self, limit: u32) -> Self {
+ self.limit = limit;
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, PartialEq, Eq, serde::Serialize)]
+pub struct TradeSellerInboxReceipt {
+ pub seller_pubkey: RadrootsPublicKey,
+ pub statuses: Vec<TradeStatusReceipt>,
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "runtime")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
-pub struct TradeDeclineRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
+pub struct TradeResyncRequest {
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>,
+ pub limit: u32,
+ pub validation_trust_policy: RadrootsTradeValidationTrustPolicy,
}
-#[cfg(any(feature = "signer-adapters", test))]
-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 {
+#[cfg(feature = "runtime")]
+impl TradeResyncRequest {
+ pub fn new(locator: RadrootsTradeLocator) -> 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,
+ limit: TRADE_STATUS_DEFAULT_LIMIT,
+ validation_trust_policy: RadrootsTradeValidationTrustPolicy::production(),
}
}
- pub fn with_privacy_confirmation(
+ pub fn with_limit(mut self, limit: u32) -> Self {
+ self.limit = limit;
+ self
+ }
+
+ pub fn with_validation_trust_policy(
mut self,
- privacy_confirmation: PrivacyPreflightConfirmation,
+ policy: RadrootsTradeValidationTrustPolicy,
) -> Self {
- self.privacy_confirmation = privacy_confirmation;
+ self.validation_trust_policy = policy;
self
}
- pub fn try_with_idempotency_key(
+ pub fn try_with_trusted_rhi_pubkeys<I, S>(
mut self,
- idempotency_key: impl AsRef<str>,
- ) -> Result<Self, RadrootsSdkError> {
- self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?);
+ pubkeys: I,
+ ) -> Result<Self, RadrootsSdkError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ {
+ self.validation_trust_policy.trusted_rhi_pubkeys = parse_worker_pubkeys(pubkeys)?;
Ok(self)
}
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-#[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(any(feature = "signer-adapters", test))]
-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(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeRevisionProposalRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub locator: RadrootsTradeLocator,
- pub revision_id: RadrootsOrderRevisionId,
- pub items: Vec<RadrootsOrderItem>,
- pub economics: RadrootsOrderEconomics,
- 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(any(feature = "signer-adapters", test))]
-impl TradeRevisionProposalRequest {
- pub fn new(
- actor: RadrootsActorContext,
- locator: RadrootsTradeLocator,
- revision_id: RadrootsOrderRevisionId,
- items: Vec<RadrootsOrderItem>,
- economics: RadrootsOrderEconomics,
- reason: impl Into<String>,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- evidence_mode: TradeEvidenceMode,
- ) -> Self {
- Self {
- actor,
- locator,
- revision_id,
- items,
- economics,
- 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(any(feature = "signer-adapters", test))]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct TradeRevisionDecisionRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: RadrootsActorContext,
- pub locator: RadrootsTradeLocator,
- pub revision_id: RadrootsOrderRevisionId,
- pub decision: RadrootsOrderRevisionOutcome,
- 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(any(feature = "signer-adapters", test))]
-impl TradeRevisionDecisionRequest {
- pub fn new(
- actor: RadrootsActorContext,
- locator: RadrootsTradeLocator,
- revision_id: RadrootsOrderRevisionId,
- decision: RadrootsOrderRevisionOutcome,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- evidence_mode: TradeEvidenceMode,
- ) -> Self {
- Self {
- actor,
- locator,
- revision_id,
- decision,
- 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
- }
-
- pub fn try_with_trusted_rhi_pubkeys<I, S>(
- mut self,
- pubkeys: I,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.validation_trust_policy.trusted_rhi_pubkeys = parse_worker_pubkeys(pubkeys)?;
- Ok(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 = "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(())
}
}
@@ -1612,7 +867,6 @@ pub enum TradeEvidenceQueryBranchKind {
LifecycleChain,
ValidationReceipts,
ListingSnapshot,
- WorkerResults,
RejectedEvidence,
}
@@ -1751,7 +1005,6 @@ impl TradeResyncNostrRelayTransportOutcomeKind {
pub struct TradeValidationReceiptListRequest {
pub order_id: RadrootsOrderId,
pub limit: u32,
- pub trusted_worker_pubkeys: Vec<RadrootsPublicKey>,
}
#[cfg(feature = "runtime")]
@@ -1760,7 +1013,6 @@ impl TradeValidationReceiptListRequest {
Self {
order_id,
limit: TRADE_STATUS_DEFAULT_LIMIT,
- trusted_worker_pubkeys: Vec::new(),
}
}
@@ -1775,18 +1027,6 @@ impl TradeValidationReceiptListRequest {
self
}
- pub fn try_with_trusted_worker_pubkeys<I, S>(
- mut self,
- pubkeys: I,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.trusted_worker_pubkeys = parse_worker_pubkeys(pubkeys)?;
- Ok(self)
- }
-
#[cfg(feature = "transport-nostr-runtime")]
fn validate(&self) -> Result<(), RadrootsSdkError> {
validate_validation_receipt_limit(self.limit)
@@ -1798,16 +1038,12 @@ impl TradeValidationReceiptListRequest {
#[non_exhaustive]
pub struct TradeValidationReceiptInspectRequest {
pub receipt_event_id: RadrootsEventId,
- pub trusted_worker_pubkeys: Vec<RadrootsPublicKey>,
}
#[cfg(feature = "runtime")]
impl TradeValidationReceiptInspectRequest {
pub fn new(receipt_event_id: RadrootsEventId) -> Self {
- Self {
- receipt_event_id,
- trusted_worker_pubkeys: Vec::new(),
- }
+ Self { receipt_event_id }
}
pub fn parse(receipt_event_id: &str) -> Result<Self, RadrootsSdkError> {
@@ -1819,18 +1055,6 @@ impl TradeValidationReceiptInspectRequest {
),
})
}
-
- pub fn try_with_trusted_worker_pubkeys<I, S>(
- mut self,
- pubkeys: I,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.trusted_worker_pubkeys = parse_worker_pubkeys(pubkeys)?;
- Ok(self)
- }
}
#[cfg(feature = "runtime")]
@@ -1838,16 +1062,12 @@ impl TradeValidationReceiptInspectRequest {
#[non_exhaustive]
pub struct TradeValidationReceiptVerifyRequest {
pub receipt_event_id: RadrootsEventId,
- pub trusted_worker_pubkeys: Vec<RadrootsPublicKey>,
}
#[cfg(feature = "runtime")]
impl TradeValidationReceiptVerifyRequest {
pub fn new(receipt_event_id: RadrootsEventId) -> Self {
- Self {
- receipt_event_id,
- trusted_worker_pubkeys: Vec::new(),
- }
+ Self { receipt_event_id }
}
pub fn parse(receipt_event_id: &str) -> Result<Self, RadrootsSdkError> {
@@ -1859,18 +1079,6 @@ impl TradeValidationReceiptVerifyRequest {
),
})
}
-
- pub fn try_with_trusted_worker_pubkeys<I, S>(
- mut self,
- pubkeys: I,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.trusted_worker_pubkeys = parse_worker_pubkeys(pubkeys)?;
- Ok(self)
- }
}
#[cfg(feature = "runtime")]
@@ -1899,7 +1107,6 @@ pub struct TradeValidationReceiptEvent {
pub event: RadrootsEventEnvelope,
pub receipt: RadrootsTradeValidationReceipt,
pub tags: TradeValidationReceiptTags,
- pub worker_evidence: TradeValidationReceiptWorkerEvidenceSelection,
}
#[cfg(feature = "runtime")]
@@ -1925,31 +1132,6 @@ pub struct TradeValidationReceiptTags {
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptWorkerEvidenceSelection {
- pub trusted: Option<TradeValidationReceiptWorkerEvidence>,
- pub untrusted: Option<TradeValidationReceiptWorkerEvidence>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct TradeValidationReceiptWorkerEvidence {
- pub result_event_id: RadrootsEventId,
- pub author: RadrootsPublicKey,
- pub status: String,
- pub prover_backend: String,
- pub proof_mode: String,
- pub proof_system: String,
- pub proof_generated: bool,
- pub sp1_execute_checked: bool,
- pub sp1_execute_public_values_hash: Option<String>,
- pub cryptographic_proof_verified: bool,
- pub public_values_hash: String,
- pub validation_authority: Option<RadrootsTradeValidationAuthority>,
- pub commitment_confidence: Option<RadrootsTradeCommitmentConfidence>,
-}
-
-#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeValidationReceiptNostrEvidenceReceipt {
pub inserted_count: usize,
@@ -2360,7 +1542,6 @@ pub struct TradeStatusReceipt {
pub decision_event_id: Option<RadrootsEventId>,
pub agreement_event_id: Option<RadrootsEventId>,
pub rhi_receipt_event_id: Option<RadrootsEventId>,
- pub pending_revision_event_id: Option<RadrootsEventId>,
pub cancellation_event_id: Option<RadrootsEventId>,
pub last_event_id: Option<RadrootsEventId>,
pub issues: Vec<SdkTradeStatusIssue>,
@@ -2381,7 +1562,6 @@ pub struct TradeStatusEvidenceSummary {
pub has_decision: bool,
pub has_agreement: bool,
pub has_validation_receipt: bool,
- pub has_pending_revision: bool,
pub has_cancellation: bool,
pub has_issues: bool,
}
@@ -2411,8 +1591,6 @@ pub struct TradeValidationTrustDecision {
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeStatusEligibility {
pub can_decide: bool,
- pub can_propose_revision: bool,
- pub can_decide_revision: bool,
pub can_cancel: bool,
}
@@ -2424,8 +1602,7 @@ pub enum TradeStatusNextActionKind {
NoLocalOrder,
InspectEvidenceIssues,
AwaitSellerDecision,
- DecideRevision,
- AwaitRhiValidation,
+ AwaitValidation,
Terminal,
}
@@ -2446,11 +1623,11 @@ pub enum TradeStatusKind {
Missing,
Ambiguous,
Requested,
- RevisionProposed,
- AgreedPendingRhi,
+ AgreedPendingValidation,
Committed,
Declined,
Cancelled,
+ ValidationExpired,
Invalid,
}
@@ -2516,28 +1693,6 @@ pub enum SdkTradeStatusIssueKind {
DecisionInventoryCommitmentMismatch,
DecisionMissingReason,
ConflictingDecisions,
- RevisionProposalPayloadInvalid,
- RevisionProposalOrderIdMismatch,
- RevisionProposalAuthorMismatch,
- RevisionProposalCounterpartyMismatch,
- RevisionProposalBuyerMismatch,
- RevisionProposalSellerMismatch,
- RevisionProposalListingAddressInvalid,
- RevisionProposalListingMismatch,
- RevisionProposalRootMismatch,
- RevisionProposalPreviousMismatch,
- RevisionDecisionWithoutProposal,
- RevisionDecisionPayloadInvalid,
- RevisionDecisionOrderIdMismatch,
- RevisionDecisionAuthorMismatch,
- RevisionDecisionCounterpartyMismatch,
- RevisionDecisionBuyerMismatch,
- RevisionDecisionSellerMismatch,
- RevisionDecisionListingAddressInvalid,
- RevisionDecisionListingMismatch,
- RevisionDecisionRootMismatch,
- RevisionDecisionPreviousMismatch,
- RevisionDecisionRevisionIdMismatch,
CancellationWithoutCancellableOrder,
CancellationPayloadInvalid,
CancellationOrderIdMismatch,
@@ -2621,7 +1776,7 @@ impl<'sdk> TradesClient<'sdk> {
})
}
- #[cfg(any(feature = "signer-adapters", test))]
+ #[cfg(feature = "signer-adapters")]
pub(crate) fn prepare_submit(
&self,
request: TradeSubmitPrepareRequest,
@@ -2636,77 +1791,6 @@ impl<'sdk> TradesClient<'sdk> {
}
#[cfg(feature = "signer-adapters")]
- #[cfg(test)]
- pub(crate) async fn enqueue_submit(
- &self,
- request: TradeSubmitEnqueueRequest,
- ) -> Result<TradeSubmitReceipt, RadrootsSdkError> {
- let TradeSubmitEnqueueRequest {
- actor,
- listing_event,
- order,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeSubmitPrepareRequest {
- actor: actor.clone(),
- listing_event,
- order,
- created_at,
- };
- let plan = self.prepare_submit(prepare_request)?;
- self.enqueue_prepared_submit(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- )
- .await
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_submit_with_explicit_signer(
- &self,
- request: TradeSubmitEnqueueRequest,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeSubmitReceipt, RadrootsSdkError> {
- let TradeSubmitEnqueueRequest {
- actor,
- listing_event,
- order,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeSubmitPrepareRequest {
- actor: actor.clone(),
- listing_event,
- order,
- created_at,
- };
- let plan = self.prepare_submit(prepare_request)?;
- self.enqueue_prepared_submit_with_explicit_signer(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- signer,
- )
- .await
- }
-
- #[cfg(feature = "signer-adapters")]
pub(crate) async fn enqueue_prepared_submit(
&self,
actor: &RadrootsActorContext,
@@ -2724,629 +1808,77 @@ impl<'sdk> TradesClient<'sdk> {
actor,
frozen_draft: &plan.frozen_draft,
target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy: satisfaction_policy,
- idempotency_key,
- },
- )
- .await?;
- Ok(order_submit_receipt(plan, enqueue))
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_prepared_submit_with_explicit_signer(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeSubmitPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeSubmitReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let enqueue = enqueue_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: satisfaction_policy,
+ satisfaction_policy,
idempotency_key,
},
- signer,
)
.await?;
Ok(order_submit_receipt(plan, enqueue))
}
- #[cfg(any(feature = "signer-adapters", test))]
+ #[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")]
- #[cfg(test)]
- pub(crate) async fn enqueue_decision(
- &self,
- request: TradeDecisionEnqueueRequest,
- ) -> Result<TradeDecisionReceipt, RadrootsSdkError> {
- let TradeDecisionEnqueueRequest {
- actor,
- request_event,
- decision,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeDecisionPrepareRequest {
- actor: actor.clone(),
- request_event,
- decision,
- created_at,
- };
- let plan = self.prepare_decision(prepare_request)?;
- self.enqueue_prepared_decision(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- )
- .await
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_decision_with_explicit_signer(
- &self,
- request: TradeDecisionEnqueueRequest,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeDecisionReceipt, RadrootsSdkError> {
- let TradeDecisionEnqueueRequest {
- actor,
- request_event,
- decision,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeDecisionPrepareRequest {
- actor: actor.clone(),
- request_event,
- decision,
- created_at,
- };
- let plan = self.prepare_decision(prepare_request)?;
- self.enqueue_prepared_decision_with_explicit_signer(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- signer,
- )
- .await
- }
-
- #[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: satisfaction_policy,
- idempotency_key,
- },
- )
- .await?;
- Ok(order_decision_receipt(plan, enqueue))
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_prepared_decision_with_explicit_signer(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeDecisionPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- signer: &dyn RadrootsEventSigner,
- ) -> 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_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: satisfaction_policy,
- idempotency_key,
- },
- signer,
- )
- .await?;
- Ok(order_decision_receipt(plan, enqueue))
- }
-
- #[cfg(any(feature = "signer-adapters", test))]
- pub(crate) fn prepare_revision_proposal(
- &self,
- request: TradeRevisionProposalPrepareRequest,
- ) -> Result<TradeRevisionProposalPlan, RadrootsSdkError> {
- let created_at = self.resolved_created_at(request.created_at)?;
- order_revision_proposal_plan(
- &request.actor,
- request.root_event,
- request.previous_event,
- request.proposal,
- created_at,
- )
- }
-
- #[cfg(feature = "signer-adapters")]
- #[cfg(test)]
- pub(crate) async fn enqueue_revision_proposal(
- &self,
- request: TradeRevisionProposalEnqueueRequest,
- ) -> Result<TradeRevisionProposalReceipt, RadrootsSdkError> {
- let TradeRevisionProposalEnqueueRequest {
- actor,
- root_event,
- previous_event,
- proposal,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeRevisionProposalPrepareRequest {
- actor: actor.clone(),
- root_event,
- previous_event,
- proposal,
- created_at,
- };
- let plan = self.prepare_revision_proposal(prepare_request)?;
- self.enqueue_prepared_revision_proposal(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- )
- .await
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_revision_proposal_with_explicit_signer(
- &self,
- request: TradeRevisionProposalEnqueueRequest,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeRevisionProposalReceipt, RadrootsSdkError> {
- let TradeRevisionProposalEnqueueRequest {
- actor,
- root_event,
- previous_event,
- proposal,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeRevisionProposalPrepareRequest {
- actor: actor.clone(),
- root_event,
- previous_event,
- proposal,
- created_at,
- };
- let plan = self.prepare_revision_proposal(prepare_request)?;
- self.enqueue_prepared_revision_proposal_with_explicit_signer(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- signer,
- )
- .await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) async fn enqueue_prepared_revision_proposal(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeRevisionProposalPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- ) -> Result<TradeRevisionProposalReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- if !self
- .prepared_order_event_exists(&plan.expected_event_id)
- .await?
- {
- self.require_revision_proposal_preflight(&plan).await?;
- }
- let enqueue = enqueue_configured_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::RevisionProposal.operation_kind(),
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy: satisfaction_policy,
- idempotency_key,
- },
- )
- .await?;
- Ok(order_revision_proposal_receipt(plan, enqueue))
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_prepared_revision_proposal_with_explicit_signer(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeRevisionProposalPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeRevisionProposalReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- if !self
- .prepared_order_event_exists(&plan.expected_event_id)
- .await?
- {
- self.require_revision_proposal_preflight(&plan).await?;
- }
- let enqueue = enqueue_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::RevisionProposal.operation_kind(),
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy: satisfaction_policy,
- idempotency_key,
- },
- signer,
- )
- .await?;
- Ok(order_revision_proposal_receipt(plan, enqueue))
- }
-
- #[cfg(any(feature = "signer-adapters", test))]
- pub(crate) fn prepare_revision_decision(
- &self,
- request: TradeRevisionDecisionPrepareRequest,
- ) -> Result<TradeRevisionDecisionPlan, RadrootsSdkError> {
- let created_at = self.resolved_created_at(request.created_at)?;
- order_revision_decision_plan(
- &request.actor,
- request.root_event,
- request.previous_event,
- request.decision,
- created_at,
- )
- }
-
- #[cfg(feature = "signer-adapters")]
- #[cfg(test)]
- pub(crate) async fn enqueue_revision_decision(
- &self,
- request: TradeRevisionDecisionEnqueueRequest,
- ) -> Result<TradeRevisionDecisionReceipt, RadrootsSdkError> {
- let TradeRevisionDecisionEnqueueRequest {
- actor,
- root_event,
- previous_event,
- decision,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeRevisionDecisionPrepareRequest {
- actor: actor.clone(),
- root_event,
- previous_event,
- decision,
- created_at,
- };
- let plan = self.prepare_revision_decision(prepare_request)?;
- self.enqueue_prepared_revision_decision(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- )
- .await
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_revision_decision_with_explicit_signer(
- &self,
- request: TradeRevisionDecisionEnqueueRequest,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeRevisionDecisionReceipt, RadrootsSdkError> {
- let TradeRevisionDecisionEnqueueRequest {
- actor,
- root_event,
- previous_event,
- decision,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeRevisionDecisionPrepareRequest {
- actor: actor.clone(),
- root_event,
- previous_event,
- decision,
- created_at,
- };
- let plan = self.prepare_revision_decision(prepare_request)?;
- self.enqueue_prepared_revision_decision_with_explicit_signer(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- signer,
- )
- .await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub(crate) async fn enqueue_prepared_revision_decision(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeRevisionDecisionPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- ) -> Result<TradeRevisionDecisionReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- if !self
- .prepared_order_event_exists(&plan.expected_event_id)
- .await?
- {
- self.require_revision_decision_preflight(&plan).await?;
- }
- let enqueue = enqueue_configured_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::RevisionDecision.operation_kind(),
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy: satisfaction_policy,
- idempotency_key,
- },
- )
- .await?;
- Ok(order_revision_decision_receipt(plan, enqueue))
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_prepared_revision_decision_with_explicit_signer(
- &self,
- actor: &RadrootsActorContext,
- plan: TradeRevisionDecisionPlan,
- target_policy: TargetPolicy,
- publish_mode: PublishMode,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeRevisionDecisionReceipt, RadrootsSdkError> {
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- if !self
- .prepared_order_event_exists(&plan.expected_event_id)
- .await?
- {
- self.require_revision_decision_preflight(&plan).await?;
- }
- let enqueue = enqueue_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::RevisionDecision.operation_kind(),
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy: satisfaction_policy,
- idempotency_key,
- },
- signer,
- )
- .await?;
- Ok(order_revision_decision_receipt(plan, enqueue))
- }
-
- #[cfg(any(feature = "signer-adapters", test))]
- 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")]
- #[cfg(test)]
- pub(crate) async fn enqueue_cancellation(
- &self,
- request: TradeCancellationEnqueueRequest,
- ) -> Result<TradeCancellationReceipt, RadrootsSdkError> {
- let TradeCancellationEnqueueRequest {
- actor,
- root_event,
- previous_event,
- cancellation,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeCancellationPrepareRequest {
- actor: actor.clone(),
- root_event,
- previous_event,
- cancellation,
- created_at,
- };
- let plan = self.prepare_cancellation(prepare_request)?;
- self.enqueue_prepared_cancellation(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- )
- .await
- }
-
- #[cfg(test)]
- pub(crate) async fn enqueue_cancellation_with_explicit_signer(
- &self,
- request: TradeCancellationEnqueueRequest,
- signer: &dyn RadrootsEventSigner,
- ) -> Result<TradeCancellationReceipt, RadrootsSdkError> {
- let TradeCancellationEnqueueRequest {
- actor,
- root_event,
- previous_event,
- cancellation,
- target_policy,
- publish_mode,
- satisfaction_policy,
- idempotency_key,
- created_at,
- } = request;
- validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
- let prepare_request = TradeCancellationPrepareRequest {
- actor: actor.clone(),
- root_event,
- previous_event,
- cancellation,
+ ) -> 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,
- };
- let plan = self.prepare_cancellation(prepare_request)?;
- self.enqueue_prepared_cancellation_with_explicit_signer(
- &actor,
- plan,
- target_policy,
- publish_mode,
- satisfaction_policy.clone(),
- idempotency_key,
- signer,
)
- .await
}
#[cfg(feature = "signer-adapters")]
- pub(crate) async fn enqueue_prepared_cancellation(
+ pub(crate) async fn enqueue_prepared_decision(
&self,
actor: &RadrootsActorContext,
- plan: TradeCancellationPlan,
+ plan: TradeDecisionPlan,
target_policy: TargetPolicy,
publish_mode: PublishMode,
satisfaction_policy: SatisfactionPolicy,
idempotency_key: Option<SdkIdempotencyKey>,
- ) -> Result<TradeCancellationReceipt, RadrootsSdkError> {
+ ) -> Result<TradeDecisionReceipt, 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?;
+ self.require_decision_preflight(&plan).await?;
}
let enqueue = enqueue_configured_signed_workflow(
self.sdk,
SdkWorkflowEnqueueRequest {
- operation_kind: TradeWorkflowKind::Cancellation.operation_kind(),
+ operation_kind: TradeWorkflowKind::Decision.operation_kind(),
actor,
frozen_draft: &plan.frozen_draft,
target_policy: target_policy.workflow_target_policy(),
- satisfaction_policy: satisfaction_policy,
+ satisfaction_policy,
idempotency_key,
},
)
.await?;
- Ok(order_cancellation_receipt(plan, enqueue))
+ 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(test)]
- pub(crate) async fn enqueue_prepared_cancellation_with_explicit_signer(
+ #[cfg(feature = "signer-adapters")]
+ pub(crate) async fn enqueue_prepared_cancellation(
&self,
actor: &RadrootsActorContext,
plan: TradeCancellationPlan,
@@ -3354,7 +1886,6 @@ impl<'sdk> TradesClient<'sdk> {
publish_mode: PublishMode,
satisfaction_policy: SatisfactionPolicy,
idempotency_key: Option<SdkIdempotencyKey>,
- signer: &dyn RadrootsEventSigner,
) -> Result<TradeCancellationReceipt, RadrootsSdkError> {
validate_trade_enqueue_policy(publish_mode, &satisfaction_policy)?;
if !self
@@ -3363,17 +1894,16 @@ impl<'sdk> TradesClient<'sdk> {
{
self.require_cancellation_preflight(&plan).await?;
}
- let enqueue = enqueue_signed_workflow(
+ 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: satisfaction_policy,
+ satisfaction_policy,
idempotency_key,
},
- signer,
)
.await?;
Ok(order_cancellation_receipt(plan, enqueue))
@@ -3500,7 +2030,7 @@ impl<'sdk> TradesClient<'sdk> {
}
}
- #[cfg(any(feature = "signer-adapters", test))]
+ #[cfg(feature = "signer-adapters")]
async fn require_decision_preflight(
&self,
plan: &TradeDecisionPlan,
@@ -3509,25 +2039,7 @@ impl<'sdk> TradesClient<'sdk> {
require_decision_request_evidence(plan, &query_result.projection)
}
- #[cfg(any(feature = "signer-adapters", test))]
- async fn require_revision_proposal_preflight(
- &self,
- plan: &TradeRevisionProposalPlan,
- ) -> Result<(), RadrootsSdkError> {
- let query_result = self.query_order_projection(&plan.order_id).await?;
- require_revision_proposal_state(plan, &query_result.projection)
- }
-
- #[cfg(any(feature = "signer-adapters", test))]
- async fn require_revision_decision_preflight(
- &self,
- plan: &TradeRevisionDecisionPlan,
- ) -> Result<(), RadrootsSdkError> {
- let query_result = self.query_order_projection(&plan.order_id).await?;
- require_revision_decision_state(plan, &query_result.projection)
- }
-
- #[cfg(any(feature = "signer-adapters", test))]
+ #[cfg(feature = "signer-adapters")]
async fn require_cancellation_preflight(
&self,
plan: &TradeCancellationPlan,
@@ -3536,7 +2048,7 @@ impl<'sdk> TradesClient<'sdk> {
require_cancellation_state(plan, &query_result.projection)
}
- #[cfg(any(feature = "signer-adapters", test))]
+ #[cfg(feature = "signer-adapters")]
async fn query_order_projection(
&self,
order_id: &RadrootsOrderId,
@@ -3550,7 +2062,7 @@ impl<'sdk> TradesClient<'sdk> {
.map_err(projection_error)
}
- #[cfg(any(feature = "signer-adapters", test))]
+ #[cfg(feature = "signer-adapters")]
async fn prepared_order_event_exists(
&self,
expected_event_id: &RadrootsEventId,
@@ -3654,7 +2166,7 @@ impl<'sdk> TradeResyncClient<'sdk> {
if status.status == TradeStatusKind::Ambiguous {
return Err(RadrootsSdkError::TradeAmbiguous {
operation: "trade.resync".to_owned(),
- locator: request.locator,
+ locator: Box::new(request.locator),
candidates: status
.ambiguity_candidates
.iter()
@@ -3750,6 +2262,9 @@ fn trade_evidence_query_plan(
.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()
@@ -3766,13 +2281,8 @@ fn trade_evidence_query_plan(
),
trade_evidence_branch(
TradeEvidenceQueryBranchKind::LifecycleChain,
- vec![
- KIND_ORDER_DECISION,
- KIND_ORDER_REVISION_PROPOSAL,
- KIND_ORDER_REVISION_DECISION,
- KIND_ORDER_CANCELLATION,
- ],
- Some((TAG_D, vec![order_id.clone()])),
+ vec![KIND_ORDER_DECISION, KIND_ORDER_CANCELLATION],
+ Some(lifecycle_tag),
None,
limit,
true,
@@ -3797,14 +2307,6 @@ fn trade_evidence_query_plan(
limit,
locator.listing_addr.is_some(),
),
- trade_evidence_branch(
- TradeEvidenceQueryBranchKind::WorkerResults,
- vec![KIND_TRADE_TRANSITION_PROOF_RESULT],
- root_event_id.map(|value| (TAG_E, vec![value])),
- None,
- limit,
- locator.root_event_id.is_some(),
- ),
];
Ok(TradeEvidenceQueryPlan {
locator,
@@ -4227,14 +2729,6 @@ impl<'sdk> TradeValidationReceiptsClient<'sdk> {
.await?;
let (mut receipts, mut invalid_receipts) =
classify_validation_receipts(events, Some(request.order_id.as_str()))?;
- attach_worker_evidence(
- self.sdk,
- adapter,
- &nostr_relay_urls,
- &request.trusted_worker_pubkeys,
- &mut receipts,
- )
- .await?;
receipts.sort_by(validation_receipt_event_order);
invalid_receipts.sort_by(validation_receipt_invalid_order);
Ok(TradeValidationReceiptListReceipt {
@@ -4278,7 +2772,6 @@ impl<'sdk> TradeValidationReceiptsClient<'sdk> {
validation_receipt_inspect(
self.sdk,
request.receipt_event_id,
- request.trusted_worker_pubkeys,
adapter,
"trade.validation_receipts.inspect",
)
@@ -4317,7 +2810,6 @@ impl<'sdk> TradeValidationReceiptsClient<'sdk> {
validation_receipt_inspect(
self.sdk,
request.receipt_event_id,
- request.trusted_worker_pubkeys,
adapter,
"trade.validation_receipts.verify",
)
@@ -4329,7 +2821,6 @@ impl<'sdk> TradeValidationReceiptsClient<'sdk> {
async fn validation_receipt_inspect<A>(
sdk: &crate::RadrootsClient,
receipt_event_id: RadrootsEventId,
- trusted_worker_pubkeys: Vec<RadrootsPublicKey>,
adapter: &A,
operation: &'static str,
) -> Result<TradeValidationReceiptInspectReceipt, RadrootsSdkError>
@@ -4351,14 +2842,6 @@ where
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());
- attach_worker_evidence(
- sdk,
- adapter,
- &nostr_relay_urls,
- &trusted_worker_pubkeys,
- &mut receipts,
- )
- .await?;
receipts.sort_by(validation_receipt_event_order);
invalid_receipts.sort_by(validation_receipt_invalid_order);
Ok(TradeValidationReceiptInspectReceipt {
@@ -4427,41 +2910,6 @@ fn validation_receipt_inspect_fetch_request(
}
#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-fn validation_receipt_worker_fetch_request(
- sdk: &crate::RadrootsClient,
- receipts: &[TradeValidationReceiptEvent],
- nostr_relay_urls: &[String],
-) -> Result<Option<RadrootsRelayFetchRequest>, RadrootsSdkError> {
- let root_event_ids = receipts
- .iter()
- .map(|receipt| receipt.tags.root_event_id.clone())
- .collect::<BTreeSet<_>>()
- .into_iter()
- .collect::<Vec<_>>();
- if root_event_ids.is_empty() {
- return Ok(None);
- }
- let filter = RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(
- KIND_TRADE_TRANSITION_PROOF_RESULT as u16,
- ))
- .limit(receipts.len().saturating_mul(4).max(1));
- let filter = radroots_nostr_filter_tag(filter, TAG_E, root_event_ids).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("validation receipt worker evidence filter invalid: {error}"),
- }
- })?;
- Ok(Some(
- RadrootsRelayFetchRequest::fetch(
- sdk_now_ms(sdk)?,
- receipts.len().saturating_mul(4).max(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],
@@ -4518,7 +2966,6 @@ fn classify_validation_receipts(
event,
receipt: verified.receipt,
tags: TradeValidationReceiptTags::from(verified.tags),
- worker_evidence: TradeValidationReceiptWorkerEvidenceSelection::default(),
}),
Err(error) => invalid_receipts.push(TradeValidationReceiptInvalidCandidate {
event,
@@ -4530,180 +2977,6 @@ fn classify_validation_receipts(
Ok((receipts, invalid_receipts))
}
-#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
-async fn attach_worker_evidence<A>(
- sdk: &crate::RadrootsClient,
- adapter: &A,
- nostr_relay_urls: &[String],
- trusted_worker_pubkeys: &[RadrootsPublicKey],
- receipts: &mut [TradeValidationReceiptEvent],
-) -> Result<(), RadrootsSdkError>
-where
- A: RadrootsRelayFetchAdapter,
-{
- if receipts.is_empty() {
- return Ok(());
- }
- let Some(fetch_request) =
- validation_receipt_worker_fetch_request(sdk, receipts, nostr_relay_urls)?
- else {
- return Ok(());
- };
- 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_TRANSITION_PROOF_RESULT,
- )
- .await?;
- let mut selections = worker_evidence_for_receipts(trusted_worker_pubkeys, receipts, events)?;
- for receipt in receipts {
- receipt.worker_evidence = selections
- .remove(receipt.event.id_str())
- .unwrap_or_default();
- }
- Ok(())
-}
-
-#[cfg(feature = "runtime")]
-fn worker_evidence_for_receipts(
- trusted_worker_pubkeys: &[RadrootsPublicKey],
- receipts: &[TradeValidationReceiptEvent],
- events: Vec<RadrootsEventEnvelope>,
-) -> Result<BTreeMap<String, TradeValidationReceiptWorkerEvidenceSelection>, RadrootsSdkError> {
- let trusted_pubkeys = trusted_worker_pubkeys
- .iter()
- .map(|pubkey| pubkey.as_str().to_ascii_lowercase())
- .collect::<BTreeSet<_>>();
- let binding_by_receipt_id = receipts
- .iter()
- .map(|receipt| (receipt.event.id_str(), receipt))
- .collect::<BTreeMap<_, _>>();
- let mut by_receipt =
- BTreeMap::<String, Vec<(u64, String, bool, TradeValidationReceiptWorkerEvidence)>>::new();
-
- for event in events {
- let payload =
- match serde_json::from_str::<RawTradeValidationReceiptWorkerResult>(event.content()) {
- Ok(payload) => payload,
- Err(_) => continue,
- };
- let Some(binding) = binding_by_receipt_id.get(payload.receipt_event_id.as_str()) else {
- continue;
- };
- if !worker_payload_binds_receipt(&payload, binding) {
- continue;
- }
- let author = RadrootsPublicKey::parse(event.author_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("validation receipt worker evidence author is invalid: {error}"),
- }
- })?;
- let result_event_id = RadrootsEventId::parse(event.id_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("validation receipt worker evidence event id is invalid: {error}"),
- }
- })?;
- let trusted = trusted_pubkeys.contains(author.as_str().to_ascii_lowercase().as_str());
- let receipt_event_id = payload.receipt_event_id.clone();
- let view = TradeValidationReceiptWorkerEvidence {
- result_event_id,
- author,
- status: payload.status,
- prover_backend: payload.prover_backend,
- proof_mode: payload.proof_mode,
- proof_system: payload.proof_system,
- proof_generated: payload.proof_generated,
- sp1_execute_checked: payload.sp1_execute_checked,
- sp1_execute_public_values_hash: payload.sp1_execute_public_values_hash,
- cryptographic_proof_verified: payload.cryptographic_proof_verified,
- public_values_hash: payload.public_values_hash,
- validation_authority: payload
- .validation_authority
- .as_deref()
- .and_then(RadrootsTradeValidationAuthority::from_label),
- commitment_confidence: payload
- .confidence
- .as_deref()
- .and_then(RadrootsTradeCommitmentConfidence::from_label),
- };
- by_receipt.entry(receipt_event_id).or_default().push((
- event.created_at_u64(),
- event.id_str().to_owned(),
- trusted,
- view,
- ));
- }
-
- Ok(by_receipt
- .into_iter()
- .map(|(receipt_event_id, mut candidates)| {
- candidates.sort_by(|left, right| {
- left.0
- .cmp(&right.0)
- .then_with(|| left.1.cmp(&right.1))
- .then_with(|| left.2.cmp(&right.2))
- });
- let mut selection = TradeValidationReceiptWorkerEvidenceSelection::default();
- for (_, _, trusted, view) in candidates.into_iter().rev() {
- if trusted && selection.trusted.is_none() {
- selection.trusted = Some(view);
- } else if !trusted && selection.untrusted.is_none() {
- selection.untrusted = Some(view);
- }
- if selection.trusted.is_some() && selection.untrusted.is_some() {
- break;
- }
- }
- (receipt_event_id, selection)
- })
- .collect())
-}
-
-#[cfg(feature = "runtime")]
-fn worker_payload_binds_receipt(
- payload: &RawTradeValidationReceiptWorkerResult,
- receipt: &TradeValidationReceiptEvent,
-) -> bool {
- payload.status == "succeeded"
- && payload.worker_role.as_deref() == Some("non_authoritative_prover")
- && payload.receipt_kind == Some(KIND_TRADE_VALIDATION_RECEIPT)
- && payload.receipt_event_id == receipt.event.id_str()
- && payload.order_id.as_deref() == Some(receipt.tags.order_id.as_str())
- && payload.listing_event_id.as_deref() == Some(receipt.tags.listing_event_id.as_str())
- && payload.event_set_root.as_deref() == Some(receipt.tags.event_set_root.as_str())
- && payload.reducer_output_root.as_deref() == Some(receipt.tags.reducer_output_root.as_str())
- && payload.request_event_id.as_deref() == Some(receipt.tags.root_event_id.as_str())
- && payload.decision_event_id.as_deref() == Some(receipt.tags.target_event_id.as_str())
- && payload.public_values_hash == receipt.receipt.public_values_hash
- && payload.proof_system == receipt.receipt.proof.system.as_str()
- && payload.proof_mode == receipt.receipt.proof.mode.as_deref().unwrap_or("none")
- && payload.proof_generated
- == (receipt.receipt.proof.system != RadrootsValidationReceiptProofSystem::None)
- && payload.cryptographic_proof_verified == payload.proof_generated
- && worker_payload_execution_binds_receipt(payload, receipt)
-}
-
-#[cfg(feature = "runtime")]
-fn worker_payload_execution_binds_receipt(
- payload: &RawTradeValidationReceiptWorkerResult,
- receipt: &TradeValidationReceiptEvent,
-) -> bool {
- if payload.sp1_execute_checked {
- return payload.sp1_execute_public_values_hash.as_deref()
- == Some(receipt.receipt.public_values_hash.as_str());
- }
- payload.sp1_execute_public_values_hash.is_none()
- && payload.proof_system == RadrootsValidationReceiptProofSystem::None.as_str()
- && payload.proof_mode == "none"
- && payload.validation_authority.as_deref()
- == Some(RadrootsTradeValidationAuthority::DevDeterministicOnly.as_str())
- && payload.confidence.as_deref()
- == Some(RadrootsTradeCommitmentConfidence::LocalOnly.as_str())
-}
-
#[cfg(feature = "runtime")]
async fn apply_trade_status_validation_trust(
sdk: &crate::RadrootsClient,
@@ -4734,13 +3007,12 @@ async fn trade_status_validation_trust_decision(
return Ok(trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Pending,
- None,
- None,
- None,
- None,
+ TradeValidationTrustEvidence::none(),
false,
- Some("validation_receipt_missing"),
- Some("validation receipt is not present in local product evidence"),
+ Some(TradeValidationTrustReason::new(
+ "validation_receipt_missing",
+ "validation receipt is not present in local product evidence",
+ )),
));
};
let Some(stored_event) = sdk
@@ -4754,26 +3026,24 @@ async fn trade_status_validation_trust_decision(
return Ok(trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Pending,
- Some(receipt_event_id),
- None,
- None,
- None,
+ TradeValidationTrustEvidence::receipt_event(receipt_event_id),
false,
- Some("validation_receipt_event_missing"),
- Some("validation receipt event is referenced by projection but missing from storage"),
+ 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,
- Some(receipt_event_id),
- None,
- None,
- None,
+ TradeValidationTrustEvidence::receipt_event(receipt_event_id),
false,
- Some("validation_receipt_event_kind_invalid"),
- Some("validation receipt reference does not point to a validation receipt event"),
+ 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)?;
@@ -4783,13 +3053,12 @@ async fn trade_status_validation_trust_decision(
return Ok(trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Invalid,
- Some(receipt_event_id),
- None,
- None,
- None,
+ TradeValidationTrustEvidence::receipt_event(receipt_event_id),
false,
- Some("validation_receipt_author_invalid"),
- Some("validation receipt author is not a valid public key"),
+ Some(TradeValidationTrustReason::new(
+ "validation_receipt_author_invalid",
+ "validation receipt author is not a valid public key",
+ )),
));
}
};
@@ -4813,13 +3082,12 @@ async fn trade_status_validation_trust_decision(
return Ok(trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Invalid,
- Some(receipt_event_id),
- Some(receipt_author),
- None,
- None,
+ TradeValidationTrustEvidence::receipt_author(receipt_event_id, receipt_author),
false,
- Some("validation_receipt_invalid"),
- Some(reason.as_str()),
+ Some(TradeValidationTrustReason::new(
+ "validation_receipt_invalid",
+ reason.as_str(),
+ )),
));
}
};
@@ -4827,149 +3095,153 @@ async fn trade_status_validation_trust_decision(
event,
receipt: verified.receipt,
tags: TradeValidationReceiptTags::from(verified.tags),
- worker_evidence: TradeValidationReceiptWorkerEvidenceSelection::default(),
};
if receipt.receipt.result == RadrootsValidationReceiptResult::Invalid {
return Ok(trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Invalid,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(&receipt.receipt),
- None,
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ &receipt.receipt,
+ None,
+ ),
false,
- Some("validation_receipt_result_invalid"),
- Some("validation receipt reports an invalid trade transition"),
+ Some(TradeValidationTrustReason::new(
+ "validation_receipt_result_invalid",
+ "validation receipt reports an invalid trade transition",
+ )),
));
}
if policy.trusted_rhi_pubkeys.is_empty() {
return Ok(trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Untrusted,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(&receipt.receipt),
- None,
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ &receipt.receipt,
+ None,
+ ),
false,
- Some("validation_trust_policy_empty"),
- Some("validation trust policy has no trusted RHI public keys"),
+ Some(TradeValidationTrustReason::new(
+ "validation_trust_policy_empty",
+ "validation trust policy has no trusted RHI public keys",
+ )),
));
}
if !policy.trusts_rhi_pubkey(&receipt_author) {
return Ok(trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Untrusted,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(&receipt.receipt),
- None,
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ &receipt.receipt,
+ None,
+ ),
false,
- Some("validation_receipt_author_untrusted"),
- Some("validation receipt author is not trusted by the active policy"),
+ Some(TradeValidationTrustReason::new(
+ "validation_receipt_author_untrusted",
+ "validation receipt author is not trusted by the active policy",
+ )),
));
}
- let result_events =
- validation_receipt_worker_result_events_for_receipt(sdk, &receipt_event_id).await?;
- let mut selections = worker_evidence_for_receipts(
- &policy.trusted_rhi_pubkeys,
- core::slice::from_ref(&receipt),
- result_events,
- )?;
- let selection = selections
- .remove(receipt_event_id.as_str())
- .unwrap_or_default();
- let Some(evidence) = selection.trusted.as_ref() else {
- if selection.untrusted.is_some() {
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Untrusted,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(&receipt.receipt),
- selection.untrusted.as_ref(),
- false,
- Some("validation_result_author_untrusted"),
- Some("validation result metadata was produced by an untrusted RHI public key"),
- ));
- }
- return Ok(trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(&receipt.receipt),
- None,
- false,
- Some("validation_result_metadata_missing"),
- Some("trusted validation result metadata is not present in local product evidence"),
- ));
- };
- Ok(evaluate_trade_validation_trust_evidence(
+ Ok(evaluate_trade_validation_trust_receipt(
policy,
receipt_event_id,
receipt_author,
&receipt.receipt,
- evidence,
))
}
#[cfg(feature = "runtime")]
-async fn validation_receipt_worker_result_events_for_receipt(
- sdk: &crate::RadrootsClient,
- receipt_event_id: &RadrootsEventId,
-) -> Result<Vec<RadrootsEventEnvelope>, RadrootsSdkError> {
- let events = sdk
- ._event_store
- .events_by_tag(
- RADROOTS_DVM_TAG_VALIDATION_RECEIPT,
- receipt_event_id.as_str(),
- TRADE_STATUS_MAX_LIMIT,
- )
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- events
- .into_iter()
- .filter(|event| event.kind == KIND_TRADE_TRANSITION_PROOF_RESULT)
- .map(|event| stored_event_to_nostr_event(&event))
- .collect()
+#[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_evidence(
+fn evaluate_trade_validation_trust_receipt(
policy: &RadrootsTradeValidationTrustPolicy,
receipt_event_id: RadrootsEventId,
receipt_author: RadrootsPublicKey,
receipt: &RadrootsTradeValidationReceipt,
- evidence: &TradeValidationReceiptWorkerEvidence,
) -> TradeValidationTrustDecision {
- let Some(authority) = evidence.validation_authority else {
- return trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
- false,
- Some("validation_authority_missing"),
- Some("trusted validation result metadata does not declare validation authority"),
- );
- };
- let Some(confidence) = evidence.commitment_confidence else {
- return trade_validation_trust_decision(
- policy,
- RadrootsTradeValidationTrustState::Pending,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
- false,
- Some("commitment_confidence_missing"),
- Some("trusted validation result metadata does not declare commitment confidence"),
- );
- };
+ let metadata = validation_receipt_trust_metadata(receipt);
+ let authority = metadata.validation_authority;
+ let confidence = metadata.commitment_confidence;
if authority == RadrootsTradeValidationAuthority::DevDeterministicOnly
|| confidence == RadrootsTradeCommitmentConfidence::LocalOnly
{
@@ -4977,38 +3249,47 @@ fn evaluate_trade_validation_trust_evidence(
return trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Untrusted,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ receipt,
+ Some(metadata),
+ ),
false,
- Some("deterministic_none_not_allowed"),
- Some("deterministic-none validation is not allowed by the active policy"),
+ Some(TradeValidationTrustReason::new(
+ "deterministic_none_not_allowed",
+ "deterministic-none validation is not allowed by the active policy",
+ )),
);
}
if policy.require_cryptographic_proof {
return trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Pending,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ receipt,
+ Some(metadata),
+ ),
false,
- Some("cryptographic_proof_required"),
- Some("active policy requires cryptographic proof for committed confidence"),
+ Some(TradeValidationTrustReason::new(
+ "cryptographic_proof_required",
+ "active policy requires cryptographic proof for committed confidence",
+ )),
);
}
return trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::TrustedLocal,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ receipt,
+ Some(metadata),
+ ),
false,
None,
- None,
);
}
let cryptographic_metadata = matches!(
@@ -5023,31 +3304,36 @@ fn evaluate_trade_validation_trust_evidence(
);
if cryptographic_metadata
&& receipt.proof.system != RadrootsValidationReceiptProofSystem::None
- && evidence.cryptographic_proof_verified
+ && metadata.cryptographic_proof_verified
{
return trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::CryptographicCommitted,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ receipt,
+ Some(metadata),
+ ),
true,
None,
- None,
);
}
if policy.require_cryptographic_proof {
return trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Pending,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ receipt,
+ Some(metadata),
+ ),
false,
- Some("cryptographic_proof_required"),
- Some("active policy requires trusted cryptographic proof metadata"),
+ Some(TradeValidationTrustReason::new(
+ "cryptographic_proof_required",
+ "active policy requires trusted cryptographic proof metadata",
+ )),
);
}
if matches!(
@@ -5066,58 +3352,92 @@ fn evaluate_trade_validation_trust_evidence(
return trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::TrustedLocal,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ receipt,
+ Some(metadata),
+ ),
false,
None,
- None,
);
}
trade_validation_trust_decision(
policy,
RadrootsTradeValidationTrustState::Pending,
- Some(receipt_event_id),
- Some(receipt_author),
- Some(receipt),
- Some(evidence),
+ TradeValidationTrustEvidence::receipt(
+ receipt_event_id,
+ receipt_author,
+ receipt,
+ Some(metadata),
+ ),
false,
- Some("validation_trust_metadata_insufficient"),
- Some("trusted validation result metadata does not satisfy the active policy"),
+ 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::DevDeterministicOnly,
+ commitment_confidence: RadrootsTradeCommitmentConfidence::LocalOnly,
+ cryptographic_proof_verified: false,
+ }
+ } else {
+ TradeValidationTrustMetadata {
+ validation_authority: RadrootsTradeValidationAuthority::CryptographicProofVerified,
+ commitment_confidence: RadrootsTradeCommitmentConfidence::CommittedByCryptographicProof,
+ cryptographic_proof_verified: true,
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
fn trade_validation_trust_decision(
policy: &RadrootsTradeValidationTrustPolicy,
state: RadrootsTradeValidationTrustState,
- receipt_event_id: Option<RadrootsEventId>,
- receipt_author: Option<RadrootsPublicKey>,
- receipt: Option<&RadrootsTradeValidationReceipt>,
- evidence: Option<&TradeValidationReceiptWorkerEvidence>,
+ evidence: TradeValidationTrustEvidence<'_>,
production_committed: bool,
- reason_code: Option<&str>,
- reason: Option<&str>,
+ 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,
trusted_rhi_pubkey_count: policy.trusted_rhi_pubkey_count(),
allow_deterministic_none: policy.allow_deterministic_none,
require_cryptographic_proof: policy.require_cryptographic_proof,
- receipt_event_id,
- receipt_author,
- result_event_id: evidence.map(|evidence| evidence.result_event_id.clone()),
- result_author: evidence.map(|evidence| evidence.author.clone()),
- proof_system: receipt.map(|receipt| receipt.proof.system.as_str().to_owned()),
- validation_authority: evidence.and_then(|evidence| evidence.validation_authority),
- commitment_confidence: evidence.and_then(|evidence| evidence.commitment_confidence),
+ 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
- .is_some_and(|evidence| evidence.cryptographic_proof_verified),
+ .metadata
+ .is_some_and(|metadata| metadata.cryptographic_proof_verified),
production_committed,
- reason_code: reason_code.map(str::to_owned),
- reason: reason.map(str::to_owned),
+ reason_code,
+ reason,
}
}
@@ -5130,9 +3450,9 @@ fn apply_validation_trust_decision_to_status(status: &mut TradeStatusReceipt) {
RadrootsTradeValidationTrustState::Pending
| RadrootsTradeValidationTrustState::Untrusted => {
if status.status == TradeStatusKind::Committed {
- status.status = TradeStatusKind::AgreedPendingRhi;
+ status.status = TradeStatusKind::AgreedPendingValidation;
status.lifecycle_terminal = false;
- status.next_action = TradeStatusNextActionKind::AwaitRhiValidation;
+ status.next_action = TradeStatusNextActionKind::AwaitValidation;
status.last_event_id = status
.agreement_event_id
.clone()
@@ -5150,31 +3470,6 @@ fn apply_validation_trust_decision_to_status(status: &mut TradeStatusReceipt) {
}
}
-#[cfg(feature = "runtime")]
-#[derive(Debug, Deserialize)]
-struct RawTradeValidationReceiptWorkerResult {
- cryptographic_proof_verified: bool,
- decision_event_id: Option<String>,
- event_set_root: Option<String>,
- listing_event_id: Option<String>,
- order_id: Option<String>,
- proof_generated: bool,
- proof_mode: String,
- proof_system: String,
- public_values_hash: String,
- prover_backend: String,
- receipt_event_id: String,
- receipt_kind: Option<u32>,
- reducer_output_root: Option<String>,
- request_event_id: Option<String>,
- sp1_execute_checked: bool,
- sp1_execute_public_values_hash: Option<String>,
- status: String,
- validation_authority: Option<String>,
- confidence: Option<String>,
- worker_role: Option<String>,
-}
-
#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
impl TradeValidationReceiptNostrEvidenceReceipt {
fn from_fetch(receipt: RadrootsRelayFetchReceipt) -> Self {
@@ -5499,102 +3794,6 @@ impl<'sdk> TradeBuyerClient<'sdk> {
)
.await
}
-
- pub async fn accept_revision(
- &self,
- mut request: TradeRevisionDecisionRequest,
- ) -> Result<
- TradeMutationOutcome<TradeRevisionDecisionPlan, TradeRevisionDecisionReceipt>,
- RadrootsSdkError,
- > {
- request.decision = RadrootsOrderRevisionOutcome::Accepted;
- self.decide_revision(request).await
- }
-
- pub async fn decline_revision(
- &self,
- request: TradeRevisionDecisionRequest,
- ) -> Result<
- TradeMutationOutcome<TradeRevisionDecisionPlan, TradeRevisionDecisionReceipt>,
- RadrootsSdkError,
- > {
- self.decide_revision(request).await
- }
-
- async fn decide_revision(
- &self,
- request: TradeRevisionDecisionRequest,
- ) -> Result<
- TradeMutationOutcome<TradeRevisionDecisionPlan, TradeRevisionDecisionReceipt>,
- RadrootsSdkError,
- > {
- validate_trade_product_publish_policy(request.publish_mode, &request.satisfaction_policy)?;
- let TradeRevisionDecisionRequest {
- actor,
- locator,
- revision_id,
- decision,
- target_policy,
- publish_mode,
- satisfaction_policy,
- evidence_mode,
- privacy_confirmation,
- idempotency_key,
- created_at,
- } = request;
- require_trade_product_privacy_preflight(
- "trade.revision_decision",
- trade_revision_decision_privacy_fields(&decision),
- &privacy_confirmation,
- )?;
- let context =
- trade_mutation_context(self.sdk, locator, evidence_mode, "trade.revision_decision")
- .await?;
- let previous_event_id = context.pending_revision_event_id.clone().ok_or_else(|| {
- RadrootsSdkError::InvalidRequest {
- message: "trade revision decision requires a pending revision".to_owned(),
- }
- })?;
- let decision = RadrootsOrderRevisionDecision {
- revision_id,
- order_id: context.order_id.clone(),
- listing_addr: context.listing_addr.clone(),
- buyer_pubkey: context.buyer_pubkey.clone(),
- seller_pubkey: context.seller_pubkey.clone(),
- root_event_id: context.root_event_id.clone(),
- prev_event_id: previous_event_id.clone(),
- decision,
- };
- let client = trades_client(self.sdk);
- let plan = client.prepare_revision_decision(TradeRevisionDecisionPrepareRequest {
- actor: actor.clone(),
- root_event: event_ptr(&context.root_event_id),
- previous_event: event_ptr(&previous_event_id),
- decision,
- created_at,
- })?;
- if publish_mode == PublishMode::DryRun {
- return Ok(TradeMutationOutcome::DryRun { plan });
- }
- let receipt = client
- .enqueue_prepared_revision_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")]
@@ -5646,16 +3845,15 @@ impl<'sdk> TradeSellerClient<'sdk> {
})?;
if let Ok(RadrootsOrderEventRecord::Request(record)) =
order_event_record_from_event(&nostr_event)
+ && record.payload.seller_pubkey == seller_pubkey
{
- if 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.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));
@@ -5875,81 +4073,6 @@ impl<'sdk> TradeSellerClient<'sdk> {
)
.await
}
-
- #[cfg(feature = "signer-adapters")]
- pub async fn propose_revision(
- &self,
- request: TradeRevisionProposalRequest,
- ) -> Result<
- TradeMutationOutcome<TradeRevisionProposalPlan, TradeRevisionProposalReceipt>,
- RadrootsSdkError,
- > {
- validate_trade_product_publish_policy(request.publish_mode, &request.satisfaction_policy)?;
- let TradeRevisionProposalRequest {
- actor,
- locator,
- revision_id,
- items,
- economics,
- 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.propose_revision")
- .await?;
- let proposal = RadrootsOrderRevisionProposal {
- revision_id,
- order_id: context.order_id.clone(),
- listing_addr: context.listing_addr.clone(),
- buyer_pubkey: context.buyer_pubkey.clone(),
- seller_pubkey: context.seller_pubkey.clone(),
- root_event_id: context.root_event_id.clone(),
- prev_event_id: context.previous_event_id.clone(),
- items,
- economics,
- reason,
- };
- require_trade_product_privacy_preflight(
- "trade.propose_revision",
- trade_revision_proposal_privacy_fields(&proposal),
- &privacy_confirmation,
- )?;
- let client = trades_client(self.sdk);
- let plan = client.prepare_revision_proposal(TradeRevisionProposalPrepareRequest {
- actor: actor.clone(),
- root_event: event_ptr(&context.root_event_id),
- previous_event: event_ptr(&context.previous_event_id),
- proposal,
- created_at,
- })?;
- if publish_mode == PublishMode::DryRun {
- return Ok(TradeMutationOutcome::DryRun { plan });
- }
- let receipt = client
- .enqueue_prepared_revision_proposal(
- &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")]
@@ -5965,7 +4088,6 @@ struct TradeProductMutationContext {
seller_pubkey: RadrootsPublicKey,
root_event_id: RadrootsEventId,
previous_event_id: RadrootsEventId,
- pending_revision_event_id: Option<RadrootsEventId>,
}
#[cfg(feature = "signer-adapters")]
@@ -6082,7 +4204,7 @@ fn trade_mutation_context_from_status(
if status.status == TradeStatusKind::Ambiguous {
return Err(RadrootsSdkError::TradeAmbiguous {
operation: operation.to_owned(),
- locator,
+ locator: Box::new(locator),
candidates: status
.ambiguity_candidates
.into_iter()
@@ -6142,7 +4264,6 @@ fn trade_mutation_context_from_status(
seller_pubkey,
root_event_id,
previous_event_id,
- pending_revision_event_id: status.pending_revision_event_id,
})
}
@@ -6307,27 +4428,6 @@ fn trade_decision_privacy_fields(decision: &RadrootsOrderDecision) -> Vec<Produc
}
#[cfg(feature = "signer-adapters")]
-fn trade_revision_proposal_privacy_fields(
- proposal: &RadrootsOrderRevisionProposal,
-) -> Vec<ProductSensitivityField> {
- let mut fields = trade_reason_privacy_fields(&proposal.reason);
- if !proposal.items.is_empty() || !proposal.economics.items.is_empty() {
- fields.push(ProductSensitivityField::ProtocolMinimizedInventoryFields);
- }
- fields
-}
-
-#[cfg(feature = "signer-adapters")]
-fn trade_revision_decision_privacy_fields(
- decision: &RadrootsOrderRevisionOutcome,
-) -> Vec<ProductSensitivityField> {
- match decision {
- RadrootsOrderRevisionOutcome::Accepted => Vec::new(),
- RadrootsOrderRevisionOutcome::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();
@@ -6359,7 +4459,7 @@ fn trade_reason_contains_private_coordination(reason: &str) -> bool {
.any(|marker| reason.contains(marker))
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn validate_trade_enqueue_policy(
publish_mode: PublishMode,
satisfaction_policy: &SatisfactionPolicy,
@@ -6415,18 +4515,6 @@ fn parse_order_evidence(
.payload;
(payload.order_id, payload.listing_addr)
}
- KIND_ORDER_REVISION_PROPOSAL => {
- let payload = order_revision_proposal_from_event(event)
- .map_err(order_evidence_parse_error)?
- .payload;
- (payload.order_id, payload.listing_addr)
- }
- KIND_ORDER_REVISION_DECISION => {
- let payload = order_revision_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)?
@@ -6533,7 +4621,6 @@ impl TradeStatusReceipt {
decision_event_id: projection.decision_event_id,
agreement_event_id: projection.agreement_event_id,
rhi_receipt_event_id: projection.validation_receipt_event_id,
- pending_revision_event_id: projection.pending_revision_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(),
@@ -6560,7 +4647,6 @@ impl TradeStatusReceipt {
lifecycle_terminal: false,
economics: None,
agreement_event_id: None,
- pending_revision_event_id: None,
pending_inventory_reservations: Vec::new(),
committed_inventory_reservations: Vec::new(),
listing_addr: locator.listing_addr.clone(),
@@ -6610,7 +4696,6 @@ impl TradeStatusEvidenceSummary {
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_pending_revision: projection.pending_revision_event_id.is_some(),
has_cancellation: projection.cancellation_event_id.is_some(),
has_issues: !projection.issues.is_empty(),
}
@@ -6623,17 +4708,10 @@ impl TradeStatusEligibility {
let clean = projection.issues.is_empty();
let open = clean && !projection.lifecycle_terminal;
let requested = projection.status == RadrootsTradeWorkflowState::Requested;
- let revision_proposed = projection.status == RadrootsTradeWorkflowState::RevisionProposed;
- let has_pending_revision = projection.pending_revision_event_id.is_some();
Self {
- can_decide: open
- && requested
- && projection.decision_event_id.is_none()
- && !has_pending_revision,
- can_propose_revision: open && requested && !has_pending_revision,
- can_decide_revision: open && revision_proposed && has_pending_revision,
- can_cancel: open && requested && !has_pending_revision,
+ can_decide: open && requested && projection.decision_event_id.is_none(),
+ can_cancel: open && requested,
}
}
}
@@ -6651,8 +4729,8 @@ impl TradeStatusNextActionKind {
{
return Self::InspectEvidenceIssues;
}
- if projection.status == RadrootsTradeWorkflowState::AgreedPendingRhi {
- return Self::AwaitRhiValidation;
+ if projection.status == RadrootsTradeWorkflowState::AgreedPendingValidation {
+ return Self::AwaitValidation;
}
if projection.lifecycle_terminal {
return Self::Terminal;
@@ -6660,14 +4738,11 @@ impl TradeStatusNextActionKind {
if eligibility.can_decide {
return Self::AwaitSellerDecision;
}
- if eligibility.can_decide_revision {
- return Self::DecideRevision;
- }
Self::Terminal
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_submit_plan(
actor: &RadrootsActorContext,
listing_event: RadrootsEventPtr,
@@ -6714,157 +4789,42 @@ fn order_submit_plan(
})
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[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(any(feature = "signer-adapters", test))]
-fn order_revision_proposal_plan(
- actor: &RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- proposal: RadrootsOrderRevisionProposal,
- created_at: RadrootsSdkTimestamp,
-) -> Result<TradeRevisionProposalPlan, RadrootsSdkError> {
- require_seller_actor(actor, "trade.prepare_revision_proposal")?;
- let root_event_id = order_reference_event_id(&root_event, "root")?;
- let previous_event_id = order_reference_event_id(&previous_event, "previous")?;
- if proposal.seller_pubkey.as_str() != actor.pubkey().as_str() {
- return Err(RadrootsSdkError::UnauthorizedActor {
- operation: "trade.prepare_revision_proposal".to_owned(),
- reason: "actor pubkey must match order seller_pubkey".to_owned(),
- });
- }
- require_payload_event_refs(
- "order revision proposal",
- &proposal.root_event_id,
- &proposal.prev_event_id,
- &root_event_id,
- &previous_event_id,
- )?;
- let created_at_nostr = created_at.try_into_nostr_created_at()?;
- let order_id = proposal.order_id.clone();
- let listing_addr = proposal.listing_addr.clone();
- let buyer_pubkey = proposal.buyer_pubkey.clone();
- let seller_pubkey = proposal.seller_pubkey.clone();
- validate_order_payload(&proposal, "order revision proposal")?;
- let draft =
- order::build_order_revision_proposal_draft(&root_event_id, &previous_event_id, &proposal)
- .expect("validated order revision proposal draft encodes");
- let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
- draft.into_wire_parts(),
- TRADE_REVISION_PROPOSAL_CONTRACT_ID,
- seller_pubkey.as_str(),
- created_at_nostr,
- "order revision proposal",
- );
- Ok(TradeRevisionProposalPlan {
- workflow: order_workflow_plan(
- TradeWorkflowKind::RevisionProposal,
- 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(any(feature = "signer-adapters", test))]
-fn order_revision_decision_plan(
- actor: &RadrootsActorContext,
- root_event: RadrootsEventPtr,
- previous_event: RadrootsEventPtr,
- decision: RadrootsOrderRevisionDecision,
- created_at: RadrootsSdkTimestamp,
-) -> Result<TradeRevisionDecisionPlan, RadrootsSdkError> {
- require_buyer_actor(actor, "trade.prepare_revision_decision")?;
- let root_event_id = order_reference_event_id(&root_event, "root")?;
- let previous_event_id = order_reference_event_id(&previous_event, "previous")?;
- if decision.buyer_pubkey.as_str() != actor.pubkey().as_str() {
+) -> 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_revision_decision".to_owned(),
- reason: "actor pubkey must match order buyer_pubkey".to_owned(),
+ operation: "trade.prepare_decision".to_owned(),
+ reason: "actor pubkey must match order seller_pubkey".to_owned(),
});
}
- require_payload_event_refs(
- "order revision decision",
- &decision.root_event_id,
- &decision.prev_event_id,
- &root_event_id,
- &previous_event_id,
- )?;
+ 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 revision decision")?;
- let draft =
- order::build_order_revision_decision_draft(&root_event_id, &previous_event_id, &decision)
- .expect("validated order revision decision draft encodes");
+ 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_REVISION_DECISION_CONTRACT_ID,
- buyer_pubkey.as_str(),
+ TRADE_DECISION_CONTRACT_ID,
+ decision.seller_pubkey.as_str(),
created_at_nostr,
- "order revision decision",
+ "trade.prepare_decision",
);
- Ok(TradeRevisionDecisionPlan {
+ Ok(TradeDecisionPlan {
workflow: order_workflow_plan(
- TradeWorkflowKind::RevisionDecision,
+ TradeWorkflowKind::Decision,
expected_event_id.clone(),
created_at,
),
@@ -6872,15 +4832,14 @@ fn order_revision_decision_plan(
listing_addr,
buyer_pubkey,
seller_pubkey,
- root_event_id,
- previous_event_id,
+ request_event_id,
expected_event_id,
frozen_draft,
created_at,
})
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_cancellation_plan(
actor: &RadrootsActorContext,
root_event: RadrootsEventPtr,
@@ -6931,7 +4890,7 @@ fn order_cancellation_plan(
})
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_workflow_plan(
kind: TradeWorkflowKind,
expected_event_id: RadrootsEventId,
@@ -6946,7 +4905,7 @@ fn order_workflow_plan(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_submit_receipt(
plan: TradeSubmitPlan,
enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
@@ -6978,7 +4937,7 @@ fn order_submit_receipt(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_decision_receipt(
plan: TradeDecisionPlan,
enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
@@ -7010,73 +4969,7 @@ fn order_decision_receipt(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
-fn order_revision_proposal_receipt(
- plan: TradeRevisionProposalPlan,
- enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> TradeRevisionProposalReceipt {
- 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());
- TradeRevisionProposalReceipt {
- workflow: order_workflow_enqueue_receipt(
- TradeWorkflowKind::RevisionProposal,
- 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(any(feature = "signer-adapters", test))]
-fn order_revision_decision_receipt(
- plan: TradeRevisionDecisionPlan,
- enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> TradeRevisionDecisionReceipt {
- 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());
- TradeRevisionDecisionReceipt {
- workflow: order_workflow_enqueue_receipt(
- TradeWorkflowKind::RevisionDecision,
- 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(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_cancellation_receipt(
plan: TradeCancellationPlan,
enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
@@ -7109,7 +5002,7 @@ fn order_cancellation_receipt(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_workflow_enqueue_receipt(
kind: TradeWorkflowKind,
expected_event_id: RadrootsEventId,
@@ -7142,7 +5035,7 @@ fn order_workflow_enqueue_receipt(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn freeze_order_workflow_draft(
parts: RadrootsNip01EventWireParts,
contract_id: &str,
@@ -7164,7 +5057,7 @@ fn freeze_order_workflow_draft(
(frozen_draft, expected_event_id)
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn validate_order_payload<T>(payload: &T, operation: &'static str) -> Result<(), RadrootsSdkError>
where
T: OrderPayloadValidate,
@@ -7176,14 +5069,14 @@ where
})
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
trait OrderPayloadValidate {
fn validate_order_payload(
&self,
) -> Result<(), radroots_event::order::RadrootsOrderPayloadError>;
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
impl OrderPayloadValidate for RadrootsOrderDecision {
fn validate_order_payload(
&self,
@@ -7192,25 +5085,7 @@ impl OrderPayloadValidate for RadrootsOrderDecision {
}
}
-#[cfg(any(feature = "signer-adapters", test))]
-impl OrderPayloadValidate for RadrootsOrderRevisionProposal {
- fn validate_order_payload(
- &self,
- ) -> Result<(), radroots_event::order::RadrootsOrderPayloadError> {
- self.validate()
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-impl OrderPayloadValidate for RadrootsOrderRevisionDecision {
- fn validate_order_payload(
- &self,
- ) -> Result<(), radroots_event::order::RadrootsOrderPayloadError> {
- self.validate()
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
impl OrderPayloadValidate for RadrootsOrderCancellation {
fn validate_order_payload(
&self,
@@ -7260,7 +5135,7 @@ fn sdk_timestamp_ms(timestamp: RadrootsSdkTimestamp) -> Result<i64, RadrootsSdkE
i64::try_from(millis).map_err(|_| RadrootsSdkError::TimestampOutOfRange { value: seconds })
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn require_decision_request_evidence(
plan: &TradeDecisionPlan,
projection: &RadrootsOrderProjection,
@@ -7289,14 +5164,6 @@ fn require_decision_request_evidence(
),
});
}
- if let Some(pending_revision_event_id) = projection.pending_revision_event_id.as_ref() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "order decision for order {} cannot follow pending revision proposal {}",
- plan.order_id, pending_revision_event_id
- ),
- });
- }
if !projection.issues.is_empty() {
return Err(RadrootsSdkError::InvalidRequest {
message: format!(
@@ -7330,7 +5197,7 @@ fn require_decision_request_evidence(
Ok(())
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
#[derive(Clone, Copy)]
struct OrderLifecycleReferences<'a> {
operation: &'static str,
@@ -7342,53 +5209,7 @@ struct OrderLifecycleReferences<'a> {
previous_event_id: &'a RadrootsEventId,
}
-#[cfg(any(feature = "signer-adapters", test))]
-fn require_revision_proposal_state(
- plan: &TradeRevisionProposalPlan,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- let refs = OrderLifecycleReferences {
- operation: "order revision proposal",
- 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)?;
- require_lifecycle_status(&refs, projection, RadrootsTradeWorkflowState::Requested)?;
- require_no_lifecycle_terminal(&refs, projection)?;
- require_no_pending_revision(&refs, projection)?;
- require_lifecycle_previous_is_current(&refs, projection)
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-fn require_revision_decision_state(
- plan: &TradeRevisionDecisionPlan,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- let refs = OrderLifecycleReferences {
- operation: "order revision decision",
- 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)?;
- require_lifecycle_status(
- &refs,
- projection,
- RadrootsTradeWorkflowState::RevisionProposed,
- )?;
- require_no_lifecycle_terminal(&refs, projection)?;
- require_pending_revision(&refs, projection)?;
- require_lifecycle_previous_is_current(&refs, projection)
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn require_cancellation_state(
plan: &TradeCancellationPlan,
projection: &RadrootsOrderProjection,
@@ -7414,11 +5235,10 @@ fn require_cancellation_state(
));
}
require_no_lifecycle_terminal(&refs, projection)?;
- require_no_pending_revision(&refs, projection)?;
require_lifecycle_previous_is_current(&refs, projection)
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn require_clean_lifecycle_projection(
refs: OrderLifecycleReferences<'_>,
projection: &RadrootsOrderProjection,
@@ -7473,27 +5293,7 @@ fn require_clean_lifecycle_projection(
)
}
-#[cfg(any(feature = "signer-adapters", test))]
-fn require_lifecycle_status(
- refs: &OrderLifecycleReferences<'_>,
- projection: &RadrootsOrderProjection,
- expected: RadrootsTradeWorkflowState,
-) -> Result<(), RadrootsSdkError> {
- if projection.status == expected {
- Ok(())
- } else {
- Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- format!(
- "requires {:?} local state; current state is {:?}",
- expected, projection.status
- ),
- ))
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn require_no_lifecycle_terminal(
refs: &OrderLifecycleReferences<'_>,
projection: &RadrootsOrderProjection,
@@ -7509,48 +5309,7 @@ fn require_no_lifecycle_terminal(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
-fn require_pending_revision(
- refs: &OrderLifecycleReferences<'_>,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- match projection.pending_revision_event_id.as_ref() {
- Some(pending_revision_event_id) if pending_revision_event_id == refs.previous_event_id => {
- Ok(())
- }
- Some(pending_revision_event_id) => Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- format!(
- "previous event {} does not match pending revision proposal {}",
- refs.previous_event_id, pending_revision_event_id
- ),
- )),
- None => Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- "requires pending revision proposal local state",
- )),
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
-fn require_no_pending_revision(
- refs: &OrderLifecycleReferences<'_>,
- projection: &RadrootsOrderProjection,
-) -> Result<(), RadrootsSdkError> {
- if let Some(pending_revision_event_id) = projection.pending_revision_event_id.as_ref() {
- Err(lifecycle_invalid(
- refs.operation,
- refs.order_id,
- format!("cannot follow pending revision proposal {pending_revision_event_id}"),
- ))
- } else {
- Ok(())
- }
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn require_lifecycle_previous_is_current(
refs: &OrderLifecycleReferences<'_>,
projection: &RadrootsOrderProjection,
@@ -7573,7 +5332,7 @@ fn require_lifecycle_previous_is_current(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn lifecycle_invalid(
operation: &'static str,
order_id: &RadrootsOrderId,
@@ -7584,7 +5343,7 @@ fn lifecycle_invalid(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn require_projection_match<T>(
operation: &'static str,
field: &'static str,
@@ -7610,7 +5369,7 @@ where
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn require_buyer_actor(
actor: &RadrootsActorContext,
operation: &'static str,
@@ -7640,7 +5399,7 @@ fn require_seller_actor(
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[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 {
@@ -7649,7 +5408,7 @@ fn listing_event_id(listing_event: &RadrootsEventPtr) -> Result<RadrootsEventId,
})
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[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 {
@@ -7658,7 +5417,7 @@ fn request_event_id(request_event: &RadrootsEventPtr) -> Result<RadrootsEventId,
})
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_reference_event_id(
event: &RadrootsEventPtr,
label: &'static str,
@@ -7668,34 +5427,7 @@ fn order_reference_event_id(
})
}
-#[cfg(any(feature = "signer-adapters", test))]
-fn require_payload_event_refs(
- operation: &'static str,
- payload_root_event_id: &RadrootsEventId,
- payload_previous_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- previous_event_id: &RadrootsEventId,
-) -> Result<(), RadrootsSdkError> {
- if payload_root_event_id != root_event_id {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "{operation} root_event_id {} does not match root evidence {}",
- payload_root_event_id, root_event_id
- ),
- });
- }
- if payload_previous_event_id != previous_event_id {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "{operation} prev_event_id {} does not match previous evidence {}",
- payload_previous_event_id, previous_event_id
- ),
- });
- }
- Ok(())
-}
-
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_canonicalization_error(error: RadrootsOrderCanonicalizationError) -> RadrootsSdkError {
match error {
RadrootsOrderCanonicalizationError::InvalidBuyerSigner => {
@@ -7710,7 +5442,7 @@ fn order_canonicalization_error(error: RadrootsOrderCanonicalizationError) -> Ra
}
}
-#[cfg(any(feature = "signer-adapters", test))]
+#[cfg(feature = "signer-adapters")]
fn order_decision_canonicalization_error(
error: RadrootsOrderCanonicalizationError,
) -> RadrootsSdkError {
@@ -7725,11 +5457,11 @@ impl From<RadrootsTradeWorkflowState> for TradeStatusKind {
match status {
RadrootsTradeWorkflowState::Missing => Self::Missing,
RadrootsTradeWorkflowState::Requested => Self::Requested,
- RadrootsTradeWorkflowState::RevisionProposed => Self::RevisionProposed,
- RadrootsTradeWorkflowState::AgreedPendingRhi => Self::AgreedPendingRhi,
+ RadrootsTradeWorkflowState::AgreedPendingValidation => Self::AgreedPendingValidation,
RadrootsTradeWorkflowState::Committed => Self::Committed,
RadrootsTradeWorkflowState::Declined => Self::Declined,
RadrootsTradeWorkflowState::Cancelled => Self::Cancelled,
+ RadrootsTradeWorkflowState::ValidationExpired => Self::ValidationExpired,
RadrootsTradeWorkflowState::Invalid => Self::Invalid,
}
}
@@ -7808,94 +5540,6 @@ impl From<RadrootsOrderIssue> for SdkTradeStatusIssue {
RadrootsOrderIssue::ConflictingDecisions { event_ids } => {
Self::new(SdkTradeStatusIssueKind::ConflictingDecisions, event_ids)
}
- RadrootsOrderIssue::RevisionProposalPayloadInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalPayloadInvalid,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalOrderIdMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalOrderIdMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalAuthorMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalAuthorMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalCounterpartyMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalCounterpartyMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalBuyerMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalBuyerMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalSellerMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalSellerMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalListingAddressInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalListingAddressInvalid,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalListingMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalListingMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalRootMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalRootMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionProposalPreviousMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionProposalPreviousMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionWithoutProposal { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionWithoutProposal,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionPayloadInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionPayloadInvalid,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionOrderIdMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionOrderIdMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionAuthorMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionAuthorMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionCounterpartyMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionCounterpartyMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionBuyerMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionBuyerMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionSellerMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionSellerMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionListingAddressInvalid { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionListingAddressInvalid,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionListingMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionListingMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionRootMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionRootMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionPreviousMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionPreviousMismatch,
- event_id,
- ),
- RadrootsOrderIssue::RevisionDecisionRevisionIdMismatch { event_id } => Self::single(
- SdkTradeStatusIssueKind::RevisionDecisionRevisionIdMismatch,
- event_id,
- ),
RadrootsOrderIssue::CancellationWithoutCancellableOrder { event_id } => Self::single(
SdkTradeStatusIssueKind::CancellationWithoutCancellableOrder,
event_id,
@@ -8022,6 +5666,6 @@ fn camel_to_snake(value: &str) -> String {
output
}
-#[cfg(all(test, feature = "runtime"))]
+#[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
@@ -247,29 +247,8 @@ async fn apply_up(pool: &SqlitePool) -> Result<(), RadrootsSdkError> {
sqlx::raw_sql(PRIVATE_STORE_MIGRATION_UP)
.execute(pool)
.await
- .map_err(private_store_error)?;
- ensure_farm_location_label_column(pool).await
-}
-
-async fn ensure_farm_location_label_column(pool: &SqlitePool) -> Result<(), RadrootsSdkError> {
- let rows = sqlx::query("PRAGMA table_info(sdk_private_farm_location)")
- .fetch_all(pool)
- .await
- .map_err(private_store_error)?;
- let has_label = rows
- .iter()
- .map(|row| row.try_get::<String, _>("name"))
- .collect::<Result<Vec<_>, _>>()
- .map_err(private_store_error)?
- .iter()
- .any(|name| name == "label");
- if !has_label {
- sqlx::query("ALTER TABLE sdk_private_farm_location ADD COLUMN label TEXT")
- .execute(pool)
- .await
- .map_err(private_store_error)?;
- }
- Ok(())
+ .map(|_| ())
+ .map_err(private_store_error)
}
async fn query_i64(pool: &SqlitePool, sql: &'static str) -> Result<i64, RadrootsSdkError> {
diff --git a/crates/sdk/src/product_clients.rs b/crates/sdk/src/product_clients.rs
@@ -109,19 +109,6 @@ pub struct TradeValidationReceiptsClient<'client> {
#[cfg(feature = "runtime")]
#[derive(Clone, Copy)]
-pub struct DvmClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> DvmClient<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
pub struct SyncClient<'client> {
pub(crate) sdk: &'client RadrootsClient,
}
diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs
@@ -1,8 +1,10 @@
#[cfg(feature = "runtime")]
use crate::private_store::{SDK_PRIVATE_STORE_SCHEMA_VERSION, SdkPrivateStore};
#[cfg(feature = "runtime")]
+use crate::studio_store::{SDK_STUDIO_STORE_SCHEMA_VERSION, SdkStudioStore};
+#[cfg(feature = "runtime")]
use crate::{
- DvmClient, FarmsClient, GeoNamesClient, ListingsClient, MarketClient, RadrootsGeoNamesConfig,
+ FarmsClient, GeoNamesClient, ListingsClient, MarketClient, RadrootsGeoNamesConfig,
RadrootsSdkError, SyncClient, TradesClient, transport::TransportProfile,
};
#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
@@ -15,12 +17,16 @@ use radroots_event_store::RadrootsEventStore;
#[cfg(feature = "runtime")]
use radroots_outbox::RadrootsOutbox;
#[cfg(feature = "runtime")]
-use sqlx::{Row, SqlitePool};
+use sqlx::{
+ Row, SqlitePool,
+ sqlite::{SqliteConnectOptions, SqlitePoolOptions},
+};
#[cfg(feature = "runtime")]
use std::{
fs,
io::ErrorKind,
path::{Component, Path, PathBuf},
+ str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
@@ -29,36 +35,67 @@ const SDK_STORAGE_MANIFEST_VERSION: u16 = 1;
#[cfg(feature = "runtime")]
const SDK_STORAGE_MANIFEST_KIND: SdkBackupManifestKind = SdkBackupManifestKind::StorageBackup;
#[cfg(feature = "runtime")]
-const SDK_EVENT_STORE_SCHEMA_VERSION: i64 = 1;
-#[cfg(feature = "runtime")]
-const SDK_OUTBOX_SCHEMA_VERSION: i64 = 1;
+const SDK_RUNTIME_SCHEMA_VERSION: i64 = 1;
#[cfg(feature = "runtime")]
const SDK_PRIVATE_STORE_SCHEMA_VERSION_CURRENT: i64 = SDK_PRIVATE_STORE_SCHEMA_VERSION;
#[cfg(feature = "runtime")]
-const EVENT_STORE_BACKUP_FILE: &str = "event_store.sqlite";
+const SDK_STUDIO_STORE_SCHEMA_VERSION_CURRENT: i64 = SDK_STUDIO_STORE_SCHEMA_VERSION;
+#[cfg(feature = "runtime")]
+const RUNTIME_SQLITE_FILE: &str = "runtime.sqlite";
#[cfg(feature = "runtime")]
-const OUTBOX_BACKUP_FILE: &str = "outbox.sqlite";
+const PRIVATE_SQLITE_FILE: &str = "private.sqlite";
#[cfg(feature = "runtime")]
-const PRIVATE_STORE_BACKUP_FILE: &str = "private.sqlite";
+const STUDIO_SQLITE_FILE: &str = "studio.sqlite";
#[cfg(feature = "runtime")]
const BACKUP_MANIFEST_FILE: &str = "manifest.json";
-
#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+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,
+ actor_pubkey TEXT NOT NULL,
+ idempotency_key TEXT NOT NULL,
+ request_digest_sha256_hex TEXT NOT NULL,
+ created_at_ms INTEGER NOT NULL,
+ completed_at_ms INTEGER,
+ UNIQUE(contract_version, operation_id, actor_pubkey, idempotency_key)
+);
+
+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
+);
+
+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
+);
+"#;
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub enum RadrootsSdkStorageConfig {
+ #[default]
Memory,
Directory(PathBuf),
}
#[cfg(feature = "runtime")]
-impl Default for RadrootsSdkStorageConfig {
- fn default() -> Self {
- Self::Memory
- }
-}
-
-#[cfg(feature = "runtime")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
pub struct RadrootsSdkTimestamp(u64);
@@ -78,10 +115,11 @@ impl RadrootsSdkTimestamp {
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RadrootsSdkClock {
+ #[default]
System,
Fixed(RadrootsSdkTimestamp),
#[cfg(test)]
@@ -89,13 +127,6 @@ pub enum RadrootsSdkClock {
}
#[cfg(feature = "runtime")]
-impl Default for RadrootsSdkClock {
- fn default() -> Self {
- Self::System
- }
-}
-
-#[cfg(feature = "runtime")]
impl RadrootsSdkClock {
pub fn now(&self) -> Result<RadrootsSdkTimestamp, RadrootsSdkError> {
match self {
@@ -120,9 +151,9 @@ fn sdk_timestamp_from_system_time(
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RadrootsSdkStoragePaths {
- pub event_store_path: PathBuf,
- pub outbox_path: PathBuf,
- pub private_store_path: PathBuf,
+ pub runtime_path: PathBuf,
+ pub private_path: PathBuf,
+ pub studio_path: PathBuf,
}
#[cfg(feature = "runtime")]
@@ -157,6 +188,7 @@ pub struct StorageStatusReceipt {
pub event_store: SdkEventStoreStorageStatus,
pub outbox: SdkOutboxStorageStatus,
pub private_store: SdkPrivateStoreStorageStatus,
+ pub studio_store: SdkStudioStoreStorageStatus,
}
#[cfg(feature = "runtime")]
@@ -176,6 +208,7 @@ pub struct StorageCheckpointReceipt {
pub event_store: SdkSqliteWalCheckpointReceipt,
pub outbox: SdkSqliteWalCheckpointReceipt,
pub private_store: SdkSqliteWalCheckpointReceipt,
+ pub studio_store: SdkSqliteWalCheckpointReceipt,
}
#[cfg(feature = "runtime")]
@@ -243,6 +276,13 @@ pub struct SdkPrivateStoreStorageStatus {
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct SdkStudioStoreStorageStatus {
+ pub store: SdkSqliteStoreStatus,
+ pub studio_state_records: i64,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct BackupRequest {
pub destination: PathBuf,
@@ -269,9 +309,9 @@ impl BackupRequest {
pub struct BackupReceipt {
pub destination: PathBuf,
pub state: SdkBackupState,
- pub event_store_path: Option<PathBuf>,
- pub outbox_path: Option<PathBuf>,
- pub private_store_path: Option<PathBuf>,
+ pub runtime_path: Option<PathBuf>,
+ pub studio_path: Option<PathBuf>,
+ pub private_path: Option<PathBuf>,
pub manifest_path: Option<PathBuf>,
pub manifest: SdkBackupManifest,
}
@@ -313,9 +353,11 @@ pub struct SdkBackupVerification {
pub event_store_ok: bool,
pub outbox_ok: bool,
pub private_store_ok: bool,
+ pub studio_store_ok: bool,
pub event_store_events: i64,
pub outbox_events: i64,
pub private_farm_locations: i64,
+ pub studio_state_records: i64,
}
#[cfg(feature = "runtime")]
@@ -337,9 +379,11 @@ pub struct IntegrityReceipt {
pub event_store_ok: bool,
pub outbox_ok: bool,
pub private_store_ok: bool,
+ pub studio_store_ok: bool,
pub event_store_result: String,
pub outbox_result: String,
pub private_store_result: String,
+ pub studio_store_result: String,
}
#[cfg(feature = "runtime")]
@@ -397,9 +441,9 @@ pub enum SdkRestoreState {
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct RestoreArchive {
pub source: PathBuf,
- pub event_store_path: PathBuf,
- pub outbox_path: PathBuf,
- pub private_store_path: PathBuf,
+ pub runtime_path: PathBuf,
+ pub studio_path: PathBuf,
+ pub private_path: PathBuf,
pub manifest_path: PathBuf,
pub manifest: SdkBackupManifest,
pub verification: SdkBackupVerification,
@@ -412,9 +456,9 @@ pub struct RestoreReceipt {
pub destination: Option<PathBuf>,
pub state: SdkRestoreState,
pub destination_paths: Option<RadrootsSdkStoragePaths>,
- pub event_store_path: PathBuf,
- pub outbox_path: PathBuf,
- pub private_store_path: PathBuf,
+ pub runtime_path: PathBuf,
+ pub studio_path: PathBuf,
+ pub private_path: PathBuf,
pub manifest_path: PathBuf,
pub manifest: SdkBackupManifest,
pub verification: SdkBackupVerification,
@@ -422,6 +466,40 @@ 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,
@@ -495,6 +573,7 @@ impl RadrootsClientBuilder {
_event_store: storage.event_store,
_outbox: storage.outbox,
_private_store: storage.private_store,
+ _studio_store: storage.studio_store,
storage_paths: storage.paths,
geonames: self.geonames,
clock: self.clock,
@@ -511,6 +590,7 @@ pub struct RadrootsClient {
pub(crate) _event_store: RadrootsEventStore,
pub(crate) _outbox: RadrootsOutbox,
pub(crate) _private_store: SdkPrivateStore,
+ pub(crate) _studio_store: SdkStudioStore,
storage_paths: Option<RadrootsSdkStoragePaths>,
geonames: Option<RadrootsGeoNamesConfig>,
clock: RadrootsSdkClock,
@@ -549,10 +629,6 @@ impl RadrootsClient {
SyncClient::new(self)
}
- pub fn dvm(&self) -> DvmClient<'_> {
- DvmClient::new(self)
- }
-
pub fn now(&self) -> Result<RadrootsSdkTimestamp, RadrootsSdkError> {
self.clock.now()
}
@@ -605,12 +681,17 @@ impl RadrootsClient {
_request: StorageStatusRequest,
) -> Result<StorageStatusReceipt, RadrootsSdkError> {
let now_ms = sdk_now_ms(self)?;
+ if let Some(paths) = &self.storage_paths {
+ return directory_storage_status_read_only(paths, now_ms).await;
+ }
let event_store_status = event_store_sqlite_status(&self._event_store).await?;
let outbox_store_status = outbox_sqlite_status(&self._outbox).await?;
let private_store_status = private_store_sqlite_status(&self._private_store).await?;
+ let studio_store_status = studio_store_sqlite_status(&self._studio_store).await?;
let event_summary = event_store_status_summary(&self._event_store).await?;
let outbox_summary = outbox_status_summary(&self._outbox, now_ms).await?;
let private_summary = self._private_store.status_summary().await?;
+ let studio_summary = self._studio_store.status_summary().await?;
Ok(StorageStatusReceipt {
storage: self.storage_kind(),
paths: self.storage_paths.clone(),
@@ -640,9 +721,23 @@ impl RadrootsClient {
store: private_store_status,
farm_private_locations: private_summary.farm_private_locations,
},
+ studio_store: SdkStudioStoreStorageStatus {
+ store: studio_store_status,
+ studio_state_records: studio_summary.studio_state_records,
+ },
})
}
+ pub async fn inspect_storage_status(
+ path: impl Into<PathBuf>,
+ _request: StorageStatusRequest,
+ ) -> Result<StorageStatusReceipt, RadrootsSdkError> {
+ let path = path.into();
+ reject_pre_v1_profile(&path)?;
+ let paths = storage_paths_for_directory(&path);
+ directory_storage_status_read_only(&paths, 0).await
+ }
+
pub async fn storage_checkpoint(
&self,
_request: StorageCheckpointRequest,
@@ -665,12 +760,19 @@ impl RadrootsClient {
SqliteStoreRole::PrivateStore,
)
.await?;
+ let studio_store = sqlite_wal_checkpoint(
+ self._studio_store.pool(),
+ &self._studio_store.pragma_journal_mode().await?,
+ SqliteStoreRole::StudioStore,
+ )
+ .await?;
Ok(StorageCheckpointReceipt {
storage: self.storage_kind(),
paths: self.storage_paths.clone(),
event_store,
outbox,
private_store,
+ studio_store,
})
}
@@ -685,14 +787,17 @@ impl RadrootsClient {
let private_store_integrity =
sqlite_integrity_result(self._private_store.pool(), SqliteStoreRole::PrivateStore)
.await?;
+ let studio_store_integrity =
+ sqlite_integrity_result(self._studio_store.pool(), SqliteStoreRole::StudioStore)
+ .await?;
let checked_paths = self
.storage_paths
.as_ref()
.map(|paths| {
vec![
- paths.event_store_path.clone(),
- paths.outbox_path.clone(),
- paths.private_store_path.clone(),
+ paths.runtime_path.clone(),
+ paths.private_path.clone(),
+ paths.studio_path.clone(),
]
})
.unwrap_or_default();
@@ -701,9 +806,11 @@ impl RadrootsClient {
event_store_ok: event_store_integrity.ok,
outbox_ok: outbox_integrity.ok,
private_store_ok: private_store_integrity.ok,
+ studio_store_ok: studio_store_integrity.ok,
event_store_result: event_store_integrity.result,
outbox_result: outbox_integrity.result,
private_store_result: private_store_integrity.result,
+ studio_store_result: studio_store_integrity.result,
})
}
@@ -716,21 +823,21 @@ impl RadrootsClient {
prepare_backup_destination(&request.destination, request.overwrite)?;
let created_at_ms = sdk_now_ms(self)?;
let backup_paths = RadrootsSdkStoragePaths {
- event_store_path: request.destination.join(EVENT_STORE_BACKUP_FILE),
- outbox_path: request.destination.join(OUTBOX_BACKUP_FILE),
- private_store_path: request.destination.join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: request.destination.join(RUNTIME_SQLITE_FILE),
+ private_path: request.destination.join(PRIVATE_SQLITE_FILE),
+ studio_path: request.destination.join(STUDIO_SQLITE_FILE),
};
let manifest_backup_paths = RadrootsSdkStoragePaths {
- event_store_path: PathBuf::from(EVENT_STORE_BACKUP_FILE),
- outbox_path: PathBuf::from(OUTBOX_BACKUP_FILE),
- private_store_path: PathBuf::from(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: PathBuf::from(RUNTIME_SQLITE_FILE),
+ private_path: PathBuf::from(PRIVATE_SQLITE_FILE),
+ studio_path: PathBuf::from(STUDIO_SQLITE_FILE),
};
let manifest_path = request.destination.join(BACKUP_MANIFEST_FILE);
let source_status = self.storage_status(StorageStatusRequest::new()).await?;
let backup_verification = backup_sqlite_stores(
self._event_store.pool(),
- self._outbox.pool(),
self._private_store.pool(),
+ self._studio_store.pool(),
&backup_paths,
)
.await?;
@@ -788,15 +895,21 @@ impl RadrootsClient {
destination: Some(destination),
state,
destination_paths: Some(destination_paths),
- event_store_path: archive.event_store_path,
- outbox_path: archive.outbox_path,
- private_store_path: archive.private_store_path,
+ runtime_path: archive.runtime_path,
+ studio_path: archive.studio_path,
+ private_path: archive.private_path,
manifest_path: archive.manifest_path,
manifest: archive.manifest,
verification: archive.verification,
restored_paths,
})
}
+
+ pub async fn quarantine_reset_storage(
+ request: QuarantineResetRequest,
+ ) -> Result<QuarantineResetReceipt, RadrootsSdkError> {
+ quarantine_reset_storage(request).await
+ }
}
#[cfg(feature = "runtime")]
@@ -805,7 +918,7 @@ async fn event_store_sqlite_status(
) -> Result<SdkSqliteStoreStatus, RadrootsSdkError> {
sqlite_store_status(
event_store.pool(),
- SDK_EVENT_STORE_SCHEMA_VERSION,
+ SDK_RUNTIME_SCHEMA_VERSION,
event_store.pragma_journal_mode().await?,
event_store.pragma_foreign_keys().await? != 0,
event_store.pragma_busy_timeout().await?,
@@ -820,7 +933,7 @@ async fn outbox_sqlite_status(
) -> Result<SdkSqliteStoreStatus, RadrootsSdkError> {
sqlite_store_status(
outbox.pool(),
- SDK_OUTBOX_SCHEMA_VERSION,
+ SDK_RUNTIME_SCHEMA_VERSION,
outbox.pragma_journal_mode().await?,
outbox.pragma_foreign_keys().await? != 0,
outbox.pragma_busy_timeout().await?,
@@ -845,6 +958,290 @@ async fn private_store_sqlite_status(
}
#[cfg(feature = "runtime")]
+async fn studio_store_sqlite_status(
+ studio_store: &SdkStudioStore,
+) -> Result<SdkSqliteStoreStatus, RadrootsSdkError> {
+ sqlite_store_status(
+ studio_store.pool(),
+ SDK_STUDIO_STORE_SCHEMA_VERSION_CURRENT,
+ studio_store.pragma_journal_mode().await?,
+ studio_store.pragma_foreign_keys().await? != 0,
+ studio_store.pragma_busy_timeout().await?,
+ SqliteStoreRole::StudioStore,
+ )
+ .await
+}
+
+#[cfg(feature = "runtime")]
+async fn directory_storage_status_read_only(
+ paths: &RadrootsSdkStoragePaths,
+ now_ms: i64,
+) -> Result<StorageStatusReceipt, RadrootsSdkError> {
+ let runtime_pool =
+ open_read_only_sqlite_pool(&paths.runtime_path, SqliteStoreRole::RuntimeStore).await?;
+ let private_pool =
+ open_read_only_sqlite_pool(&paths.private_path, SqliteStoreRole::PrivateStore).await?;
+ let studio_pool =
+ open_read_only_sqlite_pool(&paths.studio_path, SqliteStoreRole::StudioStore).await?;
+ let event_store_status = sqlite_store_status_from_pool(
+ &runtime_pool,
+ SDK_RUNTIME_SCHEMA_VERSION,
+ SqliteStoreRole::EventStore,
+ )
+ .await?;
+ let outbox_store_status = sqlite_store_status_from_pool(
+ &runtime_pool,
+ SDK_RUNTIME_SCHEMA_VERSION,
+ SqliteStoreRole::Outbox,
+ )
+ .await?;
+ let private_store_status = sqlite_store_status_from_pool(
+ &private_pool,
+ SDK_PRIVATE_STORE_SCHEMA_VERSION_CURRENT,
+ SqliteStoreRole::PrivateStore,
+ )
+ .await?;
+ let studio_store_status = sqlite_store_status_from_pool(
+ &studio_pool,
+ SDK_STUDIO_STORE_SCHEMA_VERSION_CURRENT,
+ SqliteStoreRole::StudioStore,
+ )
+ .await?;
+ let event_summary = event_store_status_summary_from_pool(&runtime_pool).await?;
+ let outbox_summary = outbox_status_summary_from_pool(&runtime_pool, now_ms).await?;
+ let private_summary = private_store_status_summary_from_pool(&private_pool).await?;
+ let studio_summary = studio_store_status_summary_from_pool(&studio_pool).await?;
+ Ok(StorageStatusReceipt {
+ storage: SdkStorageKind::Directory,
+ paths: Some(paths.clone()),
+ event_store: SdkEventStoreStorageStatus {
+ store: event_store_status,
+ total_events: event_summary.total_events,
+ projection_eligible_events: event_summary.projection_eligible_events,
+ transport_observations: event_summary.transport_observations,
+ last_event_seq: event_summary.last_event_seq,
+ last_event_updated_at_ms: event_summary.last_event_updated_at_ms,
+ },
+ outbox: SdkOutboxStorageStatus {
+ store: outbox_store_status,
+ total_events: outbox_summary.total_events,
+ pending_events: outbox_summary.pending_events,
+ retryable_events: outbox_summary.retryable_events,
+ terminal_events: outbox_summary.terminal_events,
+ failed_terminal_events: outbox_summary.failed_terminal_events,
+ preview_unavailable_events: outbox_summary.preview_unavailable_events,
+ deferred_until_implemented_events: outbox_summary.deferred_until_implemented_events,
+ ready_signed_events: outbox_summary.ready_signed_events,
+ publishing_events: outbox_summary.publishing_events,
+ last_attempt_at_ms: outbox_summary.last_attempt_at_ms,
+ last_error: outbox_summary.last_error,
+ },
+ private_store: SdkPrivateStoreStorageStatus {
+ store: private_store_status,
+ farm_private_locations: private_summary.farm_private_locations,
+ },
+ studio_store: SdkStudioStoreStorageStatus {
+ store: studio_store_status,
+ studio_state_records: studio_summary.studio_state_records,
+ },
+ })
+}
+
+#[cfg(feature = "runtime")]
+async fn open_read_only_sqlite_pool(
+ path: &Path,
+ store_role: SqliteStoreRole,
+) -> Result<SqlitePool, RadrootsSdkError> {
+ let options = SqliteConnectOptions::new()
+ .filename(path)
+ .create_if_missing(false)
+ .read_only(true);
+ SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .map_err(|error| store_role.error(error.to_string()))
+}
+
+#[cfg(feature = "runtime")]
+async fn sqlite_store_status_from_pool(
+ pool: &SqlitePool,
+ schema_version: i64,
+ store_role: SqliteStoreRole,
+) -> Result<SdkSqliteStoreStatus, RadrootsSdkError> {
+ let journal_mode = sqlite_query_string(pool, "PRAGMA journal_mode", store_role).await?;
+ let foreign_keys_enabled =
+ sqlite_query_i64(pool, "PRAGMA foreign_keys", store_role).await? != 0;
+ let busy_timeout_ms = sqlite_query_i64(pool, "PRAGMA busy_timeout", store_role).await?;
+ sqlite_store_status(
+ pool,
+ schema_version,
+ journal_mode,
+ foreign_keys_enabled,
+ busy_timeout_ms,
+ store_role,
+ )
+ .await
+}
+
+#[cfg(feature = "runtime")]
+async fn event_store_status_summary_from_pool(
+ pool: &SqlitePool,
+) -> Result<radroots_event_store::RadrootsEventStoreStatusSummary, RadrootsSdkError> {
+ let row = sqlx::query(
+ "SELECT COUNT(*) AS total_events, COALESCE(SUM(CASE WHEN projection_eligible = 1 THEN 1 ELSE 0 END), 0) AS projection_eligible_events, MAX(seq) AS last_event_seq, MAX(updated_at_ms) AS last_event_updated_at_ms FROM event_envelopes",
+ )
+ .fetch_one(pool)
+ .await
+ .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?;
+ let transport_observations = sqlite_query_i64(
+ pool,
+ "SELECT COUNT(*) FROM event_transport_observation",
+ SqliteStoreRole::EventStore,
+ )
+ .await?;
+ Ok(radroots_event_store::RadrootsEventStoreStatusSummary {
+ total_events: row
+ .try_get("total_events")
+ .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
+ projection_eligible_events: row
+ .try_get("projection_eligible_events")
+ .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
+ transport_observations,
+ last_event_seq: row
+ .try_get("last_event_seq")
+ .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
+ last_event_updated_at_ms: row
+ .try_get("last_event_updated_at_ms")
+ .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
+ })
+}
+
+#[cfg(feature = "runtime")]
+async fn outbox_status_summary_from_pool(
+ pool: &SqlitePool,
+ now_ms: i64,
+) -> Result<radroots_outbox::RadrootsOutboxStatusSummary, RadrootsSdkError> {
+ let row = sqlx::query(
+ "SELECT COUNT(*) AS total_events, COALESCE(SUM(CASE WHEN state IN ('draft_queued', 'signing', 'signed', 'publishing') THEN 1 ELSE 0 END), 0) AS pending_events, COALESCE(SUM(CASE WHEN state IN ('sign_retryable', 'publish_retryable') THEN 1 ELSE 0 END), 0) AS retryable_events, COALESCE(SUM(CASE WHEN state IN ('published', 'failed_terminal', 'cancelled') THEN 1 ELSE 0 END), 0) AS terminal_events, COALESCE(SUM(CASE WHEN state = 'failed_terminal' THEN 1 ELSE 0 END), 0) AS failed_terminal_events, COALESCE(SUM(CASE WHEN state = 'preview_unavailable' THEN 1 ELSE 0 END), 0) AS preview_unavailable_events, COALESCE(SUM(CASE WHEN state = 'deferred_until_implemented' THEN 1 ELSE 0 END), 0) AS deferred_until_implemented_events, COALESCE(SUM(CASE WHEN state = 'publishing' THEN 1 ELSE 0 END), 0) AS publishing_events FROM outbox_event",
+ )
+ .fetch_one(pool)
+ .await
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?;
+ let ready_signed_events = sqlx::query(
+ "SELECT COUNT(*) FROM outbox_event AS event WHERE event.state IN ('signed', 'publish_retryable') AND event.signed_event_json IS NOT NULL AND event.next_attempt_after_ms <= ? AND (event.claim_token IS NULL OR event.claim_expires_at_ms <= ?) AND EXISTS (SELECT 1 FROM outbox_delivery_plan AS plan JOIN outbox_delivery_target AS target ON target.delivery_plan_id = plan.delivery_plan_id WHERE plan.outbox_event_id = event.outbox_event_id AND plan.status = 'queued' AND target.status IN ('pending', 'failed_retryable'))",
+ )
+ .bind(now_ms)
+ .bind(now_ms)
+ .fetch_one(pool)
+ .await
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?
+ .try_get(0)
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?;
+ let last_attempt_at_ms =
+ sqlx::query("SELECT MAX(attempted_at_ms) FROM outbox_delivery_attempt")
+ .fetch_one(pool)
+ .await
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?
+ .try_get(0)
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?;
+ let last_error = sqlx::query(
+ "SELECT last_error FROM outbox_event WHERE last_error IS NOT NULL ORDER BY updated_at_ms DESC, outbox_event_id DESC LIMIT 1",
+ )
+ .fetch_optional(pool)
+ .await
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?
+ .map(|row| row.try_get("last_error"))
+ .transpose()
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?;
+ Ok(radroots_outbox::RadrootsOutboxStatusSummary {
+ total_events: row
+ .try_get("total_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ pending_events: row
+ .try_get("pending_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ retryable_events: row
+ .try_get("retryable_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ terminal_events: row
+ .try_get("terminal_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ failed_terminal_events: row
+ .try_get("failed_terminal_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ preview_unavailable_events: row
+ .try_get("preview_unavailable_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ deferred_until_implemented_events: row
+ .try_get("deferred_until_implemented_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ ready_signed_events,
+ publishing_events: row
+ .try_get("publishing_events")
+ .map_err(|error| SqliteStoreRole::Outbox.error(error.to_string()))?,
+ last_attempt_at_ms,
+ last_error,
+ })
+}
+
+#[cfg(feature = "runtime")]
+async fn private_store_status_summary_from_pool(
+ pool: &SqlitePool,
+) -> Result<crate::private_store::SdkPrivateStoreStatusSummary, RadrootsSdkError> {
+ Ok(crate::private_store::SdkPrivateStoreStatusSummary {
+ farm_private_locations: sqlite_query_i64(
+ pool,
+ "SELECT COUNT(*) FROM sdk_private_farm_location",
+ SqliteStoreRole::PrivateStore,
+ )
+ .await?,
+ })
+}
+
+#[cfg(feature = "runtime")]
+async fn studio_store_status_summary_from_pool(
+ pool: &SqlitePool,
+) -> Result<crate::studio_store::SdkStudioStoreStatusSummary, RadrootsSdkError> {
+ Ok(crate::studio_store::SdkStudioStoreStatusSummary {
+ studio_state_records: sqlite_query_i64(
+ pool,
+ "SELECT COUNT(*) FROM sdk_studio_state",
+ SqliteStoreRole::StudioStore,
+ )
+ .await?,
+ })
+}
+
+#[cfg(feature = "runtime")]
+async fn sqlite_query_i64(
+ pool: &SqlitePool,
+ sql: &'static str,
+ store_role: SqliteStoreRole,
+) -> Result<i64, RadrootsSdkError> {
+ let row = sqlx::query(sql)
+ .fetch_one(pool)
+ .await
+ .map_err(|error| store_role.error(error.to_string()))?;
+ row.try_get(0)
+ .map_err(|error| store_role.error(error.to_string()))
+}
+
+#[cfg(feature = "runtime")]
+async fn sqlite_query_string(
+ pool: &SqlitePool,
+ sql: &'static str,
+ store_role: SqliteStoreRole,
+) -> Result<String, RadrootsSdkError> {
+ let row = sqlx::query(sql)
+ .fetch_one(pool)
+ .await
+ .map_err(|error| store_role.error(error.to_string()))?;
+ row.try_get(0)
+ .map_err(|error| store_role.error(error.to_string()))
+}
+
+#[cfg(feature = "runtime")]
async fn event_store_status_summary(
event_store: &RadrootsEventStore,
) -> Result<radroots_event_store::RadrootsEventStoreStatusSummary, RadrootsSdkError> {
@@ -861,27 +1258,27 @@ async fn outbox_status_summary(
#[cfg(feature = "runtime")]
async fn backup_sqlite_stores(
- event_store_pool: &SqlitePool,
- outbox_pool: &SqlitePool,
+ runtime_pool: &SqlitePool,
private_store_pool: &SqlitePool,
+ studio_store_pool: &SqlitePool,
backup_paths: &RadrootsSdkStoragePaths,
) -> Result<SdkBackupVerification, RadrootsSdkError> {
sqlite_vacuum_into(
- event_store_pool,
- &backup_paths.event_store_path,
- SqliteStoreRole::EventStore,
+ runtime_pool,
+ &backup_paths.runtime_path,
+ SqliteStoreRole::RuntimeStore,
)
.await?;
sqlite_vacuum_into(
- outbox_pool,
- &backup_paths.outbox_path,
- SqliteStoreRole::Outbox,
+ private_store_pool,
+ &backup_paths.private_path,
+ SqliteStoreRole::PrivateStore,
)
.await?;
sqlite_vacuum_into(
- private_store_pool,
- &backup_paths.private_store_path,
- SqliteStoreRole::PrivateStore,
+ studio_store_pool,
+ &backup_paths.studio_path,
+ SqliteStoreRole::StudioStore,
)
.await?;
verify_backup_paths(backup_paths).await
@@ -898,9 +1295,9 @@ fn write_backup_receipt(
Ok(BackupReceipt {
destination,
state: SdkBackupState::Completed,
- event_store_path: Some(backup_paths.event_store_path),
- outbox_path: Some(backup_paths.outbox_path),
- private_store_path: Some(backup_paths.private_store_path),
+ runtime_path: Some(backup_paths.runtime_path),
+ studio_path: Some(backup_paths.studio_path),
+ private_path: Some(backup_paths.private_path),
manifest_path: Some(manifest_path),
manifest,
})
@@ -947,30 +1344,30 @@ async fn inspect_restore_archive(source: PathBuf) -> Result<RestoreArchive, Radr
}
})?;
validate_restore_manifest(&manifest)?;
- let event_store_path = restore_archive_member_path(
+ let runtime_path = restore_archive_member_path(
&source_root,
- &manifest.backup_paths.event_store_path,
- "event store",
+ &manifest.backup_paths.runtime_path,
+ "runtime store",
)?;
- let outbox_path =
- restore_archive_member_path(&source_root, &manifest.backup_paths.outbox_path, "outbox")?;
- let private_store_path = restore_archive_member_path(
+ let studio_path =
+ restore_archive_member_path(&source_root, &manifest.backup_paths.studio_path, "studio")?;
+ let private_path = restore_archive_member_path(
&source_root,
- &manifest.backup_paths.private_store_path,
+ &manifest.backup_paths.private_path,
"private store",
)?;
let verification = verify_backup_paths(&RadrootsSdkStoragePaths {
- event_store_path: event_store_path.clone(),
- outbox_path: outbox_path.clone(),
- private_store_path: private_store_path.clone(),
+ runtime_path: runtime_path.clone(),
+ studio_path: studio_path.clone(),
+ private_path: private_path.clone(),
})
.await?;
validate_restore_verification(&verification, &manifest.backup_verification)?;
Ok(RestoreArchive {
source,
- event_store_path,
- outbox_path,
- private_store_path,
+ runtime_path,
+ studio_path,
+ private_path,
manifest_path,
manifest,
verification,
@@ -1082,7 +1479,11 @@ fn validate_restore_verification(
actual: &SdkBackupVerification,
manifest: &SdkBackupVerification,
) -> Result<(), RadrootsSdkError> {
- if !actual.event_store_ok || !actual.outbox_ok || !actual.private_store_ok {
+ if !actual.event_store_ok
+ || !actual.outbox_ok
+ || !actual.private_store_ok
+ || !actual.studio_store_ok
+ {
return Err(RadrootsSdkError::InvalidRequest {
message: "restore backup stores failed integrity checks".to_owned(),
});
@@ -1165,9 +1566,9 @@ fn preflight_restore_destination(
}
}
Ok(RadrootsSdkStoragePaths {
- event_store_path: destination.join(EVENT_STORE_BACKUP_FILE),
- outbox_path: destination.join(OUTBOX_BACKUP_FILE),
- private_store_path: destination.join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: destination.join(RUNTIME_SQLITE_FILE),
+ studio_path: destination.join(STUDIO_SQLITE_FILE),
+ private_path: destination.join(PRIVATE_SQLITE_FILE),
})
}
@@ -1202,9 +1603,9 @@ async fn restore_archive_to_destination(
message: error.to_string(),
})?;
let staging_paths = RadrootsSdkStoragePaths {
- event_store_path: staging.join(EVENT_STORE_BACKUP_FILE),
- outbox_path: staging.join(OUTBOX_BACKUP_FILE),
- private_store_path: staging.join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: staging.join(RUNTIME_SQLITE_FILE),
+ studio_path: staging.join(STUDIO_SQLITE_FILE),
+ private_path: staging.join(PRIVATE_SQLITE_FILE),
};
if let Err(error) = copy_restore_archive_to_staging(archive, &staging_paths).await {
let _ = remove_existing_restore_path(&staging);
@@ -1262,14 +1663,14 @@ async fn copy_restore_archive_to_staging(
staging_paths: &RadrootsSdkStoragePaths,
) -> Result<(), RadrootsSdkError> {
copy_restore_file(
- &archive.event_store_path,
- &staging_paths.event_store_path,
- "event store",
+ &archive.runtime_path,
+ &staging_paths.runtime_path,
+ "runtime store",
)?;
- copy_restore_file(&archive.outbox_path, &staging_paths.outbox_path, "outbox")?;
+ copy_restore_file(&archive.studio_path, &staging_paths.studio_path, "studio")?;
copy_restore_file(
- &archive.private_store_path,
- &staging_paths.private_store_path,
+ &archive.private_path,
+ &staging_paths.private_path,
"private store",
)?;
let staging_verification = verify_backup_paths(staging_paths).await?;
@@ -1386,6 +1787,7 @@ struct OpenedRuntimeStorage {
event_store: RadrootsEventStore,
outbox: RadrootsOutbox,
private_store: SdkPrivateStore,
+ studio_store: SdkStudioStore,
paths: Option<RadrootsSdkStoragePaths>,
}
@@ -1394,58 +1796,253 @@ async fn open_storage(
storage: &RadrootsSdkStorageConfig,
) -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
match storage {
- RadrootsSdkStorageConfig::Memory => Ok(OpenedRuntimeStorage {
- event_store: RadrootsEventStore::open_memory().await?,
- outbox: RadrootsOutbox::open_memory().await?,
- private_store: SdkPrivateStore::open_memory().await?,
- paths: None,
- }),
+ RadrootsSdkStorageConfig::Memory => open_memory_storage().await,
RadrootsSdkStorageConfig::Directory(path) => open_directory_storage(path).await,
}
}
#[cfg(feature = "runtime")]
+async fn open_memory_storage() -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
+ let runtime_pool = open_runtime_memory_pool().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?;
+ Ok(OpenedRuntimeStorage {
+ event_store,
+ outbox,
+ private_store: SdkPrivateStore::open_memory().await?,
+ studio_store: SdkStudioStore::open_memory().await?,
+ paths: None,
+ })
+}
+
+#[cfg(feature = "runtime")]
async fn open_directory_storage(path: &Path) -> Result<OpenedRuntimeStorage, RadrootsSdkError> {
+ reject_pre_v1_profile(path)?;
fs::create_dir_all(path).map_err(|error| RadrootsSdkError::Io {
path: path.to_path_buf(),
message: error.to_string(),
})?;
- let paths = RadrootsSdkStoragePaths {
- event_store_path: path.join("event_store.sqlite"),
- outbox_path: path.join("outbox.sqlite"),
- private_store_path: path.join("private.sqlite"),
- };
+ let paths = storage_paths_for_directory(path);
+ let runtime_pool = open_runtime_file_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?;
Ok(OpenedRuntimeStorage {
- event_store: RadrootsEventStore::open_file(&paths.event_store_path).await?,
- outbox: RadrootsOutbox::open_file(&paths.outbox_path).await?,
- private_store: SdkPrivateStore::open_file(&paths.private_store_path).await?,
+ event_store,
+ outbox,
+ private_store: SdkPrivateStore::open_file(&paths.private_path).await?,
+ studio_store: SdkStudioStore::open_file(&paths.studio_path).await?,
paths: Some(paths),
})
}
#[cfg(feature = "runtime")]
+fn storage_paths_for_directory(path: &Path) -> RadrootsSdkStoragePaths {
+ RadrootsSdkStoragePaths {
+ runtime_path: path.join(RUNTIME_SQLITE_FILE),
+ private_path: path.join(PRIVATE_SQLITE_FILE),
+ studio_path: path.join(STUDIO_SQLITE_FILE),
+ }
+}
+
+#[cfg(feature = "runtime")]
+async fn open_runtime_memory_pool() -> Result<SqlitePool, RadrootsSdkError> {
+ let options = SqliteConnectOptions::from_str("sqlite::memory:")
+ .map_err(|error| runtime_store_error(error.to_string()))?;
+ SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))
+}
+
+#[cfg(feature = "runtime")]
+async fn open_runtime_file_pool(path: &Path) -> Result<SqlitePool, RadrootsSdkError> {
+ let options = SqliteConnectOptions::new()
+ .filename(path)
+ .create_if_missing(true);
+ SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))
+}
+
+#[cfg(feature = "runtime")]
+async fn apply_sdk_runtime_schema(pool: &SqlitePool) -> Result<(), RadrootsSdkError> {
+ sqlx::raw_sql(SDK_RUNTIME_MIGRATION_UP)
+ .execute(pool)
+ .await
+ .map_err(|error| runtime_store_error(error.to_string()))?;
+ sqlx::query("PRAGMA user_version = 1")
+ .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; use explicit quarantine reset"
+ .to_owned(),
+ });
+ }
+ }
+ 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)?;
+ }
+ 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(),
+ });
+ }
+ 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(),
+ });
+ }
+ Err(error) => {
+ return Err(RadrootsSdkError::Io {
+ path: request.profile.clone(),
+ message: error.to_string(),
+ });
+ }
+ }
+ 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 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()
+ ),
+ });
+ }
+ 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)
+}
+
+#[cfg(feature = "runtime")]
+fn runtime_store_error(message: String) -> RadrootsSdkError {
+ RadrootsSdkError::EventStore { message }
+}
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SqliteStoreRole {
+ RuntimeStore,
EventStore,
Outbox,
PrivateStore,
+ StudioStore,
}
#[cfg(feature = "runtime")]
impl SqliteStoreRole {
fn label(self) -> &'static str {
match self {
+ Self::RuntimeStore => "runtime store",
Self::EventStore => "event store",
Self::Outbox => "outbox",
Self::PrivateStore => "private store",
+ Self::StudioStore => "studio store",
}
}
fn error(self, message: String) -> RadrootsSdkError {
match self {
+ Self::RuntimeStore => RadrootsSdkError::EventStore { message },
Self::EventStore => RadrootsSdkError::EventStore { message },
Self::Outbox => RadrootsSdkError::Outbox { message },
Self::PrivateStore => RadrootsSdkError::PrivateStore { message },
+ Self::StudioStore => RadrootsSdkError::StudioStore { message },
}
}
}
@@ -1612,24 +2209,30 @@ async fn sqlite_vacuum_into(
async fn verify_backup_paths(
paths: &RadrootsSdkStoragePaths,
) -> Result<SdkBackupVerification, RadrootsSdkError> {
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path).await?;
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path).await?;
- let private_store = SdkPrivateStore::open_file(&paths.private_store_path).await?;
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path).await?;
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path).await?;
+ let private_store = SdkPrivateStore::open_file(&paths.private_path).await?;
+ let studio_store = SdkStudioStore::open_file(&paths.studio_path).await?;
let event_store_integrity =
sqlite_integrity_result(event_store.pool(), SqliteStoreRole::EventStore).await?;
let outbox_integrity = sqlite_integrity_result(outbox.pool(), SqliteStoreRole::Outbox).await?;
let private_store_integrity =
sqlite_integrity_result(private_store.pool(), SqliteStoreRole::PrivateStore).await?;
+ let studio_store_integrity =
+ sqlite_integrity_result(studio_store.pool(), SqliteStoreRole::StudioStore).await?;
let event_summary = event_store.status_summary().await?;
let outbox_summary = outbox.status_summary(i64::MAX).await?;
let private_summary = private_store.status_summary().await?;
+ let studio_summary = studio_store.status_summary().await?;
Ok(SdkBackupVerification {
event_store_ok: event_store_integrity.ok,
outbox_ok: outbox_integrity.ok,
private_store_ok: private_store_integrity.ok,
+ studio_store_ok: studio_store_integrity.ok,
event_store_events: event_summary.total_events,
outbox_events: outbox_summary.total_events,
private_farm_locations: private_summary.farm_private_locations,
+ studio_state_records: studio_summary.studio_state_records,
})
}
diff --git a/crates/sdk/src/signer_provider.rs b/crates/sdk/src/signer_provider.rs
@@ -10,7 +10,6 @@ use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent};
use radroots_event::ids::RadrootsPublicKey;
use radroots_event::kinds::{
KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST,
- KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL,
};
use radroots_event::wire::RadrootsNip01EventWire;
use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrKeys};
@@ -28,13 +27,11 @@ use uuid::Uuid;
pub type RadrootsSdkNip46TransportFuture<'a, T> = RadrootsNostrConnectClientTransportFuture<'a, T>;
-pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 7] = [
+pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 5] = [
KIND_FARM,
KIND_LISTING,
KIND_ORDER_REQUEST,
KIND_ORDER_DECISION,
- KIND_ORDER_REVISION_PROPOSAL,
- KIND_ORDER_REVISION_DECISION,
KIND_ORDER_CANCELLATION,
];
pub const RADROOTS_SDK_MYC_NIP46_DEFAULT_REQUEST_TIMEOUT_MS: u64 = 30_000;
@@ -172,7 +169,7 @@ pub struct RadrootsSdkSignReceipt {
pub enum RadrootsSdkSignerProvider {
#[cfg(feature = "local-signer")]
LocalKey(RadrootsSdkLocalKeySigner),
- MycNip46(RadrootsSdkMycNip46Signer),
+ MycNip46(Box<RadrootsSdkMycNip46Signer>),
}
impl RadrootsSdkSignerProvider {
diff --git a/crates/sdk/src/studio_store.rs b/crates/sdk/src/studio_store.rs
@@ -0,0 +1,129 @@
+#![cfg(feature = "runtime")]
+
+use crate::RadrootsSdkError;
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
+use sqlx::{Row, SqlitePool};
+use std::path::Path;
+use std::str::FromStr;
+
+pub(crate) const SDK_STUDIO_STORE_SCHEMA_VERSION: i64 = 1;
+
+const STUDIO_STORE_MIGRATION_UP: &str = r#"
+CREATE TABLE IF NOT EXISTS sdk_studio_state (
+ key TEXT PRIMARY KEY NOT NULL,
+ value_json TEXT NOT NULL,
+ updated_at_ms INTEGER NOT NULL
+);
+"#;
+
+#[derive(Clone)]
+pub(crate) struct SdkStudioStore {
+ pool: SqlitePool,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) struct SdkStudioStoreStatusSummary {
+ pub studio_state_records: i64,
+}
+
+impl SdkStudioStore {
+ pub async fn open_memory() -> Result<Self, RadrootsSdkError> {
+ let options = SqliteConnectOptions::from_str("sqlite::memory:").map_err(studio_error)?;
+ let pool = SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .map_err(studio_error)?;
+ configure_connection(&pool, false).await?;
+ apply_up(&pool).await?;
+ Ok(Self { pool })
+ }
+
+ pub async fn open_file(path: impl AsRef<Path>) -> Result<Self, RadrootsSdkError> {
+ let options = SqliteConnectOptions::new()
+ .filename(path)
+ .create_if_missing(true);
+ let pool = SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .map_err(studio_error)?;
+ configure_connection(&pool, true).await?;
+ apply_up(&pool).await?;
+ Ok(Self { pool })
+ }
+
+ pub fn pool(&self) -> &SqlitePool {
+ &self.pool
+ }
+
+ pub async fn pragma_foreign_keys(&self) -> Result<i64, RadrootsSdkError> {
+ query_i64(&self.pool, "PRAGMA foreign_keys").await
+ }
+
+ pub async fn pragma_busy_timeout(&self) -> Result<i64, RadrootsSdkError> {
+ query_i64(&self.pool, "PRAGMA busy_timeout").await
+ }
+
+ pub async fn pragma_journal_mode(&self) -> Result<String, RadrootsSdkError> {
+ query_string(&self.pool, "PRAGMA journal_mode").await
+ }
+
+ pub async fn status_summary(&self) -> Result<SdkStudioStoreStatusSummary, RadrootsSdkError> {
+ Ok(SdkStudioStoreStatusSummary {
+ studio_state_records: query_i64(&self.pool, "SELECT COUNT(*) FROM sdk_studio_state")
+ .await?,
+ })
+ }
+}
+
+async fn configure_connection(
+ pool: &SqlitePool,
+ file_backed: bool,
+) -> Result<(), RadrootsSdkError> {
+ sqlx::query("PRAGMA foreign_keys = ON")
+ .execute(pool)
+ .await
+ .map_err(studio_error)?;
+ sqlx::query("PRAGMA busy_timeout = 5000")
+ .execute(pool)
+ .await
+ .map_err(studio_error)?;
+ if file_backed {
+ sqlx::query("PRAGMA journal_mode = WAL")
+ .execute(pool)
+ .await
+ .map_err(studio_error)?;
+ }
+ Ok(())
+}
+
+async fn apply_up(pool: &SqlitePool) -> Result<(), RadrootsSdkError> {
+ sqlx::raw_sql(STUDIO_STORE_MIGRATION_UP)
+ .execute(pool)
+ .await
+ .map(|_| ())
+ .map_err(studio_error)
+}
+
+async fn query_i64(pool: &SqlitePool, sql: &'static str) -> Result<i64, RadrootsSdkError> {
+ let row = sqlx::query(sql)
+ .fetch_one(pool)
+ .await
+ .map_err(studio_error)?;
+ row.try_get(0).map_err(studio_error)
+}
+
+async fn query_string(pool: &SqlitePool, sql: &'static str) -> Result<String, RadrootsSdkError> {
+ let row = sqlx::query(sql)
+ .fetch_one(pool)
+ .await
+ .map_err(studio_error)?;
+ row.try_get(0).map_err(studio_error)
+}
+
+fn studio_error(error: impl std::fmt::Display) -> RadrootsSdkError {
+ RadrootsSdkError::StudioStore {
+ message: error.to_string(),
+ }
+}
diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs
@@ -332,21 +332,15 @@ impl SyncProjectionRefreshReceipt {
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SdkRelayAuthPolicy {
+ #[default]
DetectOnly,
}
#[cfg(feature = "runtime")]
-impl Default for SdkRelayAuthPolicy {
- fn default() -> Self {
- Self::DetectOnly
- }
-}
-
-#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub struct PushOutboxRequest {
diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs
@@ -196,7 +196,7 @@ impl TargetPolicy {
Self::MeshScope(scope)
}
- #[cfg(any(feature = "signer-adapters", test))]
+ #[cfg(feature = "signer-adapters")]
pub(crate) fn workflow_target_policy(self) -> Self {
self
}
@@ -544,18 +544,13 @@ impl HybridProfile {
}
}
-#[derive(Clone, PartialEq, Eq)]
+#[derive(Clone, Default, PartialEq, Eq)]
pub enum ProxyAuth {
+ #[default]
None,
BearerToken(String),
}
-impl Default for ProxyAuth {
- fn default() -> Self {
- Self::None
- }
-}
-
impl core::fmt::Debug for ProxyAuth {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
@@ -613,21 +608,24 @@ impl ProxyProfile {
}
}
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
#[non_exhaustive]
pub enum TransportProfile {
+ #[default]
LocalOnly,
- Nostr { profile: NostrProfile },
- ReticulumPreview { profile: ReticulumPreviewProfile },
- Hybrid { profile: HybridProfile },
- Proxy { profile: ProxyProfile },
-}
-
-impl Default for TransportProfile {
- fn default() -> Self {
- Self::LocalOnly
- }
+ Nostr {
+ profile: NostrProfile,
+ },
+ ReticulumPreview {
+ profile: ReticulumPreviewProfile,
+ },
+ Hybrid {
+ profile: HybridProfile,
+ },
+ Proxy {
+ profile: ProxyProfile,
+ },
}
impl TransportProfile {
diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs
@@ -17,6 +17,8 @@ use radroots_outbox::{
RadrootsOutboxReticulumPreviewBehavior, RadrootsOutboxSignedOperationInput,
};
use radroots_transport::{RadrootsTransportKind, RadrootsTransportTarget};
+use sha2::{Digest, Sha256};
+use sqlx::Row;
const SDK_LOCAL_EVENT_ENDPOINT_URI: &str = "local:sdk";
@@ -73,34 +75,37 @@ async fn enqueue_signed_workflow_event(
signed_event: RadrootsSignedEvent,
delivery_plan: SdkResolvedDeliveryPlan,
) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
- let idempotency_key = match request.idempotency_key {
- Some(idempotency_key) => idempotency_key,
- None => SdkIdempotencyKey::derive(
- request.operation_kind,
- request.frozen_draft.expected_event_id_str(),
- request.frozen_draft.expected_pubkey_str(),
- ),
- };
+ let idempotency_key =
+ request
+ .idempotency_key
+ .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 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 preflight_input = signed_outbox_input(
+ let mut tx =
+ sdk._event_store
+ .pool()
+ .begin()
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ record_runtime_operation_journal(
+ &mut tx,
request.operation_kind,
+ request.actor,
request.frozen_draft,
- signed_event.clone(),
- delivery_plan_value.clone(),
- idempotency_key,
- false,
+ &idempotency_key_for_enqueue,
observed_at_ms,
- );
- let preflight = sdk
- ._outbox
- .preflight_signed_operation_idempotency(&preflight_input)
- .await?;
- let partial_failure_digest_prefix =
- digest_prefix(preflight.operation_idempotency_digest.as_str());
+ )
+ .await?;
let local_import_observation = RadrootsTransportObservation::new(
RadrootsTransportKind::Local,
SDK_LOCAL_EVENT_ENDPOINT_URI,
@@ -109,37 +114,35 @@ async fn enqueue_signed_workflow_event(
)?;
let ingest = RadrootsEventIngest::new(signed_event.clone(), observed_at_ms)
.with_observation(local_import_observation);
- let ingest_receipt = sdk._event_store.ingest_event(ingest).await?;
+ let ingest_receipt = sdk
+ ._event_store
+ .ingest_event_in_transaction(&mut tx, ingest)
+ .await?;
let outbox_input = signed_outbox_input(
request.operation_kind,
request.frozen_draft,
signed_event,
delivery_plan_value,
- idempotency_key_for_enqueue,
+ idempotency_key_for_enqueue.clone(),
ingest_receipt.inserted,
observed_at_ms,
);
let outbox_receipt = sdk
._outbox
- .enqueue_signed_operation(outbox_input)
+ .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| {
- if matches!(
- error,
- radroots_outbox::RadrootsOutboxError::IdempotencyConflict { .. }
- ) {
- RadrootsSdkError::partial_outbox_idempotency_conflict_mutation(
- signed_event_id.as_str(),
- request.operation_kind,
- partial_failure_digest_prefix.as_str(),
- )
- } else {
- RadrootsSdkError::partial_outbox_enqueue_mutation(
- signed_event_id.as_str(),
- request.operation_kind,
- partial_failure_digest_prefix.as_str(),
- )
- }
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
})?;
let idempotency_digest_prefix =
digest_prefix(outbox_receipt.operation_idempotency_digest.as_str());
@@ -316,6 +319,100 @@ 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 = ?",
+ )
+ .bind("1")
+ .bind(operation_kind)
+ .bind(actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .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")
+ .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()),
+ });
+ }
+ return Ok(());
+ }
+
+ sqlx::query(
+ "INSERT INTO sdk_runtime_operation_journal(contract_version, operation_id, actor_pubkey, idempotency_key, request_digest_sha256_hex, created_at_ms) VALUES (?, ?, ?, ?, ?, ?)",
+ )
+ .bind("1")
+ .bind(operation_kind)
+ .bind(actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .bind(request_digest.as_str())
+ .bind(observed_at_ms)
+ .execute(&mut **tx)
+ .await
+ .map(|_| ())
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })
+}
+
+async fn complete_runtime_operation_journal(
+ tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
+ operation_kind: &'static str,
+ actor: &RadrootsActorContext,
+ idempotency_key: &SdkIdempotencyKey,
+ observed_at_ms: i64,
+) -> Result<(), RadrootsSdkError> {
+ sqlx::query(
+ "UPDATE sdk_runtime_operation_journal SET completed_at_ms = ? WHERE contract_version = ? AND operation_id = ? AND actor_pubkey = ? AND idempotency_key = ?",
+ )
+ .bind(observed_at_ms)
+ .bind("1")
+ .bind(operation_kind)
+ .bind(actor.pubkey().as_str())
+ .bind(idempotency_key.as_str())
+ .execute(&mut **tx)
+ .await
+ .map(|_| ())
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })
+}
+
+fn runtime_request_digest(
+ operation_kind: &'static str,
+ actor: &RadrootsActorContext,
+ frozen_draft: &RadrootsEventDraft,
+) -> String {
+ 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(),
+ });
+ let bytes =
+ serde_json::to_vec(&digest_document).expect("runtime journal digest document serializes");
+ hex::encode(Sha256::digest(bytes))
+}
+
#[cfg(test)]
#[path = "../tests/unit/workflow_runtime_tests.rs"]
mod tests;
diff --git a/crates/sdk/tests/dvm_runtime.rs b/crates/sdk/tests/dvm_runtime.rs
@@ -1,894 +0,0 @@
-#![cfg(feature = "runtime")]
-
-use radroots_authority::{
- RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity,
-};
-use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
-};
-use radroots_event::{
- RadrootsEventEnvelope, RadrootsEventPtr,
- contract::RadrootsActorRole,
- draft::{RadrootsEventDraft, RadrootsSignedEvent},
- ids::{
- RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
- RadrootsPublicKey,
- },
- kinds::{KIND_LISTING, KIND_TRADE_TRANSITION_PROOF_REQUEST},
- order::{
- RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem,
- RadrootsOrderEconomicLine, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment,
- RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
- },
- wire::RadrootsNip01EventWireParts,
-};
-use radroots_event_store::{RadrootsEventIngest, RadrootsEventStore};
-use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp, radroots_event_from_nostr,
- radroots_nostr_build_event, radroots_nostr_sign_frozen_draft,
-};
-use radroots_outbox::RadrootsOutbox;
-use radroots_sdk::{
- DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND, DvmTradeTransitionProofEnqueueRequest,
- DvmTradeTransitionProofPrepareRequest, DvmValidationReceiptIngestRequest, NostrRelayUrlPolicy,
- RadrootsClient, RadrootsSdkError, RadrootsSdkStorageConfig, RadrootsSdkTimestamp,
- RadrootsTradeInventoryBinWitnessDto, RadrootsTradeValidationTrustState, SdkMutationState,
- TargetPolicy, TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest,
-};
-#[cfg(feature = "signer-adapters")]
-use radroots_sdk::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
-use radroots_trade::validation_receipt::{
- RadrootsTradeCommitmentConfidence, RadrootsTradeValidationReceipt,
- RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem,
- RadrootsValidationReceiptResult, RadrootsValidationReceiptStatement,
- RadrootsValidationReceiptType, validation_receipt_event_build,
- validation_receipt_public_values_hash_hex,
-};
-
-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";
-
-#[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) -> &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 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")
-}
-
-#[tokio::test]
-async fn dvm_trade_transition_proof_request_enqueues_signed_job() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let signer = FixtureSigner::new(SERVICE_SECRET_KEY_HEX);
- let actor = service_actor(&signer);
- let listing_event_id = deterministic_event_id("listing-event");
- let request_event_id = deterministic_event_id("request-event");
- let decision_event_id = deterministic_event_id("decision-event");
- let request = DvmTradeTransitionProofEnqueueRequest::new(
- actor,
- signer.pubkey().clone(),
- listing_address(),
- listing_event_id.clone(),
- request_event_id.clone(),
- decision_event_id.clone(),
- inventory_bins(),
- explicit_relays(),
- )
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_050))
- .with_inventory_sequence(7);
-
- let receipt = sdk
- .dvm()
- .enqueue_trade_transition_proof_request_with_explicit_signer(request, &signer)
- .await
- .expect("enqueue DVM proof request");
-
- assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
- assert_eq!(receipt.local_event_seq, 1);
- assert_eq!(receipt.listing_event_id, listing_event_id);
- assert_eq!(receipt.request_event_id, request_event_id);
- assert_eq!(receipt.decision_event_id, decision_event_id);
- assert_eq!(
- outbox_operation_kind(&sdk, receipt.outbox_operation_id).await,
- DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND
- );
-
- let stored = store
- .get_event(receipt.signed_event_id.as_str())
- .await
- .expect("get event")
- .expect("stored DVM request");
- let content: serde_json::Value =
- serde_json::from_str(&stored.content).expect("proof request content");
- let tags: Vec<Vec<String>> = serde_json::from_str(&stored.tags_json).expect("stored tags");
- assert_eq!(stored.kind, KIND_TRADE_TRANSITION_PROOF_REQUEST);
- assert_eq!(content["proof_mode"], "none");
- assert_eq!(content["inventory_sequence"], 7);
- assert!(
- tags.iter()
- .any(|tag| tag == &vec!["p".to_owned(), signer.pubkey().as_str().to_owned()])
- );
- assert!(
- tags.iter()
- .any(|tag| tag.first().map(String::as_str) == Some("a"))
- );
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
-}
-
-#[cfg(feature = "signer-adapters")]
-#[tokio::test]
-async fn dvm_trade_transition_proof_request_uses_configured_local_signer() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let signer_keys = keys_from_secret(SERVICE_SECRET_KEY_HEX);
- let signer_pubkey = signer_keys.public_key().to_hex();
- let sdk = RadrootsClient::builder()
- .storage(RadrootsSdkStorageConfig::Directory(
- tempdir.path().join("sdk"),
- ))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::new(signer_keys).expect("local signer"),
- ))
- .build()
- .await
- .expect("sdk");
- let request = DvmTradeTransitionProofEnqueueRequest::new(
- RadrootsActorContext::test(signer_pubkey.as_str(), [RadrootsActorRole::Service])
- .expect("service actor"),
- signer_pubkey.parse().expect("worker pubkey"),
- listing_address(),
- deterministic_event_id("listing-event"),
- deterministic_event_id("request-event"),
- deterministic_event_id("decision-event"),
- inventory_bins(),
- explicit_relays(),
- )
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_050));
-
- let receipt = sdk
- .dvm()
- .enqueue_trade_transition_proof_request(request)
- .await
- .expect("configured signer enqueue");
-
- assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(
- outbox_operation_kind(&sdk, receipt.outbox_operation_id).await,
- DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND
- );
-}
-
-#[cfg(feature = "signer-adapters")]
-#[tokio::test]
-async fn dvm_configured_enqueue_reports_prepare_and_target_errors_without_mutation() {
- let (tempdir, sdk, store) = directory_sdk_and_store_with_signer().await;
- let signer_keys = keys_from_secret(SERVICE_SECRET_KEY_HEX);
- let signer_pubkey = signer_keys.public_key().to_hex();
- let invalid = DvmTradeTransitionProofEnqueueRequest::new(
- RadrootsActorContext::test(signer_pubkey.as_str(), [RadrootsActorRole::Service])
- .expect("service actor"),
- signer_pubkey.parse().expect("worker pubkey"),
- listing_address(),
- deterministic_event_id("listing-event"),
- deterministic_event_id("request-event"),
- deterministic_event_id("decision-event"),
- Vec::new(),
- explicit_relays(),
- );
- let error = sdk
- .dvm()
- .enqueue_trade_transition_proof_request(invalid)
- .await
- .expect_err("prepare failure");
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
-
- let missing_relays = DvmTradeTransitionProofEnqueueRequest::new(
- RadrootsActorContext::test(signer_pubkey.as_str(), [RadrootsActorRole::Service])
- .expect("service actor"),
- signer_pubkey.parse().expect("worker pubkey"),
- listing_address(),
- deterministic_event_id("listing-event"),
- deterministic_event_id("request-event"),
- deterministic_event_id("decision-event"),
- inventory_bins(),
- TargetPolicy::default_profile(),
- );
- let error = sdk
- .dvm()
- .enqueue_trade_transition_proof_request(missing_relays)
- .await
- .expect_err("missing configured relays");
- assert!(matches!(
- error,
- RadrootsSdkError::EmptyTransportTargets { .. }
- ));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- drop(tempdir);
-}
-
-#[tokio::test]
-async fn dvm_validation_receipt_ingest_commits_pending_trade_status() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-dvm-ingest", 10);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let decision_event = signed_order_decision_event("order-dvm-ingest", &request_event_id, 11);
- let decision_event_id = RadrootsEventId::parse(decision_event.id_str()).expect("decision id");
- for (event, observed_at_ms) in [
- (request_event.clone(), 1_000),
- (decision_event.clone(), 1_100),
- ] {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- observed_at_ms,
- ))
- .await
- .expect("ingest order event");
- }
- let pending = sdk
- .trades()
- .status(status_request("order-dvm-ingest"))
- .await
- .expect("pending status");
- assert_eq!(pending.status, TradeStatusKind::AgreedPendingRhi);
- assert!(pending.rhi_receipt_event_id.is_none());
-
- let listing_event_id = deterministic_event_id("listing-event");
- let receipt_event = signed_validation_receipt_event(
- "order-dvm-ingest",
- &listing_event_id,
- &request_event_id,
- &decision_event_id,
- 12,
- );
- let receipt_event_id = RadrootsEventId::parse(receipt_event.id_str()).expect("receipt id");
- let ingest = sdk
- .dvm()
- .ingest_validation_receipt(
- DvmValidationReceiptIngestRequest::new(signed_event_from_envelope(receipt_event))
- .with_expected_order_id(order_id("order-dvm-ingest"))
- .with_expected_listing_event_id(listing_event_id.clone())
- .with_expected_root_event_id(request_event_id.clone())
- .with_expected_target_event_id(decision_event_id.clone())
- .with_observed_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_060)),
- )
- .await
- .expect("ingest validation receipt");
-
- assert_eq!(ingest.receipt_event_id, receipt_event_id);
- assert_eq!(ingest.order_id.as_str(), "order-dvm-ingest");
- assert_eq!(ingest.listing_event_id, listing_event_id);
- assert_eq!(ingest.root_event_id, request_event_id);
- assert_eq!(ingest.target_event_id, decision_event_id);
- assert_eq!(
- ingest.receipt_type,
- RadrootsValidationReceiptType::TradeTransition
- );
- assert_eq!(ingest.result, RadrootsValidationReceiptResult::Valid);
- assert_eq!(
- ingest.proof_system,
- RadrootsValidationReceiptProofSystem::None
- );
- assert_eq!(ingest.validation_authority, None);
- assert_eq!(
- ingest.commitment_confidence,
- RadrootsTradeCommitmentConfidence::LocalOnly
- );
- assert_eq!(ingest.local_event_seq, 3);
- assert!(ingest.inserted);
- assert_eq!(ingest.refresh.validation_receipts, 1);
- assert_eq!(ingest.refresh.trade_upserts, 1);
-
- let committed = sdk
- .trades()
- .status(status_request("order-dvm-ingest"))
- .await
- .expect("committed status");
- assert_eq!(committed.status, TradeStatusKind::AgreedPendingRhi);
- assert!(!committed.lifecycle_terminal);
- assert_eq!(
- committed.next_action,
- TradeStatusNextActionKind::AwaitRhiValidation
- );
- assert_eq!(
- committed.rhi_receipt_event_id,
- Some(receipt_event_id.clone())
- );
- assert_eq!(committed.last_event_id, Some(decision_event_id));
- let trust = committed.validation_trust.expect("validation trust");
- assert_eq!(trust.state, RadrootsTradeValidationTrustState::Untrusted);
- assert_eq!(
- trust.reason_code.as_deref(),
- Some("validation_trust_policy_empty")
- );
- assert!(!trust.production_committed);
- assert_eq!(trust.proof_system.as_deref(), Some("none"));
-}
-
-#[tokio::test]
-async fn dvm_validation_receipt_ingest_rejects_binding_mismatch_without_mutation() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let receipt_event = signed_validation_receipt_event(
- "order-dvm-mismatch",
- &deterministic_event_id("listing-event"),
- &deterministic_event_id("request-event"),
- &deterministic_event_id("decision-event"),
- 12,
- );
-
- let error = sdk
- .dvm()
- .ingest_validation_receipt(
- DvmValidationReceiptIngestRequest::new(signed_event_from_envelope(receipt_event))
- .with_expected_order_id(order_id("other-order")),
- )
- .await
- .expect_err("binding mismatch");
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-#[tokio::test]
-async fn dvm_trade_transition_proof_request_reports_prepare_and_target_errors_without_mutation() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let signer = FixtureSigner::new(SERVICE_SECRET_KEY_HEX);
- let actor = service_actor(&signer);
- let invalid = DvmTradeTransitionProofEnqueueRequest::new(
- actor.clone(),
- signer.pubkey().clone(),
- listing_address(),
- deterministic_event_id("listing-event"),
- deterministic_event_id("request-event"),
- deterministic_event_id("decision-event"),
- Vec::new(),
- explicit_relays(),
- );
- let error = sdk
- .dvm()
- .enqueue_trade_transition_proof_request_with_explicit_signer(invalid, &signer)
- .await
- .expect_err("prepare failure");
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
-
- let missing_relays = DvmTradeTransitionProofEnqueueRequest::new(
- actor,
- signer.pubkey().clone(),
- listing_address(),
- deterministic_event_id("listing-event"),
- deterministic_event_id("request-event"),
- deterministic_event_id("decision-event"),
- inventory_bins(),
- TargetPolicy::default_profile(),
- );
- let error = sdk
- .dvm()
- .enqueue_trade_transition_proof_request_with_explicit_signer(missing_relays, &signer)
- .await
- .expect_err("missing configured relays");
- assert!(matches!(
- error,
- RadrootsSdkError::EmptyTransportTargets { .. }
- ));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-#[tokio::test]
-async fn dvm_validation_receipt_ingest_reports_timestamp_and_refresh_errors() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let overflow_observed = signed_validation_receipt_event(
- "order-dvm-observed-overflow",
- &deterministic_event_id("listing-event"),
- &deterministic_event_id("request-event"),
- &deterministic_event_id("decision-event"),
- 12,
- );
- let error = sdk
- .dvm()
- .ingest_validation_receipt(
- DvmValidationReceiptIngestRequest::new(signed_event_from_envelope(overflow_observed))
- .with_observed_at(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)),
- )
- .await
- .expect_err("observed overflow");
- assert!(matches!(
- error,
- RadrootsSdkError::TimestampOutOfRange { value } if value == u64::MAX
- ));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-
- let (_tempdir, sdk, store) =
- directory_sdk_and_store_with_clock(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)).await;
- let default_observed = signed_validation_receipt_event(
- "order-dvm-default-observed-overflow",
- &deterministic_event_id("listing-event"),
- &deterministic_event_id("request-event"),
- &deterministic_event_id("decision-event"),
- 12,
- );
- let error = sdk
- .dvm()
- .ingest_validation_receipt(DvmValidationReceiptIngestRequest::new(
- signed_event_from_envelope(default_observed),
- ))
- .await
- .expect_err("default observed overflow");
- assert!(matches!(
- error,
- RadrootsSdkError::TimestampOutOfRange { value } if value == u64::MAX
- ));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-
- let (_tempdir, sdk, store) =
- directory_sdk_and_store_with_clock(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)).await;
- let refresh_overflow = signed_validation_receipt_event(
- "order-dvm-refresh-overflow",
- &deterministic_event_id("listing-event"),
- &deterministic_event_id("request-event"),
- &deterministic_event_id("decision-event"),
- 12,
- );
- let error = sdk
- .dvm()
- .ingest_validation_receipt(
- DvmValidationReceiptIngestRequest::new(signed_event_from_envelope(refresh_overflow))
- .with_observed_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)),
- )
- .await
- .expect_err("refresh overflow");
- assert!(matches!(
- error,
- RadrootsSdkError::TimestampOutOfRange { value } if value == u64::MAX
- ));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
-}
-
-#[tokio::test]
-async fn dvm_prepare_and_ingest_use_sdk_clock_defaults() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let signer = FixtureSigner::new(SERVICE_SECRET_KEY_HEX);
- let plan = sdk
- .dvm()
- .prepare_trade_transition_proof_request(DvmTradeTransitionProofPrepareRequest::new(
- service_actor(&signer),
- signer.pubkey().clone(),
- listing_address(),
- deterministic_event_id("listing-event"),
- deterministic_event_id("request-event"),
- deterministic_event_id("decision-event"),
- inventory_bins(),
- ))
- .expect("default timestamp plan");
- assert_eq!(
- plan.created_at,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)
- );
-
- let receipt_event = signed_validation_receipt_event(
- "order-dvm-clock-default",
- &deterministic_event_id("listing-event"),
- &deterministic_event_id("request-event"),
- &deterministic_event_id("decision-event"),
- 12,
- );
- let ingest = sdk
- .dvm()
- .ingest_validation_receipt(DvmValidationReceiptIngestRequest::new(
- signed_event_from_envelope(receipt_event),
- ))
- .await
- .expect("default observed timestamp ingest");
-
- assert_eq!(ingest.local_event_seq, 1);
-}
-
-#[tokio::test]
-async fn dvm_validation_receipt_ingest_rejects_invalid_verified_order_id() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let receipt_event = signed_validation_receipt_event(
- "bad order id",
- &deterministic_event_id("listing-event"),
- &deterministic_event_id("request-event"),
- &deterministic_event_id("decision-event"),
- 12,
- );
-
- let error = sdk
- .dvm()
- .ingest_validation_receipt(DvmValidationReceiptIngestRequest::new(
- signed_event_from_envelope(receipt_event),
- ))
- .await
- .expect_err("invalid order id");
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-#[tokio::test]
-async fn dvm_validation_receipt_ingest_rejects_invalid_receipt_event_id() {
- let (_tempdir, _sdk, store) = directory_sdk_and_store().await;
- let receipt_event = signed_validation_receipt_event(
- "order-dvm-bad-receipt-id",
- &deterministic_event_id("listing-event"),
- &deterministic_event_id("request-event"),
- &deterministic_event_id("decision-event"),
- 12,
- );
- let mut wire = receipt_event.to_nip01_wire();
- wire.id = "bad id".to_owned();
- let raw_json = serde_json::to_string(&wire).expect("raw event json");
- let error = RadrootsSignedEvent::from_wire_verified_id(wire, raw_json)
- .expect_err("invalid receipt event id");
-
- assert!(error.to_string().contains("event wire id mismatch"));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-}
-
-async fn directory_sdk_and_store() -> (tempfile::TempDir, RadrootsClient, RadrootsEventStore) {
- directory_sdk_and_store_with_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)).await
-}
-
-async fn directory_sdk_and_store_with_clock(
- timestamp: RadrootsSdkTimestamp,
-) -> (tempfile::TempDir, RadrootsClient, RadrootsEventStore) {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let sdk = RadrootsClient::builder()
- .storage(RadrootsSdkStorageConfig::Directory(
- tempdir.path().join("sdk"),
- ))
- .fixed_clock(timestamp)
- .build()
- .await
- .expect("sdk");
- let store =
- RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").event_store_path)
- .await
- .expect("event store");
- (tempdir, sdk, store)
-}
-
-#[cfg(feature = "signer-adapters")]
-async fn directory_sdk_and_store_with_signer()
--> (tempfile::TempDir, RadrootsClient, RadrootsEventStore) {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let signer_keys = keys_from_secret(SERVICE_SECRET_KEY_HEX);
- let sdk = RadrootsClient::builder()
- .storage(RadrootsSdkStorageConfig::Directory(
- tempdir.path().join("sdk"),
- ))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::new(signer_keys).expect("local signer"),
- ))
- .build()
- .await
- .expect("sdk");
- let store =
- RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").event_store_path)
- .await
- .expect("event store");
- (tempdir, sdk, store)
-}
-
-fn service_actor(signer: &FixtureSigner) -> RadrootsActorContext {
- RadrootsActorContext::test(signer.pubkey().as_str(), [RadrootsActorRole::Service])
- .expect("service actor")
-}
-
-fn explicit_relays() -> TargetPolicy {
- TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("relay targets")
-}
-
-fn inventory_bins() -> Vec<RadrootsTradeInventoryBinWitnessDto> {
- vec![RadrootsTradeInventoryBinWitnessDto {
- bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- listing_capacity: 5,
- previous_reserved: 1,
- }]
-}
-
-fn order_id(raw: &str) -> RadrootsOrderId {
- RadrootsOrderId::parse(raw).expect("order id")
-}
-
-fn status_request(raw: &str) -> TradeStatusRequest {
- TradeStatusRequest::parse(raw).expect("status request")
-}
-
-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()),
- }
-}
-
-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 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(),
- }
-}
-
-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,
- }],
- },
- }
-}
-
-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)
-}
-
-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_validation_receipt_event(
- raw_order_id: &str,
- listing_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- target_event_id: &RadrootsEventId,
- created_at: u32,
-) -> RadrootsEventEnvelope {
- 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: RadrootsValidationReceiptProof {
- inline_proof_base64: None,
- mode: None,
- program_hash: None,
- proof_reference: None,
- system: RadrootsValidationReceiptProofSystem::None,
- verifying_key_hash: None,
- },
- 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(),
- statement_type: RadrootsValidationReceiptType::TradeTransition,
- },
- version: 1,
- };
- let parts = validation_receipt_event_build(raw_order_id, &receipt).expect("receipt event");
- signed_event(SERVICE_SECRET_KEY_HEX, created_at, parts)
-}
-
-fn signed_event(
- secret_key_hex: &str,
- created_at: u32,
- parts: RadrootsNip01EventWireParts,
-) -> RadrootsEventEnvelope {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
- let event = 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");
- radroots_event_from_nostr(&event)
-}
-
-#[cfg(feature = "signer-adapters")]
-fn keys_from_secret(secret_key_hex: &str) -> RadrootsNostrKeys {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- RadrootsNostrKeys::new(secret_key)
-}
-
-fn hash32(ch: char) -> String {
- format!("0x{}", ch.to_string().repeat(64))
-}
-
-async fn outbox_operation_kind(sdk: &RadrootsClient, operation_id: i64) -> String {
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- outbox
- .get_operation(operation_id)
- .await
- .expect("outbox operation")
- .expect("outbox operation")
- .operation_kind
-}
diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs
@@ -264,7 +264,7 @@ async fn farm_prepare_publish_is_side_effect_free() {
);
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
assert_eq!(
@@ -282,7 +282,7 @@ async fn farm_prepare_publish_is_side_effect_free() {
.expect("event lookup")
.is_none()
);
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
assert!(
@@ -630,7 +630,7 @@ async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_prof
farm(FARM_B_D_TAG, "North Farm"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("farm-idem-b")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000229")
.expect("idempotency key");
let prepared = sdk
.farms()
@@ -655,7 +655,7 @@ async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_prof
assert!(receipt.idempotency_digest_prefix.is_some());
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
let status = event_store
@@ -675,7 +675,7 @@ async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_prof
Some("radroots.farm.profile.v1")
);
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
let outbox_event = outbox
@@ -711,7 +711,7 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation()
assert!(!message.contains("ffff"));
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
assert_eq!(
@@ -722,7 +722,7 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation()
.total_events,
0
);
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
assert!(
@@ -735,7 +735,7 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation()
}
#[tokio::test]
-async fn farm_enqueue_publish_derives_order_independent_idempotency_key() {
+async fn farm_enqueue_publish_uses_explicit_idempotency_key_across_equivalent_target_order() {
let (_tempdir, sdk) = directory_sdk().await;
let first = FarmEnqueuePublishRequest::new(
farmer_actor(),
@@ -743,7 +743,9 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() {
TargetPolicy::default_profile(),
)
.try_with_nostr_targets([RELAY_B, RELAY], NostrRelayUrlPolicy::Public)
- .expect("first transport targets");
+ .expect("first transport targets")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022d")
+ .expect("first idempotency key");
let second = FarmEnqueuePublishRequest::new(
farmer_actor(),
farm(FARM_D_D_TAG, "North Farm"),
@@ -751,7 +753,9 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() {
TargetSet::nostr_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
.expect("second transport targets"),
),
- );
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022d")
+ .expect("second idempotency key");
let first_receipt = sdk
.farms()
@@ -775,7 +779,7 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() {
assert_eq!(second_receipt.state, SdkMutationState::AlreadyQueued);
let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
let relay_urls = outbox
@@ -797,7 +801,9 @@ async fn farm_enqueue_publish_pushes_queued_event_with_mock_relay_sync() {
TargetPolicy::default_profile(),
)
.try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets");
+ .expect("transport targets")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022e")
+ .expect("idempotency key");
let enqueue_receipt = sdk
.farms()
.enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(FARMER))
@@ -837,7 +843,7 @@ async fn farm_enqueue_publish_pushes_queued_event_with_mock_relay_sync() {
);
assert_eq!(adapter.captured_raw_events().len(), 1);
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stored = outbox
@@ -858,7 +864,9 @@ async fn farm_hybrid_profile_publishes_after_nostr_success_and_retains_reticulum
farmer_actor(),
farm(FARM_E_D_TAG, "Hybrid Farm"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022f")
+ .expect("idempotency key"),
&FixtureSigner::new(FARMER),
)
.await
@@ -891,7 +899,7 @@ async fn farm_hybrid_profile_publishes_after_nostr_success_and_retains_reticulum
);
assert_eq!(adapter.captured_raw_events().len(), 1);
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stored = outbox
@@ -931,17 +939,17 @@ async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mut
farm(FARM_E_D_TAG, "North Farm"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("farm-idem-e")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022a")
.expect("idempotency key");
sdk.farms()
.enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(FARMER))
.await
.expect("first enqueue");
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
assert_eq!(
@@ -966,7 +974,7 @@ async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mut
farm(FARM_F_D_TAG, "Changed Farm"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("farm-idem-e")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022a")
.expect("idempotency key");
let error = sdk
.farms()
@@ -983,7 +991,11 @@ async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mut
error.recovery_actions(),
vec![RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey]
);
- assert!(!error.to_string().contains("farm-idem-e"));
+ assert!(
+ !error
+ .to_string()
+ .contains("01890f0e-6c00-7000-8000-00000000022a")
+ );
assert_eq!(
event_store
.status_summary()
@@ -1043,7 +1055,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
.try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
.expect("relay targets")
.with_idempotency_key(
- SdkIdempotencyKey::new("farm-serialized-idempotency").expect("idempotency"),
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000022b").expect("idempotency"),
)
.with_created_at(created_at);
let enqueue_json = serde_json::to_value(&enqueue_request).expect("enqueue request json");
@@ -1091,14 +1103,14 @@ async fn farm_runtime_dtos_serialize_deterministically() {
"a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
]
},
- "idempotency_key": { "value": "<redacted>", "len": 27 },
+ "idempotency_key": { "value": "<redacted>", "len": 36 },
"created_at": 1_700_000_123
})
);
assert!(
!enqueue_json
.to_string()
- .contains("farm-serialized-idempotency")
+ .contains("01890f0e-6c00-7000-8000-00000000022b")
);
let try_key_request = FarmEnqueuePublishRequest::new(
@@ -1106,11 +1118,11 @@ async fn farm_runtime_dtos_serialize_deterministically() {
farm(FARM_C_D_TAG, "Queued Farm"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("farm-serialized-try-key")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022c")
.expect("try idempotency key");
assert_eq!(
serde_json::to_value(&try_key_request).expect("try key request json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 23 })
+ serde_json::json!({ "value": "<redacted>", "len": 36 })
);
let private_upsert = FarmPrivateLocationUpsertRequest::new(
diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs
@@ -27,8 +27,9 @@ use radroots_sdk::{
RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, ReticulumPreviewProfile, SdkIdempotencyKey,
SdkMutationState, TargetPolicy, TargetSet, TransportProfile,
};
-use radroots_trade::listing::RadrootsListingDraftDocumentV1;
+use radroots_trade::listing::RadrootsListingEditDocumentV1;
use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsNostrTransport};
+use sqlx::Row;
#[path = "support/serializer_failure.rs"]
mod serializer_failure;
@@ -219,7 +220,7 @@ async fn prepare_publish_is_side_effect_free() {
);
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
assert!(
@@ -229,7 +230,7 @@ async fn prepare_publish_is_side_effect_free() {
.expect("event lookup")
.is_none()
);
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
assert!(
@@ -263,7 +264,7 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish()
listing(LISTING_B_D_TAG, "Coffee"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("idem-b")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000224")
.expect("idempotency key");
let prepared = sdk
.listings()
@@ -281,7 +282,6 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish()
assert_eq!(receipt.expected_event_id, prepared.expected_event_id);
assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
assert_eq!(receipt.public_listing_addr, prepared.public_listing_addr);
- assert_eq!(receipt.draft_listing_addr, prepared.draft_listing_addr);
assert_eq!(receipt.local_event_seq, 1);
assert_eq!(receipt.outbox_operation_id, 1);
assert_eq!(receipt.outbox_event_id, 1);
@@ -289,7 +289,7 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish()
assert!(receipt.idempotency_digest_prefix.is_some());
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
assert!(
@@ -300,7 +300,7 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish()
.is_some()
);
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
let outbox_event = outbox
@@ -351,7 +351,10 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() {
&actor,
prepared.clone(),
TargetPolicy::default_profile(),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000238")
+ .expect("idempotency"),
+ ),
&FixtureSigner::new(SELLER),
)
.await
@@ -361,7 +364,7 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() {
assert_eq!(receipt.signed_event_id, prepared.expected_event_id);
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
assert!(
@@ -372,7 +375,7 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() {
.is_some()
);
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
let outbox_event = outbox
@@ -391,7 +394,7 @@ async fn enqueue_receipt_debug_omits_signed_event_payload_material() {
listing(LISTING_A_D_TAG, "Coffee"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("debug-secret-idempotency")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000225")
.expect("idempotency key");
let receipt = sdk
.listings()
@@ -402,7 +405,7 @@ async fn enqueue_receipt_debug_omits_signed_event_payload_material() {
assert!(debug.contains("ListingEnqueueReceipt"));
assert!(debug.contains("StoredAndQueued"));
- assert!(!debug.contains("debug-secret-idempotency"));
+ assert!(!debug.contains("01890f0e-6c00-7000-8000-000000000225"));
assert!(!debug.contains("raw_json"));
assert!(!debug.contains("\"tags\""));
assert!(!debug.contains("\"content\""));
@@ -427,7 +430,7 @@ async fn listing_runtime_dtos_serialize_deterministically() {
let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_123);
let prepare_request = ListingPreparePublishRequest::from_document(
actor(),
- RadrootsListingDraftDocumentV1::new(listing(LISTING_A_D_TAG, "Serialized Coffee")),
+ RadrootsListingEditDocumentV1::new(listing(LISTING_A_D_TAG, "Serialized Coffee")),
)
.with_created_at(created_at);
let prepare_json = serde_json::to_value(&prepare_request).expect("prepare request json");
@@ -452,7 +455,9 @@ async fn listing_runtime_dtos_serialize_deterministically() {
)
.try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
.expect("relay targets")
- .with_idempotency_key(SdkIdempotencyKey::new("serialized-idempotency").expect("idempotency"))
+ .with_idempotency_key(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000226").expect("idempotency"),
+ )
.with_created_at(created_at);
let enqueue_json = serde_json::to_value(&enqueue_request).expect("enqueue request json");
assert_struct_serialize_error_paths(&enqueue_request, 5);
@@ -486,20 +491,24 @@ async fn listing_runtime_dtos_serialize_deterministically() {
);
assert_eq!(
enqueue_json["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 22 })
+ serde_json::json!({ "value": "<redacted>", "len": 36 })
+ );
+ assert!(
+ !enqueue_json
+ .to_string()
+ .contains("01890f0e-6c00-7000-8000-000000000226")
);
- assert!(!enqueue_json.to_string().contains("serialized-idempotency"));
let try_key_request = ListingEnqueuePublishRequest::from_document(
actor(),
- RadrootsListingDraftDocumentV1::new(listing(LISTING_C_D_TAG, "Queued Coffee")),
+ RadrootsListingEditDocumentV1::new(listing(LISTING_C_D_TAG, "Queued Coffee")),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("listing-serialized-try-key")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000227")
.expect("try idempotency key");
assert_eq!(
serde_json::to_value(&try_key_request).expect("try key request json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 26 })
+ serde_json::json!({ "value": "<redacted>", "len": 36 })
);
let receipt = sdk
@@ -531,7 +540,10 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() {
&prepared_actor,
prepared_plan,
TargetPolicy::default_profile(),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000239")
+ .expect("idempotency"),
+ ),
&FixtureSigner::new(SELLER),
)
.await
@@ -542,7 +554,9 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() {
actor(),
listing(LISTING_H_D_TAG, "Coffee"),
TargetPolicy::default_profile(),
- );
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000239")
+ .expect("idempotency");
let convenience_receipt = convenience_sdk
.listings()
.enqueue_publish_with_explicit_signer(convenience_request, &FixtureSigner::new(SELLER))
@@ -619,6 +633,8 @@ async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() {
listing(LISTING_K_D_TAG, "Coffee"),
TargetPolicy::default_profile(),
)
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000023a")
+ .expect("idempotency")
.with_created_at(created_at);
let receipt = sdk
@@ -628,7 +644,7 @@ async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() {
.expect("enqueue");
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
let stored_event = event_store
@@ -640,7 +656,7 @@ async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() {
assert_eq!(stored_event.inserted_at_ms, observed_at_ms);
assert_eq!(stored_event.updated_at_ms, observed_at_ms);
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
let outbox_event = outbox
@@ -688,17 +704,17 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation
listing(LISTING_D_D_TAG, "Coffee"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("idem-d")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000228")
.expect("idempotency key");
sdk.listings()
.enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(SELLER))
.await
.expect("first enqueue");
let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
assert_eq!(
@@ -723,7 +739,7 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation
listing(LISTING_E_D_TAG, "Changed"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("idem-d")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000228")
.expect("idempotency key");
let error = sdk
.listings()
@@ -740,7 +756,11 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation
error.recovery_actions(),
vec![RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey]
);
- assert!(!error.to_string().contains("idem-d"));
+ assert!(
+ !error
+ .to_string()
+ .contains("01890f0e-6c00-7000-8000-000000000228")
+ );
assert_eq!(
event_store
.status_summary()
@@ -760,7 +780,95 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation
}
#[tokio::test]
-async fn enqueue_publish_derives_order_independent_idempotency_key() {
+async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_after_ingest() {
+ let (_tempdir, sdk) = directory_sdk().await;
+ let paths = sdk.storage_paths().expect("paths");
+ let idempotency_key = "01890f0e-6c00-7000-8000-000000000120";
+ let seeded_event_store = RadrootsEventStore::open_file(&paths.runtime_path)
+ .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', ?, ?)",
+ )
+ .bind(LISTING_PUBLISH_OPERATION_KIND)
+ .bind(SELLER)
+ .bind(idempotency_key)
+ .bind("conflicting-digest")
+ .bind(1_700_000_000_000i64)
+ .bind(1_700_000_000_000i64)
+ .execute(seeded_event_store.pool())
+ .await
+ .expect("seed conflicting operation")
+ .last_insert_rowid();
+ sqlx::query(
+ "INSERT INTO outbox_event(operation_id, event_id, expected_pubkey, draft_json, state, attempt_count, next_attempt_after_ms, event_store_ingested, event_store_inserted, created_at_ms, updated_at_ms) VALUES (?, ?, ?, '{}', 'signed', 0, ?, 0, 0, ?, ?)",
+ )
+ .bind(conflicting_operation)
+ .bind("0".repeat(64))
+ .bind(SELLER)
+ .bind(1_700_000_000_000i64)
+ .bind(1_700_000_000_000i64)
+ .bind(1_700_000_000_000i64)
+ .execute(seeded_event_store.pool())
+ .await
+ .expect("seed conflicting event");
+
+ let request = ListingEnqueuePublishRequest::new(
+ actor(),
+ listing(LISTING_E_D_TAG, "Rollback Coffee"),
+ TargetPolicy::default_profile(),
+ )
+ .try_with_idempotency_key(idempotency_key)
+ .expect("idempotency key");
+ let prepared = sdk
+ .listings()
+ .prepare_publish(ListingPreparePublishRequest::new(
+ actor(),
+ listing(LISTING_E_D_TAG, "Rollback Coffee"),
+ ))
+ .expect("prepared");
+ let error = sdk
+ .listings()
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER))
+ .await
+ .expect_err("outbox conflict");
+ assert!(matches!(
+ error,
+ RadrootsSdkError::IdempotencyConflict { .. }
+ ));
+
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
+ .await
+ .expect("event store");
+ assert!(
+ event_store
+ .get_event(prepared.expected_event_id.as_str())
+ .await
+ .expect("event lookup")
+ .is_none()
+ );
+ let journal_count: i64 = sqlx::query("SELECT COUNT(*) FROM sdk_runtime_operation_journal")
+ .fetch_one(event_store.pool())
+ .await
+ .expect("journal count")
+ .try_get(0)
+ .expect("journal count value");
+ assert_eq!(journal_count, 0);
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
+ .await
+ .expect("outbox");
+ assert_eq!(
+ outbox
+ .status_summary(0)
+ .await
+ .expect("outbox status after rollback")
+ .total_events,
+ 1
+ );
+}
+
+#[tokio::test]
+async fn enqueue_publish_uses_explicit_idempotency_key_across_equivalent_target_order() {
let (_tempdir, sdk) = directory_sdk().await;
let first = ListingEnqueuePublishRequest::new(
actor(),
@@ -768,7 +876,9 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() {
TargetPolicy::default_profile(),
)
.try_with_nostr_targets([RELAY_B, RELAY], NostrRelayUrlPolicy::Public)
- .expect("first transport targets");
+ .expect("first transport targets")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000023b")
+ .expect("first idempotency key");
let second = ListingEnqueuePublishRequest::new(
actor(),
listing(LISTING_F_D_TAG, "Coffee"),
@@ -776,7 +886,9 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() {
TargetSet::nostr_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
.expect("second transport targets"),
),
- );
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000023b")
+ .expect("second idempotency key");
let first_receipt = sdk
.listings()
@@ -800,7 +912,7 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() {
assert_eq!(second_receipt.state, SdkMutationState::AlreadyQueued);
let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
let relay_urls = outbox
@@ -823,7 +935,9 @@ async fn listing_hybrid_profile_publishes_after_nostr_success_and_retains_reticu
actor(),
listing(LISTING_G_D_TAG, "Hybrid Coffee"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000023c")
+ .expect("idempotency"),
&FixtureSigner::new(SELLER),
)
.await
@@ -856,7 +970,7 @@ async fn listing_hybrid_profile_publishes_after_nostr_success_and_retains_reticu
);
assert_eq!(adapter.captured_raw_events().len(), 1);
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stored = outbox
diff --git a/crates/sdk/tests/market_runtime.rs b/crates/sdk/tests/market_runtime.rs
@@ -153,7 +153,9 @@ async fn market_search_refreshes_local_projection_and_reads_fts() {
TargetPolicy::default_profile(),
)
.try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("target relays");
+ .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))
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -10,27 +10,25 @@ use std::time::{Duration, Instant};
use futures::future::BoxFuture;
#[cfg(feature = "transport-nostr-runtime")]
use nostr::JsonUtil;
+#[cfg(feature = "signer-adapters")]
use radroots_authority::RadrootsActorContext;
use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
};
+#[cfg(feature = "signer-adapters")]
+use radroots_event::contract::RadrootsActorRole;
#[cfg(feature = "transport-nostr-runtime")]
use radroots_event::ids::RadrootsPublicKey;
use radroots_event::wire::RadrootsNip01EventWireParts;
use radroots_event::{
RadrootsEventEnvelope, RadrootsEventPtr,
- contract::RadrootsActorRole,
draft::RadrootsSignedEvent,
- ids::{RadrootsEventId, RadrootsListingAddress, RadrootsOrderId, RadrootsOrderRevisionId},
- kinds::{
- KIND_LISTING, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, KIND_POST,
- KIND_TRADE_TRANSITION_PROOF_RESULT, KIND_TRADE_VALIDATION_RECEIPT,
- },
+ 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,
- RadrootsOrderRevisionOutcome,
},
};
use radroots_event_store::{RadrootsEventIngest, RadrootsEventStore};
@@ -38,29 +36,37 @@ use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp, radroots_event_from_nostr,
radroots_nostr_build_event,
};
+#[cfg(feature = "signer-adapters")]
use radroots_outbox::RadrootsOutbox;
-#[cfg(feature = "transport-nostr-runtime")]
+#[cfg(any(feature = "signer-adapters", feature = "transport-nostr-runtime"))]
+use radroots_sdk::{NostrProfile, NostrRelayUrlPolicy, TransportProfile};
+#[cfg(feature = "signer-adapters")]
use radroots_sdk::{
- DvmValidationReceiptIngestRequest, TradeEvidenceQueryBranchKind,
- TradeResyncNostrRelayOutcomeKind, TradeResyncNostrRelayTransportOutcomeKind,
- TradeResyncRequest, TradeSellerInboxRequest, TradeValidationReceiptInspectRequest,
- TradeValidationReceiptListRequest, TradeValidationReceiptVerifyRequest,
+ PrivacyPreflightConfirmation, PrivacyPreflightStatus, ProductSensitivityField, PublishMode,
+ RadrootsSdkRecoveryAction, SatisfactionPolicy, SdkMutationState, TargetPolicy, TargetSet,
};
use radroots_sdk::{
- NostrProfile, NostrRelayUrlPolicy, PublishMode, RadrootsClient, RadrootsSdkError,
- RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, RadrootsTradeValidationTrustPolicy,
- RadrootsTradeValidationTrustState, SatisfactionPolicy, SdkMutationState, SdkTradeStatusIssue,
- SdkTradeStatusIssueKind, SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT,
- TRADE_STATUS_MAX_LIMIT, TRADE_STATUS_WATCH_MAX_CAPACITY, TRADE_SUBMIT_OPERATION_KIND,
- TargetPolicy, TargetSet, TradeAcceptRequest, TradeCancelRequest, TradeDeclineRequest,
- TradeEvidenceIngestRequest, TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest,
- TradeRequestEvidenceIngestRequest, TradeRevisionDecisionRequest, TradeRevisionProposalRequest,
+ 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, TransportProfile,
+ TradeStatusWatchRequest,
};
-use radroots_sdk::{PrivacyPreflightConfirmation, PrivacyPreflightStatus, ProductSensitivityField};
#[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;
@@ -95,9 +101,6 @@ fn signed_event_from_envelope(event: RadrootsEventEnvelope) -> RadrootsSignedEve
let raw_json = serde_json::to_string(&wire).expect("raw event json");
RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
}
-#[cfg(any())]
-const OTHER_PUBLIC_KEY_HEX: &str =
- "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
#[cfg(feature = "transport-nostr-runtime")]
const RELAY_B: &str = "wss://relay-b.radroots.test";
const PERF_TOTAL_LOCAL_EVENTS: i64 = 100_000;
@@ -413,45 +416,6 @@ where
.expect_err("struct end failure");
}
-#[cfg(any())]
-#[derive(Clone)]
-struct FixtureSigner {
- identity: radroots_authority::RadrootsSignerIdentity,
- keys: RadrootsNostrKeys,
-}
-
-#[cfg(any())]
-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: radroots_authority::RadrootsSignerIdentity::new(pubkey).expect("identity"),
- keys,
- }
- }
-}
-
-#[cfg(any())]
-impl radroots_authority::RadrootsEventSigner for FixtureSigner {
- fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
- self.identity.pubkey()
- }
-
- fn sign_frozen_draft(
- &self,
- draft: &radroots_event::draft::RadrootsEventDraft,
- ) -> Result<radroots_event::draft::RadrootsSignedEvent, radroots_authority::RadrootsSignerError>
- {
- radroots_nostr::prelude::radroots_nostr_sign_frozen_draft(&self.keys, draft).map_err(
- |error| radroots_authority::RadrootsSignerError::SigningFailed {
- message: error.to_string(),
- },
- )
- }
-}
-
async fn directory_sdk_and_store() -> (tempfile::TempDir, RadrootsClient, RadrootsEventStore) {
let tempdir = tempfile::tempdir().expect("tempdir");
let sdk = RadrootsClient::builder()
@@ -460,10 +424,9 @@ async fn directory_sdk_and_store() -> (tempfile::TempDir, RadrootsClient, Radroo
.build()
.await
.expect("sdk");
- let store =
- RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").event_store_path)
- .await
- .expect("event store");
+ let store = RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").runtime_path)
+ .await
+ .expect("event store");
(tempdir, sdk, store)
}
@@ -482,10 +445,9 @@ async fn directory_sdk_and_store_with_relays(
));
}
let sdk = builder.build().await.expect("sdk");
- let store =
- RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").event_store_path)
- .await
- .expect("event store");
+ let store = RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").runtime_path)
+ .await
+ .expect("event store");
(tempdir, sdk, store)
}
@@ -525,34 +487,16 @@ 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")
}
-#[cfg(any())]
-fn other_buyer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(OTHER_PUBLIC_KEY_HEX, [RadrootsActorRole::Buyer]).expect("actor")
-}
-
-#[cfg(any())]
-fn other_seller_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(OTHER_PUBLIC_KEY_HEX, [RadrootsActorRole::Seller]).expect("actor")
-}
-
-#[cfg(any())]
-fn non_buyer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(BUYER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer]).expect("actor")
-}
-
-#[cfg(any())]
-fn non_seller_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER_PUBLIC_KEY_HEX, [RadrootsActorRole::Buyer]).expect("actor")
-}
-
fn listing_address() -> RadrootsListingAddress {
RadrootsListingAddress::parse(format!(
"{KIND_LISTING}:{SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg"
@@ -567,16 +511,19 @@ fn listing_event_ptr() -> RadrootsEventPtr {
}
}
+#[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,
@@ -649,6 +596,7 @@ fn order_request(raw_order_id: &str) -> RadrootsOrderRequest {
}
}
+#[cfg(feature = "signer-adapters")]
fn trade_propose_request(
raw_order_id: &str,
publish_mode: PublishMode,
@@ -658,314 +606,13 @@ fn trade_propose_request(
TradeProposeRequest::new(
buyer_actor(),
listing_event_ptr(),
- order.order_id,
- order.listing_addr,
- order.seller_pubkey,
- order.items,
- order.economics,
+ order,
explicit_trade_relays(),
publish_mode,
satisfaction_policy,
)
}
-#[cfg(any())]
-fn invalid_listing_event_ptr() -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: String::new(),
- relays: Some(RELAY.to_owned()),
- }
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_prepare_is_side_effect_free() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let listing_event = listing_event_ptr();
- let request = TradeSubmitPrepareRequest::new(
- buyer_actor(),
- listing_event.clone(),
- order_request("order-submit-prepare"),
- );
-
- let prepared = sdk.trades().prepare_submit(request).expect("prepared");
-
- assert_eq!(prepared.order_id.as_str(), "order-submit-prepare");
- assert_eq!(prepared.listing_addr, listing_address());
- assert_eq!(
- prepared.listing_event_id.as_str(),
- listing_event.id.as_str()
- );
- assert_eq!(prepared.frozen_draft.kind_u32(), KIND_ORDER_REQUEST);
- assert_eq!(prepared.created_at.unix_seconds(), 1_700_000_000);
- assert_eq!(
- prepared.expected_event_id,
- prepared.frozen_draft.expected_event_id_str()
- );
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- assert!(
- store
- .get_event(prepared.expected_event_id.as_str())
- .await
- .expect("event lookup")
- .is_none()
- );
-
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- assert!(
- outbox
- .claim_next_ready_event("worker", "claim", 2_000, 1_700_000_000_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_prepare_rejects_missing_listing_evidence() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let request = TradeSubmitPrepareRequest::new(
- buyer_actor(),
- invalid_listing_event_ptr(),
- order_request("order-submit-missing-listing"),
- );
-
- let error = sdk
- .trades()
- .prepare_submit(request)
- .expect_err("missing listing evidence");
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_prepare_rejects_invalid_actor_or_payload() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
-
- let non_buyer = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- non_buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-non-buyer"),
- ))
- .expect_err("non buyer");
- assert!(matches!(
- non_buyer,
- RadrootsSdkError::UnauthorizedActor { .. }
- ));
-
- let wrong_actor = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- other_buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-wrong-actor"),
- ))
- .expect_err("wrong actor");
- assert!(matches!(
- wrong_actor,
- RadrootsSdkError::UnauthorizedActor { .. }
- ));
-
- let mut seller_mismatch = order_request("order-submit-seller-mismatch");
- seller_mismatch.seller_pubkey = OTHER_PUBLIC_KEY_HEX.parse().expect("seller pubkey");
- let seller_error = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- seller_mismatch,
- ))
- .expect_err("seller mismatch");
- assert!(matches!(
- seller_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let mut empty_items = order_request("order-submit-empty-items");
- empty_items.items.clear();
- let empty_items_error = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- empty_items,
- ))
- .expect_err("empty items");
- assert!(matches!(
- empty_items_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let mut empty_economics = order_request("order-submit-empty-economics");
- empty_economics.economics.items.clear();
- let empty_economics_error = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- empty_economics,
- ))
- .expect_err("empty economics");
- assert!(matches!(
- empty_economics_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_enqueue_stores_event_queues_outbox_and_status_sees_request() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let order = order_request("order-submit-enqueue");
- let prepared = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order.clone(),
- ))
- .expect("prepared");
- assert_eq!(prepared.workflow.kind, TradeWorkflowKind::Submit);
- assert_eq!(
- prepared.workflow.operation_kind,
- TRADE_SUBMIT_OPERATION_KIND
- );
- assert_eq!(prepared.workflow.contract_id, "radroots.order.request.v1");
- assert_eq!(
- prepared.workflow.expected_event_id,
- prepared.expected_event_id
- );
- assert_eq!(prepared.workflow.created_at, prepared.created_at);
- let request = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets")
- .try_with_idempotency_key("order-submit-enqueue-idempotency")
- .expect("idempotency key");
-
- let receipt = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX))
- .await
- .expect("enqueue");
-
- assert_eq!(receipt.order_id, prepared.order_id);
- assert_eq!(receipt.listing_addr, prepared.listing_addr);
- assert_eq!(receipt.listing_event_id, prepared.listing_event_id);
- assert_eq!(receipt.workflow.kind, TradeWorkflowKind::Submit);
- assert_eq!(receipt.workflow.operation_kind, TRADE_SUBMIT_OPERATION_KIND);
- assert_eq!(
- receipt.workflow.expected_event_id,
- prepared.expected_event_id
- );
- assert_eq!(receipt.workflow.signed_event_id, receipt.signed_event_id);
- assert_eq!(receipt.workflow.local_event_seq, receipt.local_event_seq);
- assert_eq!(
- receipt.workflow.outbox_operation_id,
- receipt.outbox_operation_id
- );
- assert_eq!(receipt.workflow.outbox_event_id, receipt.outbox_event_id);
- assert_eq!(receipt.workflow.state, receipt.state);
- assert_eq!(
- receipt.workflow.idempotency_digest_prefix,
- receipt.idempotency_digest_prefix
- );
- assert_eq!(
- receipt.workflow.idempotency.digest_prefix,
- receipt.idempotency_digest_prefix
- );
- assert!(!receipt.workflow.idempotency.replayed_existing_operation);
- assert!(
- receipt
- .workflow
- .idempotency
- .safe_to_retry_with_same_idempotency_key
- );
- assert!(!receipt.workflow.retry.retryable_after_error);
- assert!(
- receipt
- .workflow
- .retry
- .safe_to_retry_enqueue_with_same_idempotency_key
- );
- assert!(receipt.workflow.retry.recovery_actions.is_empty());
- assert_eq!(receipt.expected_event_id, prepared.expected_event_id);
- assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.local_event_seq, 1);
- assert_eq!(receipt.outbox_operation_id, 1);
- assert_eq!(receipt.outbox_event_id, 1);
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
- assert!(receipt.idempotency_digest_prefix.is_some());
-
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
- let stored_event = store
- .get_event(receipt.signed_event_id.as_str())
- .await
- .expect("event lookup")
- .expect("stored event");
- assert_eq!(stored_event.kind, KIND_ORDER_REQUEST);
- assert_eq!(
- stored_event.contract_id.as_deref(),
- Some("radroots.order.request.v1")
- );
-
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- let outbox_event = outbox
- .get_event(receipt.outbox_event_id)
- .await
- .expect("outbox event")
- .expect("outbox event");
- assert_eq!(outbox_event.state, RadrootsOutboxEventState::Signed);
- assert_eq!(outbox_event.draft.kind_u32(), KIND_ORDER_REQUEST);
- assert!(outbox_event.signed_event.is_some());
-
- let status = sdk
- .trades()
- .status(status_request("order-submit-enqueue"))
- .await
- .expect("status");
- assert!(status.found);
- assert_eq!(status.status, TradeStatusKind::Requested);
- assert_eq!(status.event_count, 1);
- assert_eq!(
- status
- .request_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(receipt.signed_event_id.as_str())
- );
-}
-
#[cfg(all(
feature = "signer-adapters",
feature = "local-signer",
@@ -991,7 +638,7 @@ async fn trade_product_clients_propose_inbox_accept_status_and_resync() {
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-facade-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000204")
.expect("propose idempotency"),
)
.await
@@ -1063,7 +710,7 @@ async fn trade_product_clients_propose_inbox_accept_status_and_resync() {
SatisfactionPolicy::NoWait,
TradeEvidenceMode::LocalOnly,
)
- .try_with_idempotency_key("trade-product-facade-accept")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000205")
.expect("accept idempotency"),
)
.await
@@ -1081,7 +728,7 @@ async fn trade_product_clients_propose_inbox_accept_status_and_resync() {
.status(TradeStatusRequest::new(propose_receipt.locator.clone()))
.await
.expect("facade status");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
+ assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
assert_eq!(
status
.decision_event_id
@@ -1102,7 +749,10 @@ async fn trade_product_clients_propose_inbox_accept_status_and_resync() {
.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::AgreedPendingRhi);
+ assert_eq!(
+ resync.status.status,
+ TradeStatusKind::AgreedPendingValidation
+ );
assert_eq!(
resync.status.last_event_id,
Some(accept_receipt.signed_event_id)
@@ -1132,11 +782,11 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
)
.await;
let buyer_store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").event_store_path)
+ 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").event_store_path)
+ RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("seller event store");
let propose_receipt = expect_enqueued(
@@ -1149,7 +799,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-committed-resync-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000206")
.expect("propose idempotency"),
)
.await
@@ -1199,7 +849,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
SatisfactionPolicy::NoWait,
TradeEvidenceMode::LocalOnly,
)
- .try_with_idempotency_key("trade-product-committed-resync-accept")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000207")
.expect("accept idempotency"),
)
.await
@@ -1216,35 +866,14 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
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);
- let worker_raw_event = signed_raw_worker_result_event(
- "trade-product-committed-resync",
- &receipt_event_id,
- &propose_receipt.listing_event_id,
- &propose_receipt.signed_event_id,
- &accept_receipt.signed_event_id,
- 34,
- );
- let ingest = seller_sdk
- .dvm()
- .ingest_validation_receipt(
- DvmValidationReceiptIngestRequest::new(signed_event_from_envelope(receipt_event))
- .with_expected_order_id(propose_receipt.order_id.clone())
- .with_expected_listing_event_id(propose_receipt.listing_event_id.clone())
- .with_expected_root_event_id(propose_receipt.signed_event_id.clone())
- .with_expected_target_event_id(accept_receipt.signed_event_id.clone()),
- )
- .await
- .expect("ingest validation receipt");
- assert!(ingest.inserted);
- assert_eq!(ingest.receipt_event_id, receipt_event_id);
seller_store
.ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(radroots_event_from_nostr(&worker_raw_event)),
- 4_050,
+ signed_event_from_envelope(receipt_event),
+ 4_040,
))
.await
- .expect("ingest validation result");
+ .expect("ingest validation receipt");
let seller_resync = seller_sdk
.trades()
@@ -1257,7 +886,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
.expect("seller resync");
assert_eq!(
seller_resync.status.status,
- TradeStatusKind::AgreedPendingRhi
+ TradeStatusKind::AgreedPendingValidation
);
assert_eq!(
seller_resync.status.rhi_receipt_event_id,
@@ -1278,6 +907,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
let trusted_local_policy = RadrootsTradeValidationTrustPolicy::production()
.with_trusted_rhi_pubkeys(vec![service_pubkey.parse().expect("service pubkey")])
+ .with_allow_deterministic_none(true)
.with_require_cryptographic_proof(false);
let seller_trusted_local = seller_sdk
.trades()
@@ -1299,7 +929,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
assert!(!seller_trust.production_committed);
assert_eq!(
seller_trust
- .result_author
+ .receipt_author
.as_ref()
.map(|pubkey| pubkey.as_str()),
Some(service_pubkey.as_str())
@@ -1309,7 +939,6 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
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_raw_event_item(&worker_raw_event, RELAY, 4_201),
relay_eose(RELAY),
]);
let buyer_resync = buyer_sdk
@@ -1323,13 +952,13 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() {
.expect("buyer resync");
assert_eq!(
buyer_resync.status.status,
- TradeStatusKind::AgreedPendingRhi
+ TradeStatusKind::AgreedPendingValidation
);
assert_eq!(
buyer_resync.status.rhi_receipt_event_id,
Some(receipt_event_id)
);
- assert_eq!(buyer_resync.evidence.inserted_count, 3);
+ assert_eq!(buyer_resync.evidence.inserted_count, 2);
}
#[tokio::test]
@@ -1352,19 +981,10 @@ async fn trade_status_trust_policy_requires_trusted_cryptographic_receipt_for_co
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");
- let worker_raw_event = signed_raw_sp1_worker_result_event(
- order_id,
- &receipt_event_id,
- &listing_event_id,
- &request_event_id,
- &decision_event_id,
- 73,
- );
for (event, observed_at_ms) in [
(request_event, 7_000),
(decision_event, 7_100),
(receipt_event, 7_200),
- (radroots_event_from_nostr(&worker_raw_event), 7_300),
] {
store
.ingest_event(RadrootsEventIngest::new(
@@ -1380,7 +1000,10 @@ async fn trade_status_trust_policy_requires_trusted_cryptographic_receipt_for_co
.status(status_request(order_id))
.await
.expect("default status");
- assert_eq!(default_status.status, TradeStatusKind::AgreedPendingRhi);
+ assert_eq!(
+ default_status.status,
+ TradeStatusKind::AgreedPendingValidation
+ );
assert_eq!(
default_status
.validation_trust
@@ -1399,6 +1022,10 @@ async fn trade_status_trust_policy_requires_trusted_cryptographic_receipt_for_co
.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,
@@ -1414,7 +1041,7 @@ async fn trade_status_trust_policy_requires_trusted_cryptographic_receipt_for_co
assert!(trust.cryptographic_proof_verified);
assert_eq!(trust.proof_system.as_deref(), Some("sp1_core"));
assert_eq!(
- trust.result_author.as_ref().map(|pubkey| pubkey.as_str()),
+ trust.receipt_author.as_ref().map(|pubkey| pubkey.as_str()),
Some(service_pubkey.as_str())
);
}
@@ -1442,11 +1069,11 @@ async fn trade_product_accept_resync_before_mutation_imports_relay_visible_reque
)
.await;
let buyer_store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").event_store_path)
+ 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").event_store_path)
+ RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("seller event store");
let propose_receipt = expect_enqueued(
@@ -1455,11 +1082,11 @@ async fn trade_product_accept_resync_before_mutation_imports_relay_visible_reque
.buyer()
.propose_trade(
trade_propose_request(
- "trade-product-resync-before-accept",
+ "01890f0e-6c00-7000-8000-000000000209",
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-resync-before-accept-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000208")
.expect("propose idempotency"),
)
.await
@@ -1495,7 +1122,7 @@ async fn trade_product_accept_resync_before_mutation_imports_relay_visible_reque
SatisfactionPolicy::NoWait,
TradeEvidenceMode::ResyncBeforeMutation,
)
- .try_with_idempotency_key("trade-product-resync-before-accept")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000209")
.expect("accept idempotency"),
&adapter,
)
@@ -1511,7 +1138,7 @@ async fn trade_product_accept_resync_before_mutation_imports_relay_visible_reque
.status(TradeStatusRequest::new(propose_receipt.locator))
.await
.expect("seller status after accept");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
+ assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
assert_eq!(
status.decision_event_id,
Some(accept_receipt.signed_event_id)
@@ -1532,11 +1159,10 @@ async fn trade_product_accept_resync_before_mutation_imports_relay_visible_reque
feature = "transport-nostr-runtime"
))]
#[tokio::test]
-async fn trade_product_revision_status_resync_imports_pending_revision_proposal() {
+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 decision_storage_root = tempdir.path().join("decision-sdk");
let buyer_sdk = directory_sdk_with_signer_and_relays(
buyer_storage_root.as_path(),
BUYER_SECRET_KEY_HEX,
@@ -1549,190 +1175,44 @@ async fn trade_product_revision_status_resync_imports_pending_revision_proposal(
&[RELAY],
)
.await;
- let decision_sdk = directory_sdk_with_signer_and_relays(
- decision_storage_root.as_path(),
- BUYER_SECRET_KEY_HEX,
- &[RELAY],
- )
- .await;
- let buyer_store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").event_store_path)
- .await
- .expect("buyer event store");
let seller_store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").event_store_path)
+ RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("seller event store");
- let decision_store = RadrootsEventStore::open_file(
- &decision_sdk
- .storage_paths()
- .expect("paths")
- .event_store_path,
- )
- .await
- .expect("decision event store");
let propose_receipt = expect_enqueued(
buyer_sdk
.trades()
.buyer()
.propose_trade(
trade_propose_request(
- "trade-product-resync-before-revision-decision",
+ "trade-product-local-only-no-fetch",
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-resync-before-revision-decision-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020a")
.expect("propose idempotency"),
)
.await
.expect("propose trade"),
);
- seller_sdk
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![relay_closed(RELAY, "must not fetch")]);
+ let error = seller_sdk
.trades()
- .resync()
- .resync_with_fetch_adapter(
- TradeResyncRequest::new(propose_receipt.locator.clone()),
- &RadrootsMockRelayFetchAdapter::new(vec![
- relay_event_item_from_store(
- &buyer_store,
- &propose_receipt.signed_event_id,
- RELAY,
- 4_400,
- )
- .await,
- relay_eose(RELAY),
- ]),
- )
- .await
- .expect("seller request resync");
- let revision_id: RadrootsOrderRevisionId = "revision-product-resync-before-decision"
- .parse()
- .expect("revision id");
- let proposal_receipt = expect_enqueued(
- seller_sdk
- .trades()
- .seller()
- .propose_revision(
- TradeRevisionProposalRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- revision_id,
- vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 3,
- }],
- revision_economics(),
- "increase quantity",
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .with_privacy_confirmation(public_note_confirmation())
- .try_with_idempotency_key("trade-product-resync-before-revision-proposal")
- .expect("revision proposal idempotency"),
- )
- .await
- .expect("propose revision"),
- );
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_event_item_from_store(&buyer_store, &propose_receipt.signed_event_id, RELAY, 4_500)
- .await,
- relay_event_item_from_store(
- &seller_store,
- &proposal_receipt.signed_event_id,
- RELAY,
- 4_501,
- )
- .await,
- relay_eose(RELAY),
- ]);
- let status = decision_sdk
- .trades()
- .status_with_fetch_adapter(
- TradeStatusRequest::new(propose_receipt.locator.clone())
- .with_source(SdkTradeStatusSource::ResyncThenLocal),
- &adapter,
- )
- .await
- .expect("decision resync status");
-
- assert_eq!(status.status, TradeStatusKind::RevisionProposed);
- assert_eq!(
- status.pending_revision_event_id,
- Some(proposal_receipt.signed_event_id)
- );
- assert!(status.eligibility.can_decide_revision);
- assert_eq!(
- decision_store
- .status_summary()
- .await
- .expect("decision imported request and proposal")
- .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").event_store_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("trade-product-local-only-no-fetch-propose")
- .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,
+ .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");
@@ -1761,11 +1241,11 @@ async fn trade_product_accept_require_explicit_evidence_ingests_supplied_request
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").event_store_path)
+ 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").event_store_path)
+ RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("seller event store");
let propose_receipt = expect_enqueued(
@@ -1774,11 +1254,11 @@ async fn trade_product_accept_require_explicit_evidence_ingests_supplied_request
.buyer()
.propose_trade(
trade_propose_request(
- "trade-product-explicit-accept",
+ "01890f0e-6c00-7000-8000-00000000020c",
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-explicit-accept-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020b")
.expect("propose idempotency"),
)
.await
@@ -1804,7 +1284,7 @@ async fn trade_product_accept_require_explicit_evidence_ingests_supplied_request
TradeEvidenceIngestRequest::new(signed_event_from_envelope(request_event)),
]),
)
- .try_with_idempotency_key("trade-product-explicit-accept")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020c")
.expect("accept idempotency"),
)
.await
@@ -1819,7 +1299,7 @@ async fn trade_product_accept_require_explicit_evidence_ingests_supplied_request
.status(TradeStatusRequest::new(propose_receipt.locator))
.await
.expect("seller explicit status after accept");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
+ assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
assert_eq!(
seller_store
.status_summary()
@@ -1841,7 +1321,7 @@ async fn trade_product_accept_require_explicit_evidence_rejects_empty_evidence()
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").event_store_path)
+ RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("seller event store");
let propose_receipt = expect_enqueued(
@@ -1854,7 +1334,7 @@ async fn trade_product_accept_require_explicit_evidence_rejects_empty_evidence()
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-empty-explicit-accept-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020d")
.expect("propose idempotency"),
)
.await
@@ -1893,7 +1373,7 @@ async fn trade_product_accept_require_explicit_evidence_rejects_empty_evidence()
#[cfg(feature = "transport-nostr-runtime")]
#[tokio::test]
-async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence() {
+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");
@@ -1908,30 +1388,8 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
);
let receipt_event_id =
RadrootsEventId::parse(receipt_raw_event.id.to_hex()).expect("receipt event id");
- let worker_raw_event = signed_raw_worker_result_event(
- order_id,
- &receipt_event_id,
- &listing_event_id,
- &root_event_id,
- &target_event_id,
- 34,
- );
- let worker_event_id = worker_raw_event.id.to_hex();
- let wrong_root_event_id = deterministic_event_id("validation-receipt-worker-wrong-root");
- let wrong_worker_raw_event = signed_raw_worker_result_event(
- order_id,
- &receipt_event_id,
- &listing_event_id,
- &wrong_root_event_id,
- &target_event_id,
- 35,
- );
- let wrong_worker_event_id = wrong_worker_raw_event.id.to_hex();
- let service_pubkey = public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX);
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
relay_raw_event_item(&receipt_raw_event, RELAY, 4_000),
- relay_raw_event_item(&worker_raw_event, RELAY, 4_001),
- relay_raw_event_item(&wrong_worker_raw_event, RELAY, 4_002),
relay_eose(RELAY),
]);
@@ -1939,10 +1397,7 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
.trades()
.validation_receipts()
.list_with_fetch_adapter(
- TradeValidationReceiptListRequest::parse(order_id)
- .expect("list request")
- .try_with_trusted_worker_pubkeys([service_pubkey.as_str()])
- .expect("trusted worker"),
+ TradeValidationReceiptListRequest::parse(order_id).expect("list request"),
&adapter,
)
.await
@@ -1951,41 +1406,18 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
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, 2);
+ assert_eq!(list.nostr_evidence.out_of_filter_count, 0);
assert_eq!(list.receipts[0].event.id_str(), receipt_event_id.as_str());
- let trusted = list.receipts[0]
- .worker_evidence
- .trusted
- .as_ref()
- .expect("trusted evidence");
- assert_eq!(trusted.author.as_str(), service_pubkey);
- assert_eq!(
- trusted
- .validation_authority
- .map(|authority| authority.as_str()),
- Some("trusted_rhi_service_key")
- );
assert_eq!(
- trusted
- .commitment_confidence
- .map(|confidence| confidence.as_str()),
- Some("committed_by_trusted_service")
- );
- assert!(
- store
- .get_event(wrong_worker_event_id.as_str())
- .await
- .expect("wrong worker event lookup")
- .is_none()
+ 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())
- .try_with_trusted_worker_pubkeys([service_pubkey.as_str()])
- .expect("trusted worker"),
+ TradeValidationReceiptInspectRequest::new(receipt_event_id.clone()),
&adapter,
)
.await
@@ -1997,110 +1429,32 @@ async fn trade_validation_receipts_fetch_from_relays_and_select_worker_evidence(
inspect
.receipt
.as_ref()
- .and_then(|receipt| receipt.worker_evidence.trusted.as_ref())
- .map(|evidence| evidence.result_event_id.as_str()),
- Some(worker_event_id.as_str())
- );
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-#[tokio::test]
-async fn trade_validation_receipts_classify_worker_evidence_untrusted_without_trust_config() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store_with_relays(&[RELAY]).await;
- let order_id = "trade-validation-receipts-untrusted-default";
- let listing_event_id = deterministic_event_id("validation-receipt-untrusted-listing");
- let root_event_id = deterministic_event_id("validation-receipt-untrusted-request");
- let target_event_id = deterministic_event_id("validation-receipt-untrusted-decision");
- let receipt_raw_event = signed_raw_validation_receipt_event(
- order_id,
- &listing_event_id,
- &root_event_id,
- &target_event_id,
- 36,
- );
- let receipt_event_id =
- RadrootsEventId::parse(receipt_raw_event.id.to_hex()).expect("receipt event id");
- let worker_raw_event = signed_raw_worker_result_event(
- order_id,
- &receipt_event_id,
- &listing_event_id,
- &root_event_id,
- &target_event_id,
- 37,
- );
- let worker_event_id = worker_raw_event.id.to_hex();
- let service_pubkey = public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX);
- let adapter = RadrootsMockRelayFetchAdapter::new(vec![
- relay_raw_event_item(&receipt_raw_event, RELAY, 4_000),
- relay_raw_event_item(&worker_raw_event, RELAY, 4_001),
- 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");
- let list_evidence = &list.receipts[0].worker_evidence;
- assert!(list_evidence.trusted.is_none());
- let list_untrusted = list_evidence.untrusted.as_ref().expect("list untrusted");
- assert_eq!(list_untrusted.result_event_id.as_str(), worker_event_id);
- assert_eq!(list_untrusted.author.as_str(), service_pubkey);
- assert_eq!(
- list_untrusted
- .commitment_confidence
- .map(|confidence| confidence.as_str()),
- Some("committed_by_trusted_service")
- );
-
- let inspect = sdk
- .trades()
- .validation_receipts()
- .inspect_with_fetch_adapter(
- TradeValidationReceiptInspectRequest::new(receipt_event_id.clone()),
- &adapter,
- )
- .await
- .expect("validation receipt inspect");
- let inspect_evidence = &inspect
- .receipt
- .as_ref()
- .expect("inspect receipt")
- .worker_evidence;
- assert!(inspect_evidence.trusted.is_none());
- assert_eq!(
- inspect_evidence
- .untrusted
- .as_ref()
- .map(|evidence| evidence.result_event_id.as_str()),
- Some(worker_event_id.as_str())
+ .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),
+ TradeValidationReceiptVerifyRequest::new(receipt_event_id.clone()),
&adapter,
)
.await
.expect("validation receipt verify");
- let verify_evidence = &verify
- .receipt
- .as_ref()
- .expect("verify receipt")
- .worker_evidence;
- assert!(verify_evidence.trusted.is_none());
assert_eq!(
- verify_evidence
- .untrusted
+ verify
+ .receipt
.as_ref()
- .map(|evidence| evidence.result_event_id.as_str()),
- Some(worker_event_id.as_str())
+ .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()
);
}
@@ -2294,7 +1648,7 @@ async fn trade_resync_imports_nostr_evidence_into_empty_local_store() {
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(), 5);
+ assert_eq!(resync.evidence.query_plan.branches.len(), 4);
assert!(
resync
.evidence
@@ -2486,8 +1840,8 @@ async fn trade_resync_splits_lifecycle_branch_into_single_kind_filters() {
.expect("resync");
let filters = adapter.filters_json();
- assert_eq!(filters.len(), 6);
- for kind in [3422, 3423, 3424, 3425, 3432, 3440] {
+ assert_eq!(filters.len(), 4);
+ for kind in [3422, 3423, 3432, 3440] {
assert!(
filters
.iter()
@@ -2650,6 +2004,7 @@ async fn relay_event_item_from_store(
}
}
+#[cfg(feature = "signer-adapters")]
async fn event_from_store(
source: &RadrootsEventStore,
event_id: &RadrootsEventId,
@@ -2708,10 +2063,10 @@ async fn trade_product_propose_idempotency_replays_same_payload_and_conflicts_di
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.event_store_path)
+ let store = RadrootsEventStore::open_file(&storage_paths.runtime_path)
.await
.expect("event store");
- let outbox = RadrootsOutbox::open_file(&storage_paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&storage_paths.runtime_path)
.await
.expect("outbox");
let request = trade_propose_request(
@@ -2719,7 +2074,7 @@ async fn trade_product_propose_idempotency_replays_same_payload_and_conflicts_di
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-idempotent-key")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020e")
.expect("idempotency");
let first = expect_enqueued(
@@ -2774,7 +2129,7 @@ async fn trade_product_propose_idempotency_replays_same_payload_and_conflicts_di
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-idempotent-key")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020e")
.expect("conflict idempotency"),
)
.await
@@ -2848,7 +2203,7 @@ async fn trade_product_propose_requires_public_note_privacy_confirmation() {
);
let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").event_store_path)
+ RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("event store");
assert_eq!(
@@ -2874,13 +2229,13 @@ async fn trade_product_propose_publishes_public_note_after_confirmation() {
.buyer()
.propose_trade(
trade_propose_request(
- "trade-product-propose-public-note-confirmed",
+ "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("trade-product-propose-public-note-confirmed")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000020f")
.expect("proposal idempotency"),
)
.await
@@ -2889,10 +2244,10 @@ async fn trade_product_propose_publishes_public_note_after_confirmation() {
assert_eq!(
receipt.order_id.as_str(),
- "trade-product-propose-public-note-confirmed"
+ "01890f0e-6c00-7000-8000-00000000020f"
);
let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").event_store_path)
+ RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("event store");
assert_eq!(
@@ -2939,7 +2294,7 @@ async fn trade_product_propose_blocks_sensitive_fulfillment_note_even_when_confi
assert_eq!(*status, PrivacyPreflightStatus::ForbiddenPublicFields);
assert!(fields.contains(&ProductSensitivityField::SensitiveFulfillmentDetails));
let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").event_store_path)
+ RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("event store");
assert_eq!(
@@ -2968,7 +2323,7 @@ async fn trade_product_decline_requires_public_reason_privacy_confirmation() {
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-privacy-decline-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000210")
.expect("propose idempotency"),
)
.await
@@ -3011,7 +2366,7 @@ async fn trade_product_decline_requires_public_reason_privacy_confirmation() {
"public_but_sensitive_notes"
);
let store =
- RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").event_store_path)
+ RadrootsEventStore::open_file(&seller_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("event store");
assert_eq!(
@@ -3038,7 +2393,7 @@ async fn trade_product_decline_requires_public_reason_privacy_confirmation() {
TradeEvidenceMode::LocalOnly,
)
.with_privacy_confirmation(public_note_confirmation())
- .try_with_idempotency_key("trade-product-privacy-decline-confirmed")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000211")
.expect("decline idempotency"),
)
.await
@@ -3072,7 +2427,7 @@ async fn trade_product_cancel_blocks_sensitive_fulfillment_reason_before_mutatio
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-privacy-cancel-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000212")
.expect("propose idempotency"),
)
.await
@@ -3109,7 +2464,7 @@ async fn trade_product_cancel_blocks_sensitive_fulfillment_reason_before_mutatio
assert_eq!(*status, PrivacyPreflightStatus::ForbiddenPublicFields);
assert!(fields.contains(&ProductSensitivityField::SensitiveFulfillmentDetails));
let store =
- RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").event_store_path)
+ RadrootsEventStore::open_file(&buyer_sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("event store");
assert_eq!(
@@ -3134,11 +2489,11 @@ async fn trade_product_cancel_enqueues_with_locator_and_updates_status() {
.buyer()
.propose_trade(
trade_propose_request(
- "trade-product-cancel",
+ "01890f0e-6c00-7000-8000-000000000214",
PublishMode::EnqueueOnly,
SatisfactionPolicy::NoWait,
)
- .try_with_idempotency_key("trade-product-cancel-propose")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000213")
.expect("propose idempotency"),
)
.await
@@ -3160,7 +2515,7 @@ async fn trade_product_cancel_enqueues_with_locator_and_updates_status() {
TradeEvidenceMode::LocalOnly,
)
.with_privacy_confirmation(public_note_confirmation())
- .try_with_idempotency_key("trade-product-cancel")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000214")
.expect("cancel idempotency"),
)
.await
@@ -3186,105 +2541,6 @@ async fn trade_product_cancel_enqueues_with_locator_and_updates_status() {
assert_eq!(status.next_action, TradeStatusNextActionKind::Terminal);
}
-#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
-#[tokio::test]
-async fn trade_product_revision_lifecycle_uses_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 seller_sdk = directory_sdk_with_signer(storage_root.as_path(), SELLER_SECRET_KEY_HEX).await;
- let propose_receipt = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .propose_trade(
- trade_propose_request(
- "trade-product-revision",
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("trade-product-revision-propose")
- .expect("propose idempotency"),
- )
- .await
- .expect("propose trade"),
- );
- let revision_id: RadrootsOrderRevisionId =
- "revision-product-revision".parse().expect("revision id");
- let proposal = expect_enqueued(
- seller_sdk
- .trades()
- .seller()
- .propose_revision(
- TradeRevisionProposalRequest::new(
- seller_actor(),
- propose_receipt.locator.clone(),
- revision_id.clone(),
- vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 3,
- }],
- revision_economics(),
- "increase quantity",
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .with_privacy_confirmation(public_note_confirmation())
- .try_with_idempotency_key("trade-product-revision-proposal")
- .expect("revision proposal idempotency"),
- )
- .await
- .expect("propose revision"),
- );
- let pending = buyer_sdk
- .trades()
- .status(TradeStatusRequest::new(propose_receipt.locator.clone()))
- .await
- .expect("pending revision status");
- assert_eq!(pending.status, TradeStatusKind::RevisionProposed);
- assert_eq!(
- pending.pending_revision_event_id,
- Some(proposal.signed_event_id.clone())
- );
- assert!(pending.eligibility.can_decide_revision);
-
- let decision = expect_enqueued(
- buyer_sdk
- .trades()
- .buyer()
- .accept_revision(
- TradeRevisionDecisionRequest::new(
- buyer_actor(),
- propose_receipt.locator.clone(),
- revision_id,
- RadrootsOrderRevisionOutcome::Accepted,
- explicit_trade_relays(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- TradeEvidenceMode::LocalOnly,
- )
- .try_with_idempotency_key("trade-product-revision-decision")
- .expect("revision decision idempotency"),
- )
- .await
- .expect("accept revision"),
- );
- assert_eq!(decision.locator, propose_receipt.locator);
- assert_eq!(decision.root_event_id, propose_receipt.signed_event_id);
- assert_eq!(decision.previous_event_id, proposal.signed_event_id);
- let status = buyer_sdk
- .trades()
- .status(TradeStatusRequest::new(propose_receipt.locator))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
- assert_eq!(status.last_event_id, Some(decision.signed_event_id));
- assert_eq!(status.pending_revision_event_id, None);
- assert_eq!(status.economics, Some(revision_economics()));
-}
-
#[cfg(feature = "signer-adapters")]
#[tokio::test]
async fn trade_product_propose_dry_run_returns_plan_without_local_side_effects() {
@@ -3316,7 +2572,7 @@ async fn trade_product_propose_dry_run_returns_plan_without_local_side_effects()
.total_events,
0
);
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
assert!(
@@ -3328,449 +2584,6 @@ async fn trade_product_propose_dry_run_returns_plan_without_local_side_effects()
);
}
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_enqueue_returns_sanitized_signer_errors_before_mutation() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-wrong-signer"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets");
-
- let error = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX))
- .await
- .expect_err("signer error");
- let message = error.to_string();
-
- assert!(matches!(
- error,
- RadrootsSdkError::SignerPubkeyMismatch { .. }
- ));
- assert!(!message.contains("raw"));
- assert!(!message.contains("ffff"));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- assert!(
- outbox
- .claim_next_ready_event("worker", "claim", 2_000, 1_700_000_000_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_enqueue_derives_order_independent_idempotency_key() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let first = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-idempotent"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY_B, RELAY], NostrRelayUrlPolicy::Public)
- .expect("first transport targets");
- let second = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-idempotent"),
- TargetPolicy::explicit(
- TargetSet::nostr_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("second transport targets"),
- ),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- );
-
- let first_receipt = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(first, &FixtureSigner::new(BUYER_SECRET_KEY_HEX))
- .await
- .expect("first enqueue");
- let second_receipt = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(second, &FixtureSigner::new(BUYER_SECRET_KEY_HEX))
- .await
- .expect("second enqueue");
-
- assert_eq!(
- first_receipt.outbox_event_id,
- second_receipt.outbox_event_id
- );
- assert_eq!(
- first_receipt.idempotency_digest_prefix,
- second_receipt.idempotency_digest_prefix
- );
- assert_eq!(second_receipt.state, SdkMutationState::AlreadyQueued);
- assert!(
- !first_receipt
- .workflow
- .idempotency
- .replayed_existing_operation
- );
- assert!(
- second_receipt
- .workflow
- .idempotency
- .replayed_existing_operation
- );
- assert!(
- second_receipt
- .workflow
- .idempotency
- .safe_to_retry_with_same_idempotency_key
- );
- assert!(
- second_receipt
- .workflow
- .retry
- .safe_to_retry_enqueue_with_same_idempotency_key
- );
- assert!(!second_receipt.workflow.retry.retryable_after_error);
- assert!(second_receipt.workflow.retry.recovery_actions.is_empty());
-
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- let relay_urls = outbox
- .delivery_targets(first_receipt.outbox_event_id)
- .await
- .expect("delivery targets")
- .into_iter()
- .map(|target| target.endpoint_uri.to_string())
- .collect::<Vec<_>>();
- assert_eq!(relay_urls, vec![RELAY_B.to_owned(), RELAY.to_owned()]);
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_enqueue_pushes_queued_event_with_mock_relay_sync() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let enqueue_request = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-sync"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets");
- let enqueue_receipt = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(
- enqueue_request,
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue");
- let adapter = RadrootsMockRelayPublishAdapter::new();
-
- let push_receipt = sdk
- .sync()
- .push_outbox_with_transport(
- &RadrootsNostrTransport::new(&adapter),
- PushOutboxRequest::new().with_limit(1),
- )
- .await
- .expect("push");
-
- assert_eq!(push_receipt.attempted_events, 1);
- assert_eq!(push_receipt.published_events, 1);
- assert_eq!(push_receipt.retryable_events, 0);
- assert_eq!(push_receipt.terminal_events, 0);
- assert_eq!(push_receipt.events.len(), 1);
- let event = &push_receipt.events[0];
- assert_eq!(event.event_id, enqueue_receipt.signed_event_id);
- assert_eq!(event.outbox_event_id, enqueue_receipt.outbox_event_id);
- assert_eq!(event.final_state, PushOutboxEventState::Published);
- assert_eq!(event.attempted_count, 1);
- assert_eq!(event.accepted_count, 1);
- assert_eq!(event.retryable_count, 0);
- assert_eq!(event.terminal_count, 0);
- assert_eq!(event.quorum, 1);
- assert!(event.quorum_met);
- assert_eq!(event.targets.len(), 1);
- assert_eq!(event.targets[0].endpoint_uri, RELAY);
- assert_eq!(
- event.targets[0].outcome_kind,
- PushOutboxTargetOutcomeKind::Accepted
- );
- assert_eq!(adapter.captured_raw_events().len(), 1);
-
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
- .await
- .expect("outbox");
- let stored = outbox
- .get_event(enqueue_receipt.outbox_event_id)
- .await
- .expect("stored")
- .expect("stored");
- assert_eq!(stored.state, RadrootsOutboxEventState::Published);
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_enqueue_reports_partial_local_mutation_after_outbox_conflict() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let first = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-conflict-a"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("first transport targets")
- .try_with_idempotency_key("order-submit-conflict-idempotency")
- .expect("first idempotency key");
- sdk.trades()
- .enqueue_submit_with_explicit_signer(first, &FixtureSigner::new(BUYER_SECRET_KEY_HEX))
- .await
- .expect("first enqueue");
-
- let second = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-conflict-b"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("second transport targets")
- .try_with_idempotency_key("order-submit-conflict-idempotency")
- .expect("second idempotency key");
- let error = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(second, &FixtureSigner::new(BUYER_SECRET_KEY_HEX))
- .await
- .expect_err("partial");
-
- assert!(matches!(
- error,
- RadrootsSdkError::PartialLocalMutation(ref partial)
- if partial.stored
- && !partial.queued
- && partial.event_id.is_some()
- && partial.operation_kind == TRADE_SUBMIT_OPERATION_KIND
- && partial.idempotency_digest_prefix.is_some()
- && partial.failure == RadrootsSdkPartialLocalMutationFailure::OutboxIdempotencyConflict
- && partial.recovery == RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey
- ));
- assert!(error.retryable());
- assert_eq!(
- error.recovery_actions(),
- vec![RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey]
- );
- let detail = error.detail_json();
- assert_eq!(detail["code"], "partial_local_mutation");
- assert_eq!(detail["retryable"], true);
- assert_eq!(
- detail["recovery_actions"],
- serde_json::json!(["retry_operation_with_same_idempotency_key"])
- );
- assert!(
- !error
- .to_string()
- .contains("order-submit-conflict-idempotency")
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_submit_runtime_dtos_serialize_deterministically() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_123);
- let prepare_request = TradeSubmitPrepareRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-serialized"),
- )
- .with_created_at(created_at);
- let prepare_json = serde_json::to_value(&prepare_request).expect("prepare request json");
- assert_struct_serialize_error_paths(&prepare_request, 4);
-
- assert_eq!(
- prepare_json["actor"],
- serde_json::json!({
- "pubkey": BUYER_PUBLIC_KEY_HEX,
- "roles": ["buyer"],
- "account_id": null,
- "source": "test"
- })
- );
- assert_eq!(
- prepare_json["listing_event"],
- serde_json::json!({
- "id": deterministic_event_id("listing-event").as_str(),
- "relays": RELAY
- })
- );
- assert_eq!(prepare_json["order"]["order_id"], "order-submit-serialized");
- assert_eq!(
- prepare_json["order"]["listing_addr"],
- listing_address().as_str()
- );
- assert_eq!(prepare_json["order"]["buyer_pubkey"], BUYER_PUBLIC_KEY_HEX);
- assert_eq!(
- prepare_json["order"]["seller_pubkey"],
- SELLER_PUBLIC_KEY_HEX
- );
- assert_eq!(prepare_json["order"]["items"][0]["bin_id"], "bin-1");
- assert_eq!(prepare_json["order"]["items"][0]["bin_count"], 2);
- assert_eq!(prepare_json["created_at"], 1_700_000_123);
-
- let enqueue_request = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-serialized-enqueue"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("relay targets")
- .with_idempotency_key(
- SdkIdempotencyKey::new("order-serialized-idempotency").expect("idempotency"),
- )
- .with_created_at(created_at);
- let enqueue_json = serde_json::to_value(&enqueue_request).expect("enqueue request json");
- assert_struct_serialize_error_paths(&enqueue_request, 6);
-
- assert_eq!(
- enqueue_json["target_policy"],
- serde_json::json!({
- "kind": "explicit",
- "targets": [
- {
- "kind": "nostr",
- "uri": RELAY,
- "scope": null,
- "label": null,
- "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- },
- {
- "kind": "nostr",
- "uri": RELAY_B,
- "scope": null,
- "label": null,
- "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05"
- }
- ],
- "canonical_targets": [
- "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05",
- "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- ]
- })
- );
- assert_eq!(
- enqueue_json["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 28 })
- );
- assert_eq!(enqueue_json["created_at"], 1_700_000_123);
- assert!(
- !enqueue_json
- .to_string()
- .contains("order-serialized-idempotency")
- );
-
- let try_key_enqueue = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- listing_event_ptr(),
- order_request("order-submit-try-idempotency"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("order-submit-try-key")
- .expect("try idempotency key");
- assert_eq!(
- serde_json::to_value(&try_key_enqueue).expect("try key request json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 20 })
- );
-
- let receipt = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(
- enqueue_request,
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue");
- let receipt_json = serde_json::to_value(&receipt).expect("receipt json");
-
- assert_eq!(
- receipt_json,
- serde_json::json!({
- "workflow": {
- "kind": "submit",
- "operation_kind": TRADE_SUBMIT_OPERATION_KIND,
- "expected_event_id": receipt.workflow.expected_event_id.as_str(),
- "signed_event_id": receipt.workflow.signed_event_id.as_str(),
- "local_event_seq": 1,
- "outbox_operation_id": 1,
- "outbox_event_id": 1,
- "state": "stored_and_queued",
- "idempotency_digest_prefix": receipt.workflow.idempotency_digest_prefix.as_deref(),
- "idempotency": {
- "digest_prefix": receipt.workflow.idempotency.digest_prefix.as_deref(),
- "replayed_existing_operation": false,
- "safe_to_retry_with_same_idempotency_key": true
- },
- "retry": {
- "retryable_after_error": false,
- "safe_to_retry_enqueue_with_same_idempotency_key": true,
- "recovery_actions": []
- }
- },
- "order_id": receipt.order_id.as_str(),
- "locator": {
- "trade_id": receipt.order_id.as_str(),
- "root_event_id": receipt.signed_event_id.as_str(),
- "listing_addr": receipt.listing_addr.as_str(),
- "buyer_pubkey": BUYER_PUBLIC_KEY_HEX,
- "seller_pubkey": SELLER_PUBLIC_KEY_HEX
- },
- "listing_addr": receipt.listing_addr.as_str(),
- "buyer_pubkey": BUYER_PUBLIC_KEY_HEX,
- "seller_pubkey": SELLER_PUBLIC_KEY_HEX,
- "listing_event_id": receipt.listing_event_id.as_str(),
- "expected_event_id": receipt.expected_event_id.as_str(),
- "signed_event_id": receipt.signed_event_id.as_str(),
- "local_event_seq": 1,
- "outbox_operation_id": 1,
- "outbox_event_id": 1,
- "state": "stored_and_queued",
- "idempotency_digest_prefix": receipt.idempotency_digest_prefix.as_deref()
- })
- );
-}
-
fn order_decision(raw_order_id: &str) -> RadrootsOrderDecision {
RadrootsOrderDecision {
order_id: order_id(raw_order_id),
@@ -3786,84 +2599,6 @@ fn order_decision(raw_order_id: &str) -> RadrootsOrderDecision {
}
}
-#[cfg(any())]
-fn order_revision_proposal(
- raw_order_id: &str,
- root_event_id: &RadrootsEventId,
- previous_event_id: &RadrootsEventId,
-) -> RadrootsOrderRevisionProposal {
- RadrootsOrderRevisionProposal {
- revision_id: format!("revision-{raw_order_id}")
- .parse()
- .expect("revision id"),
- 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"),
- root_event_id: root_event_id.clone(),
- prev_event_id: previous_event_id.clone(),
- items: vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 3,
- }],
- economics: revision_economics(),
- reason: "increase quantity".to_owned(),
- }
-}
-
-#[cfg(any())]
-fn order_revision_decision(
- proposal: &RadrootsOrderRevisionProposal,
- previous_event_id: &RadrootsEventId,
- decision: RadrootsOrderRevisionOutcome,
-) -> RadrootsOrderRevisionDecision {
- RadrootsOrderRevisionDecision {
- revision_id: proposal.revision_id.clone(),
- order_id: proposal.order_id.clone(),
- listing_addr: proposal.listing_addr.clone(),
- buyer_pubkey: proposal.buyer_pubkey.clone(),
- seller_pubkey: proposal.seller_pubkey.clone(),
- root_event_id: proposal.root_event_id.clone(),
- prev_event_id: previous_event_id.clone(),
- decision,
- }
-}
-
-#[cfg(any())]
-fn order_cancellation(raw_order_id: &str) -> RadrootsOrderCancellation {
- RadrootsOrderCancellation {
- 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"),
- reason: "buyer changed pickup plan".to_owned(),
- }
-}
-
-fn revision_economics() -> RadrootsOrderEconomics {
- RadrootsOrderEconomics {
- quote_id: "revision-quote-1".parse().expect("revision quote id"),
- quote_version: 2,
- pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
- currency: RadrootsCoreCurrency::USD,
- items: vec![RadrootsOrderEconomicItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 3,
- quantity_amount: decimal("1"),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: decimal("5"),
- unit_price_currency: RadrootsCoreCurrency::USD,
- line_subtotal: usd("15"),
- }],
- discounts: Vec::<RadrootsOrderEconomicLine>::new(),
- adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
- subtotal: usd("15"),
- discount_total: usd("0"),
- adjustment_total: usd("0"),
- total: usd("15"),
- }
-}
-
#[cfg(feature = "transport-nostr-runtime")]
fn signed_raw_validation_receipt_event(
raw_order_id: &str,
@@ -3991,109 +2726,6 @@ fn validation_receipt_wire_parts_with_proof(
validation_receipt_event_build(raw_order_id, &receipt).expect("receipt event")
}
-fn signed_raw_sp1_worker_result_event(
- raw_order_id: &str,
- receipt_event_id: &RadrootsEventId,
- listing_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- target_event_id: &RadrootsEventId,
- created_at: u32,
-) -> nostr::Event {
- let content = serde_json::json!({
- "confidence": "committed_by_trusted_service_and_proof",
- "cryptographic_proof_verified": true,
- "customer_pubkey": BUYER_PUBLIC_KEY_HEX,
- "decision_event_id": target_event_id.as_str(),
- "event_set_root": hash32('c'),
- "listing_event_id": listing_event_id.as_str(),
- "order_id": raw_order_id,
- "proof_generated": true,
- "proof_mode": "core",
- "proof_system": "sp1_core",
- "public_values_hash": validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#),
- "prover_backend": "local_execute",
- "receipt_event_id": receipt_event_id.as_str(),
- "receipt_kind": KIND_TRADE_VALIDATION_RECEIPT,
- "reducer_output_root": hash32('4'),
- "request_event_id": root_event_id.as_str(),
- "request_hash": hash32('9'),
- "sp1_execute_checked": true,
- "sp1_execute_public_values_hash": validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#),
- "status": "succeeded",
- "validation_authority": "trusted_service_and_proof_verified",
- "worker_pubkey": public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX),
- "worker_role": "non_authoritative_prover"
- })
- .to_string();
- signed_raw_event(
- SERVICE_SECRET_KEY_HEX,
- created_at,
- RadrootsNip01EventWireParts {
- kind: KIND_TRADE_TRANSITION_PROOF_RESULT,
- content,
- tags: vec![
- vec!["e".to_owned(), root_event_id.as_str().to_owned()],
- vec![
- "radroots:validation_receipt".to_owned(),
- receipt_event_id.as_str().to_owned(),
- ],
- ],
- },
- )
-}
-
-#[cfg(feature = "transport-nostr-runtime")]
-fn signed_raw_worker_result_event(
- raw_order_id: &str,
- receipt_event_id: &RadrootsEventId,
- listing_event_id: &RadrootsEventId,
- root_event_id: &RadrootsEventId,
- target_event_id: &RadrootsEventId,
- created_at: u32,
-) -> nostr::Event {
- let content = serde_json::json!({
- "confidence": "committed_by_trusted_service",
- "cryptographic_proof_verified": false,
- "customer_pubkey": BUYER_PUBLIC_KEY_HEX,
- "decision_event_id": target_event_id.as_str(),
- "event_set_root": hash32('c'),
- "listing_event_id": listing_event_id.as_str(),
- "order_id": raw_order_id,
- "proof_generated": false,
- "proof_mode": "none",
- "proof_system": "none",
- "public_values_hash": validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#),
- "prover_backend": "local_execute",
- "receipt_event_id": receipt_event_id.as_str(),
- "receipt_kind": KIND_TRADE_VALIDATION_RECEIPT,
- "reducer_output_root": hash32('4'),
- "request_event_id": root_event_id.as_str(),
- "request_hash": hash32('9'),
- "sp1_execute_checked": true,
- "sp1_execute_public_values_hash": validation_receipt_public_values_hash_hex(br#"{"schema_version":1}"#),
- "status": "succeeded",
- "validation_authority": "trusted_rhi_service_key",
- "worker_pubkey": public_key_hex_for_secret(SERVICE_SECRET_KEY_HEX),
- "worker_role": "non_authoritative_prover"
- })
- .to_string();
- signed_raw_event(
- SERVICE_SECRET_KEY_HEX,
- created_at,
- RadrootsNip01EventWireParts {
- kind: KIND_TRADE_TRANSITION_PROOF_RESULT,
- content,
- tags: vec![
- vec!["e".to_owned(), root_event_id.as_str().to_owned()],
- vec![
- "radroots:validation_receipt".to_owned(),
- receipt_event_id.as_str().to_owned(),
- ],
- ],
- },
- )
-}
-
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()
@@ -4236,36 +2868,6 @@ fn signed_raw_order_request_event(raw_order_id: &str, created_at: u32) -> nostr:
signed_raw_event(BUYER_SECRET_KEY_HEX, created_at, draft)
}
-#[cfg(any())]
-fn request_event_ptr(event: &RadrootsEventEnvelope) -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: event.id_str().to_owned(),
- relays: Some(RELAY.to_owned()),
- }
-}
-
-#[cfg(any())]
-fn order_event_ptr(event_id: &RadrootsEventId) -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: event_id.as_str().to_owned(),
- relays: Some(RELAY.to_owned()),
- }
-}
-
-#[cfg(any())]
-async fn outbox_operation_kind(sdk: &RadrootsClient, operation_id: i64) -> String {
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- outbox
- .get_operation(operation_id)
- .await
- .expect("outbox operation")
- .expect("outbox operation")
- .operation_kind
-}
-
fn signed_order_decision_event(
raw_order_id: &str,
root_event_id: &RadrootsEventId,
@@ -4278,1689 +2880,110 @@ fn signed_order_decision_event(
)
.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()]],
- },
- )
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_request_evidence_ingest_stores_request_and_enables_decision_enqueue() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-decision-ingested", 39);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let ingest_request =
- TradeRequestEvidenceIngestRequest::new(signed_event_from_envelope(request_event.clone()))
- .with_observed_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_039));
-
- let ingest_receipt = sdk
- .trades()
- .ingest_request_evidence(ingest_request)
- .await
- .expect("ingest request evidence");
-
- assert_eq!(ingest_receipt.order_id.as_str(), "order-decision-ingested");
- assert_eq!(ingest_receipt.listing_addr, listing_address());
- assert_eq!(ingest_receipt.buyer_pubkey.as_str(), BUYER_PUBLIC_KEY_HEX);
- assert_eq!(ingest_receipt.seller_pubkey.as_str(), SELLER_PUBLIC_KEY_HEX);
- assert_eq!(ingest_receipt.request_event_id, request_event_id);
- assert_eq!(ingest_receipt.local_event_seq, 1);
- assert!(ingest_receipt.inserted);
-
- let actor = seller_actor();
- let plan = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- actor.clone(),
- request_event_ptr(&request_event),
- order_decision("order-decision-ingested"),
- ))
- .expect("prepare decision");
- let receipt = sdk
- .trades()
- .enqueue_prepared_decision_with_explicit_signer(
- &actor,
- plan,
- TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets"),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue decision");
-
- assert_eq!(receipt.local_event_seq, 2);
- let duplicate_receipt = sdk
- .trades()
- .ingest_request_evidence(TradeRequestEvidenceIngestRequest::new(
- request_event.clone(),
- ))
- .await
- .expect("duplicate request evidence");
- assert_eq!(duplicate_receipt.local_event_seq, 1);
- assert!(!duplicate_receipt.inserted);
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 2
- );
-}
-
-#[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::AgreedPendingRhi);
- 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
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_prepare_accept_and_decline_are_side_effect_free() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event_id = deterministic_event_id("order-decision-prepare-request");
- let request_event = RadrootsEventPtr {
- id: request_event_id.as_str().to_owned(),
- relays: Some(RELAY.to_owned()),
- };
- let accepted_request = TradeDecisionPrepareRequest::new(
- seller_actor(),
- request_event.clone(),
- order_decision("order-decision-prepare-accept"),
- );
-
- let accepted = sdk
- .trades()
- .prepare_decision(accepted_request)
- .expect("accepted plan");
-
- assert_eq!(accepted.order_id.as_str(), "order-decision-prepare-accept");
- assert_eq!(accepted.listing_addr, listing_address());
- assert_eq!(accepted.buyer_pubkey.as_str(), BUYER_PUBLIC_KEY_HEX);
- assert_eq!(accepted.seller_pubkey.as_str(), SELLER_PUBLIC_KEY_HEX);
- assert_eq!(accepted.request_event_id, request_event_id);
- assert_eq!(accepted.frozen_draft.kind_u32(), KIND_ORDER_DECISION);
- assert_eq!(accepted.created_at.unix_seconds(), 1_700_000_000);
- assert_eq!(
- accepted.expected_event_id,
- accepted.frozen_draft.expected_event_id_str()
- );
-
- let mut declined_payload = order_decision("order-decision-prepare-decline");
- declined_payload.decision = RadrootsOrderDecisionOutcome::Declined {
- reason: " out of stock ".to_owned(),
- };
- let declined = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller_actor(),
- request_event,
- declined_payload,
- ))
- .expect("declined plan");
-
- assert_eq!(declined.order_id.as_str(), "order-decision-prepare-decline");
- assert_eq!(declined.frozen_draft.kind_u32(), KIND_ORDER_DECISION);
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
-
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- assert!(
- outbox
- .claim_next_ready_event("worker", "claim", 2_000, 1_700_000_000_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_prepare_rejects_invalid_actor_evidence_and_payload() {
- let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
- let request_event = RadrootsEventPtr {
- id: deterministic_event_id("order-decision-invalid-request")
- .as_str()
- .to_owned(),
- relays: Some(RELAY.to_owned()),
- };
-
- let non_seller = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- non_seller_actor(),
- request_event.clone(),
- order_decision("order-decision-non-seller"),
- ))
- .expect_err("non seller");
- assert!(matches!(
- non_seller,
- RadrootsSdkError::UnauthorizedActor { .. }
- ));
-
- let wrong_actor = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- other_seller_actor(),
- request_event.clone(),
- order_decision("order-decision-wrong-seller"),
- ))
- .expect_err("wrong seller");
- assert!(matches!(
- wrong_actor,
- RadrootsSdkError::UnauthorizedActor { .. }
- ));
-
- let invalid_evidence = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller_actor(),
- RadrootsEventPtr {
- id: String::new(),
- relays: Some(RELAY.to_owned()),
- },
- order_decision("order-decision-invalid-evidence"),
- ))
- .expect_err("invalid evidence");
- assert!(matches!(
- invalid_evidence,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let mut empty_commitments = order_decision("order-decision-empty-commitments");
- empty_commitments.decision = RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: Vec::new(),
- };
- let commitment_error = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller_actor(),
- request_event.clone(),
- empty_commitments,
- ))
- .expect_err("missing commitments");
- assert!(matches!(
- commitment_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let mut missing_reason = order_decision("order-decision-missing-reason");
- missing_reason.decision = RadrootsOrderDecisionOutcome::Declined {
- reason: " ".to_owned(),
- };
- let reason_error = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller_actor(),
- request_event,
- missing_reason,
- ))
- .expect_err("missing reason");
- assert!(matches!(
- reason_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_runtime_dtos_serialize_deterministically() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_321);
- let prepare_event_id = deterministic_event_id("order-decision-serialized-request");
- let prepare_request = TradeDecisionPrepareRequest::new(
- seller_actor(),
- RadrootsEventPtr {
- id: prepare_event_id.as_str().to_owned(),
- relays: Some(RELAY.to_owned()),
- },
- order_decision("order-decision-serialized"),
- )
- .with_created_at(created_at);
- let prepare_json = serde_json::to_value(&prepare_request).expect("prepare request json");
- assert_struct_serialize_error_paths(&prepare_request, 4);
-
- assert_eq!(
- prepare_json["actor"],
- serde_json::json!({
- "pubkey": SELLER_PUBLIC_KEY_HEX,
- "roles": ["seller"],
- "account_id": null,
- "source": "test"
- })
- );
- assert_eq!(
- prepare_json["request_event"],
- serde_json::json!({
- "id": prepare_event_id.as_str(),
- "relays": RELAY
- })
- );
- assert_eq!(
- prepare_json["decision"]["order_id"],
- "order-decision-serialized"
- );
- assert_eq!(
- prepare_json["decision"]["seller_pubkey"],
- SELLER_PUBLIC_KEY_HEX
- );
- assert_eq!(prepare_json["created_at"], 1_700_000_321);
-
- let request_event = signed_order_request_event("order-decision-serialized-enqueue", 45);
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 4_500,
- ))
- .await
- .expect("ingest request");
- let enqueue_request = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- order_decision("order-decision-serialized-enqueue"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("transport targets")
- .with_idempotency_key(
- SdkIdempotencyKey::new("order-decision-serialized-idempotency").expect("idempotency"),
- )
- .with_created_at(created_at);
- let enqueue_json = serde_json::to_value(&enqueue_request).expect("enqueue request json");
- assert_struct_serialize_error_paths(&enqueue_request, 6);
-
- assert_eq!(
- enqueue_json["target_policy"],
- serde_json::json!({
- "kind": "explicit",
- "targets": [
- {
- "kind": "nostr",
- "uri": RELAY,
- "scope": null,
- "label": null,
- "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- },
- {
- "kind": "nostr",
- "uri": RELAY_B,
- "scope": null,
- "label": null,
- "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05"
- }
- ],
- "canonical_targets": [
- "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05",
- "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- ]
- })
- );
- assert_eq!(
- enqueue_json["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 37 })
- );
- assert_eq!(enqueue_json["created_at"], 1_700_000_321);
- assert!(
- !enqueue_json
- .to_string()
- .contains("order-decision-serialized-idempotency")
- );
-
- let try_key_enqueue = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- order_decision("order-decision-try-idempotency"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("order-decision-try-key")
- .expect("try idempotency key");
- assert_eq!(
- serde_json::to_value(&try_key_enqueue).expect("try key request json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 22 })
- );
-
- let receipt = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(
- enqueue_request,
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue");
- assert_eq!(receipt.workflow.kind, TradeWorkflowKind::Decision);
- assert_eq!(
- receipt.workflow.operation_kind,
- TRADE_DECISION_OPERATION_KIND
- );
- assert_eq!(
- receipt.workflow.expected_event_id,
- receipt.expected_event_id
- );
- assert_eq!(receipt.workflow.signed_event_id, receipt.signed_event_id);
- assert_eq!(receipt.workflow.local_event_seq, receipt.local_event_seq);
- assert_eq!(
- receipt.workflow.outbox_operation_id,
- receipt.outbox_operation_id
- );
- assert_eq!(receipt.workflow.outbox_event_id, receipt.outbox_event_id);
- assert_eq!(receipt.workflow.state, receipt.state);
- assert_eq!(
- receipt.workflow.idempotency_digest_prefix,
- receipt.idempotency_digest_prefix
- );
- assert!(
- receipt
- .workflow
- .idempotency
- .safe_to_retry_with_same_idempotency_key
- );
- assert!(!receipt.workflow.retry.retryable_after_error);
- assert!(receipt.workflow.retry.recovery_actions.is_empty());
- let receipt_json = serde_json::to_value(&receipt).expect("receipt json");
-
- assert_eq!(
- receipt_json,
- serde_json::json!({
- "workflow": {
- "kind": "decision",
- "operation_kind": TRADE_DECISION_OPERATION_KIND,
- "expected_event_id": receipt.workflow.expected_event_id.as_str(),
- "signed_event_id": receipt.workflow.signed_event_id.as_str(),
- "local_event_seq": 2,
- "outbox_operation_id": 1,
- "outbox_event_id": 1,
- "state": "stored_and_queued",
- "idempotency_digest_prefix": receipt.workflow.idempotency_digest_prefix.as_deref(),
- "idempotency": {
- "digest_prefix": receipt.workflow.idempotency.digest_prefix.as_deref(),
- "replayed_existing_operation": false,
- "safe_to_retry_with_same_idempotency_key": true
- },
- "retry": {
- "retryable_after_error": false,
- "safe_to_retry_enqueue_with_same_idempotency_key": true,
- "recovery_actions": []
- }
- },
- "order_id": receipt.order_id.as_str(),
- "locator": {
- "trade_id": receipt.order_id.as_str(),
- "root_event_id": request_event.id_str(),
- "listing_addr": receipt.listing_addr.as_str(),
- "buyer_pubkey": BUYER_PUBLIC_KEY_HEX,
- "seller_pubkey": SELLER_PUBLIC_KEY_HEX
- },
- "listing_addr": receipt.listing_addr.as_str(),
- "buyer_pubkey": BUYER_PUBLIC_KEY_HEX,
- "seller_pubkey": SELLER_PUBLIC_KEY_HEX,
- "request_event_id": request_event.id_str(),
- "expected_event_id": receipt.expected_event_id.as_str(),
- "signed_event_id": receipt.signed_event_id.as_str(),
- "local_event_seq": 2,
- "outbox_operation_id": 1,
- "outbox_event_id": 1,
- "state": "stored_and_queued",
- "idempotency_digest_prefix": receipt.idempotency_digest_prefix.as_deref()
- })
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_revision_and_cancellation_dtos_serialize_deterministically() {
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_654);
- let root_event_id = deterministic_event_id("order-dto-root");
- let previous_event_id = deterministic_event_id("order-dto-previous");
- let root_event = order_event_ptr(&root_event_id);
- let previous_event = order_event_ptr(&previous_event_id);
- let proposal =
- order_revision_proposal("order-revision-dto", &root_event_id, &previous_event_id);
- let revision_decision = order_revision_decision(
- &proposal,
- &previous_event_id,
- RadrootsOrderRevisionOutcome::Declined {
- reason: "not available".to_owned(),
- },
- );
- let cancellation = order_cancellation("order-revision-dto");
-
- let proposal_prepare = TradeRevisionProposalPrepareRequest::new(
- seller_actor(),
- root_event.clone(),
- previous_event.clone(),
- proposal.clone(),
- )
- .with_created_at(created_at);
- let proposal_prepare_json =
- serde_json::to_value(&proposal_prepare).expect("proposal prepare json");
- assert_struct_serialize_error_paths(&proposal_prepare, 5);
- assert_eq!(
- proposal_prepare_json["actor"]["pubkey"],
- SELLER_PUBLIC_KEY_HEX
- );
- assert_eq!(
- proposal_prepare_json["root_event"]["id"],
- root_event_id.as_str()
- );
- assert_eq!(
- proposal_prepare_json["previous_event"]["id"],
- previous_event_id.as_str()
- );
- assert_eq!(
- proposal_prepare_json["proposal"]["order_id"],
- "order-revision-dto"
- );
- assert_eq!(
- proposal_prepare_json["proposal"]["reason"],
- "increase quantity"
- );
- assert_eq!(proposal_prepare_json["created_at"], 1_700_000_654);
-
- let proposal_enqueue = TradeRevisionProposalEnqueueRequest::new(
- seller_actor(),
- root_event.clone(),
- previous_event.clone(),
- proposal.clone(),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("proposal relays")
- .with_idempotency_key(SdkIdempotencyKey::new("order-revision-proposal-dto").expect("key"))
- .with_created_at(created_at);
- let proposal_enqueue_json =
- serde_json::to_value(&proposal_enqueue).expect("proposal enqueue json");
- assert_struct_serialize_error_paths(&proposal_enqueue, 7);
- assert_eq!(
- proposal_enqueue_json["target_policy"],
- serde_json::json!({
- "kind": "explicit",
- "targets": [
- {
- "kind": "nostr",
- "uri": RELAY,
- "scope": null,
- "label": null,
- "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- },
- {
- "kind": "nostr",
- "uri": RELAY_B,
- "scope": null,
- "label": null,
- "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05"
- }
- ],
- "canonical_targets": [
- "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05",
- "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- ]
- })
- );
- assert_eq!(
- proposal_enqueue_json["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 27 })
- );
- assert!(!proposal_enqueue_json.to_string().contains("proposal-dto"));
-
- let proposal_try_key = TradeRevisionProposalEnqueueRequest::new(
- seller_actor(),
- root_event.clone(),
- previous_event.clone(),
- proposal.clone(),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("order-revision-proposal-try")
- .expect("proposal try key");
- assert_eq!(
- serde_json::to_value(&proposal_try_key).expect("proposal try json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 27 })
- );
-
- let decision_prepare = TradeRevisionDecisionPrepareRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- revision_decision.clone(),
- )
- .with_created_at(created_at);
- let decision_prepare_json =
- serde_json::to_value(&decision_prepare).expect("decision prepare json");
- assert_struct_serialize_error_paths(&decision_prepare, 5);
- assert_eq!(
- decision_prepare_json["actor"]["pubkey"],
- BUYER_PUBLIC_KEY_HEX
- );
- assert_eq!(
- decision_prepare_json["decision"]["revision_id"],
- proposal.revision_id.as_str()
- );
- assert_eq!(
- decision_prepare_json["decision"]["decision"],
- serde_json::json!({
- "decision": "declined",
- "reason": "not available"
- })
- );
- assert_eq!(decision_prepare_json["created_at"], 1_700_000_654);
-
- let decision_enqueue = TradeRevisionDecisionEnqueueRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- revision_decision,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("decision relays")
- .with_idempotency_key(
- SdkIdempotencyKey::new("order-revision-decision-dto").expect("decision idempotency"),
- )
- .with_created_at(created_at);
- let decision_enqueue_json =
- serde_json::to_value(&decision_enqueue).expect("decision enqueue json");
- assert_struct_serialize_error_paths(&decision_enqueue, 7);
- assert_eq!(
- decision_enqueue_json["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 27 })
- );
- assert_eq!(decision_enqueue_json["created_at"], 1_700_000_654);
- assert!(!decision_enqueue_json.to_string().contains("decision-dto"));
-
- let decision_try_key = TradeRevisionDecisionEnqueueRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- order_revision_decision(
- &proposal,
- &previous_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("order-revision-decision-try")
- .expect("decision try key");
- assert_eq!(
- serde_json::to_value(&decision_try_key).expect("decision try json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 27 })
- );
-
- let cancellation_prepare = TradeCancellationPrepareRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- cancellation.clone(),
- )
- .with_created_at(created_at);
- let cancellation_prepare_json =
- serde_json::to_value(&cancellation_prepare).expect("cancellation prepare json");
- assert_struct_serialize_error_paths(&cancellation_prepare, 5);
- assert_eq!(
- cancellation_prepare_json["cancellation"]["reason"],
- "buyer changed pickup plan"
- );
- assert_eq!(cancellation_prepare_json["created_at"], 1_700_000_654);
-
- let cancellation_enqueue = TradeCancellationEnqueueRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- cancellation,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("cancellation relays")
- .with_idempotency_key(
- SdkIdempotencyKey::new("order-cancellation-dto").expect("cancellation idempotency"),
- )
- .with_created_at(created_at);
- let cancellation_enqueue_json =
- serde_json::to_value(&cancellation_enqueue).expect("cancellation enqueue json");
- assert_struct_serialize_error_paths(&cancellation_enqueue, 7);
- assert_eq!(
- cancellation_enqueue_json["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 22 })
- );
- assert_eq!(cancellation_enqueue_json["created_at"], 1_700_000_654);
- assert!(
- !cancellation_enqueue_json
- .to_string()
- .contains("cancellation-dto")
- );
-
- let cancellation_try_key = TradeCancellationEnqueueRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- order_cancellation("order-revision-dto"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("order-cancellation-try")
- .expect("cancellation try key");
- assert_eq!(
- serde_json::to_value(&cancellation_try_key).expect("cancellation try json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 22 })
- );
-
- let event = signed_order_request_event("order-evidence-dto", 77);
- let request_evidence =
- TradeRequestEvidenceIngestRequest::new(signed_event_from_envelope(event.clone()))
- .with_observed_at(created_at);
- let request_evidence_json =
- serde_json::to_value(&request_evidence).expect("request evidence json");
- assert_struct_serialize_error_paths(&request_evidence, 2);
- assert_eq!(request_evidence_json["event"]["id"], event.id_str());
- assert_eq!(request_evidence_json["observed_at"], 1_700_000_654);
-
- let order_evidence = TradeEvidenceIngestRequest::new(signed_event_from_envelope(event.clone()))
- .with_observed_at(created_at);
- let order_evidence_json = serde_json::to_value(&order_evidence).expect("order evidence json");
- assert_struct_serialize_error_paths(&order_evidence, 2);
- assert_eq!(order_evidence_json["event"]["id"], event.id_str());
- assert_eq!(order_evidence_json["observed_at"], 1_700_000_654);
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_enqueue_accept_stores_event_queues_outbox_and_updates_status() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-decision-accept", 40);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 4_000,
- ))
- .await
- .expect("ingest request");
- let request = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- order_decision("order-decision-accept"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets")
- .try_with_idempotency_key("order-decision-accept-idempotency")
- .expect("idempotency");
-
- let receipt = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX))
- .await
- .expect("enqueue");
-
- assert_eq!(receipt.order_id.as_str(), "order-decision-accept");
- assert_eq!(receipt.listing_addr, listing_address());
- assert_eq!(receipt.buyer_pubkey.as_str(), BUYER_PUBLIC_KEY_HEX);
- assert_eq!(receipt.seller_pubkey.as_str(), SELLER_PUBLIC_KEY_HEX);
- assert_eq!(receipt.request_event_id, request_event_id);
- assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.local_event_seq, 2);
- assert_eq!(receipt.outbox_operation_id, 1);
- assert_eq!(receipt.outbox_event_id, 1);
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
- assert!(receipt.idempotency_digest_prefix.is_some());
-
- let stored_event = store
- .get_event(receipt.signed_event_id.as_str())
- .await
- .expect("event lookup")
- .expect("stored event");
- assert_eq!(stored_event.kind, KIND_ORDER_DECISION);
- assert_eq!(
- stored_event.contract_id.as_deref(),
- Some("radroots.order.decision.v1")
- );
-
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- let operation = outbox
- .get_operation(receipt.outbox_operation_id)
- .await
- .expect("outbox operation")
- .expect("outbox operation");
- assert_eq!(operation.operation_kind, TRADE_DECISION_OPERATION_KIND);
- let outbox_event = outbox
- .get_event(receipt.outbox_event_id)
- .await
- .expect("outbox event")
- .expect("outbox event");
- assert_eq!(outbox_event.state, RadrootsOutboxEventState::Signed);
- assert_eq!(outbox_event.draft.kind_u32(), KIND_ORDER_DECISION);
- assert!(outbox_event.signed_event.is_some());
-
- let status = sdk
- .trades()
- .status(status_request("order-decision-accept"))
- .await
- .expect("status");
- assert!(status.found);
- assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
- assert_eq!(status.event_count, 2);
- assert_eq!(
- status
- .request_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(request_event.id_str())
- );
- assert_eq!(
- status
- .decision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(receipt.signed_event_id.as_str())
- );
- assert_eq!(
- status
- .agreement_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(receipt.signed_event_id.as_str())
- );
- assert_eq!(status.pending_revision_event_id, None);
- assert!(status.issues.is_empty());
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_enqueue_decline_stores_event_and_status_sees_declined() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-decision-decline", 41);
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 4_100,
- ))
- .await
- .expect("ingest request");
- let mut decision = order_decision("order-decision-decline");
- decision.decision = RadrootsOrderDecisionOutcome::Declined {
- reason: " unavailable ".to_owned(),
- };
- let request = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- decision,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets");
-
- let receipt = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX))
- .await
- .expect("enqueue");
-
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
- let status = sdk
- .trades()
- .status(status_request("order-decision-decline"))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::Declined);
- assert_eq!(
- status
- .decision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(receipt.signed_event_id.as_str())
- );
- assert!(status.issues.is_empty());
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_enqueue_rejects_missing_request_evidence_before_mutation() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let missing_request = RadrootsEventPtr {
- id: deterministic_event_id("missing-order-request")
- .as_str()
- .to_owned(),
- relays: Some(RELAY.to_owned()),
- };
- let request = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- missing_request,
- order_decision("order-decision-missing-request"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets");
-
- let error = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX))
- .await
- .expect_err("missing request evidence");
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
- .await
- .expect("outbox");
- assert!(
- outbox
- .claim_next_ready_event("worker", "claim", 2_000, 1_700_000_000_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_enqueue_returns_sanitized_signer_errors_before_decision_mutation() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-decision-wrong-signer", 42);
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 4_200,
- ))
- .await
- .expect("ingest request");
- let request = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- order_decision("order-decision-wrong-signer"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets");
-
- let error = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX))
- .await
- .expect_err("signer error");
- let message = error.to_string();
-
- assert!(matches!(
- error,
- RadrootsSdkError::SignerPubkeyMismatch { .. }
- ));
- assert!(!message.contains("raw"));
- assert!(!message.contains("ffff"));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_decision_enqueue_rejects_existing_decision_state_before_mutation() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-decision-conflict", 43);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let decision_event =
- signed_order_decision_event("order-decision-conflict", &request_event_id, 44);
- for (event, observed_at_ms) in [
- (request_event.clone(), 4_300),
- (decision_event.clone(), 4_400),
- ] {
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(event),
- observed_at_ms,
- ))
- .await
- .expect("ingest");
- }
- let mut decline = order_decision("order-decision-conflict");
- decline.decision = RadrootsOrderDecisionOutcome::Declined {
- reason: "too late".to_owned(),
- };
- let request = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- decline,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets");
-
- let error = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX))
- .await
- .expect_err("existing decision");
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 2
- );
- let status = sdk
- .trades()
- .status(status_request("order-decision-conflict"))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
- assert_eq!(
- status
- .decision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(decision_event.id_str())
- );
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_revision_lifecycle_accepts_proposal_and_waits_for_rhi() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-lifecycle-agreement", 50);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 5_000,
- ))
- .await
- .expect("ingest request");
-
- let proposal = order_revision_proposal(
- "order-lifecycle-agreement",
- &request_event_id,
- &request_event_id,
- );
- let proposal_actor = seller_actor();
- let proposal_plan = sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- proposal_actor.clone(),
- request_event_ptr(&request_event),
- request_event_ptr(&request_event),
- proposal.clone(),
- ))
- .expect("prepare revision proposal");
- let proposal_receipt = sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &proposal_actor,
- proposal_plan,
- TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public)
- .expect("proposal transport targets"),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- Some(
- SdkIdempotencyKey::new("order-lifecycle-revision-proposal")
- .expect("proposal idempotency"),
- ),
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision proposal");
- assert_eq!(
- proposal_receipt.signed_event_id,
- proposal_receipt.expected_event_id
- );
- assert_eq!(
- outbox_operation_kind(&sdk, proposal_receipt.outbox_operation_id).await,
- TRADE_REVISION_PROPOSAL_OPERATION_KIND
- );
- let stored_proposal = store
- .get_event(proposal_receipt.signed_event_id.as_str())
- .await
- .expect("proposal event lookup")
- .expect("proposal event");
- assert_eq!(stored_proposal.kind, KIND_ORDER_REVISION_PROPOSAL);
- assert_eq!(
- stored_proposal.contract_id.as_deref(),
- Some("radroots.order.revision_proposal.v1")
- );
-
- let revision_decision = order_revision_decision(
- &proposal,
- &proposal_receipt.signed_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- );
- let revision_decision_actor = buyer_actor();
- let revision_decision_plan = sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- revision_decision_actor.clone(),
- request_event_ptr(&request_event),
- order_event_ptr(&proposal_receipt.signed_event_id),
- revision_decision,
- ))
- .expect("prepare revision decision");
- let revision_decision_receipt = sdk
- .trades()
- .enqueue_prepared_revision_decision_with_explicit_signer(
- &revision_decision_actor,
- revision_decision_plan,
- TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public)
- .expect("revision decision transport targets"),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision decision");
- assert_eq!(
- outbox_operation_kind(&sdk, revision_decision_receipt.outbox_operation_id).await,
- TRADE_REVISION_DECISION_OPERATION_KIND
- );
- assert_eq!(
- store
- .get_event(revision_decision_receipt.signed_event_id.as_str())
- .await
- .expect("revision decision lookup")
- .expect("revision decision")
- .kind,
- KIND_ORDER_REVISION_DECISION
- );
-
- let status = sdk
- .trades()
- .status(status_request("order-lifecycle-agreement"))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
- assert_eq!(status.event_count, 3);
- assert_eq!(
- status
- .agreement_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(revision_decision_receipt.signed_event_id.as_str())
- );
- assert!(status.decision_event_id.is_none());
- assert!(status.cancellation_event_id.is_none());
- assert_eq!(status.pending_revision_event_id, None);
- assert_eq!(status.listing_addr, Some(listing_address()));
- assert_eq!(
- status.buyer_pubkey.as_ref().map(ToString::to_string),
- Some(BUYER_PUBLIC_KEY_HEX.to_owned())
- );
- assert_eq!(
- status.seller_pubkey.as_ref().map(ToString::to_string),
- Some(SELLER_PUBLIC_KEY_HEX.to_owned())
- );
- assert_eq!(status.economics, Some(revision_economics()));
- assert!(!status.lifecycle_terminal);
- assert_eq!(
- status.next_action,
- TradeStatusNextActionKind::AwaitRhiValidation
- );
- assert!(status.evidence.has_request);
- assert!(!status.evidence.has_decision);
- assert!(status.evidence.has_agreement);
- assert!(!status.evidence.has_pending_revision);
- assert!(!status.evidence.has_cancellation);
- assert!(!status.evidence.has_issues);
- assert!(!status.eligibility.can_decide);
- assert!(!status.eligibility.can_propose_revision);
- assert!(!status.eligibility.can_decide_revision);
- assert!(!status.eligibility.can_cancel);
- assert!(status.issues.is_empty());
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lifecycle() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-lifecycle-pending-revision", 55);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 5_500,
- ))
- .await
- .expect("ingest request");
- let proposal = order_revision_proposal(
- "order-lifecycle-pending-revision",
- &request_event_id,
- &request_event_id,
- );
- let proposal_receipt = sdk
- .trades()
- .enqueue_revision_proposal_with_explicit_signer(
- TradeRevisionProposalEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- request_event_ptr(&request_event),
- proposal,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("proposal transport targets"),
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision proposal");
-
- let status = sdk
- .trades()
- .status(status_request("order-lifecycle-pending-revision"))
- .await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::RevisionProposed);
- assert_eq!(status.event_count, 2);
- assert!(status.agreement_event_id.is_none());
- assert_eq!(
- status
- .pending_revision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(proposal_receipt.signed_event_id.as_str())
- );
- assert_eq!(
- status.last_event_id.as_ref().map(RadrootsEventId::as_str),
- Some(proposal_receipt.signed_event_id.as_str())
- );
- assert!(status.issues.is_empty());
- assert!(!status.eligibility.can_decide);
- assert!(!status.eligibility.can_propose_revision);
- assert!(status.eligibility.can_decide_revision);
- assert!(!status.eligibility.can_cancel);
-
- let decision_error = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(
- TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- order_decision("order-lifecycle-pending-revision"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("decision transport targets"),
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("pending revision blocks direct decision");
- assert!(matches!(
- decision_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
+}
- let blocked_proposal = order_revision_proposal(
- "order-lifecycle-pending-revision",
- &request_event_id,
- &proposal_receipt.signed_event_id,
- );
- let proposal_error = sdk
- .trades()
- .enqueue_revision_proposal_with_explicit_signer(
- TradeRevisionProposalEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- order_event_ptr(&proposal_receipt.signed_event_id),
- blocked_proposal,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("blocked proposal transport targets"),
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("pending revision blocks new proposal");
- assert!(matches!(
- proposal_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 2
- );
+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()]],
+ },
+ )
}
-#[cfg(any())]
#[tokio::test]
-async fn order_declined_revision_finalizes_declined_negotiation() {
+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-lifecycle-declined-revision", 56);
+ let request_event = signed_order_request_event("order-evidence-ingest", 39);
let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 5_600,
- ))
- .await
- .expect("ingest request");
- let proposal = order_revision_proposal(
- "order-lifecycle-declined-revision",
- &request_event_id,
- &request_event_id,
- );
- let proposal_receipt = sdk
- .trades()
- .enqueue_revision_proposal_with_explicit_signer(
- TradeRevisionProposalEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- request_event_ptr(&request_event),
- proposal.clone(),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("proposal transport targets"),
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision proposal");
- let declined_revision = order_revision_decision(
- &proposal,
- &proposal_receipt.signed_event_id,
- RadrootsOrderRevisionOutcome::Declined {
- reason: "keep original order".to_owned(),
- },
- );
- let declined_revision_receipt = sdk
+ let decision_event =
+ signed_order_decision_event("order-evidence-ingest", &request_event_id, 40);
+
+ let request_receipt = sdk
.trades()
- .enqueue_revision_decision_with_explicit_signer(
- TradeRevisionDecisionEnqueueRequest::new(
- buyer_actor(),
- request_event_ptr(&request_event),
- order_event_ptr(&proposal_receipt.signed_event_id),
- declined_revision,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("declined revision transport targets"),
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
+ .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
+ request_event.clone(),
+ )))
.await
- .expect("enqueue declined revision");
+ .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 status = sdk
+ let decision_receipt = sdk
.trades()
- .status(status_request("order-lifecycle-declined-revision"))
+ .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
+ decision_event.clone(),
+ )))
.await
- .expect("status");
- assert_eq!(status.status, TradeStatusKind::Declined);
- assert_eq!(status.event_count, 3);
- assert!(status.agreement_event_id.is_none());
- assert_eq!(
- status
- .pending_revision_event_id
- .as_ref()
- .map(RadrootsEventId::as_str),
- Some(proposal_receipt.signed_event_id.as_str())
- );
- assert_eq!(
- status.last_event_id.as_ref().map(RadrootsEventId::as_str),
- Some(declined_revision_receipt.signed_event_id.as_str())
- );
- assert!(status.lifecycle_terminal);
- assert_eq!(status.next_action, TradeStatusNextActionKind::Terminal);
- assert!(!status.eligibility.can_decide);
- assert!(!status.eligibility.can_propose_revision);
- assert!(!status.eligibility.can_decide_revision);
- assert!(!status.eligibility.can_cancel);
+ .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 second_decision = order_revision_decision(
- &proposal,
- &proposal_receipt.signed_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- );
- let second_decision_error = sdk
+ let duplicate_receipt = sdk
.trades()
- .enqueue_revision_decision_with_explicit_signer(
- TradeRevisionDecisionEnqueueRequest::new(
- buyer_actor(),
- request_event_ptr(&request_event),
- order_event_ptr(&proposal_receipt.signed_event_id),
- second_decision,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("second decision transport targets"),
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
+ .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
+ decision_event,
+ )))
.await
- .expect_err("second revision decision after decline");
- assert!(matches!(
- second_decision_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
+ .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,
- 3
+ 2
);
-}
-
-#[cfg(any())]
-#[tokio::test]
-async fn order_cancel_lifecycle_enqueue_updates_status() {
- let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-lifecycle-cancel", 60);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 6_000,
- ))
- .await
- .expect("ingest request");
- let cancellation_actor = buyer_actor();
- let cancellation_plan = sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- cancellation_actor.clone(),
- request_event_ptr(&request_event),
- request_event_ptr(&request_event),
- order_cancellation("order-lifecycle-cancel"),
- ))
- .expect("prepare cancellation");
- let cancellation = sdk
- .trades()
- .enqueue_prepared_cancellation_with_explicit_signer(
- &cancellation_actor,
- cancellation_plan,
- TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public)
- .expect("cancellation transport targets"),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- Some(
- SdkIdempotencyKey::new("order-lifecycle-cancel").expect("cancellation idempotency"),
- ),
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue cancellation");
- assert_eq!(cancellation.root_event_id, request_event_id);
- assert_eq!(cancellation.previous_event_id, request_event_id);
- assert_eq!(
- outbox_operation_kind(&sdk, cancellation.outbox_operation_id).await,
- TRADE_CANCELLATION_OPERATION_KIND
- );
- assert_eq!(
- store
- .get_event(cancellation.signed_event_id.as_str())
- .await
- .expect("cancellation lookup")
- .expect("cancellation")
- .kind,
- KIND_ORDER_CANCELLATION
- );
- let replay = sdk
- .trades()
- .enqueue_cancellation_with_explicit_signer(
- TradeCancellationEnqueueRequest::new(
- buyer_actor(),
- request_event_ptr(&request_event),
- request_event_ptr(&request_event),
- order_cancellation("order-lifecycle-cancel"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("replay transport targets")
- .try_with_idempotency_key("order-lifecycle-cancel")
- .expect("replay idempotency"),
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("replay cancellation");
- assert_eq!(replay.state, SdkMutationState::AlreadyQueued);
- assert_eq!(replay.signed_event_id, cancellation.signed_event_id);
- assert_eq!(replay.outbox_event_id, cancellation.outbox_event_id);
let status = sdk
.trades()
- .status(status_request("order-lifecycle-cancel"))
+ .status(status_request("order-evidence-ingest"))
.await
.expect("status");
- assert_eq!(status.status, TradeStatusKind::Cancelled);
+ assert_eq!(status.status, TradeStatusKind::AgreedPendingValidation);
+ assert_eq!(status.event_count, 2);
assert_eq!(
status
- .cancellation_event_id
+ .decision_event_id
.as_ref()
.map(RadrootsEventId::as_str),
- Some(cancellation.signed_event_id.as_str())
+ Some(decision_receipt.event_id.as_str())
);
- assert!(status.lifecycle_terminal);
- assert_eq!(status.next_action, TradeStatusNextActionKind::Terminal);
- assert!(status.evidence.has_request);
- assert!(!status.evidence.has_decision);
- assert!(status.evidence.has_cancellation);
- assert!(!status.evidence.has_issues);
- assert!(!status.eligibility.can_cancel);
- assert!(status.issues.is_empty());
}
-#[cfg(any())]
#[tokio::test]
-async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() {
+async fn order_evidence_ingest_rejects_non_order_events() {
let (_tempdir, sdk, store) = directory_sdk_and_store().await;
- let request_event = signed_order_request_event("order-lifecycle-invalid", 70);
- let request_event_id = RadrootsEventId::parse(request_event.id_str()).expect("request id");
- let missing = sdk
+ let error = sdk
.trades()
- .enqueue_revision_proposal_with_explicit_signer(
- TradeRevisionProposalEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- request_event_ptr(&request_event),
- order_revision_proposal(
- "order-lifecycle-invalid",
- &request_event_id,
- &request_event_id,
- ),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("missing transport targets"),
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
+ .ingest_evidence(TradeEvidenceIngestRequest::new(signed_event_from_envelope(
+ signed_non_order_event(41),
+ )))
.await
- .expect_err("missing local evidence");
- assert!(matches!(missing, RadrootsSdkError::InvalidRequest { .. }));
+ .expect_err("non order event");
+
+ assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
assert_eq!(
store
.status_summary()
@@ -5969,92 +2992,30 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() {
.total_events,
0
);
+}
- store
- .ingest_event(RadrootsEventIngest::new(
- signed_event_from_envelope(request_event.clone()),
- 7_000,
- ))
- .await
- .expect("ingest request");
- let decision_receipt = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(
- TradeDecisionEnqueueRequest::new(
- seller_actor(),
- request_event_ptr(&request_event),
- order_decision("order-lifecycle-invalid"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("decision transport targets"),
- &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue decision");
- let revision_without_proposal = order_revision_decision(
- &order_revision_proposal(
- "order-lifecycle-invalid",
- &request_event_id,
- &decision_receipt.signed_event_id,
- ),
- &decision_receipt.signed_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- );
- let revision_error = sdk
- .trades()
- .enqueue_revision_decision_with_explicit_signer(
- TradeRevisionDecisionEnqueueRequest::new(
- buyer_actor(),
- request_event_ptr(&request_event),
- order_event_ptr(&decision_receipt.signed_event_id),
- revision_without_proposal,
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("revision decision transport targets"),
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("revision decision without proposal");
- assert!(matches!(
- revision_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
+#[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 cancellation_error = sdk
+ let error = sdk
.trades()
- .enqueue_cancellation_with_explicit_signer(
- TradeCancellationEnqueueRequest::new(
- buyer_actor(),
- request_event_ptr(&request_event),
- order_event_ptr(&decision_receipt.signed_event_id),
- order_cancellation("order-lifecycle-invalid"),
- TargetPolicy::default_profile(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("cancellation transport targets"),
- &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
+ .ingest_request_evidence(TradeRequestEvidenceIngestRequest::new(
+ signed_event_from_envelope(decision_event),
+ ))
.await
- .expect_err("cancellation after accepted agreement");
- assert!(matches!(
- cancellation_error,
- RadrootsSdkError::InvalidRequest { .. }
- ));
+ .expect_err("non request event");
+
+ assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
assert_eq!(
store
.status_summary()
.await
.expect("event store status")
.total_events,
- 2
+ 0
);
}
@@ -6085,8 +3046,6 @@ async fn order_status_returns_not_found_for_missing_local_order() {
assert!(!receipt.evidence.has_issues);
assert!(!receipt.eligibility.can_decide);
assert!(!receipt.eligibility.can_cancel);
- assert!(!receipt.eligibility.can_propose_revision);
- assert!(!receipt.eligibility.can_decide_revision);
assert!(receipt.issues.is_empty());
}
@@ -6253,8 +3212,6 @@ async fn order_status_contract_dtos_serialize_deterministically() {
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);
- assert_eq!(receipt_json["eligibility"]["can_propose_revision"], false);
- assert_eq!(receipt_json["eligibility"]["can_decide_revision"], false);
let issue = SdkTradeStatusIssue {
kind: SdkTradeStatusIssueKind::DecisionPayloadInvalid,
@@ -6409,116 +3366,6 @@ fn order_status_issue_mapping_preserves_kind_codes_and_event_ids() {
"conflicting_decisions"
),
single_issue!(
- RevisionProposalPayloadInvalid,
- RevisionProposalPayloadInvalid,
- "revision_proposal_payload_invalid"
- ),
- single_issue!(
- RevisionProposalOrderIdMismatch,
- RevisionProposalOrderIdMismatch,
- "revision_proposal_order_id_mismatch"
- ),
- single_issue!(
- RevisionProposalAuthorMismatch,
- RevisionProposalAuthorMismatch,
- "revision_proposal_author_mismatch"
- ),
- single_issue!(
- RevisionProposalCounterpartyMismatch,
- RevisionProposalCounterpartyMismatch,
- "revision_proposal_counterparty_mismatch"
- ),
- single_issue!(
- RevisionProposalBuyerMismatch,
- RevisionProposalBuyerMismatch,
- "revision_proposal_buyer_mismatch"
- ),
- single_issue!(
- RevisionProposalSellerMismatch,
- RevisionProposalSellerMismatch,
- "revision_proposal_seller_mismatch"
- ),
- single_issue!(
- RevisionProposalListingAddressInvalid,
- RevisionProposalListingAddressInvalid,
- "revision_proposal_listing_address_invalid"
- ),
- single_issue!(
- RevisionProposalListingMismatch,
- RevisionProposalListingMismatch,
- "revision_proposal_listing_mismatch"
- ),
- single_issue!(
- RevisionProposalRootMismatch,
- RevisionProposalRootMismatch,
- "revision_proposal_root_mismatch"
- ),
- single_issue!(
- RevisionProposalPreviousMismatch,
- RevisionProposalPreviousMismatch,
- "revision_proposal_previous_mismatch"
- ),
- single_issue!(
- RevisionDecisionWithoutProposal,
- RevisionDecisionWithoutProposal,
- "revision_decision_without_proposal"
- ),
- single_issue!(
- RevisionDecisionPayloadInvalid,
- RevisionDecisionPayloadInvalid,
- "revision_decision_payload_invalid"
- ),
- single_issue!(
- RevisionDecisionOrderIdMismatch,
- RevisionDecisionOrderIdMismatch,
- "revision_decision_order_id_mismatch"
- ),
- single_issue!(
- RevisionDecisionAuthorMismatch,
- RevisionDecisionAuthorMismatch,
- "revision_decision_author_mismatch"
- ),
- single_issue!(
- RevisionDecisionCounterpartyMismatch,
- RevisionDecisionCounterpartyMismatch,
- "revision_decision_counterparty_mismatch"
- ),
- single_issue!(
- RevisionDecisionBuyerMismatch,
- RevisionDecisionBuyerMismatch,
- "revision_decision_buyer_mismatch"
- ),
- single_issue!(
- RevisionDecisionSellerMismatch,
- RevisionDecisionSellerMismatch,
- "revision_decision_seller_mismatch"
- ),
- single_issue!(
- RevisionDecisionListingAddressInvalid,
- RevisionDecisionListingAddressInvalid,
- "revision_decision_listing_address_invalid"
- ),
- single_issue!(
- RevisionDecisionListingMismatch,
- RevisionDecisionListingMismatch,
- "revision_decision_listing_mismatch"
- ),
- single_issue!(
- RevisionDecisionRootMismatch,
- RevisionDecisionRootMismatch,
- "revision_decision_root_mismatch"
- ),
- single_issue!(
- RevisionDecisionPreviousMismatch,
- RevisionDecisionPreviousMismatch,
- "revision_decision_previous_mismatch"
- ),
- single_issue!(
- RevisionDecisionRevisionIdMismatch,
- RevisionDecisionRevisionIdMismatch,
- "revision_decision_revision_id_mismatch"
- ),
- single_issue!(
CancellationWithoutCancellableOrder,
CancellationWithoutCancellableOrder,
"cancellation_without_cancellable_order"
@@ -6684,7 +3531,7 @@ async fn order_status_projects_local_request_and_decision_events() {
.collect::<Vec<_>>(),
vec![request_event.id_str(), decision_event.id_str()]
);
- assert_eq!(receipt.status, TradeStatusKind::AgreedPendingRhi);
+ assert_eq!(receipt.status, TradeStatusKind::AgreedPendingValidation);
assert_eq!(
receipt
.request_event_id
@@ -6717,17 +3564,14 @@ async fn order_status_projects_local_request_and_decision_events() {
assert!(!receipt.lifecycle_terminal);
assert_eq!(
receipt.next_action,
- TradeStatusNextActionKind::AwaitRhiValidation
+ 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_pending_revision);
assert!(!receipt.evidence.has_issues);
assert!(!receipt.eligibility.can_decide);
- assert!(!receipt.eligibility.can_propose_revision);
- assert!(!receipt.eligibility.can_decide_revision);
assert!(!receipt.eligibility.can_cancel);
}
diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs
@@ -9,9 +9,9 @@ use radroots_sdk::{
SDK_TRANSPORT_TARGET_MAX_COUNT, SdkBackupState, SdkBackupVerification,
SdkEventStoreStorageStatus, SdkIdempotencyKey, SdkOutboxStorageStatus,
SdkPrivateStoreStorageStatus, SdkRestoreState, SdkSqliteStoreStatus,
- SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, StorageCheckpointReceipt,
- StorageCheckpointRequest, StorageStatusReceipt, StorageStatusRequest, TargetPolicy, TargetSet,
- TransportProfile,
+ SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, SdkStudioStoreStorageStatus,
+ StorageCheckpointReceipt, StorageCheckpointRequest, StorageStatusReceipt, StorageStatusRequest,
+ TargetPolicy, TargetSet, TransportProfile,
};
use radroots_trade::identity::RadrootsTradeLocator;
use sqlx::Row;
@@ -40,7 +40,6 @@ async fn sdk_builder_defaults_to_memory_storage_and_no_relays() {
let _geonames = sdk.geonames();
let _trades = sdk.trades();
let _sync = sdk.sync();
- let _dvm = sdk.dvm();
}
#[tokio::test]
@@ -156,48 +155,63 @@ async fn sdk_directory_storage_creates_deterministic_sqlite_files() {
let paths = sdk.storage_paths().expect("paths");
assert_eq!(
- paths.event_store_path,
- tempdir
- .path()
- .join("sdk-runtime")
- .join("event_store.sqlite")
+ paths.runtime_path,
+ tempdir.path().join("sdk-runtime").join("runtime.sqlite")
);
assert_eq!(
- paths.outbox_path,
- tempdir.path().join("sdk-runtime").join("outbox.sqlite")
+ paths.private_path,
+ tempdir.path().join("sdk-runtime").join("private.sqlite")
);
assert_eq!(
- paths.private_store_path,
- tempdir.path().join("sdk-runtime").join("private.sqlite")
+ paths.studio_path,
+ tempdir.path().join("sdk-runtime").join("studio.sqlite")
);
- assert!(paths.event_store_path.exists());
- assert!(paths.outbox_path.exists());
- assert!(paths.private_store_path.exists());
- let event_tables = sqlite_table_names(&paths.event_store_path).await;
- assert!(event_tables.iter().any(|name| name == "event_envelopes"));
+ assert!(paths.runtime_path.exists());
+ assert!(paths.private_path.exists());
+ assert!(paths.studio_path.exists());
+ let runtime_tables = sqlite_table_names(&paths.runtime_path).await;
+ assert!(runtime_tables.iter().any(|name| name == "event_envelopes"));
assert!(
- event_tables
+ runtime_tables
.iter()
.any(|name| name == "event_envelope_tags")
);
- assert!(event_tables.iter().any(|name| name == "listing_projection"));
- assert!(event_tables.iter().any(|name| name == "trade_projection"));
- assert!(event_tables.iter().any(|name| name == "listing_search_fts"));
- assert!(!event_tables.iter().any(|name| name == "nostr_event"));
- assert!(!event_tables.iter().any(|name| name == "nostr_event_tag"));
+ assert!(
+ runtime_tables
+ .iter()
+ .any(|name| name == "listing_projection")
+ );
+ assert!(runtime_tables.iter().any(|name| name == "trade_projection"));
+ assert!(
+ runtime_tables
+ .iter()
+ .any(|name| name == "listing_search_fts")
+ );
+ assert!(
+ runtime_tables
+ .iter()
+ .any(|name| name == "outbox_operations")
+ );
+ assert!(
+ runtime_tables
+ .iter()
+ .any(|name| name == "sdk_runtime_operation_journal")
+ );
+ 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.event_store_path).await,
+ sqlite_trade_projection_primary_key(&paths.runtime_path).await,
vec!["order_id", "root_event_id", "projection_version"]
);
- let outbox_tables = sqlite_table_names(&paths.outbox_path).await;
- assert!(outbox_tables.iter().any(|name| name == "outbox_operations"));
- assert!(!outbox_tables.iter().any(|name| name == "outbox_operation"));
- let private_tables = sqlite_table_names(&paths.private_store_path).await;
+ 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")
);
+ let studio_tables = sqlite_table_names(&paths.studio_path).await;
+ assert!(studio_tables.iter().any(|name| name == "sdk_studio_state"));
}
#[tokio::test]
@@ -326,25 +340,6 @@ fn sdk_timestamp_rejects_values_outside_nostr_created_at_range() {
}
#[test]
-fn sdk_partial_local_mutation_error_is_sanitized() {
- let event_id = "a".repeat(64);
- let error = RadrootsSdkError::partial_outbox_enqueue_mutation(
- event_id,
- LISTING_PUBLISH_OPERATION_KIND,
- "abcdef123456",
- );
- let message = error.to_string();
-
- assert!(message.contains(LISTING_PUBLISH_OPERATION_KIND));
- assert!(message.contains("abcdef123456"));
- assert!(message.contains("stored=true"));
- assert!(message.contains("queued=false"));
- assert!(!message.contains("sig"));
- assert!(!message.contains("raw"));
- assert!(!message.contains("idempotency-key"));
-}
-
-#[test]
fn sdk_error_contract_methods_cover_all_variants() {
let cases = vec![
(
@@ -455,9 +450,9 @@ fn sdk_error_contract_methods_cover_all_variants() {
(
RadrootsSdkError::TradeAmbiguous {
operation: "trade.accept".to_owned(),
- locator: RadrootsTradeLocator::from_order_id(
+ locator: Box::new(RadrootsTradeLocator::from_order_id(
RadrootsOrderId::parse("trade-error").expect("order id"),
- ),
+ )),
candidates: vec![RadrootsTradeLocator::from_order_id(
RadrootsOrderId::parse("trade-error").expect("order id"),
)],
@@ -536,10 +531,10 @@ fn sdk_error_contract_methods_cover_all_variants() {
vec![RadrootsSdkRecoveryAction::FixRequest],
),
(
- RadrootsSdkError::ListingDraft {
- message: "draft".to_owned(),
+ RadrootsSdkError::ListingEdit {
+ message: "edit".to_owned(),
},
- "listing_draft",
+ "listing_edit",
RadrootsSdkErrorClass::Request,
false,
vec![RadrootsSdkRecoveryAction::FixRequest],
@@ -590,17 +585,6 @@ fn sdk_error_contract_methods_cover_all_variants() {
true,
vec![RadrootsSdkRecoveryAction::InspectLocalStores],
),
- (
- RadrootsSdkError::partial_outbox_enqueue_mutation(
- "a".repeat(64),
- LISTING_PUBLISH_OPERATION_KIND,
- "abcdef123456",
- ),
- "partial_local_mutation",
- RadrootsSdkErrorClass::LocalMutation,
- true,
- vec![RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey],
- ),
];
for (error, code, class, retryable, recovery_actions) in cases {
@@ -702,14 +686,14 @@ fn relay_target_set_validates_normalizes_preserves_order_and_caps() {
#[test]
fn idempotency_key_validation_is_bounded_and_debug_redacted() {
- let key = SdkIdempotencyKey::new("idem-a").expect("key");
- assert_eq!(key.as_str(), "idem-a");
+ let key = SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000022d").expect("key");
+ assert_eq!(key.as_str(), "01890f0e-6c00-7000-8000-00000000022d");
let debug = format!("{key:?}");
assert!(debug.contains("<redacted>"));
- assert!(!debug.contains("idem-a"));
+ assert!(!debug.contains("01890f0e-6c00-7000-8000-00000000022d"));
assert_eq!(
serde_json::to_value(&key).expect("key json"),
- serde_json::json!({ "value": "<redacted>", "len": 6 })
+ serde_json::json!({ "value": "<redacted>", "len": 36 })
);
assert!(matches!(
@@ -722,7 +706,11 @@ fn idempotency_key_validation_is_bounded_and_debug_redacted() {
RadrootsSdkError::InvalidRequest { ref message }
if message == "idempotency key must not include boundary whitespace"
));
- assert!(!untrimmed.to_string().contains("idem-a"));
+ assert!(
+ !untrimmed
+ .to_string()
+ .contains("01890f0e-6c00-7000-8000-00000000022d")
+ );
assert!(matches!(
SdkIdempotencyKey::new("idem\nbad"),
Err(RadrootsSdkError::InvalidRequest { .. })
@@ -789,6 +777,10 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
store: private_store,
farm_private_locations: 4,
},
+ studio_store: SdkStudioStoreStorageStatus {
+ store: store.clone(),
+ studio_state_records: 5,
+ },
})
.expect("status receipt"),
serde_json::json!({
@@ -849,6 +841,20 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
"integrity_result": "ok"
},
"farm_private_locations": 4
+ },
+ "studio_store": {
+ "store": {
+ "schema_version": 1,
+ "journal_mode": "wal",
+ "foreign_keys_enabled": true,
+ "busy_timeout_ms": 5000,
+ "wal_status": {
+ "wal_enabled": true
+ },
+ "integrity_ok": true,
+ "integrity_result": "ok"
+ },
+ "studio_state_records": 5
}
})
);
@@ -862,7 +868,8 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
paths: None,
event_store: checkpoint.clone(),
outbox: checkpoint.clone(),
- private_store: checkpoint,
+ private_store: checkpoint.clone(),
+ studio_store: checkpoint,
})
.expect("checkpoint receipt"),
serde_json::json!({
@@ -888,6 +895,13 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
"log_frame_count": 8,
"checkpointed_frame_count": 8,
"checkpoint_complete": true
+ },
+ "studio_store": {
+ "wal_enabled": true,
+ "busy": 0,
+ "log_frame_count": 8,
+ "checkpointed_frame_count": 8,
+ "checkpoint_complete": true
}
})
);
@@ -926,18 +940,22 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
event_store_ok: true,
outbox_ok: true,
private_store_ok: true,
+ studio_store_ok: true,
event_store_events: 2,
outbox_events: 3,
private_farm_locations: 4,
+ studio_state_records: 5,
})
.expect("backup verification"),
serde_json::json!({
"event_store_ok": true,
"outbox_ok": true,
"private_store_ok": true,
+ "studio_store_ok": true,
"event_store_events": 2,
"outbox_events": 3,
- "private_farm_locations": 4
+ "private_farm_locations": 4,
+ "studio_state_records": 5
})
);
assert_eq!(
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -74,7 +74,11 @@ const FORBIDDEN_SDK_README_CONCEPTS: &[ForbiddenSdkConcept] = &[
},
ForbiddenSdkConcept {
pattern: "sdk.trade_validation()",
- reason: "SDK docs must use sdk.dvm() for validation receipt ingestion",
+ 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",
@@ -249,7 +253,6 @@ const REQUIRED_SDK_README_CONCEPTS: &[&str] = &[
"sdk.trades().status(...)",
"sdk.trades().resync()",
"sdk.trades().validation_receipts()",
- "sdk.dvm()",
"event-envelope timestamp",
];
@@ -295,8 +298,6 @@ const REQUIRED_TRADE_RUNTIME_EXPORTS: &[&str] = &[
"TradeValidationReceiptTags",
"TradeValidationReceiptVerifyRequest",
"TradeValidationTrustDecision",
- "TradeValidationReceiptWorkerEvidence",
- "TradeValidationReceiptWorkerEvidenceSelection",
"SdkTradeStatusIssue",
"SdkTradeStatusIssueKind",
"SdkTradeStatusSource",
@@ -305,8 +306,6 @@ const REQUIRED_TRADE_RUNTIME_EXPORTS: &[&str] = &[
const REQUIRED_TRADE_SIGNER_EXPORTS: &[&str] = &[
"TRADE_CANCELLATION_OPERATION_KIND",
"TRADE_DECISION_OPERATION_KIND",
- "TRADE_REVISION_DECISION_OPERATION_KIND",
- "TRADE_REVISION_PROPOSAL_OPERATION_KIND",
"TRADE_SUBMIT_OPERATION_KIND",
"TradeAcceptRequest",
"TradeCancelRequest",
@@ -318,12 +317,6 @@ const REQUIRED_TRADE_SIGNER_EXPORTS: &[&str] = &[
"TradeEvidenceMode",
"TradeMutationOutcome",
"TradeProposeRequest",
- "TradeRevisionDecisionPlan",
- "TradeRevisionDecisionReceipt",
- "TradeRevisionDecisionRequest",
- "TradeRevisionProposalPlan",
- "TradeRevisionProposalRequest",
- "TradeRevisionProposalReceipt",
"TradeSubmitPlan",
"TradeSubmitReceipt",
"TradeWorkflowEnqueueReceipt",
@@ -333,20 +326,6 @@ const REQUIRED_TRADE_SIGNER_EXPORTS: &[&str] = &[
"TradeWorkflowRetryAdvice",
];
-const REQUIRED_DVM_RUNTIME_EXPORTS: &[&str] = &[
- "DVM_TRADE_TRANSITION_PROOF_REQUEST_CONTRACT_ID",
- "DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND",
- "DvmProofMode",
- "DvmTradeTransitionProofEnqueueRequest",
- "DvmTradeTransitionProofPlan",
- "DvmTradeTransitionProofPrepareRequest",
- "DvmTradeTransitionProofReceipt",
- "DvmTradeTransitionProofRequestPayload",
- "DvmValidationReceiptIngestReceipt",
- "DvmValidationReceiptIngestRequest",
- "RadrootsTradeInventoryBinWitnessDto",
-];
-
const REQUIRED_IDENTITY_MODEL_EXPORTS: &[&str] = &[
"DEFAULT_IDENTITY_PATH",
"IdentityError",
@@ -401,15 +380,6 @@ const REQUIRED_TRADES_CLIENT_METHODS: &[&str] = &[
"pub async fn status(",
];
-const REQUIRED_DVM_CLIENT_METHODS: &[&str] = &[
- "pub fn prepare_trade_transition_proof_request(",
- "pub async fn enqueue_trade_transition_proof_request_with_explicit_signer(",
- "pub async fn ingest_validation_receipt(",
-];
-
-const REQUIRED_DVM_CLIENT_CONFIGURED_SIGNER_METHODS: &[&str] =
- &["pub async fn enqueue_trade_transition_proof_request("];
-
const FORBIDDEN_TRADES_CLIENT_PUBLIC_METHODS: &[&str] = &[
"pub fn prepare_submit(",
"pub async fn enqueue_submit(",
@@ -438,18 +408,13 @@ const FORBIDDEN_TRADES_CLIENT_PUBLIC_METHODS: &[&str] = &[
"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(",
- "pub async fn accept_revision(",
- "pub async fn decline_revision(",
-];
+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(",
- "pub async fn propose_revision(",
];
const REQUIRED_TRADE_RESYNC_CLIENT_METHODS: &[&str] = &["pub async fn resync("];
@@ -461,6 +426,7 @@ const REQUIRED_TRADE_VALIDATION_RECEIPTS_CLIENT_METHODS: &[&str] = &[
];
const FORBIDDEN_PRODUCT_CLIENT_HANDLES: &[&str] = &[
+ "DvmClient",
"TradeStatusClient",
"TradeValidationClient",
"pub struct TradeStatusClient",
@@ -1020,15 +986,12 @@ fn private_protocol_helper_modules_are_runtime_gated_by_lib() {
for helper in [
"build_order_request_draft",
"build_order_decision_draft",
- "build_order_revision_proposal_draft",
- "build_order_revision_decision_draft",
"build_order_cancellation_draft",
] {
- let helper_gate =
- format!("#[cfg(any(feature = \"signer-adapters\", test))]\npub fn {helper}(");
+ 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 and unit tests"
+ "src/order.rs must keep `{helper}` available only for signer adapters"
);
}
}
@@ -1265,8 +1228,6 @@ fn order_runtime_mutation_requests_require_evidence_mode() {
"TradeAcceptRequest",
"TradeDeclineRequest",
"TradeCancelRequest",
- "TradeRevisionProposalRequest",
- "TradeRevisionDecisionRequest",
] {
let struct_block = struct_block(source.as_str(), request);
assert!(
@@ -1278,8 +1239,6 @@ fn order_runtime_mutation_requests_require_evidence_mode() {
"impl TradeAcceptRequest",
"impl TradeDeclineRequest",
"impl TradeCancelRequest",
- "impl TradeRevisionProposalRequest",
- "impl TradeRevisionDecisionRequest",
] {
let impl_source = impl_block(source.as_str(), constructor);
assert!(
@@ -1290,34 +1249,28 @@ fn order_runtime_mutation_requests_require_evidence_mode() {
}
#[test]
-fn dvm_runtime_public_exports_are_explicit() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/lib.rs")
- .as_path(),
- );
+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!(
- source.contains("mod dvm_runtime;"),
- "src/lib.rs must keep dvm_runtime as an internal implementation module"
- );
- assert!(
- source.contains("pub use crate::dvm_runtime::{"),
- "src/lib.rs must explicitly re-export approved DVM runtime types"
+ !manifest_dir.join("src/dvm_runtime.rs").exists(),
+ "src/dvm_runtime.rs must not exist after V1 validation receipt consolidation"
);
- assert!(
- !source.contains("pub mod dvm_runtime;"),
- "src/lib.rs must not expose the dvm_runtime module path"
- );
- assert!(
- !source.contains("pub use crate::dvm_runtime::*;"),
- "src/lib.rs must not wildcard-export the DVM runtime"
- );
-
- for export in REQUIRED_DVM_RUNTIME_EXPORTS {
+ 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(export),
- "src/lib.rs must explicitly expose DVM SDK runtime export `{export}`"
+ !source.contains(forbidden),
+ "src/lib.rs must not expose retired DVM runtime surface `{forbidden}`"
);
}
}
@@ -1489,7 +1442,7 @@ fn trade_product_facade_feature_gates_are_explicit() {
seller_impl.contains("pub async fn inbox("),
"TradeSellerClient must expose seller inbox in the runtime impl"
);
- for method in ["accept_trade", "decline_trade", "propose_revision"] {
+ for method in ["accept_trade", "decline_trade"] {
let gated_method =
format!("#[cfg(feature = \"signer-adapters\")]\n pub async fn {method}(");
assert!(
@@ -1528,29 +1481,24 @@ fn trade_propose_request_stays_product_shaped() {
}
#[test]
-fn dvm_client_surface_is_inventory_guarded() {
- let source = read_source(
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/dvm_runtime.rs")
- .as_path(),
- );
+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!(
- source.contains("impl<'sdk> DvmClient<'sdk> {"),
- "src/dvm_runtime.rs must own DvmClient runtime methods"
+ !manifest_dir.join("src/dvm_runtime.rs").exists(),
+ "src/dvm_runtime.rs must not exist after V1 validation receipt consolidation"
);
-
- for method in REQUIRED_DVM_CLIENT_METHODS {
- assert!(
- source.contains(method),
- "DvmClient must expose inventory-guarded method `{method}`"
- );
- }
-
- for method in REQUIRED_DVM_CLIENT_CONFIGURED_SIGNER_METHODS {
+ for forbidden in [
+ "DvmClient",
+ "prepare_trade_transition_proof_request",
+ "enqueue_trade_transition_proof_request",
+ "ingest_validation_receipt",
+ ] {
assert!(
- source.contains(method),
- "DvmClient must expose configured-signer method `{method}`"
+ !lib_source.contains(forbidden) && !clients_source.contains(forbidden),
+ "SDK product client surface must not expose retired DVM client concept `{forbidden}`"
);
}
}
@@ -1582,7 +1530,6 @@ fn product_clients_remain_thin_sdk_handles() {
);
for client in [
- "DvmClient",
"FarmsClient",
"ListingsClient",
"MarketClient",
diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs
@@ -550,7 +550,7 @@ async fn enqueue_scoped_duplicate_listing(sdk: &RadrootsClient, d_tag: &str, tit
let signed_event = signer
.sign_frozen_draft(&plan.frozen_draft)
.expect("signed listing");
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
outbox
@@ -570,7 +570,7 @@ async fn enqueue_scoped_duplicate_listing(sdk: &RadrootsClient, d_tag: &str, tit
async fn assert_local_import_observation(sdk: &RadrootsClient, outbox_event_id: i64) {
let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.outbox_path)
+ let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
.await
.expect("outbox");
let stored_event = outbox
@@ -578,7 +578,7 @@ async fn assert_local_import_observation(sdk: &RadrootsClient, outbox_event_id:
.await
.expect("outbox event")
.expect("outbox event");
- let event_store = RadrootsEventStore::open_file(&paths.event_store_path)
+ let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
.await
.expect("event store");
let observations = event_store
@@ -659,6 +659,22 @@ fn rewrite_backup_manifest(source: &Path, mutate: impl FnOnce(&mut serde_json::V
.expect("write manifest");
}
+fn sync_fixture_idempotency_key(d_tag: &str, title: &str, relays: &[&str]) -> String {
+ let mut suffix = 0xcbf29ce484222325u64;
+ for byte in d_tag
+ .bytes()
+ .chain(title.bytes())
+ .chain(relays.iter().flat_map(|relay| relay.bytes()))
+ {
+ suffix ^= u64::from(byte);
+ suffix = suffix.wrapping_mul(0x100000001b3);
+ }
+ format!(
+ "01890f0e-6c00-7000-8000-{:012x}",
+ suffix & 0x0000_ffff_ffff_ffff
+ )
+}
+
async fn enqueue_listing_with_policy(
sdk: &RadrootsClient,
d_tag: &str,
@@ -674,7 +690,9 @@ async fn enqueue_listing_with_policy(
TargetPolicy::default_profile(),
)
.try_with_nostr_targets(relays, url_policy)
- .expect("relay targets"),
+ .expect("relay targets")
+ .try_with_idempotency_key(sync_fixture_idempotency_key(d_tag, title, relays))
+ .expect("idempotency key"),
&FixtureSigner::new(SELLER),
)
.await
@@ -934,9 +952,9 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
assert_eq!(
integrity.checked_paths,
vec![
- source_paths.event_store_path.clone(),
- source_paths.outbox_path.clone(),
- source_paths.private_store_path.clone()
+ source_paths.runtime_path.clone(),
+ source_paths.private_path.clone(),
+ source_paths.studio_path.clone()
]
);
assert!(integrity.event_store_ok);
@@ -948,35 +966,29 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
.backup(BackupRequest::new(backup_destination.clone()))
.await
.expect("backup");
- let event_store_path = backup
- .event_store_path
- .as_ref()
- .expect("event store backup");
- let outbox_path = backup.outbox_path.as_ref().expect("outbox backup");
- let private_store_path = backup
- .private_store_path
- .as_ref()
- .expect("private store backup");
+ let runtime_path = backup.runtime_path.as_ref().expect("runtime backup");
+ let private_store_path = backup.private_path.as_ref().expect("private store backup");
+ let studio_store_path = backup.studio_path.as_ref().expect("studio store backup");
let manifest_path = backup.manifest_path.as_ref().expect("manifest");
- assert!(event_store_path.exists());
- assert!(outbox_path.exists());
+ assert!(runtime_path.exists());
assert!(private_store_path.exists());
+ assert!(studio_store_path.exists());
assert!(manifest_path.exists());
assert_eq!(
backup.manifest.manifest_kind,
SdkBackupManifestKind::StorageBackup
);
assert_eq!(
- backup.manifest.backup_paths.event_store_path,
- PathBuf::from("event_store.sqlite")
+ backup.manifest.backup_paths.runtime_path,
+ PathBuf::from("runtime.sqlite")
);
assert_eq!(
- backup.manifest.backup_paths.outbox_path,
- PathBuf::from("outbox.sqlite")
+ backup.manifest.backup_paths.private_path,
+ PathBuf::from("private.sqlite")
);
assert_eq!(
- backup.manifest.backup_paths.private_store_path,
- PathBuf::from("private.sqlite")
+ backup.manifest.backup_paths.studio_path,
+ PathBuf::from("studio.sqlite")
);
assert_eq!(backup.manifest.created_at_ms, 1_700_000_000_000);
assert_eq!(backup.manifest.source_status.event_store.total_events, 1);
@@ -992,10 +1004,12 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
assert!(backup.manifest.backup_verification.event_store_ok);
assert!(backup.manifest.backup_verification.outbox_ok);
assert!(backup.manifest.backup_verification.private_store_ok);
+ assert!(backup.manifest.backup_verification.studio_store_ok);
assert_eq!(
backup.manifest.backup_verification.private_farm_locations,
0
);
+ assert_eq!(backup.manifest.backup_verification.studio_state_records, 0);
let restore_archive = RadrootsClient::inspect_restore_archive(backup_destination.clone())
.await
@@ -1006,24 +1020,26 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
backup.manifest.backup_verification
);
assert_eq!(
- restore_archive.event_store_path,
- event_store_path.canonicalize().expect("event canonical")
+ restore_archive.runtime_path,
+ runtime_path.canonicalize().expect("runtime canonical")
);
assert_eq!(
- restore_archive.outbox_path,
- outbox_path.canonicalize().expect("outbox canonical")
- );
- assert_eq!(
- restore_archive.private_store_path,
+ restore_archive.private_path,
private_store_path
.canonicalize()
.expect("private store canonical")
);
+ assert_eq!(
+ restore_archive.studio_path,
+ studio_store_path
+ .canonicalize()
+ .expect("studio store canonical")
+ );
- let backup_event_store = RadrootsEventStore::open_file(event_store_path)
+ let backup_event_store = RadrootsEventStore::open_file(runtime_path)
.await
.expect("backup event store");
- let backup_outbox = RadrootsOutbox::open_file(outbox_path)
+ let backup_outbox = RadrootsOutbox::open_file(runtime_path)
.await
.expect("backup outbox");
assert_eq!(
@@ -1092,21 +1108,15 @@ async fn runtime_backup_rejects_empty_destination_and_overwrites_file_destinatio
.await
.expect("overwrite file backup");
assert!(destination.is_dir());
+ assert!(receipt.runtime_path.as_ref().expect("runtime").exists());
assert!(
receipt
- .event_store_path
- .as_ref()
- .expect("event store")
- .exists()
- );
- assert!(receipt.outbox_path.as_ref().expect("outbox").exists());
- assert!(
- receipt
- .private_store_path
+ .private_path
.as_ref()
.expect("private store")
.exists()
);
+ assert!(receipt.studio_path.as_ref().expect("studio store").exists());
}
#[cfg(unix)]
@@ -1246,7 +1256,7 @@ async fn runtime_restore_rejects_manifest_contract_edges() {
let empty_path_source = backup_source(&sdk, tempdir.path(), "backup-empty-path").await;
rewrite_backup_manifest(&empty_path_source, |manifest| {
- manifest["backup_paths"]["event_store_path"] = serde_json::json!("");
+ manifest["backup_paths"]["runtime_path"] = serde_json::json!("");
});
let empty_path_error = RadrootsClient::inspect_restore_archive(empty_path_source)
.await
@@ -1282,7 +1292,7 @@ async fn sdk_restore_archive_rejects_traversal_backup_paths() {
let mut manifest: serde_json::Value =
serde_json::from_slice(&std::fs::read(&manifest_path).expect("read manifest"))
.expect("manifest json");
- manifest["backup_paths"]["event_store_path"] = serde_json::json!("../event_store.sqlite");
+ manifest["backup_paths"]["runtime_path"] = serde_json::json!("../runtime.sqlite");
std::fs::write(
&manifest_path,
serde_json::to_vec_pretty(&manifest).expect("manifest bytes"),
@@ -1303,7 +1313,7 @@ async fn sdk_restore_archive_rejects_corrupt_store() {
sdk.backup(BackupRequest::new(source.clone()))
.await
.expect("backup");
- std::fs::write(source.join("event_store.sqlite"), b"not sqlite").expect("corrupt store");
+ std::fs::write(source.join("runtime.sqlite"), b"not sqlite").expect("corrupt store");
let error = RadrootsClient::inspect_restore_archive(source)
.await
@@ -1323,10 +1333,10 @@ async fn sdk_restore_archive_rejects_symlink_store_member() {
sdk.backup(BackupRequest::new(source.clone()))
.await
.expect("backup");
- let event_store_path = source.join("event_store.sqlite");
- let target = tempdir.path().join("sdk").join("event_store.sqlite");
- std::fs::remove_file(&event_store_path).expect("remove backup event store");
- std::os::unix::fs::symlink(target, &event_store_path).expect("symlink");
+ let runtime_path = source.join("runtime.sqlite");
+ let target = tempdir.path().join("sdk").join("runtime.sqlite");
+ std::fs::remove_file(&runtime_path).expect("remove backup runtime store");
+ std::os::unix::fs::symlink(target, &runtime_path).expect("symlink");
let error = RadrootsClient::inspect_restore_archive(source)
.await
@@ -1385,8 +1395,8 @@ async fn sdk_restore_dry_run_validates_destination_without_writing() {
.destination_paths
.as_ref()
.expect("destination paths")
- .event_store_path,
- destination.join("event_store.sqlite")
+ .runtime_path,
+ destination.join("runtime.sqlite")
);
assert!(!destination.exists());
assert_eq!(receipt.restored_paths, None);
@@ -1402,7 +1412,7 @@ async fn sdk_restore_dry_run_rejects_existing_destination_without_overwrite() {
.await
.expect("backup");
std::fs::create_dir(&destination).expect("destination");
- std::fs::write(destination.join("event_store.sqlite"), b"existing").expect("existing file");
+ std::fs::write(destination.join("runtime.sqlite"), b"existing").expect("existing file");
let error = RadrootsClient::restore(
RestoreRequest::new(source)
@@ -1413,7 +1423,7 @@ async fn sdk_restore_dry_run_rejects_existing_destination_without_overwrite() {
.expect_err("existing destination");
assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert!(destination.join("event_store.sqlite").exists());
+ assert!(destination.join("runtime.sqlite").exists());
}
#[tokio::test]
@@ -1426,7 +1436,7 @@ async fn sdk_restore_dry_run_overwrite_keeps_existing_destination_untouched() {
.await
.expect("backup");
std::fs::create_dir(&destination).expect("destination");
- std::fs::write(destination.join("event_store.sqlite"), b"existing").expect("existing file");
+ std::fs::write(destination.join("runtime.sqlite"), b"existing").expect("existing file");
let receipt = RadrootsClient::restore(
RestoreRequest::new(source)
@@ -1439,7 +1449,7 @@ async fn sdk_restore_dry_run_overwrite_keeps_existing_destination_untouched() {
assert_eq!(receipt.state, SdkRestoreState::DryRun);
assert_eq!(
- std::fs::read(destination.join("event_store.sqlite")).expect("existing file"),
+ std::fs::read(destination.join("runtime.sqlite")).expect("existing file"),
b"existing"
);
}
@@ -1474,7 +1484,7 @@ async fn sdk_restore_dry_run_rejects_corrupt_source_without_destination_writes()
sdk.backup(BackupRequest::new(source.clone()))
.await
.expect("backup");
- std::fs::write(source.join("event_store.sqlite"), b"not sqlite").expect("corrupt store");
+ std::fs::write(source.join("runtime.sqlite"), b"not sqlite").expect("corrupt store");
let error = RadrootsClient::restore(
RestoreRequest::new(source)
@@ -1549,7 +1559,7 @@ async fn runtime_restore_handles_existing_file_destinations_by_overwrite_policy(
.restored_paths
.as_ref()
.expect("restored paths")
- .event_store_path
+ .runtime_path
.exists()
);
}
@@ -1576,8 +1586,9 @@ async fn sdk_restore_to_empty_destination_succeeds() {
receipt.restored_paths.as_ref(),
receipt.destination_paths.as_ref()
);
- assert!(destination.join("event_store.sqlite").exists());
- assert!(destination.join("outbox.sqlite").exists());
+ assert!(destination.join("runtime.sqlite").exists());
+ assert!(destination.join("private.sqlite").exists());
+ assert!(destination.join("studio.sqlite").exists());
let restored_sdk = RadrootsClient::builder()
.directory_storage(destination)
.build()
@@ -1666,7 +1677,7 @@ async fn sdk_restore_corrupt_backup_leaves_destination_unchanged() {
sdk.backup(BackupRequest::new(source.clone()))
.await
.expect("backup");
- std::fs::write(source.join("event_store.sqlite"), b"not sqlite").expect("corrupt store");
+ std::fs::write(source.join("runtime.sqlite"), b"not sqlite").expect("corrupt store");
std::fs::create_dir(&destination).expect("destination");
std::fs::write(destination.join("sentinel"), b"keep").expect("sentinel");
@@ -1744,13 +1755,15 @@ async fn product_push_outbox_uses_radrootsd_proxy_transport_with_daemon_resolved
actor(),
listing(LISTING_A_D_TAG, "Proxy Coffee"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000250")
+ .expect("idempotency key"),
&FixtureSigner::new(SELLER),
)
.await
.expect("enqueue");
let pre_push_outbox =
- RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("pre-push outbox");
let pre_push_stored = pre_push_outbox
@@ -1853,12 +1866,14 @@ async fn product_push_outbox_radrootsd_proxy_recovers_expired_publishing_claim_b
actor(),
listing(LISTING_A_D_TAG, "Recovered Proxy Coffee"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000251")
+ .expect("idempotency key"),
&FixtureSigner::new(SELLER),
)
.await
.expect("enqueue");
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stale_claim = outbox
@@ -1954,12 +1969,14 @@ async fn product_push_outbox_radrootsd_proxy_idempotency_is_attempt_scoped() {
actor(),
listing(LISTING_A_D_TAG, "Retry Coffee"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000252")
+ .expect("idempotency key"),
&FixtureSigner::new(SELLER),
)
.await
.expect("enqueue");
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let plans = outbox
@@ -2059,7 +2076,9 @@ async fn product_push_outbox_radrootsd_proxy_error_and_terminal_paths_update_out
actor(),
listing(LISTING_A_D_TAG, "Proxy Error Coffee"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000253")
+ .expect("idempotency key"),
&FixtureSigner::new(SELLER),
)
.await
@@ -2123,7 +2142,9 @@ async fn product_push_outbox_radrootsd_proxy_error_and_terminal_paths_update_out
actor(),
listing(LISTING_B_D_TAG, "Terminal Coffee"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000254")
+ .expect("idempotency key"),
&FixtureSigner::new(SELLER),
)
.await
@@ -2185,7 +2206,7 @@ fn push_outbox_contract_dtos_serialize_deterministically() {
retryable_events: 0,
terminal_events: 0,
events: vec![PushOutboxEventReceipt {
- event_id: RadrootsEventId::parse(&"a".repeat(64)).expect("event id"),
+ event_id: RadrootsEventId::parse("a".repeat(64)).expect("event id"),
outbox_event_id: 7,
final_state: PushOutboxEventState::Published,
attempted_count: 2,
@@ -2305,7 +2326,9 @@ async fn sync_runtime_product_push_outbox_reticulum_preview_reports_zero_attempt
actor(),
listing(LISTING_A_D_TAG, "Reticulum Preview Coffee"),
TargetPolicy::default_profile(),
- ),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000255")
+ .expect("idempotency key"),
&FixtureSigner::new(SELLER),
)
.await
@@ -2495,7 +2518,7 @@ async fn push_outbox_with_transport_uses_queued_targets_without_builder_relays()
);
assert_eq!(adapter.captured_raw_events().len(), 1);
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stored = outbox
@@ -2550,7 +2573,7 @@ async fn push_outbox_with_transport_preserves_scoped_duplicate_target_metadata()
&& target.target_label.as_deref() == Some("Farm B")
}));
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stored = outbox
@@ -2623,7 +2646,7 @@ async fn push_outbox_adapter_transport_failure_preserves_scoped_target_metadata(
&& target.target_label.as_deref() == Some("Farm B")
}));
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stored = outbox
@@ -2664,7 +2687,7 @@ async fn push_outbox_with_transport_recovers_expired_publishing_claim_before_sel
.expect("sdk");
let outbox_event_id =
enqueue_listing(&sdk, LISTING_A_D_TAG, "Recovered Coffee", &[RELAY_A]).await;
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stale_claim = outbox
@@ -2753,13 +2776,13 @@ async fn push_outbox_with_transport_scopes_duplicate_endpoint_sibling_plans() {
)
.try_with_nostr_targets([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("first targets")
- .try_with_idempotency_key("sdk-duplicate-endpoint-plan")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022e")
.expect("first idempotency"),
&FixtureSigner::new(SELLER),
)
.await
.expect("first enqueue");
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let stored_event = outbox
@@ -2787,7 +2810,7 @@ async fn push_outbox_with_transport_scopes_duplicate_endpoint_sibling_plans() {
1_700_000_000_000,
1_700_000_000_000,
)
- .with_idempotency_key("sdk-duplicate-endpoint-plan"),
+ .with_idempotency_key("01890f0e-6c00-7000-8000-00000000022e"),
)
.await
.expect("second plan");
@@ -2933,7 +2956,7 @@ async fn push_outbox_with_transport_can_publish_targeted_ready_event() {
assert_eq!(receipt.events[0].outbox_event_id, targeted_outbox_event_id);
assert_eq!(adapter.captured_raw_events().len(), 1);
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let older = outbox
@@ -3178,7 +3201,7 @@ async fn push_outbox_continues_after_adapter_transport_failure_and_releases_clai
})
);
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
for outbox_event_id in [first_outbox_event_id, second_outbox_event_id] {
@@ -3246,7 +3269,7 @@ async fn push_outbox_returns_fatal_error_for_malformed_signed_event_data() {
enqueue_listing(&sdk, LISTING_A_D_TAG, "Corrupt Coffee", &[RELAY_A]).await;
let safe_outbox_event_id =
enqueue_listing(&sdk, LISTING_B_D_TAG, "Safe Coffee", &[RELAY_B]).await;
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let changed =
@@ -3289,7 +3312,7 @@ async fn push_outbox_does_not_claim_unsigned_outbox_work() {
listing(LISTING_C_D_TAG, "Unsigned"),
))
.expect("prepared");
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
.expect("outbox");
let unsigned = outbox
diff --git a/crates/sdk/tests/trade_product_publish_runtime.rs b/crates/sdk/tests/trade_product_publish_runtime.rs
@@ -216,11 +216,7 @@ fn trade_propose_request(
TradeProposeRequest::new(
buyer_actor(),
listing_event_ptr(),
- order.order_id,
- order.listing_addr,
- order.seller_pubkey,
- order.items,
- order.economics,
+ order,
explicit_trade_relays(),
publish_mode,
satisfaction_policy,
@@ -255,11 +251,11 @@ async fn trade_product_propose_enqueue_and_publish_uses_ack_policy() {
.buyer()
.propose_trade(
trade_propose_request(
- "trade-product-publish",
+ "01890f0e-6c00-7000-8000-000000000223",
PublishMode::EnqueueAndPublish,
SatisfactionPolicy::AnyAccepted,
)
- .try_with_idempotency_key("trade-product-publish")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000223")
.expect("idempotency"),
)
.await
@@ -270,7 +266,10 @@ async fn trade_product_propose_enqueue_and_publish_uses_ack_policy() {
TradeMutationOutcome::Enqueued { .. } => panic!("expected published outcome"),
};
- assert_eq!(receipt.order_id.as_str(), "trade-product-publish");
+ 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);
diff --git a/crates/sdk/tests/unit/actor_json_tests.rs b/crates/sdk/tests/unit/actor_json_tests.rs
@@ -2,10 +2,7 @@ use super::{SdkActorContextJson, actor_role_code, actor_source_code};
use radroots_authority::{RadrootsActorContext, RadrootsActorSource};
use radroots_event::contract::RadrootsActorRole;
-#[path = "../support/serializer_failure.rs"]
-mod serializer_failure;
-
-use serializer_failure::assert_struct_serialize_error_paths;
+use crate::serializer_failure::assert_struct_serialize_error_paths;
const PUBKEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
diff --git a/crates/sdk/tests/unit/dvm_runtime_tests.rs b/crates/sdk/tests/unit/dvm_runtime_tests.rs
@@ -1,390 +0,0 @@
-use super::{
- DvmProofMode, DvmTradeTransitionProofEnqueueRequest, DvmTradeTransitionProofPrepareRequest,
- DvmValidationReceiptIngestRequest, RadrootsTradeInventoryBinWitnessDto,
- dvm_trade_transition_proof_plan, sdk_timestamp_ms,
-};
-use crate::{
- NostrRelayUrlPolicy, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey,
- SyncProjectionRefreshRequest, TargetPolicy,
-};
-use radroots_authority::RadrootsActorContext;
-use radroots_event::{
- contract::RadrootsActorRole,
- draft::RadrootsSignedEvent,
- ids::{RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsPublicKey},
- kinds::KIND_TRADE_TRANSITION_PROOF_REQUEST,
- wire::RadrootsNip01EventWire,
-};
-use radroots_trade::validation_receipt::RadrootsValidationReceiptProofSystem;
-
-const SERVICE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
-const BUYER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
-const SELLER: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
-const WORKER: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
-const RELAY: &str = "wss://relay.radroots.test";
-
-#[test]
-fn trade_transition_proof_plan_builds_microstandard_wire_payload() {
- let request = proof_request(service_actor())
- .with_inventory_sequence(7)
- .with_previous_state_root(hash32('3'));
- let plan = dvm_trade_transition_proof_plan(
- request,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- )
- .expect("plan");
- let payload: serde_json::Value =
- serde_json::from_str(plan.frozen_draft.content()).expect("payload json");
-
- assert_eq!(
- plan.frozen_draft.contract_id(),
- super::DVM_TRADE_TRANSITION_PROOF_REQUEST_CONTRACT_ID
- );
- assert_eq!(
- plan.frozen_draft.kind_u32(),
- KIND_TRADE_TRANSITION_PROOF_REQUEST
- );
- assert_eq!(plan.frozen_draft.expected_pubkey_str(), SERVICE);
- assert_eq!(plan.worker_pubkey.as_str(), WORKER);
- assert_eq!(plan.proof_mode, DvmProofMode::None);
- assert_eq!(
- plan.expected_receipt_proof_system,
- RadrootsValidationReceiptProofSystem::None
- );
- assert_eq!(payload["proof_mode"], "none");
- assert_eq!(payload["witness_version"], 1);
- assert_eq!(payload["proof_target"], "trade.order_acceptance.v1");
- assert_eq!(payload["radroots_protocol_version"], "radroots.trade.v1");
- assert_eq!(payload["inventory_sequence"], 7);
- assert_eq!(payload["previous_state_root"], hash32('3'));
- assert_eq!(payload["listing_event_id"], event_id('1').as_str());
- let tags = plan.frozen_draft.tags_as_vec();
- assert_eq!(tags[0], vec!["a", listing_addr().as_str()]);
- assert_eq!(
- tags[1],
- vec![
- "i",
- event_id('3').as_str(),
- "event",
- "radroots:order_decision_event"
- ]
- );
- assert_eq!(tags[2], vec!["p", WORKER]);
-}
-
-#[test]
-fn trade_transition_proof_plan_rejects_non_service_actor_before_mutation() {
- let error = dvm_trade_transition_proof_plan(
- proof_request(buyer_actor()),
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- )
- .expect_err("role error");
-
- assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. }));
-}
-
-#[test]
-fn trade_transition_proof_plan_rejects_inventory_and_sp1_identity_edges() {
- let empty_bins = DvmTradeTransitionProofPrepareRequest::new(
- service_actor(),
- worker_pubkey(),
- listing_addr(),
- event_id('1'),
- event_id('2'),
- event_id('3'),
- Vec::new(),
- );
- assert!(matches!(
- dvm_trade_transition_proof_plan(
- empty_bins,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let over_reserved = DvmTradeTransitionProofPrepareRequest::new(
- service_actor(),
- worker_pubkey(),
- listing_addr(),
- event_id('1'),
- event_id('2'),
- event_id('3'),
- vec![RadrootsTradeInventoryBinWitnessDto {
- bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- listing_capacity: 2,
- previous_reserved: 3,
- }],
- );
- assert!(matches!(
- dvm_trade_transition_proof_plan(
- over_reserved,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let invalid_previous_state_root =
- proof_request(service_actor()).with_previous_state_root("not-a-hash");
- assert!(matches!(
- dvm_trade_transition_proof_plan(
- invalid_previous_state_root,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let missing_sp1_identity = proof_request(service_actor()).with_proof_mode(DvmProofMode::Core);
- assert!(matches!(
- dvm_trade_transition_proof_plan(
- missing_sp1_identity,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let none_with_sp1_identity =
- proof_request(service_actor()).with_sp1_identity(hash32('a'), hash32('b'));
- assert!(matches!(
- dvm_trade_transition_proof_plan(
- none_with_sp1_identity,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let invalid_hash = proof_request(service_actor())
- .with_proof_mode(DvmProofMode::Core)
- .with_sp1_identity("0xABC", hash32('b'));
- assert!(matches!(
- dvm_trade_transition_proof_plan(
- invalid_hash,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let invalid_previous_state = proof_request(service_actor()).with_previous_state_root("0xABC");
- assert!(matches!(
- dvm_trade_transition_proof_plan(
- invalid_previous_state,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-}
-
-#[test]
-fn proof_modes_map_to_expected_receipt_proof_systems_and_labels() {
- let cases = [
- (
- DvmProofMode::None,
- "none",
- RadrootsValidationReceiptProofSystem::None,
- ),
- (
- DvmProofMode::Core,
- "core",
- RadrootsValidationReceiptProofSystem::Sp1Core,
- ),
- (
- DvmProofMode::Compressed,
- "compressed",
- RadrootsValidationReceiptProofSystem::Sp1Compressed,
- ),
- (
- DvmProofMode::Groth16,
- "groth16",
- RadrootsValidationReceiptProofSystem::Sp1Groth16,
- ),
- (
- DvmProofMode::Plonk,
- "plonk",
- RadrootsValidationReceiptProofSystem::Sp1Plonk,
- ),
- ];
-
- for (mode, label, proof_system) in cases {
- assert_eq!(mode.as_str(), label);
- assert_eq!(mode.proof_system(), proof_system);
- }
-
- let request = proof_request(service_actor())
- .with_proof_mode(DvmProofMode::Compressed)
- .with_sp1_identity(hash32('a'), hash32('b'));
- let plan = dvm_trade_transition_proof_plan(
- request,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- )
- .expect("sp1 plan");
-
- assert_eq!(plan.payload.proof_mode, DvmProofMode::Compressed);
- assert_eq!(
- plan.expected_receipt_proof_system,
- RadrootsValidationReceiptProofSystem::Sp1Compressed
- );
-}
-
-#[test]
-fn enqueue_request_builders_and_ingest_request_builders_are_deterministic() {
- let prepare = proof_request(service_actor())
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_010));
- let idempotency = SdkIdempotencyKey::new("dvm-proof-request").expect("idempotency");
- let idempotency_len = idempotency.as_str().len();
- let request = DvmTradeTransitionProofEnqueueRequest::from_prepare(
- prepare.clone(),
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("relays")
- .with_idempotency_key(idempotency)
- .with_inventory_sequence(11)
- .with_previous_state_root(hash32('1'));
- let serialized = serde_json::to_value(&request).expect("request json");
-
- assert_eq!(request.prepare.inventory_sequence, 11);
- assert_eq!(request.prepare.previous_state_root, Some(hash32('1')));
- assert_eq!(serialized["proof_mode"], "none");
- assert_eq!(serialized["target_policy"]["kind"], "explicit");
- assert_eq!(serialized["idempotency_key"]["value"], "<redacted>");
- assert_eq!(serialized["idempotency_key"]["len"], idempotency_len);
-
- let request = DvmTradeTransitionProofEnqueueRequest::new(
- service_actor(),
- worker_pubkey(),
- listing_addr(),
- event_id('1'),
- event_id('2'),
- event_id('3'),
- inventory_bins(),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("dvm-proof-request-2")
- .expect("idempotency")
- .with_proof_mode(DvmProofMode::Core)
- .with_sp1_identity(hash32('a'), hash32('b'))
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_011));
- assert_eq!(request.prepare.proof_mode, DvmProofMode::Core);
- assert_eq!(request.prepare.sp1_program_hash, Some(hash32('a')));
- assert_eq!(request.prepare.sp1_verifying_key_hash, Some(hash32('b')));
- assert!(matches!(
- DvmTradeTransitionProofEnqueueRequest::from_prepare(
- prepare,
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets(["ws://relay.example.com"], NostrRelayUrlPolicy::Public),
- Err(RadrootsSdkError::InvalidRelayUrl { .. })
- ));
- assert!(matches!(
- DvmTradeTransitionProofEnqueueRequest::from_prepare(
- proof_request(service_actor()),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key(""),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let ingest = DvmValidationReceiptIngestRequest::new(dummy_event())
- .with_projection_refresh(SyncProjectionRefreshRequest::new().with_limit(5));
- assert_eq!(ingest.projection_refresh.limit, 5);
-}
-
-#[test]
-fn dvm_timestamp_edges_return_structured_errors() {
- let error = dvm_trade_transition_proof_plan(
- proof_request(service_actor()),
- RadrootsSdkTimestamp::from_unix_seconds(u64::from(u32::MAX) + 1),
- )
- .expect_err("nostr timestamp range");
- assert!(matches!(
- error,
- RadrootsSdkError::TimestampOutOfRange { value } if value == u64::from(u32::MAX) + 1
- ));
-
- let error = sdk_timestamp_ms(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX))
- .expect_err("millisecond timestamp range");
- assert!(matches!(
- error,
- RadrootsSdkError::TimestampOutOfRange { value } if value == u64::MAX
- ));
-}
-
-#[tokio::test]
-async fn dvm_client_prepare_reports_default_clock_errors() {
- let sdk = crate::RadrootsClient::builder()
- .clock(crate::RadrootsSdkClock::BeforeUnixEpoch)
- .build()
- .await
- .expect("sdk");
-
- let error = sdk
- .dvm()
- .prepare_trade_transition_proof_request(proof_request(service_actor()))
- .expect_err("clock error");
-
- assert!(matches!(error, RadrootsSdkError::ClockBeforeUnixEpoch));
-}
-
-fn proof_request(actor: RadrootsActorContext) -> DvmTradeTransitionProofPrepareRequest {
- DvmTradeTransitionProofPrepareRequest::new(
- actor,
- worker_pubkey(),
- listing_addr(),
- event_id('1'),
- event_id('2'),
- event_id('3'),
- vec![RadrootsTradeInventoryBinWitnessDto {
- bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- listing_capacity: 5,
- previous_reserved: 1,
- }],
- )
-}
-
-fn inventory_bins() -> Vec<RadrootsTradeInventoryBinWitnessDto> {
- vec![RadrootsTradeInventoryBinWitnessDto {
- bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- listing_capacity: 5,
- previous_reserved: 1,
- }]
-}
-
-fn service_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SERVICE, [RadrootsActorRole::Service]).expect("service actor")
-}
-
-fn buyer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(BUYER, [RadrootsActorRole::Buyer]).expect("buyer actor")
-}
-
-fn worker_pubkey() -> RadrootsPublicKey {
- RadrootsPublicKey::parse(WORKER).expect("worker pubkey")
-}
-
-fn listing_addr() -> RadrootsListingAddress {
- RadrootsListingAddress::parse(format!("30402:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg"))
- .expect("listing addr")
-}
-
-fn event_id(ch: char) -> RadrootsEventId {
- RadrootsEventId::parse(ch.to_string().repeat(64)).expect("event id")
-}
-
-fn hash32(ch: char) -> String {
- format!("0x{}", ch.to_string().repeat(64))
-}
-
-fn dummy_event() -> RadrootsSignedEvent {
- let mut wire = RadrootsNip01EventWire {
- id: String::new(),
- pubkey: event_id('a').into_string(),
- created_at: 1,
- kind: 3440,
- tags: Vec::new(),
- content: "{}".to_owned(),
- sig: "b".repeat(128),
- extra: Default::default(),
- };
- wire.id = wire.computed_event_id().expect("event id").into_string();
- let raw_json = serde_json::to_string(&wire).expect("raw event json");
- RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
-}
diff --git a/crates/sdk/tests/unit/error_tests.rs b/crates/sdk/tests/unit/error_tests.rs
@@ -1,7 +1,4 @@
-use super::{
- RadrootsSdkError, RadrootsSdkGeoNamesErrorKind, RadrootsSdkPartialLocalMutationError,
- RadrootsSdkPartialLocalMutationFailure, RadrootsSdkRecoveryAction, redacted_relay_url,
-};
+use super::{RadrootsSdkError, RadrootsSdkGeoNamesErrorKind, redacted_relay_url};
use crate::privacy::{PrivacyPreflightStatus, ProductSensitivityField};
use crate::transport::ReticulumPreviewBehavior;
use radroots_authority::RadrootsAuthorityError;
@@ -9,64 +6,6 @@ use radroots_event::contract::RadrootsActorRole;
use radroots_geocoder::{GeoNamesAssetFetcher, GeoNamesBlockingHttpFetcher, GeocoderError};
#[test]
-fn partial_local_mutation_constructor_preserves_supplied_error() {
- let error = RadrootsSdkPartialLocalMutationError {
- event_id: None,
- operation_kind: "listing.publish.v1".to_owned(),
- idempotency_digest_prefix: None,
- stored: true,
- queued: false,
- recovery: RadrootsSdkRecoveryAction::RetryOutboxEnqueue,
- failure: RadrootsSdkPartialLocalMutationFailure::OutboxEnqueue,
- };
-
- assert!(matches!(
- RadrootsSdkError::partial_local_mutation(error),
- RadrootsSdkError::PartialLocalMutation(RadrootsSdkPartialLocalMutationError {
- stored: true,
- queued: false,
- recovery: RadrootsSdkRecoveryAction::RetryOutboxEnqueue,
- failure: RadrootsSdkPartialLocalMutationFailure::OutboxEnqueue,
- ..
- })
- ));
-
- assert!(matches!(
- RadrootsSdkError::partial_outbox_enqueue_mutation(
- "a".repeat(64),
- "listing.publish.v1",
- "digest-prefix",
- ),
- RadrootsSdkError::PartialLocalMutation(RadrootsSdkPartialLocalMutationError {
- event_id: Some(_),
- operation_kind,
- idempotency_digest_prefix: Some(_),
- stored: true,
- queued: false,
- recovery: RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey,
- failure: RadrootsSdkPartialLocalMutationFailure::OutboxEnqueue,
- }) if operation_kind == "listing.publish.v1"
- ));
-
- assert!(matches!(
- RadrootsSdkError::partial_outbox_idempotency_conflict_mutation(
- "a".repeat(64),
- "listing.publish.v1",
- "digest-prefix",
- ),
- RadrootsSdkError::PartialLocalMutation(RadrootsSdkPartialLocalMutationError {
- event_id: Some(_),
- operation_kind,
- idempotency_digest_prefix: Some(_),
- stored: true,
- queued: false,
- recovery: RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey,
- failure: RadrootsSdkPartialLocalMutationFailure::OutboxIdempotencyConflict,
- }) if operation_kind == "listing.publish.v1"
- ));
-}
-
-#[test]
fn authority_error_conversion_redacts_pubkey_mismatches_and_falls_back() {
let actor_error = RadrootsSdkError::from(RadrootsAuthorityError::ActorPubkeyMismatch {
expected_pubkey: "a".repeat(64),
@@ -90,7 +29,7 @@ fn authority_error_conversion_redacts_pubkey_mismatches_and_falls_back() {
#[test]
fn listing_and_store_errors_convert_to_sdk_error_classes() {
let draft = RadrootsSdkError::from(
- radroots_trade::listing::RadrootsListingDraftError::ActorRoleUnsatisfied {
+ radroots_trade::listing::RadrootsListingEditError::ActorRoleUnsatisfied {
required_role: RadrootsActorRole::Seller,
},
);
@@ -101,14 +40,14 @@ fn listing_and_store_errors_convert_to_sdk_error_classes() {
));
let draft_fallback = RadrootsSdkError::from(
- radroots_trade::listing::RadrootsListingDraftError::InvalidFarmPubkey(
+ radroots_trade::listing::RadrootsListingEditError::InvalidFarmPubkey(
radroots_event::ids::RadrootsIdParseError::InvalidCharacter,
),
);
assert!(matches!(
draft_fallback,
- RadrootsSdkError::ListingDraft { ref message }
- if message.contains("invalid listing draft farm pubkey")
+ RadrootsSdkError::ListingEdit { ref message }
+ if message.contains("invalid listing edit farm pubkey")
));
let mutation = RadrootsSdkError::from(
@@ -389,8 +328,8 @@ fn sdk_error_contract_methods_cover_representative_classes_and_details() {
RadrootsSdkError::InvalidRequest {
message: "invalid".to_owned(),
},
- RadrootsSdkError::ListingDraft {
- message: "draft".to_owned(),
+ RadrootsSdkError::ListingEdit {
+ message: "edit".to_owned(),
},
RadrootsSdkError::ListingMutation {
message: "mutation".to_owned(),
@@ -431,11 +370,6 @@ fn sdk_error_contract_methods_cover_representative_classes_and_details() {
RadrootsSdkError::Projection {
message: "projection".to_owned(),
},
- RadrootsSdkError::partial_outbox_idempotency_conflict_mutation(
- "a".repeat(64),
- "listing.publish.v1",
- "digest-prefix",
- ),
];
for error in errors {
diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs
@@ -3,13 +3,8 @@ use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
use radroots_nostr::prelude::RadrootsNostrKeys;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
-#[path = "../support/fixture_signer.rs"]
-mod fixture_signer;
-#[path = "../support/serializer_failure.rs"]
-mod serializer_failure;
-
-use fixture_signer::FixtureSigner;
-use serializer_failure::assert_struct_serialize_error_paths;
+use crate::fixture_signer::FixtureSigner;
+use crate::serializer_failure::assert_struct_serialize_error_paths;
const FARMER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const FARM_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
@@ -140,8 +135,7 @@ fn farm_publish_plan_rejects_invalid_draft_tags() {
farm,
RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
)
- .err()
- .expect("invalid farm plan");
+ .expect_err("invalid farm plan");
assert!(matches!(
error,
RadrootsSdkError::InvalidRequest { message } if message.contains("draft encode failed")
@@ -171,24 +165,30 @@ fn farm_runtime_request_builders_and_serializers_cover_success_paths() {
)
.try_with_nostr_targets([RELAY_A, RELAY_B], NostrRelayUrlPolicy::Public)
.expect("relay targets")
- .with_idempotency_key(SdkIdempotencyKey::new("farm-unit-key").expect("key"))
+ .with_idempotency_key(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000233").expect("key"),
+ )
.with_created_at(created_at);
assert_struct_serialize_error_paths(&enqueue, 5);
let enqueue_json = serde_json::to_value(&enqueue).expect("enqueue json");
assert_eq!(enqueue_json["target_policy"]["kind"], "explicit");
assert_eq!(enqueue_json["created_at"], 1_700_000_321);
- assert!(!enqueue_json.to_string().contains("farm-unit-key"));
+ assert!(
+ !enqueue_json
+ .to_string()
+ .contains("01890f0e-6c00-7000-8000-000000000233")
+ );
let try_key = FarmEnqueuePublishRequest::new(
farmer_actor(),
farm(FARM_C_D_TAG, "Try Key Farm"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("farm-unit-try-key")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000234")
.expect("try key");
assert_eq!(
serde_json::to_value(&try_key).expect("try key json")["idempotency_key"]["len"],
- "farm-unit-try-key".len()
+ "01890f0e-6c00-7000-8000-000000000234".len()
);
let private_upsert = FarmPrivateLocationUpsertRequest::new(
@@ -375,8 +375,7 @@ fn farm_request_builders_reject_invalid_options_and_timestamp_bounds() {
farm(FARM_C_D_TAG, "Future Farm"),
RadrootsSdkTimestamp::from_unix_seconds(u64::MAX),
)
- .err()
- .expect("timestamp error");
+ .expect_err("timestamp error");
assert!(matches!(
timestamp_error,
RadrootsSdkError::TimestampOutOfRange { .. }
@@ -559,7 +558,7 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() {
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
)
- .try_with_idempotency_key("farm-source-attached-enqueue")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000235")
.expect("idempotency"),
&signer,
)
@@ -582,7 +581,10 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() {
plan,
TargetPolicy::try_nostr_relays([RELAY_B], NostrRelayUrlPolicy::Public)
.expect("prepared transport targets"),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000236")
+ .expect("prepared idempotency"),
+ ),
&signer,
)
.await
@@ -615,7 +617,7 @@ async fn farm_configured_local_signer_enqueues_publish_without_explicit_signer()
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
)
- .try_with_idempotency_key("farm-configured-local")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000236")
.expect("idempotency"),
)
.await
diff --git a/crates/sdk/tests/unit/idempotency_tests.rs b/crates/sdk/tests/unit/idempotency_tests.rs
@@ -2,10 +2,7 @@ use super::{SdkIdempotencyKey, SdkTradeIdempotencyRecord};
use crate::RadrootsSdkError;
use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey};
-#[path = "../support/serializer_failure.rs"]
-mod serializer_failure;
-
-use serializer_failure::assert_struct_serialize_error_paths;
+use crate::serializer_failure::assert_struct_serialize_error_paths;
#[test]
fn empty_key_is_rejected_before_redacted_storage() {
@@ -32,17 +29,21 @@ fn empty_key_is_rejected_before_redacted_storage() {
}
#[test]
-fn derived_key_is_deterministic_and_consumable() {
- let first = SdkIdempotencyKey::derive("listing.publish.v1", "event-a", "pubkey-a");
- let second = SdkIdempotencyKey::derive("listing.publish.v1", "event-a", "pubkey-a");
+fn explicit_uuid_v7_key_is_accepted_and_other_shapes_are_rejected() {
+ let key = SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000001").expect("key");
- assert_eq!(first.as_str(), second.as_str());
- assert!(first.into_string().starts_with("listing.publish.v1:"));
+ assert_eq!(key.as_str(), "01890f0e-6c00-7000-8000-000000000001");
+ assert_eq!(key.into_string(), "01890f0e-6c00-7000-8000-000000000001");
+ assert!(matches!(
+ SdkIdempotencyKey::new("01890f0e-6c00-6000-8000-000000000001"),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message == "idempotency key must be a UUIDv7"
+ ));
}
#[test]
fn idempotency_key_reports_serializer_failures() {
- let key = SdkIdempotencyKey::new("idempotent").expect("key");
+ let key = SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000002").expect("key");
assert_struct_serialize_error_paths(&key, 2);
}
@@ -50,7 +51,8 @@ fn idempotency_key_reports_serializer_failures() {
#[test]
fn trade_idempotency_record_binds_payload_and_reports_conflicts() {
let record = SdkTradeIdempotencyRecord {
- idempotency_key: SdkIdempotencyKey::new("trade-idempotent").expect("key"),
+ idempotency_key: SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000003")
+ .expect("key"),
operation_kind: "trade.submit.v1".to_owned(),
actor_pubkey: RadrootsPublicKey::parse(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
diff --git a/crates/sdk/tests/unit/listings_runtime_tests.rs b/crates/sdk/tests/unit/listings_runtime_tests.rs
@@ -13,13 +13,8 @@ use radroots_event::{
};
use radroots_nostr::prelude::RadrootsNostrKeys;
-#[path = "../support/fixture_signer.rs"]
-mod fixture_signer;
-#[path = "../support/serializer_failure.rs"]
-mod serializer_failure;
-
-use fixture_signer::FixtureSigner;
-use serializer_failure::assert_struct_serialize_error_paths;
+use crate::fixture_signer::FixtureSigner;
+use crate::serializer_failure::assert_struct_serialize_error_paths;
const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
@@ -103,29 +98,35 @@ fn listing_runtime_request_builders_and_serializers_cover_success_paths() {
let enqueue = ListingEnqueuePublishRequest::from_document(
actor(),
- RadrootsListingDraftDocumentV1::new(listing(LISTING_B_D_TAG, "Queued Greens")),
+ RadrootsListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Queued Greens")),
TargetPolicy::default_profile(),
)
.try_with_nostr_targets([RELAY_A, RELAY_B], NostrRelayUrlPolicy::Public)
.expect("relay targets")
- .with_idempotency_key(SdkIdempotencyKey::new("listing-unit-key").expect("key"))
+ .with_idempotency_key(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000022f").expect("key"),
+ )
.with_created_at(created_at);
assert_struct_serialize_error_paths(&enqueue, 5);
let enqueue_json = serde_json::to_value(&enqueue).expect("enqueue json");
assert_eq!(enqueue_json["target_policy"]["kind"], "explicit");
assert_eq!(enqueue_json["created_at"], 1_700_000_321);
- assert!(!enqueue_json.to_string().contains("listing-unit-key"));
+ assert!(
+ !enqueue_json
+ .to_string()
+ .contains("01890f0e-6c00-7000-8000-00000000022f")
+ );
let try_key = ListingEnqueuePublishRequest::new(
actor(),
listing(LISTING_C_D_TAG, "Try Key Greens"),
TargetPolicy::default_profile(),
)
- .try_with_idempotency_key("listing-unit-try-key")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000230")
.expect("try key");
assert_eq!(
serde_json::to_value(&try_key).expect("try key json")["idempotency_key"]["len"],
- "listing-unit-try-key".len()
+ "01890f0e-6c00-7000-8000-000000000230".len()
);
}
@@ -141,11 +142,10 @@ fn listing_request_builders_reject_invalid_options_and_timestamp_bounds() {
let timestamp_error = listing_publish_plan(
&actor(),
- RadrootsListingDraftDocumentV1::new(listing(LISTING_B_D_TAG, "Future Greens")),
+ RadrootsListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Future Greens")),
RadrootsSdkTimestamp::from_unix_seconds(u64::MAX),
)
- .err()
- .expect("timestamp error");
+ .expect_err("timestamp error");
assert!(matches!(
timestamp_error,
RadrootsSdkError::TimestampOutOfRange { .. }
@@ -159,11 +159,10 @@ fn listing_request_builders_reject_invalid_options_and_timestamp_bounds() {
});
let mutation_error = listing_publish_plan(
&actor(),
- RadrootsListingDraftDocumentV1::new(invalid_resource_area_listing),
+ RadrootsListingEditDocumentV1::new(invalid_resource_area_listing),
RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
)
- .err()
- .expect("mutation error");
+ .expect_err("mutation error");
assert!(matches!(
mutation_error,
RadrootsSdkError::ListingMutation { message } if message.contains("failed to encode")
@@ -267,7 +266,7 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() {
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
)
- .try_with_idempotency_key("listing-source-attached-enqueue")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000231")
.expect("idempotency"),
&signer,
)
@@ -280,7 +279,7 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() {
.listings()
.prepare_publish(ListingPreparePublishRequest::from_document(
actor.clone(),
- RadrootsListingDraftDocumentV1::new(listing(LISTING_B_D_TAG, "Prepared Greens")),
+ RadrootsListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Prepared Greens")),
))
.expect("prepared listing");
let prepared = sdk
@@ -290,7 +289,10 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() {
plan,
TargetPolicy::try_nostr_relays([RELAY_B], NostrRelayUrlPolicy::Public)
.expect("prepared transport targets"),
- None,
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000232")
+ .expect("prepared idempotency"),
+ ),
&signer,
)
.await
@@ -323,7 +325,7 @@ async fn listing_configured_local_signer_enqueues_publish_without_explicit_signe
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
)
- .try_with_idempotency_key("listing-configured-local")
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000232")
.expect("idempotency"),
)
.await
diff --git a/crates/sdk/tests/unit/orders_runtime_tests.rs b/crates/sdk/tests/unit/orders_runtime_tests.rs
@@ -1,4238 +1,29 @@
use super::*;
-use crate::{RadrootsClient, RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
-use radroots_authority::{RadrootsSignerError, RadrootsSignerIdentity};
-use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
-};
-use radroots_event::{
- draft::RadrootsSignedEvent,
- envelope::RadrootsEventEnvelopeParts,
- kinds::KIND_LISTING,
- order::{
- RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem, RadrootsOrderEconomicLine,
- RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis,
- RadrootsOrderRevisionOutcome,
- },
-};
-use radroots_event_store::RadrootsEventStoreError;
-use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft,
-};
-use radroots_trade::{
- identity::RadrootsTradeLocator,
- order::{RadrootsOrderEventDecodeError, RadrootsOrderIssue},
- projection::RadrootsTradeProjectionError,
- workflow::RadrootsTradeWorkflowState,
-};
-
-#[path = "../support/serializer_failure.rs"]
-mod serializer_failure;
-
-use serializer_failure::assert_struct_serialize_error_paths;
-
-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 RELAY: &str = "wss://relay.radroots.test";
-
-fn hex_64(character: char) -> String {
- core::iter::repeat_n(character, 64).collect()
-}
-
-fn hex_128(character: char) -> String {
- core::iter::repeat_n(character, 128).collect()
-}
-
-fn event_id(character: char) -> RadrootsEventId {
- RadrootsEventId::parse(hex_64(character)).expect("event id")
-}
-
-fn pubkey(character: char) -> RadrootsPublicKey {
- RadrootsPublicKey::parse(hex_64(character)).expect("pubkey")
-}
-
-fn order_id() -> RadrootsOrderId {
- RadrootsOrderId::parse("order-test-1").expect("order id")
-}
-
-fn listing_addr(seller_pubkey: &RadrootsPublicKey) -> RadrootsListingAddress {
- RadrootsListingAddress::parse(format!(
- "30402:{}:AAAAAAAAAAAAAAAAAAAAAg",
- seller_pubkey.as_str()
- ))
- .expect("listing address")
-}
-
-fn projection(
- order_id: &RadrootsOrderId,
- listing_addr: &RadrootsListingAddress,
- buyer_pubkey: &RadrootsPublicKey,
- seller_pubkey: &RadrootsPublicKey,
- root_event_id: &RadrootsEventId,
- previous_event_id: &RadrootsEventId,
-) -> RadrootsOrderProjection {
- RadrootsOrderProjection {
- order_id: order_id.clone(),
- status: RadrootsTradeWorkflowState::Requested,
- request_event_id: Some(root_event_id.clone()),
- decision_event_id: None,
- cancellation_event_id: None,
- validation_receipt_event_id: None,
- lifecycle_terminal: false,
- economics: None,
- agreement_event_id: None,
- pending_revision_event_id: None,
- pending_inventory_reservations: Vec::new(),
- committed_inventory_reservations: Vec::new(),
- listing_addr: Some(listing_addr.clone()),
- buyer_pubkey: Some(buyer_pubkey.clone()),
- seller_pubkey: Some(seller_pubkey.clone()),
- last_event_id: Some(previous_event_id.clone()),
- issues: Vec::new(),
- }
-}
-
-fn refs<'a>(
- 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,
-) -> OrderLifecycleReferences<'a> {
- OrderLifecycleReferences {
- operation: "order test",
- order_id,
- listing_addr,
- buyer_pubkey,
- seller_pubkey,
- root_event_id,
- previous_event_id,
- }
-}
-
-fn ptr(id: String) -> RadrootsEventPtr {
- RadrootsEventPtr { id, relays: None }
-}
-
-fn nostr_event(id: String, kind: u32) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id,
- author: hex_64('c'),
- created_at: 1_700_000_000,
- kind,
- tags: Vec::new(),
- content: "{}".to_owned(),
- sig: hex_128('f'),
- })
- .expect("event envelope")
-}
-
-fn actor(pubkey: &RadrootsPublicKey, role: RadrootsActorRole) -> RadrootsActorContext {
- RadrootsActorContext::test(pubkey.as_str(), [role]).expect("actor")
-}
-
-fn buyer_actor() -> RadrootsActorContext {
- actor(&pubkey('c'), RadrootsActorRole::Buyer)
-}
-
-fn seller_actor() -> RadrootsActorContext {
- actor(&pubkey('d'), RadrootsActorRole::Seller)
-}
-
-fn decimal(value: &str) -> RadrootsCoreDecimal {
- value.parse().expect("decimal")
-}
-
-fn usd(value: &str) -> RadrootsCoreMoney {
- RadrootsCoreMoney::new(decimal(value), RadrootsCoreCurrency::USD)
-}
-
-fn economics(bin_count: u32, total: &str) -> 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,
- quantity_amount: decimal("1"),
- quantity_unit: RadrootsCoreUnit::Each,
- unit_price_amount: decimal("5"),
- unit_price_currency: RadrootsCoreCurrency::USD,
- line_subtotal: usd(total),
- }],
- discounts: Vec::<RadrootsOrderEconomicLine>::new(),
- adjustments: Vec::<RadrootsOrderEconomicLine>::new(),
- subtotal: usd(total),
- discount_total: usd("0"),
- adjustment_total: usd("0"),
- total: usd(total),
- }
-}
-
-fn order_request_payload() -> RadrootsOrderRequest {
- let buyer_pubkey = pubkey('c');
- let seller_pubkey = pubkey('d');
- RadrootsOrderRequest {
- order_id: order_id(),
- listing_addr: listing_addr(&seller_pubkey),
- buyer_pubkey,
- seller_pubkey,
- items: vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- economics: economics(2, "10"),
- }
-}
-
-fn order_decision_payload() -> RadrootsOrderDecision {
- let buyer_pubkey = pubkey('c');
- let seller_pubkey = pubkey('d');
- RadrootsOrderDecision {
- order_id: order_id(),
- listing_addr: listing_addr(&seller_pubkey),
- buyer_pubkey,
- seller_pubkey,
- decision: RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: vec![RadrootsOrderInventoryCommitment {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 2,
- }],
- },
- }
-}
-
-fn revision_proposal_payload(
- root_event_id: &RadrootsEventId,
- previous_event_id: &RadrootsEventId,
-) -> RadrootsOrderRevisionProposal {
- let buyer_pubkey = pubkey('c');
- let seller_pubkey = pubkey('d');
- RadrootsOrderRevisionProposal {
- revision_id: "revision-order-test-1".parse().expect("revision id"),
- order_id: order_id(),
- listing_addr: listing_addr(&seller_pubkey),
- buyer_pubkey,
- seller_pubkey,
- root_event_id: root_event_id.clone(),
- prev_event_id: previous_event_id.clone(),
- items: vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 3,
- }],
- economics: economics(3, "15"),
- reason: "increase quantity".to_owned(),
- }
-}
-
-fn revision_decision_payload(
- proposal: &RadrootsOrderRevisionProposal,
- previous_event_id: &RadrootsEventId,
- decision: RadrootsOrderRevisionOutcome,
-) -> RadrootsOrderRevisionDecision {
- RadrootsOrderRevisionDecision {
- revision_id: proposal.revision_id.clone(),
- order_id: proposal.order_id.clone(),
- listing_addr: proposal.listing_addr.clone(),
- buyer_pubkey: proposal.buyer_pubkey.clone(),
- seller_pubkey: proposal.seller_pubkey.clone(),
- root_event_id: proposal.root_event_id.clone(),
- prev_event_id: previous_event_id.clone(),
- decision,
- }
-}
-
-fn cancellation_payload() -> RadrootsOrderCancellation {
- let buyer_pubkey = pubkey('c');
- let seller_pubkey = pubkey('d');
- RadrootsOrderCancellation {
- order_id: order_id(),
- listing_addr: listing_addr(&seller_pubkey),
- buyer_pubkey,
- seller_pubkey,
- reason: "buyer changed pickup plan".to_owned(),
- }
-}
-
-#[derive(Clone)]
-struct OrderFixtureSigner {
- identity: RadrootsSignerIdentity,
- keys: RadrootsNostrKeys,
-}
-
-impl OrderFixtureSigner {
- fn new(secret_key_hex: &str) -> Self {
- let keys = keys_from_secret(secret_key_hex);
- let pubkey = keys.public_key().to_hex();
- Self {
- identity: RadrootsSignerIdentity::new(pubkey).expect("identity"),
- keys,
- }
- }
-}
-
-fn keys_from_secret(secret_key_hex: &str) -> RadrootsNostrKeys {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- RadrootsNostrKeys::new(secret_key)
-}
-
-impl RadrootsEventSigner for OrderFixtureSigner {
- fn pubkey(&self) -> &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 fixture_buyer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(BUYER_PUBLIC_KEY_HEX, [RadrootsActorRole::Buyer]).expect("actor")
-}
-
-fn fixture_seller_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER_PUBLIC_KEY_HEX, [RadrootsActorRole::Seller]).expect("actor")
-}
-
-fn fixture_listing_addr() -> RadrootsListingAddress {
- RadrootsListingAddress::parse(format!(
- "{KIND_LISTING}:{SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("listing address")
-}
-
-fn fixture_event_ptr(character: char) -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: hex_64(character),
- relays: Some(RELAY.to_owned()),
- }
-}
-
-fn fixture_order_event_ptr(event_id: &RadrootsEventId) -> RadrootsEventPtr {
- RadrootsEventPtr {
- id: event_id.as_str().to_owned(),
- relays: Some(RELAY.to_owned()),
- }
-}
-
-fn fixture_order_id(raw: &str) -> RadrootsOrderId {
- RadrootsOrderId::parse(raw).expect("order id")
-}
-
-fn fixture_order_request(raw_order_id: &str) -> RadrootsOrderRequest {
- RadrootsOrderRequest {
- order_id: fixture_order_id(raw_order_id),
- listing_addr: fixture_listing_addr(),
- 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(2, "10"),
- }
-}
-
-fn fixture_order_decision(raw_order_id: &str) -> RadrootsOrderDecision {
- RadrootsOrderDecision {
- order_id: fixture_order_id(raw_order_id),
- listing_addr: fixture_listing_addr(),
- 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,
- }],
- },
- }
-}
-
-fn fixture_revision_proposal(
- raw_order_id: &str,
- root_event_id: &RadrootsEventId,
- previous_event_id: &RadrootsEventId,
-) -> RadrootsOrderRevisionProposal {
- RadrootsOrderRevisionProposal {
- revision_id: format!("revision-{raw_order_id}")
- .parse()
- .expect("revision id"),
- order_id: fixture_order_id(raw_order_id),
- listing_addr: fixture_listing_addr(),
- buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"),
- seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"),
- root_event_id: root_event_id.clone(),
- prev_event_id: previous_event_id.clone(),
- items: vec![RadrootsOrderItem {
- bin_id: "bin-1".parse().expect("bin id"),
- bin_count: 3,
- }],
- economics: economics(3, "15"),
- reason: "increase quantity".to_owned(),
- }
-}
-
-fn fixture_revision_decision(
- proposal: &RadrootsOrderRevisionProposal,
- previous_event_id: &RadrootsEventId,
-) -> RadrootsOrderRevisionDecision {
- RadrootsOrderRevisionDecision {
- revision_id: proposal.revision_id.clone(),
- order_id: proposal.order_id.clone(),
- listing_addr: proposal.listing_addr.clone(),
- buyer_pubkey: proposal.buyer_pubkey.clone(),
- seller_pubkey: proposal.seller_pubkey.clone(),
- root_event_id: proposal.root_event_id.clone(),
- prev_event_id: previous_event_id.clone(),
- decision: RadrootsOrderRevisionOutcome::Accepted,
- }
-}
-
-fn fixture_cancellation(raw_order_id: &str) -> RadrootsOrderCancellation {
- RadrootsOrderCancellation {
- order_id: fixture_order_id(raw_order_id),
- listing_addr: fixture_listing_addr(),
- buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"),
- seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"),
- reason: "buyer changed pickup plan".to_owned(),
- }
-}
-
-fn fixture_target_policy() -> TargetPolicy {
- TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("transport targets")
-}
-
-async fn prepared_order_sdk() -> RadrootsClient {
- RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .build()
- .await
- .expect("sdk")
-}
-
-async fn configured_order_sdk(secret_key_hex: &str) -> RadrootsClient {
- RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::new(keys_from_secret(secret_key_hex)).expect("signer"),
- ))
- .build()
- .await
- .expect("sdk")
-}
-
-#[tokio::test]
-async fn order_configured_local_signer_enqueues_submit_without_explicit_signer() {
- let sdk = configured_order_sdk(BUYER_SECRET_KEY_HEX).await;
-
- let receipt = sdk
- .trades()
- .enqueue_submit(TradeSubmitEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_event_ptr('a'),
- fixture_order_request("order-configured-local-1"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await
- .expect("enqueue submit");
-
- assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
-}
-
-#[tokio::test]
-async fn order_configured_local_signer_enqueues_lifecycle_wrappers_without_explicit_signers() {
- let seller_sdk = configured_order_sdk(SELLER_SECRET_KEY_HEX).await;
- let decision_submit = enqueue_fixture_submit(&seller_sdk, "order-configured-decision").await;
- let decision = seller_sdk
- .trades()
- .enqueue_decision(TradeDecisionEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_order_event_ptr(&decision_submit.signed_event_id),
- fixture_order_decision("order-configured-decision"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await
- .expect("configured decision");
- assert_eq!(decision.request_event_id, decision_submit.signed_event_id);
- assert_eq!(decision.state, SdkMutationState::StoredAndQueued);
-
- let proposal_submit = enqueue_fixture_submit(&seller_sdk, "order-configured-proposal").await;
- let proposal_payload = fixture_revision_proposal(
- "order-configured-proposal",
- &proposal_submit.signed_event_id,
- &proposal_submit.signed_event_id,
- );
- let proposal = seller_sdk
- .trades()
- .enqueue_revision_proposal(TradeRevisionProposalEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- proposal_payload,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await
- .expect("configured revision proposal");
- assert_eq!(proposal.root_event_id, proposal_submit.signed_event_id);
- assert_eq!(proposal.state, SdkMutationState::StoredAndQueued);
-
- let buyer_sdk = configured_order_sdk(BUYER_SECRET_KEY_HEX).await;
- let revision_submit =
- enqueue_fixture_submit(&buyer_sdk, "order-configured-revision-decision").await;
- let revision_proposal_payload = fixture_revision_proposal(
- "order-configured-revision-decision",
- &revision_submit.signed_event_id,
- &revision_submit.signed_event_id,
- );
- let revision_proposal = buyer_sdk
- .trades()
- .enqueue_revision_proposal_with_explicit_signer(
- TradeRevisionProposalEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- revision_proposal_payload.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("explicit revision proposal");
- let revision_decision_payload = fixture_revision_decision(
- &revision_proposal_payload,
- &revision_proposal.signed_event_id,
- );
- let revision_decision = buyer_sdk
- .trades()
- .enqueue_revision_decision(TradeRevisionDecisionEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&revision_proposal.signed_event_id),
- revision_decision_payload,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await
- .expect("configured revision decision");
- assert_eq!(
- revision_decision.root_event_id,
- revision_submit.signed_event_id
- );
- assert_eq!(
- revision_decision.previous_event_id,
- revision_proposal.signed_event_id
- );
- assert_eq!(revision_decision.state, SdkMutationState::StoredAndQueued);
-
- let cancel_submit = enqueue_fixture_submit(&buyer_sdk, "order-configured-cancel").await;
- let cancellation = buyer_sdk
- .trades()
- .enqueue_cancellation(TradeCancellationEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_order_event_ptr(&cancel_submit.signed_event_id),
- fixture_order_event_ptr(&cancel_submit.signed_event_id),
- fixture_cancellation("order-configured-cancel"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await
- .expect("configured cancellation");
- assert_eq!(cancellation.root_event_id, cancel_submit.signed_event_id);
- assert_eq!(
- cancellation.previous_event_id,
- cancel_submit.signed_event_id
- );
- assert_eq!(cancellation.state, SdkMutationState::StoredAndQueued);
-}
-
-async fn enqueue_fixture_submit(sdk: &RadrootsClient, raw_order_id: &str) -> TradeSubmitReceipt {
- let buyer = fixture_buyer_actor();
- let plan = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer.clone(),
- fixture_event_ptr('a'),
- fixture_order_request(raw_order_id),
- ))
- .expect("submit plan");
- sdk.trades()
- .enqueue_prepared_submit_with_explicit_signer(
- &buyer,
- plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue submit")
-}
-
-fn event_from_parts(
- parts: RadrootsNip01EventWireParts,
- contract_id: &str,
- expected_pubkey: &RadrootsPublicKey,
-) -> RadrootsEventEnvelope {
- let frozen = RadrootsEventDraft::new(
- contract_id,
- parts.kind,
- 1_700_000_000,
- parts.tags,
- parts.content,
- expected_pubkey.as_str(),
- )
- .expect("frozen draft");
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: frozen.expected_event_id_str().to_owned(),
- author: expected_pubkey.as_str().to_owned(),
- created_at: frozen.created_at_u64(),
- kind: frozen.kind_u32(),
- tags: frozen.tags_as_vec(),
- content: frozen.content().to_owned(),
- sig: hex_128('f'),
- })
- .expect("event envelope")
-}
-
-fn request_event() -> RadrootsEventEnvelope {
- let listing_event = ptr(event_id('a').as_str().to_owned());
- let request = order_request_payload();
- event_from_parts(
- order::build_order_request_draft(&listing_event, &request)
- .expect("request draft")
- .into_wire_parts(),
- TRADE_SUBMIT_CONTRACT_ID,
- &request.buyer_pubkey,
- )
-}
-
-fn event_parts_from(event: &RadrootsEventEnvelope) -> RadrootsEventEnvelopeParts {
- RadrootsEventEnvelopeParts {
- id: event.id_str().to_owned(),
- author: event.author_str().to_owned(),
- created_at: event.created_at_u64(),
- kind: event.kind_u32(),
- tags: event.tags_as_vec(),
- content: event.content().to_owned(),
- sig: event.sig_str().to_owned(),
- }
-}
-
-fn event_with_content(event: &RadrootsEventEnvelope, content: String) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- content,
- ..event_parts_from(event)
- })
- .expect("event envelope")
-}
-
-fn signed_event_from(event: RadrootsEventEnvelope) -> RadrootsSignedEvent {
- let mut wire = event.to_nip01_wire();
- wire.id = wire.computed_event_id().expect("event id").into_string();
- let raw_json = serde_json::to_string(&wire).expect("raw event json");
- RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
-}
-
-fn request_signed_event() -> RadrootsSignedEvent {
- signed_event_from(request_event())
-}
-
-fn order_request_evidence_error(
- result: Result<TradeRequestEvidence, RadrootsSdkError>,
-) -> RadrootsSdkError {
- result.err().expect("expected order request evidence error")
-}
-
-fn invalid_request_message(error: RadrootsSdkError) -> String {
- match error {
- RadrootsSdkError::InvalidRequest { message } => message,
- other => panic!("expected invalid request error, got {other:?}"),
- }
-}
-
-fn parsed_order_evidence_error(
- result: Result<ParsedOrderEvidence, RadrootsSdkError>,
-) -> RadrootsSdkError {
- result.err().expect("expected parse error")
-}
-
-fn projection_message(error: RadrootsSdkError) -> String {
- match error {
- RadrootsSdkError::Projection { message } => message,
- other => panic!("expected projection error, got {other:?}"),
- }
-}
-
-fn assert_error_display<T: core::fmt::Debug>(result: Result<T, RadrootsSdkError>, expected: &str) {
- assert!(result.unwrap_err().to_string().contains(expected));
-}
-
-fn assert_outbox_preflight_error(error: RadrootsSdkError) {
- assert!(matches!(error, RadrootsSdkError::Outbox { .. }));
-}
-
-async fn local_event_count(sdk: &RadrootsClient) -> i64 {
- sdk._event_store
- .status_summary()
- .await
- .expect("event store summary")
- .total_events
-}
-
-#[test]
-fn workflow_plan_builders_cover_success_and_actor_mismatch_paths() {
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000);
- let buyer_actor = buyer_actor();
- let seller_actor = seller_actor();
- let listing_event_id = event_id('a');
- let submit_plan = order_submit_plan(
- &buyer_actor,
- ptr(listing_event_id.as_str().to_owned()),
- order_request_payload(),
- created_at,
- )
- .expect("submit plan");
- assert_eq!(submit_plan.workflow.kind, TradeWorkflowKind::Submit);
- assert_eq!(submit_plan.listing_event_id, listing_event_id);
-
- let request_event = ptr(submit_plan.expected_event_id.as_str().to_owned());
- let decision_plan = order_decision_plan(
- &seller_actor,
- request_event.clone(),
- order_decision_payload(),
- created_at,
- )
- .expect("decision plan");
- assert_eq!(decision_plan.workflow.kind, TradeWorkflowKind::Decision);
-
- let proposal = revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- );
- let proposal_plan = order_revision_proposal_plan(
- &seller_actor,
- request_event.clone(),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- proposal.clone(),
- created_at,
- )
- .expect("revision proposal plan");
- assert_eq!(
- proposal_plan.workflow.kind,
- TradeWorkflowKind::RevisionProposal
- );
-
- let revision_decision = revision_decision_payload(
- &proposal,
- &proposal_plan.expected_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- );
- let revision_decision_plan = order_revision_decision_plan(
- &buyer_actor,
- request_event.clone(),
- ptr(proposal_plan.expected_event_id.as_str().to_owned()),
- revision_decision,
- created_at,
- )
- .expect("revision decision plan");
- assert_eq!(
- revision_decision_plan.workflow.kind,
- TradeWorkflowKind::RevisionDecision
- );
-
- let cancellation_plan = order_cancellation_plan(
- &buyer_actor,
- request_event,
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- cancellation_payload(),
- created_at,
- )
- .expect("cancellation plan");
- assert_eq!(
- cancellation_plan.workflow.kind,
- TradeWorkflowKind::Cancellation
- );
-
- let mut wrong_submit = order_request_payload();
- wrong_submit.buyer_pubkey = pubkey('e');
- assert!(matches!(
- order_submit_plan(
- &buyer_actor,
- ptr(listing_event_id.as_str().to_owned()),
- wrong_submit,
- created_at,
- ),
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
-
- let mut wrong_proposal = proposal;
- wrong_proposal.seller_pubkey = pubkey('e');
- assert!(matches!(
- order_revision_proposal_plan(
- &seller_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- wrong_proposal,
- created_at,
- ),
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
-
- let mut wrong_proposal_refs = revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- );
- wrong_proposal_refs.prev_event_id = event_id('f');
- assert!(
- invalid_request_message(
- order_revision_proposal_plan(
- &seller_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- wrong_proposal_refs,
- created_at,
- )
- .unwrap_err()
- )
- .contains("prev_event_id")
- );
-
- let mut wrong_revision_decision = revision_decision_payload(
- &revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- ),
- &proposal_plan.expected_event_id,
- RadrootsOrderRevisionOutcome::Declined {
- reason: "not workable".to_owned(),
- },
- );
- wrong_revision_decision.buyer_pubkey = pubkey('e');
- assert!(matches!(
- order_revision_decision_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(proposal_plan.expected_event_id.as_str().to_owned()),
- wrong_revision_decision,
- created_at,
- ),
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
-
- let mut wrong_revision_decision_refs = revision_decision_payload(
- &revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- ),
- &proposal_plan.expected_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- );
- wrong_revision_decision_refs.root_event_id = event_id('f');
- assert!(
- invalid_request_message(
- order_revision_decision_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(proposal_plan.expected_event_id.as_str().to_owned()),
- wrong_revision_decision_refs,
- created_at,
- )
- .unwrap_err()
- )
- .contains("root_event_id")
- );
- assert!(matches!(
- order_revision_decision_plan(
- &buyer_actor,
- ptr("not-hex".to_owned()),
- ptr(proposal_plan.expected_event_id.as_str().to_owned()),
- revision_decision_payload(
- &revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- ),
- &proposal_plan.expected_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- assert!(matches!(
- order_revision_decision_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr("not-hex".to_owned()),
- revision_decision_payload(
- &revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- ),
- &proposal_plan.expected_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let mut wrong_cancellation = cancellation_payload();
- wrong_cancellation.buyer_pubkey = pubkey('e');
- assert!(matches!(
- order_cancellation_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- wrong_cancellation,
- created_at,
- ),
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- order_cancellation_plan(
- &buyer_actor,
- ptr("not-hex".to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- cancellation_payload(),
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- assert!(matches!(
- order_cancellation_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr("not-hex".to_owned()),
- cancellation_payload(),
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- assert!(matches!(
- sdk_timestamp_ms(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk_timestamp_ms(RadrootsSdkTimestamp::from_unix_seconds(
- (i64::MAX as u64 / 1_000) + 1
- )),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
-
- let mut invalid_decision = order_decision_payload();
- invalid_decision.decision = RadrootsOrderDecisionOutcome::Accepted {
- inventory_commitments: Vec::new(),
- };
- assert!(
- invalid_request_message(
- validate_order_payload(&invalid_decision, "order decision").unwrap_err()
- )
- .contains("payload is invalid")
- );
- assert!(matches!(
- order_decision_plan(
- &seller_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- invalid_decision,
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let mut invalid_proposal = revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- );
- invalid_proposal.reason.clear();
- assert!(
- invalid_request_message(
- order_revision_proposal_plan(
- &seller_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- invalid_proposal,
- created_at,
- )
- .unwrap_err()
- )
- .contains("payload is invalid")
- );
-
- let invalid_revision_decision = revision_decision_payload(
- &revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- ),
- &proposal_plan.expected_event_id,
- RadrootsOrderRevisionOutcome::Declined {
- reason: " ".to_owned(),
- },
- );
- assert!(
- invalid_request_message(
- order_revision_decision_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(proposal_plan.expected_event_id.as_str().to_owned()),
- invalid_revision_decision,
- created_at,
- )
- .unwrap_err()
- )
- .contains("payload is invalid")
- );
-
- let mut invalid_cancellation = cancellation_payload();
- invalid_cancellation.reason = " ".to_owned();
- assert!(
- invalid_request_message(
- order_cancellation_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- invalid_cancellation,
- created_at,
- )
- .unwrap_err()
- )
- .contains("payload is invalid")
- );
-
- let validation_error = validate_order_payload(
- &RadrootsOrderCancellation {
- reason: String::new(),
- ..cancellation_payload()
- },
- "order cancellation",
- )
- .err()
- .expect("validation error");
- assert!(invalid_request_message(validation_error).contains("payload is invalid"));
-
- let valid_submit_draft = order::build_order_request_draft(
- &ptr(event_id('d').as_str().to_owned()),
- &order_request_payload(),
- )
- .expect("valid submit draft");
- let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
- valid_submit_draft.into_wire_parts(),
- TRADE_SUBMIT_CONTRACT_ID,
- pubkey('c').as_str(),
- 1_700_000_000,
- "order test",
- );
- assert_eq!(expected_event_id, frozen_draft.expected_event_id_str());
-}
-
-#[test]
-fn workflow_plan_builders_cover_timestamp_reference_and_role_errors() {
- let out_of_range = RadrootsSdkTimestamp::from_unix_seconds(u64::MAX);
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000);
- let buyer_actor = buyer_actor();
- let seller_actor = seller_actor();
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
-
- assert!(matches!(
- order_submit_plan(
- &buyer_actor,
- ptr(root_event_id.as_str().to_owned()),
- order_request_payload(),
- out_of_range,
- ),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- order_decision_plan(
- &seller_actor,
- ptr(root_event_id.as_str().to_owned()),
- order_decision_payload(),
- out_of_range,
- ),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- order_revision_proposal_plan(
- &seller_actor,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- revision_proposal_payload(&root_event_id, &previous_event_id),
- out_of_range,
- ),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- order_revision_decision_plan(
- &buyer_actor,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- revision_decision_payload(
- &revision_proposal_payload(&root_event_id, &previous_event_id),
- &previous_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- out_of_range,
- ),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- order_cancellation_plan(
- &buyer_actor,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- cancellation_payload(),
- out_of_range,
- ),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
-
- assert!(matches!(
- order_decision_plan(
- &seller_actor,
- ptr("not-hex".to_owned()),
- order_decision_payload(),
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- assert!(matches!(
- order_revision_proposal_plan(
- &buyer_actor,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- revision_proposal_payload(&root_event_id, &previous_event_id),
- created_at,
- ),
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- order_revision_proposal_plan(
- &seller_actor,
- ptr("not-hex".to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- revision_proposal_payload(&root_event_id, &previous_event_id),
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- assert!(matches!(
- order_revision_proposal_plan(
- &seller_actor,
- ptr(root_event_id.as_str().to_owned()),
- ptr("not-hex".to_owned()),
- revision_proposal_payload(&root_event_id, &previous_event_id),
- created_at,
- ),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- assert!(matches!(
- order_revision_decision_plan(
- &seller_actor,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- revision_decision_payload(
- &revision_proposal_payload(&root_event_id, &previous_event_id),
- &previous_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- created_at,
- ),
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- order_cancellation_plan(
- &seller_actor,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- cancellation_payload(),
- created_at,
- ),
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
-}
-
-#[test]
-fn order_evidence_parses_all_lifecycle_event_kinds() {
- let request_event = request_event();
- let request_evidence = parse_order_evidence(&request_event).expect("request evidence");
- assert_eq!(request_evidence.event_kind, KIND_ORDER_REQUEST);
-
- let root_event_id = request_evidence.event_id.clone();
- let decision = order_decision_payload();
- let decision_event = event_from_parts(
- order::build_order_decision_draft(&root_event_id, &root_event_id, &decision)
- .expect("decision draft")
- .into_wire_parts(),
- TRADE_DECISION_CONTRACT_ID,
- &decision.seller_pubkey,
- );
- assert_eq!(
- parse_order_evidence(&decision_event)
- .expect("decision evidence")
- .event_kind,
- KIND_ORDER_DECISION
- );
-
- let decision_event_id =
- RadrootsEventId::parse(decision_event.id_str()).expect("decision event id");
- let proposal = revision_proposal_payload(&root_event_id, &decision_event_id);
- let proposal_event = event_from_parts(
- order::build_order_revision_proposal_draft(&root_event_id, &decision_event_id, &proposal)
- .expect("proposal draft")
- .into_wire_parts(),
- TRADE_REVISION_PROPOSAL_CONTRACT_ID,
- &proposal.seller_pubkey,
- );
- assert_eq!(
- parse_order_evidence(&proposal_event)
- .expect("proposal evidence")
- .event_kind,
- KIND_ORDER_REVISION_PROPOSAL
- );
-
- let proposal_event_id =
- RadrootsEventId::parse(proposal_event.id_str()).expect("proposal event id");
- let revision_decision = revision_decision_payload(
- &proposal,
- &proposal_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- );
- let revision_decision_event = event_from_parts(
- order::build_order_revision_decision_draft(
- &root_event_id,
- &proposal_event_id,
- &revision_decision,
- )
- .expect("revision decision draft")
- .into_wire_parts(),
- TRADE_REVISION_DECISION_CONTRACT_ID,
- &revision_decision.buyer_pubkey,
- );
- assert_eq!(
- parse_order_evidence(&revision_decision_event)
- .expect("revision decision evidence")
- .event_kind,
- KIND_ORDER_REVISION_DECISION
- );
-
- let cancellation = cancellation_payload();
- let cancellation_event = event_from_parts(
- order::build_order_cancellation_draft(&root_event_id, &decision_event_id, &cancellation)
- .expect("cancellation draft")
- .into_wire_parts(),
- TRADE_CANCELLATION_CONTRACT_ID,
- &cancellation.buyer_pubkey,
- );
- assert_eq!(
- parse_order_evidence(&cancellation_event)
- .expect("cancellation evidence")
- .event_kind,
- KIND_ORDER_CANCELLATION
- );
-}
-
-#[test]
-fn order_request_evidence_parses_and_rejects_malformed_envelopes() {
- let event = request_event();
- let evidence = parse_order_request_evidence(&event).expect("request evidence");
- assert_eq!(evidence.order_id, order_id());
- assert_eq!(evidence.buyer_pubkey, pubkey('c'));
- assert_eq!(evidence.seller_pubkey, pubkey('d'));
-
- let invalid_id = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: "not-hex".to_owned(),
- ..event_parts_from(&event)
- })
- .expect_err("invalid id");
- assert!(
- invalid_id
- .to_string()
- .contains("event envelope id is invalid")
- );
-
- let invalid_author = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- author: "not-hex".to_owned(),
- ..event_parts_from(&event)
- })
- .expect_err("invalid author");
- assert!(
- invalid_author
- .to_string()
- .contains("event envelope author is invalid")
- );
-
- let request = order_request_payload();
- let author_mismatch = event_from_parts(
- order::build_order_request_draft(&ptr(event_id('a').as_str().to_owned()), &request)
- .expect("request draft")
- .into_wire_parts(),
- TRADE_SUBMIT_CONTRACT_ID,
- &pubkey('d'),
- );
- assert!(
- invalid_request_message(order_request_evidence_error(parse_order_request_evidence(
- &author_mismatch,
- )))
- .contains("decode failed")
- );
-
- let decode_failure = event_with_content(&event, "{}".to_owned());
- assert!(
- invalid_request_message(order_request_evidence_error(parse_order_request_evidence(
- &decode_failure,
- )))
- .contains("decode failed")
- );
-
- let mut envelope =
- serde_json::from_str::<serde_json::Value>(event.content()).expect("request envelope");
- envelope["order_id"] = serde_json::Value::String("other-order".to_owned());
- let order_mismatch = event_with_content(
- &event,
- serde_json::to_string(&envelope).expect("mismatched envelope"),
- );
- assert!(
- invalid_request_message(order_request_evidence_error(parse_order_request_evidence(
- &order_mismatch,
- )))
- .contains("decode failed")
- );
-
- let mut envelope =
- serde_json::from_str::<serde_json::Value>(event.content()).expect("request envelope");
- envelope["listing_addr"] = serde_json::Value::String(format!("30402:{}:other", hex_64('d')));
- let listing_mismatch = event_with_content(
- &event,
- serde_json::to_string(&envelope).expect("mismatched envelope"),
- );
- assert!(
- invalid_request_message(order_request_evidence_error(parse_order_request_evidence(
- &listing_mismatch,
- )))
- .contains("decode failed")
- );
-}
-
-#[test]
-fn decision_request_evidence_preflight_covers_all_rejection_branches() {
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000);
- let request_event_id = event_id('a');
- let plan = order_decision_plan(
- &seller_actor(),
- ptr(request_event_id.as_str().to_owned()),
- order_decision_payload(),
- created_at,
- )
- .expect("decision plan");
- let mut projection = projection(
- &plan.order_id,
- &plan.listing_addr,
- &plan.buyer_pubkey,
- &plan.seller_pubkey,
- &request_event_id,
- &request_event_id,
- );
- assert!(require_decision_request_evidence(&plan, &projection).is_ok());
-
- projection.request_event_id = Some(event_id('b'));
- assert!(
- invalid_request_message(require_decision_request_evidence(&plan, &projection).unwrap_err())
- .contains("does not match local request")
- );
-
- projection.request_event_id = Some(request_event_id.clone());
- projection.status = RadrootsTradeWorkflowState::AgreedPendingRhi;
- assert!(
- invalid_request_message(require_decision_request_evidence(&plan, &projection).unwrap_err())
- .contains("requires requested local state")
- );
-
- projection.status = RadrootsTradeWorkflowState::Requested;
- projection.pending_revision_event_id = Some(event_id('c'));
- assert!(
- invalid_request_message(require_decision_request_evidence(&plan, &projection).unwrap_err())
- .contains("cannot follow pending revision")
- );
-
- projection.pending_revision_event_id = None;
- projection.issues = vec![RadrootsOrderIssue::MissingRequest];
- assert!(
- invalid_request_message(require_decision_request_evidence(&plan, &projection).unwrap_err())
- .contains("reducer issue")
- );
-
- projection.issues.clear();
- projection.seller_pubkey = None;
- assert!(
- invalid_request_message(require_decision_request_evidence(&plan, &projection).unwrap_err())
- .contains("missing seller_pubkey")
- );
-
- projection.seller_pubkey = Some(plan.seller_pubkey.clone());
- projection.listing_addr = Some(listing_addr(&pubkey('e')));
- assert!(
- invalid_request_message(require_decision_request_evidence(&plan, &projection).unwrap_err())
- .contains("listing_addr")
- );
-
- projection.listing_addr = Some(plan.listing_addr.clone());
- projection.buyer_pubkey = Some(pubkey('e'));
- assert!(
- invalid_request_message(require_decision_request_evidence(&plan, &projection).unwrap_err())
- .contains("buyer_pubkey")
- );
-}
-
-#[test]
-fn order_status_next_action_covers_revision_and_fallback_terminal_paths() {
- let order_id = order_id();
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let buyer_pubkey = pubkey('c');
- let seller_pubkey = pubkey('d');
- let listing_addr = listing_addr(&seller_pubkey);
- let mut projection = projection(
- &order_id,
- &listing_addr,
- &buyer_pubkey,
- &seller_pubkey,
- &root_event_id,
- &previous_event_id,
- );
- projection.status = RadrootsTradeWorkflowState::RevisionProposed;
- projection.pending_revision_event_id = Some(previous_event_id.clone());
-
- let eligibility = TradeStatusEligibility::from_projection(&projection);
- assert!(eligibility.can_decide_revision);
- assert_eq!(
- TradeStatusNextActionKind::from_projection(&projection, &eligibility),
- TradeStatusNextActionKind::DecideRevision
- );
-
- let receipt = TradeStatusReceipt::from_projection(
- RadrootsTradeLocator::from_order_id(order_id.clone())
- .with_root_event_id(root_event_id.clone()),
- Some(root_event_id.clone()),
- Vec::new(),
- projection.clone(),
- 2,
- 10,
- vec![root_event_id.clone(), previous_event_id.clone()],
- );
- assert_eq!(
- receipt.next_action,
- TradeStatusNextActionKind::DecideRevision
- );
- assert!(receipt.evidence.has_pending_revision);
-
- projection.pending_revision_event_id = None;
- projection.status = RadrootsTradeWorkflowState::Requested;
- let fallback = TradeStatusNextActionKind::from_projection(
- &projection,
- &TradeStatusEligibility {
- can_decide: false,
- can_propose_revision: true,
- can_decide_revision: false,
- can_cancel: true,
- },
- );
- assert_eq!(fallback, TradeStatusNextActionKind::Terminal);
-
- projection.lifecycle_terminal = true;
- let terminal_eligibility = TradeStatusEligibility::from_projection(&projection);
- assert_eq!(
- TradeStatusNextActionKind::from_projection(&projection, &terminal_eligibility),
- TradeStatusNextActionKind::Terminal
- );
-
- projection.lifecycle_terminal = false;
- projection.issues = vec![RadrootsOrderIssue::ForkedLifecycle {
- event_ids: vec![event_id('e')],
- }];
- let issue_eligibility = TradeStatusEligibility::from_projection(&projection);
- assert_eq!(
- TradeStatusNextActionKind::from_projection(&projection, &issue_eligibility),
- TradeStatusNextActionKind::InspectEvidenceIssues
- );
-}
-
-#[test]
-fn order_issue_mapping_covers_every_trade_issue_variant() {
- let one = event_id('a');
- let two = event_id('b');
- let many = vec![one.clone(), two.clone()];
-
- macro_rules! single {
- ($issue:ident, $kind:ident) => {
- (
- RadrootsOrderIssue::$issue {
- event_id: one.clone(),
- },
- SdkTradeStatusIssueKind::$kind,
- 1,
- )
- };
- }
-
- let cases = vec![
- (
- RadrootsOrderIssue::MissingRequest,
- SdkTradeStatusIssueKind::MissingRequest,
- 0,
- ),
- (
- RadrootsOrderIssue::MultipleRequests {
- event_ids: many.clone(),
- },
- SdkTradeStatusIssueKind::MultipleRequests,
- 2,
- ),
- single!(RequestPayloadInvalid, RequestPayloadInvalid),
- single!(RequestOrderIdMismatch, RequestOrderIdMismatch),
- single!(RequestAuthorMismatch, RequestAuthorMismatch),
- single!(RequestListingAddressInvalid, RequestListingAddressInvalid),
- single!(RequestSellerListingMismatch, RequestSellerListingMismatch),
- single!(DecisionPayloadInvalid, DecisionPayloadInvalid),
- single!(DecisionOrderIdMismatch, DecisionOrderIdMismatch),
- single!(DecisionAuthorMismatch, DecisionAuthorMismatch),
- single!(DecisionCounterpartyMismatch, DecisionCounterpartyMismatch),
- single!(DecisionBuyerMismatch, DecisionBuyerMismatch),
- single!(DecisionSellerMismatch, DecisionSellerMismatch),
- single!(DecisionListingAddressInvalid, DecisionListingAddressInvalid),
- single!(DecisionListingMismatch, DecisionListingMismatch),
- single!(DecisionRootMismatch, DecisionRootMismatch),
- single!(DecisionPreviousMismatch, DecisionPreviousMismatch),
- single!(
- DecisionMissingInventoryCommitments,
- DecisionMissingInventoryCommitments
- ),
- single!(
- DecisionInventoryCommitmentMismatch,
- DecisionInventoryCommitmentMismatch
- ),
- single!(DecisionMissingReason, DecisionMissingReason),
- (
- RadrootsOrderIssue::ConflictingDecisions {
- event_ids: many.clone(),
- },
- SdkTradeStatusIssueKind::ConflictingDecisions,
- 2,
- ),
- single!(
- RevisionProposalPayloadInvalid,
- RevisionProposalPayloadInvalid
- ),
- single!(
- RevisionProposalOrderIdMismatch,
- RevisionProposalOrderIdMismatch
- ),
- single!(
- RevisionProposalAuthorMismatch,
- RevisionProposalAuthorMismatch
- ),
- single!(
- RevisionProposalCounterpartyMismatch,
- RevisionProposalCounterpartyMismatch
- ),
- single!(RevisionProposalBuyerMismatch, RevisionProposalBuyerMismatch),
- single!(
- RevisionProposalSellerMismatch,
- RevisionProposalSellerMismatch
- ),
- single!(
- RevisionProposalListingAddressInvalid,
- RevisionProposalListingAddressInvalid
- ),
- single!(
- RevisionProposalListingMismatch,
- RevisionProposalListingMismatch
- ),
- single!(RevisionProposalRootMismatch, RevisionProposalRootMismatch),
- single!(
- RevisionProposalPreviousMismatch,
- RevisionProposalPreviousMismatch
- ),
- single!(
- RevisionDecisionWithoutProposal,
- RevisionDecisionWithoutProposal
- ),
- single!(
- RevisionDecisionPayloadInvalid,
- RevisionDecisionPayloadInvalid
- ),
- single!(
- RevisionDecisionOrderIdMismatch,
- RevisionDecisionOrderIdMismatch
- ),
- single!(
- RevisionDecisionAuthorMismatch,
- RevisionDecisionAuthorMismatch
- ),
- single!(
- RevisionDecisionCounterpartyMismatch,
- RevisionDecisionCounterpartyMismatch
- ),
- single!(RevisionDecisionBuyerMismatch, RevisionDecisionBuyerMismatch),
- single!(
- RevisionDecisionSellerMismatch,
- RevisionDecisionSellerMismatch
- ),
- single!(
- RevisionDecisionListingAddressInvalid,
- RevisionDecisionListingAddressInvalid
- ),
- single!(
- RevisionDecisionListingMismatch,
- RevisionDecisionListingMismatch
- ),
- single!(RevisionDecisionRootMismatch, RevisionDecisionRootMismatch),
- single!(
- RevisionDecisionPreviousMismatch,
- RevisionDecisionPreviousMismatch
- ),
- single!(
- RevisionDecisionRevisionIdMismatch,
- RevisionDecisionRevisionIdMismatch
- ),
- single!(
- CancellationWithoutCancellableOrder,
- CancellationWithoutCancellableOrder
- ),
- single!(CancellationPayloadInvalid, CancellationPayloadInvalid),
- single!(CancellationOrderIdMismatch, CancellationOrderIdMismatch),
- single!(CancellationAuthorMismatch, CancellationAuthorMismatch),
- single!(
- CancellationCounterpartyMismatch,
- CancellationCounterpartyMismatch
- ),
- single!(CancellationBuyerMismatch, CancellationBuyerMismatch),
- single!(CancellationSellerMismatch, CancellationSellerMismatch),
- single!(
- CancellationListingAddressInvalid,
- CancellationListingAddressInvalid
- ),
- single!(CancellationListingMismatch, CancellationListingMismatch),
- single!(CancellationRootMismatch, CancellationRootMismatch),
- single!(CancellationPreviousMismatch, CancellationPreviousMismatch),
- (
- RadrootsOrderIssue::ForkedLifecycle {
- event_ids: many.clone(),
- },
- SdkTradeStatusIssueKind::ForkedLifecycle,
- 2,
- ),
- single!(
- ValidationReceiptWithoutPendingAgreement,
- ValidationReceiptWithoutPendingAgreement
- ),
- single!(
- ValidationReceiptOrderIdMismatch,
- ValidationReceiptOrderIdMismatch
- ),
- single!(ValidationReceiptTypeMismatch, ValidationReceiptTypeMismatch),
- single!(ValidationReceiptRootMismatch, ValidationReceiptRootMismatch),
- single!(
- ValidationReceiptTargetMismatch,
- ValidationReceiptTargetMismatch
- ),
- single!(
- ValidationReceiptListingMismatch,
- ValidationReceiptListingMismatch
- ),
- (
- RadrootsOrderIssue::ConflictingValidationReceipts {
- event_ids: many.clone(),
- },
- SdkTradeStatusIssueKind::ConflictingValidationReceipts,
- 2,
- ),
- (
- RadrootsOrderIssue::DeterministicValidationFailure {
- event_id: one.clone(),
- reason: "fixture validation failed".to_owned(),
- },
- SdkTradeStatusIssueKind::DeterministicValidationFailure,
- 1,
- ),
- (
- RadrootsOrderIssue::StaleListingEvent {
- expected_event_id: one,
- current_event_id: two,
- },
- SdkTradeStatusIssueKind::StaleListingEvent,
- 2,
- ),
- ];
-
- for (issue, expected_kind, expected_event_count) in cases {
- let sdk_issue = SdkTradeStatusIssue::from(issue);
- assert_eq!(sdk_issue.kind, expected_kind);
- assert_eq!(sdk_issue.event_ids.len(), expected_event_count);
- assert_eq!(sdk_issue.code(), expected_kind.code());
- }
-}
-
-#[test]
-fn lifecycle_projection_helpers_reject_unclean_mismatched_and_stale_state() {
- let order_id = order_id();
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let buyer_pubkey = pubkey('c');
- let seller_pubkey = pubkey('d');
- let listing_addr = listing_addr(&seller_pubkey);
- let refs = refs(
- &order_id,
- &listing_addr,
- &buyer_pubkey,
- &seller_pubkey,
- &root_event_id,
- &previous_event_id,
- );
- let clean = projection(
- &order_id,
- &listing_addr,
- &buyer_pubkey,
- &seller_pubkey,
- &root_event_id,
- &previous_event_id,
- );
- assert!(require_clean_lifecycle_projection(refs, &clean).is_ok());
- assert!(require_lifecycle_status(&refs, &clean, RadrootsTradeWorkflowState::Requested).is_ok());
- assert!(require_no_lifecycle_terminal(&refs, &clean).is_ok());
- assert!(require_no_pending_revision(&refs, &clean).is_ok());
- assert!(require_lifecycle_previous_is_current(&refs, &clean).is_ok());
-
- let mut missing_request = clean.clone();
- missing_request.request_event_id = None;
- assert!(
- invalid_request_message(
- require_clean_lifecycle_projection(refs, &missing_request).unwrap_err()
- )
- .contains("requires local order request evidence")
- );
-
- let mut wrong_request = clean.clone();
- wrong_request.request_event_id = Some(event_id('e'));
- assert!(
- invalid_request_message(
- require_clean_lifecycle_projection(refs, &wrong_request).unwrap_err()
- )
- .contains("root event")
- );
-
- let mut issued = clean.clone();
- issued.issues = vec![RadrootsOrderIssue::ForkedLifecycle {
- event_ids: vec![event_id('f')],
- }];
- assert!(
- invalid_request_message(require_clean_lifecycle_projection(refs, &issued).unwrap_err())
- .contains("reducer issue")
- );
-
- let mut missing_listing = clean.clone();
- missing_listing.listing_addr = None;
- assert!(
- invalid_request_message(
- require_clean_lifecycle_projection(refs, &missing_listing).unwrap_err()
- )
- .contains("missing listing_addr")
- );
-
- let mut wrong_buyer = clean.clone();
- wrong_buyer.buyer_pubkey = Some(pubkey('e'));
- assert!(
- invalid_request_message(
- require_clean_lifecycle_projection(refs, &wrong_buyer).unwrap_err()
- )
- .contains("buyer_pubkey")
- );
-
- let mut accepted = clean.clone();
- accepted.status = RadrootsTradeWorkflowState::AgreedPendingRhi;
- assert!(
- invalid_request_message(
- require_lifecycle_status(&refs, &accepted, RadrootsTradeWorkflowState::Requested)
- .unwrap_err()
- )
- .contains("requires Requested")
- );
-
- let mut terminal = clean.clone();
- terminal.lifecycle_terminal = true;
- assert!(
- invalid_request_message(require_no_lifecycle_terminal(&refs, &terminal).unwrap_err())
- .contains("non-terminal")
- );
-
- assert!(
- invalid_request_message(require_pending_revision(&refs, &clean).unwrap_err())
- .contains("requires pending revision proposal")
- );
-
- let mut wrong_pending = clean.clone();
- wrong_pending.pending_revision_event_id = Some(event_id('e'));
- assert!(
- invalid_request_message(require_pending_revision(&refs, &wrong_pending).unwrap_err())
- .contains("does not match pending revision")
- );
-
- let mut pending = clean.clone();
- pending.pending_revision_event_id = Some(previous_event_id.clone());
- assert!(require_pending_revision(&refs, &pending).is_ok());
- assert!(
- invalid_request_message(require_no_pending_revision(&refs, &pending).unwrap_err())
- .contains("cannot follow pending revision")
- );
-
- let mut wrong_previous = clean.clone();
- wrong_previous.last_event_id = Some(event_id('e'));
- assert!(
- invalid_request_message(
- require_lifecycle_previous_is_current(&refs, &wrong_previous).unwrap_err()
- )
- .contains("does not match current lifecycle event")
- );
-
- let mut missing_previous = clean;
- missing_previous.last_event_id = None;
- assert!(
- invalid_request_message(
- require_lifecycle_previous_is_current(&refs, &missing_previous).unwrap_err()
- )
- .contains("requires current lifecycle event evidence")
- );
-}
-
-#[test]
-fn lifecycle_plan_state_wrappers_reject_invalid_status_terminal_and_pending_state() {
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000);
- let buyer_actor = buyer_actor();
- let seller_actor = seller_actor();
- let listing_event_id = event_id('a');
- let submit_plan = order_submit_plan(
- &buyer_actor,
- ptr(listing_event_id.as_str().to_owned()),
- order_request_payload(),
- created_at,
- )
- .expect("submit plan");
- let decision_plan = order_decision_plan(
- &seller_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- order_decision_payload(),
- created_at,
- )
- .expect("decision plan");
- let proposal_payload = revision_proposal_payload(
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- );
- let proposal_plan = order_revision_proposal_plan(
- &seller_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- proposal_payload.clone(),
- created_at,
- )
- .expect("proposal plan");
- let revision_plan = order_revision_decision_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(proposal_plan.expected_event_id.as_str().to_owned()),
- revision_decision_payload(
- &proposal_payload,
- &proposal_plan.expected_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- created_at,
- )
- .expect("revision decision plan");
- let cancellation_plan = order_cancellation_plan(
- &buyer_actor,
- ptr(submit_plan.expected_event_id.as_str().to_owned()),
- ptr(decision_plan.expected_event_id.as_str().to_owned()),
- cancellation_payload(),
- created_at,
- )
- .expect("cancellation plan");
- let base = projection(
- &proposal_plan.order_id,
- &proposal_plan.listing_addr,
- &proposal_plan.buyer_pubkey,
- &proposal_plan.seller_pubkey,
- &submit_plan.expected_event_id,
- &decision_plan.expected_event_id,
- );
-
- assert!(require_revision_proposal_state(&proposal_plan, &base).is_ok());
-
- let mut accepted = base.clone();
- accepted.status = RadrootsTradeWorkflowState::AgreedPendingRhi;
- assert!(
- invalid_request_message(
- require_revision_proposal_state(&proposal_plan, &accepted).unwrap_err()
- )
- .contains("requires Requested")
- );
-
- let mut terminal = base.clone();
- terminal.lifecycle_terminal = true;
- assert!(
- invalid_request_message(
- require_revision_proposal_state(&proposal_plan, &terminal).unwrap_err()
- )
- .contains("non-terminal")
- );
-
- let mut proposal_pending = base.clone();
- proposal_pending.pending_revision_event_id = Some(proposal_plan.expected_event_id.clone());
- assert!(
- invalid_request_message(
- require_revision_proposal_state(&proposal_plan, &proposal_pending).unwrap_err()
- )
- .contains("cannot follow pending revision")
- );
-
- let mut revision_ready = base.clone();
- revision_ready.status = RadrootsTradeWorkflowState::RevisionProposed;
- revision_ready.last_event_id = Some(proposal_plan.expected_event_id.clone());
- revision_ready.pending_revision_event_id = Some(proposal_plan.expected_event_id.clone());
- assert!(require_revision_decision_state(&revision_plan, &revision_ready).is_ok());
-
- let mut revision_terminal = revision_ready.clone();
- revision_terminal.lifecycle_terminal = true;
- assert!(
- invalid_request_message(
- require_revision_decision_state(&revision_plan, &revision_terminal).unwrap_err()
- )
- .contains("non-terminal")
- );
-
- let mut revision_without_pending = revision_ready;
- revision_without_pending.pending_revision_event_id = None;
- assert!(
- invalid_request_message(
- require_revision_decision_state(&revision_plan, &revision_without_pending).unwrap_err()
- )
- .contains("requires pending revision proposal")
- );
-
- assert!(require_cancellation_state(&cancellation_plan, &base).is_ok());
-
- let mut cancellation_accepted = base.clone();
- cancellation_accepted.status = RadrootsTradeWorkflowState::AgreedPendingRhi;
- assert!(
- invalid_request_message(
- require_cancellation_state(&cancellation_plan, &cancellation_accepted).unwrap_err()
- )
- .contains("cancellation requires requested")
- );
-
- let mut cancellation_terminal = base.clone();
- cancellation_terminal.lifecycle_terminal = true;
- assert!(
- invalid_request_message(
- require_cancellation_state(&cancellation_plan, &cancellation_terminal).unwrap_err()
- )
- .contains("non-terminal")
- );
-
- let mut cancellation_pending = base;
- cancellation_pending.pending_revision_event_id = Some(proposal_plan.expected_event_id);
- assert!(
- invalid_request_message(
- require_cancellation_state(&cancellation_plan, &cancellation_pending).unwrap_err()
- )
- .contains("cannot follow pending revision")
- );
-}
-
-#[test]
-fn evidence_reference_helpers_reject_invalid_ids_and_payload_mismatches() {
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let alternate_event_id = event_id('c');
-
- assert_eq!(
- request_event_id(&ptr(root_event_id.as_str().to_owned())).expect("request event"),
- root_event_id
- );
- assert_eq!(
- order_reference_event_id(&ptr(previous_event_id.as_str().to_owned()), "decision")
- .expect("reference event"),
- previous_event_id
- );
- assert!(
- invalid_request_message(request_event_id(&ptr("not-hex".to_owned())).unwrap_err())
- .contains("order request evidence event id is invalid")
- );
- assert!(
- invalid_request_message(
- order_reference_event_id(&ptr("not-hex".to_owned()), "decision").unwrap_err()
- )
- .contains("order decision evidence event id is invalid")
- );
-
- assert!(
- require_payload_event_refs(
- "order decision",
- &root_event_id,
- &previous_event_id,
- &root_event_id,
- &previous_event_id,
- )
- .is_ok()
- );
- assert!(
- invalid_request_message(
- require_payload_event_refs(
- "order decision",
- &alternate_event_id,
- &previous_event_id,
- &root_event_id,
- &previous_event_id,
- )
- .unwrap_err()
- )
- .contains("root_event_id")
- );
- assert!(
- invalid_request_message(
- require_payload_event_refs(
- "order decision",
- &root_event_id,
- &alternate_event_id,
- &root_event_id,
- &previous_event_id,
- )
- .unwrap_err()
- )
- .contains("prev_event_id")
- );
-}
-
-#[test]
-fn parse_order_evidence_reports_constructor_id_unsupported_kind_and_decode_errors() {
- let invalid_id = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
- id: "not-hex".to_owned(),
- author: hex_64('c'),
- created_at: 1_700_000_000,
- kind: KIND_ORDER_REQUEST,
- tags: Vec::new(),
- content: "{}".to_owned(),
- sig: hex_128('f'),
- })
- .expect_err("invalid id");
- assert!(
- invalid_id
- .to_string()
- .contains("event envelope id is invalid")
- );
- assert!(
- invalid_request_message(parsed_order_evidence_error(parse_order_evidence(
- &nostr_event(hex_64('a'), 1),
- )))
- .contains("order evidence event kind 1 is not supported")
- );
- assert!(
- invalid_request_message(parsed_order_evidence_error(parse_order_evidence(
- &nostr_event(hex_64('a'), KIND_ORDER_REQUEST),
- )))
- .contains("order evidence event is invalid")
- );
- for kind in [
- KIND_ORDER_DECISION,
- KIND_ORDER_REVISION_PROPOSAL,
- KIND_ORDER_REVISION_DECISION,
- KIND_ORDER_CANCELLATION,
- ] {
- assert!(
- invalid_request_message(parsed_order_evidence_error(parse_order_evidence(
- &nostr_event(hex_64('a'), kind),
- )))
- .contains("order evidence event is invalid")
- );
- }
-}
-
-#[test]
-fn projection_error_maps_store_tag_and_decode_errors() {
- assert_eq!(
- projection_message(projection_error(RadrootsOrderStoreQueryError::Store(
- RadrootsEventStoreError::MissingEvent("event-a".to_owned())
- ))),
- "order status store query failed"
- );
- assert_eq!(
- projection_message(projection_error(
- RadrootsOrderStoreQueryError::InvalidStoredTagsJson {
- event_id: "event-a".to_owned(),
- source: serde_json::from_str::<serde_json::Value>("{").unwrap_err(),
- }
- )),
- "stored order event tags could not be decoded"
- );
- assert_eq!(
- projection_message(projection_error(RadrootsOrderStoreQueryError::Decode {
- event_id: "event-a".to_owned(),
- source: RadrootsOrderEventDecodeError::UnsupportedKind { kind: 999 },
- })),
- "stored order event could not decode as order record"
- );
- let invalid_limit = projection_error(RadrootsOrderStoreQueryError::Projection(
- RadrootsTradeProjectionError::InvalidLimit { max: 1000 },
- ));
- assert!(matches!(
- invalid_limit,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-}
-
-#[test]
-fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() {
- let buyer = buyer_actor();
- let seller = seller_actor();
- let listing_event = ptr(event_id('a').as_str().to_owned());
- let request_event = ptr(event_id('b').as_str().to_owned());
- let previous_event = ptr(event_id('c').as_str().to_owned());
- let submit_payload = order_request_payload();
- let decision_payload = order_decision_payload();
- let proposal_payload = revision_proposal_payload(&event_id('b'), &event_id('c'));
- let revision_decision_payload = revision_decision_payload(
- &proposal_payload,
- &event_id('d'),
- RadrootsOrderRevisionOutcome::Accepted,
- );
- let cancellation_payload = cancellation_payload();
- let policy = TargetPolicy::default_profile();
-
- assert_error_display(
- TradeSubmitEnqueueRequest::new(
- buyer.clone(),
- listing_event.clone(),
- submit_payload.clone(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(Vec::<String>::new(), NostrRelayUrlPolicy::Public),
- "transport targets",
- );
- assert_error_display(
- TradeSubmitEnqueueRequest::new(
- buyer.clone(),
- listing_event,
- submit_payload,
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key(""),
- "idempotency key",
- );
-
- assert_error_display(
- TradeDecisionEnqueueRequest::new(
- seller.clone(),
- request_event.clone(),
- decision_payload.clone(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(Vec::<String>::new(), NostrRelayUrlPolicy::Public),
- "transport targets",
- );
- assert_error_display(
- TradeDecisionEnqueueRequest::new(
- seller.clone(),
- request_event.clone(),
- decision_payload,
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key(" leading"),
- "idempotency key",
- );
-
- assert_error_display(
- TradeRevisionProposalEnqueueRequest::new(
- seller.clone(),
- request_event.clone(),
- previous_event.clone(),
- proposal_payload.clone(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(Vec::<String>::new(), NostrRelayUrlPolicy::Public),
- "transport targets",
- );
- assert_error_display(
- TradeRevisionProposalEnqueueRequest::new(
- seller.clone(),
- request_event.clone(),
- previous_event.clone(),
- proposal_payload.clone(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("trailing "),
- "idempotency key",
- );
-
- assert_error_display(
- TradeRevisionDecisionEnqueueRequest::new(
- buyer.clone(),
- request_event.clone(),
- previous_event.clone(),
- revision_decision_payload.clone(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(Vec::<String>::new(), NostrRelayUrlPolicy::Public),
- "transport targets",
- );
- assert_error_display(
- TradeRevisionDecisionEnqueueRequest::new(
- buyer.clone(),
- request_event.clone(),
- previous_event.clone(),
- revision_decision_payload,
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key("invalid\nkey"),
- "idempotency key",
- );
-
- assert_error_display(
- TradeCancellationEnqueueRequest::new(
- buyer.clone(),
- request_event.clone(),
- previous_event.clone(),
- cancellation_payload.clone(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(Vec::<String>::new(), NostrRelayUrlPolicy::Public),
- "transport targets",
- );
- assert_error_display(
- TradeCancellationEnqueueRequest::new(
- buyer,
- request_event,
- previous_event,
- cancellation_payload,
- policy,
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_idempotency_key(""),
- "idempotency key",
- );
-}
#[test]
-fn trade_enqueue_policy_rejects_publish_modes_without_matching_side_effects() {
- assert!(matches!(
- validate_trade_enqueue_policy(PublishMode::DryRun, &SatisfactionPolicy::NoWait),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message == "trade dry-run publish mode must use a prepare request"
- ));
- assert!(matches!(
- validate_trade_enqueue_policy(PublishMode::EnqueueOnly, &SatisfactionPolicy::AnyAccepted),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message == "trade enqueue-only publish mode only supports no-wait satisfaction"
- ));
- assert!(matches!(
- validate_trade_enqueue_policy(PublishMode::EnqueueAndPublish, &SatisfactionPolicy::NoWait),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message == "trade enqueue-and-publish requires a transport satisfaction policy"
- ));
- assert!(
- validate_trade_enqueue_policy(PublishMode::EnqueueOnly, &SatisfactionPolicy::NoWait)
- .is_ok()
- );
- assert!(
- validate_trade_enqueue_policy(
- PublishMode::EnqueueAndPublish,
- &SatisfactionPolicy::AnyAccepted
- )
- .is_ok()
- );
-}
-
-#[tokio::test]
-async fn orders_client_prepare_methods_resolve_request_created_at() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_010))
- .build()
- .await
- .expect("sdk");
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let root_event = ptr(root_event_id.as_str().to_owned());
- let previous_event = ptr(previous_event_id.as_str().to_owned());
-
- assert_eq!(
- sdk.trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer_actor(),
- root_event.clone(),
- order_request_payload(),
- ))
- .expect("submit plan")
- .created_at,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_010)
- );
- assert_eq!(
- sdk.trades()
- .prepare_decision(
- TradeDecisionPrepareRequest::new(
- seller_actor(),
- root_event.clone(),
- order_decision_payload(),
- )
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_011)),
- )
- .expect("decision plan")
- .created_at,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_011)
- );
-
- let proposal = revision_proposal_payload(&root_event_id, &previous_event_id);
- assert_eq!(
- sdk.trades()
- .prepare_revision_proposal(
- TradeRevisionProposalPrepareRequest::new(
- seller_actor(),
- root_event.clone(),
- previous_event.clone(),
- proposal.clone(),
- )
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_012)),
- )
- .expect("proposal plan")
- .created_at,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_012)
- );
- assert_eq!(
- sdk.trades()
- .prepare_revision_decision(
- TradeRevisionDecisionPrepareRequest::new(
- buyer_actor(),
- root_event.clone(),
- ptr(event_id('c').as_str().to_owned()),
- revision_decision_payload(
- &proposal,
- &event_id('c'),
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- )
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_013)),
- )
- .expect("revision decision plan")
- .created_at,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_013)
- );
- assert_eq!(
- sdk.trades()
- .prepare_cancellation(
- TradeCancellationPrepareRequest::new(
- buyer_actor(),
- root_event,
- previous_event,
- cancellation_payload(),
- )
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_014)),
- )
- .expect("cancellation plan")
- .created_at,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_014)
- );
-}
-
-#[tokio::test]
-async fn prepared_submit_and_decision_enqueue_cover_source_attached_success_path() {
- let sdk = prepared_order_sdk().await;
- let submit = enqueue_fixture_submit(&sdk, "order-prepared-decision");
- let submit = submit.await;
- assert_eq!(submit.signed_event_id, submit.expected_event_id);
- assert_eq!(submit.workflow.kind, TradeWorkflowKind::Submit);
-
- let seller = fixture_seller_actor();
- let decision_plan = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&submit.signed_event_id),
- fixture_order_decision("order-prepared-decision"),
- ))
- .expect("decision plan");
- let decision = sdk
- .trades()
- .enqueue_prepared_decision_with_explicit_signer(
- &seller,
- decision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- Some(SdkIdempotencyKey::new("prepared-decision").expect("idempotency")),
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue decision");
-
- assert_eq!(decision.signed_event_id, decision.expected_event_id);
- assert_eq!(decision.workflow.kind, TradeWorkflowKind::Decision);
- assert_eq!(decision.request_event_id, submit.signed_event_id);
-}
-
-#[tokio::test]
-async fn prepared_revision_lifecycle_enqueue_cover_source_attached_success_paths() {
- let sdk = prepared_order_sdk().await;
- let submit = enqueue_fixture_submit(&sdk, "order-prepared-revision").await;
- let seller = fixture_seller_actor();
- let proposal_payload = fixture_revision_proposal(
- "order-prepared-revision",
- &submit.signed_event_id,
- &submit.signed_event_id,
- );
- let proposal_plan = sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&submit.signed_event_id),
- fixture_order_event_ptr(&submit.signed_event_id),
- proposal_payload.clone(),
- ))
- .expect("proposal plan");
- let proposal = sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- proposal_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- Some(SdkIdempotencyKey::new("prepared-proposal").expect("idempotency")),
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue proposal");
-
- assert_eq!(proposal.signed_event_id, proposal.expected_event_id);
- assert_eq!(proposal.workflow.kind, TradeWorkflowKind::RevisionProposal);
- assert_eq!(proposal.root_event_id, submit.signed_event_id);
- assert_eq!(proposal.previous_event_id, submit.signed_event_id);
-
- let buyer = fixture_buyer_actor();
- let revision_decision = fixture_revision_decision(&proposal_payload, &proposal.signed_event_id);
- let revision_decision_plan = sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&submit.signed_event_id),
- fixture_order_event_ptr(&proposal.signed_event_id),
- revision_decision,
- ))
- .expect("revision decision plan");
- let revision = sdk
- .trades()
- .enqueue_prepared_revision_decision_with_explicit_signer(
- &buyer,
- revision_decision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision decision");
-
- assert_eq!(revision.signed_event_id, revision.expected_event_id);
- assert_eq!(revision.workflow.kind, TradeWorkflowKind::RevisionDecision);
- assert_eq!(revision.root_event_id, submit.signed_event_id);
- assert_eq!(revision.previous_event_id, proposal.signed_event_id);
-}
-
-#[tokio::test]
-async fn prepared_cancellation_enqueue_covers_source_attached_success_path() {
- let sdk = prepared_order_sdk().await;
- let submit = enqueue_fixture_submit(&sdk, "order-prepared-cancellation").await;
- let buyer = fixture_buyer_actor();
- let cancellation_plan = sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&submit.signed_event_id),
- fixture_order_event_ptr(&submit.signed_event_id),
- fixture_cancellation("order-prepared-cancellation"),
- ))
- .expect("cancellation plan");
- let cancellation = sdk
- .trades()
- .enqueue_prepared_cancellation_with_explicit_signer(
- &buyer,
- cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- Some(SdkIdempotencyKey::new("prepared-cancellation").expect("idempotency")),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue cancellation");
-
- assert_eq!(cancellation.signed_event_id, cancellation.expected_event_id);
- assert_eq!(cancellation.workflow.kind, TradeWorkflowKind::Cancellation);
- assert_eq!(cancellation.root_event_id, submit.signed_event_id);
- assert_eq!(cancellation.previous_event_id, submit.signed_event_id);
-}
-
-#[tokio::test]
-async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() {
- let sdk = prepared_order_sdk().await;
- let decision_submit = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(
- TradeSubmitEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_event_ptr('b'),
- fixture_order_request("order-wrapper-decision"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue submit");
- let decision = sdk
- .trades()
- .enqueue_decision_with_explicit_signer(
- TradeDecisionEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_order_event_ptr(&decision_submit.signed_event_id),
- fixture_order_decision("order-wrapper-decision"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue decision");
- assert_eq!(decision.request_event_id, decision_submit.signed_event_id);
-
- let revision_submit = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(
- TradeSubmitEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_event_ptr('c'),
- fixture_order_request("order-wrapper-revision"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision submit");
- let proposal_payload = fixture_revision_proposal(
- "order-wrapper-revision",
- &revision_submit.signed_event_id,
- &revision_submit.signed_event_id,
- );
- let proposal = sdk
- .trades()
- .enqueue_revision_proposal_with_explicit_signer(
- TradeRevisionProposalEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- proposal_payload.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue proposal");
- let revision = sdk
- .trades()
- .enqueue_revision_decision_with_explicit_signer(
- TradeRevisionDecisionEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&proposal.signed_event_id),
- fixture_revision_decision(&proposal_payload, &proposal.signed_event_id),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision decision");
- assert_eq!(revision.previous_event_id, proposal.signed_event_id);
-
- let cancellation_submit = sdk
- .trades()
- .enqueue_submit_with_explicit_signer(
- TradeSubmitEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_event_ptr('d'),
- fixture_order_request("order-wrapper-cancellation"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue cancellation submit");
- let cancellation = sdk
- .trades()
- .enqueue_cancellation_with_explicit_signer(
- TradeCancellationEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_cancellation("order-wrapper-cancellation"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue cancellation");
- assert_eq!(
- cancellation.previous_event_id,
- cancellation_submit.signed_event_id
- );
-}
-
-#[tokio::test]
-async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors() {
- let sdk = prepared_order_sdk().await;
- let buyer = fixture_buyer_actor();
- let seller = fixture_seller_actor();
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let root = fixture_order_event_ptr(&root_event_id);
- let previous = fixture_order_event_ptr(&previous_event_id);
- let proposal =
- fixture_revision_proposal("order-preflight-errors", &root_event_id, &previous_event_id);
-
- let decision_plan = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- root.clone(),
- fixture_order_decision("order-preflight-errors"),
- ))
- .expect("decision plan");
- let decision_missing = sdk
- .trades()
- .enqueue_prepared_decision_with_explicit_signer(
- &seller,
- decision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("missing decision evidence");
- assert!(matches!(
- decision_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let proposal_plan = sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- root.clone(),
- previous.clone(),
- proposal.clone(),
- ))
- .expect("proposal plan");
- let proposal_missing = sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- proposal_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("missing proposal evidence");
- assert!(matches!(
- proposal_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let revision_decision_plan = sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- root.clone(),
- previous.clone(),
- fixture_revision_decision(&proposal, &previous_event_id),
- ))
- .expect("revision decision plan");
- let revision_missing = sdk
- .trades()
- .enqueue_prepared_revision_decision_with_explicit_signer(
- &buyer,
- revision_decision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("missing revision evidence");
- assert!(matches!(
- revision_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let cancellation_plan = sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- root,
- previous,
- fixture_cancellation("order-preflight-errors"),
- ))
- .expect("cancellation plan");
- let cancellation_missing = sdk
- .trades()
- .enqueue_prepared_cancellation_with_explicit_signer(
- &buyer,
- cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("missing cancellation evidence");
- assert!(matches!(
- cancellation_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let closed_sdk = prepared_order_sdk().await;
- let closed_root_event_id = event_id('c');
- let closed_previous_event_id = event_id('d');
- let closed_root = fixture_order_event_ptr(&closed_root_event_id);
- let closed_previous = fixture_order_event_ptr(&closed_previous_event_id);
- let closed_proposal = fixture_revision_proposal(
- "order-closed-preflight",
- &closed_root_event_id,
- &closed_previous_event_id,
- );
- let closed_plan = closed_sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- closed_root.clone(),
- fixture_order_decision("order-closed-preflight"),
- ))
- .expect("closed decision plan");
- let closed_proposal_plan = closed_sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- closed_root.clone(),
- closed_previous.clone(),
- closed_proposal.clone(),
- ))
- .expect("closed proposal plan");
- let closed_revision_plan = closed_sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- closed_root.clone(),
- closed_previous.clone(),
- fixture_revision_decision(&closed_proposal, &closed_previous_event_id),
- ))
- .expect("closed revision decision plan");
- let closed_cancellation_plan = closed_sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- closed_root,
- closed_previous,
- fixture_cancellation("order-closed-preflight"),
- ))
- .expect("closed cancellation plan");
- closed_sdk._event_store.pool().close().await;
- let closed_error = closed_sdk
- .trades()
- .enqueue_prepared_decision_with_explicit_signer(
- &seller,
- closed_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("closed preflight");
- assert!(matches!(closed_error, RadrootsSdkError::EventStore { .. }));
- let closed_proposal_error = closed_sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- closed_proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("closed proposal preflight");
- assert!(matches!(
- closed_proposal_error,
- RadrootsSdkError::EventStore { .. }
- ));
- let closed_revision_error = closed_sdk
- .trades()
- .enqueue_prepared_revision_decision_with_explicit_signer(
- &buyer,
- closed_revision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("closed revision preflight");
- assert!(matches!(
- closed_revision_error,
- RadrootsSdkError::EventStore { .. }
- ));
- let closed_cancellation_error = closed_sdk
- .trades()
- .enqueue_prepared_cancellation_with_explicit_signer(
- &buyer,
- closed_cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("closed cancellation preflight");
- assert!(matches!(
- closed_cancellation_error,
- RadrootsSdkError::EventStore { .. }
- ));
-}
-
-#[tokio::test]
-async fn configured_prepared_lifecycle_enqueues_run_preflight_guards() {
- let buyer = fixture_buyer_actor();
- let seller = fixture_seller_actor();
- let root_event_id = event_id('e');
- let previous_event_id = event_id('f');
- let root = fixture_order_event_ptr(&root_event_id);
- let previous = fixture_order_event_ptr(&previous_event_id);
- let proposal = fixture_revision_proposal(
- "order-configured-preflight",
- &root_event_id,
- &previous_event_id,
- );
-
- let seller_sdk = configured_order_sdk(SELLER_SECRET_KEY_HEX).await;
- let decision_plan = seller_sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- root.clone(),
- fixture_order_decision("order-configured-preflight"),
- ))
- .expect("decision plan");
- let decision_missing = seller_sdk
- .trades()
- .enqueue_prepared_decision(
- &seller,
- decision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await
- .expect_err("configured missing decision evidence");
- assert!(matches!(
- decision_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let proposal_plan = seller_sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- root.clone(),
- previous.clone(),
- proposal.clone(),
- ))
- .expect("proposal plan");
- let proposal_missing = seller_sdk
- .trades()
- .enqueue_prepared_revision_proposal(
- &seller,
- proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await
- .expect_err("configured missing proposal evidence");
- assert!(matches!(
- proposal_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let buyer_sdk = configured_order_sdk(BUYER_SECRET_KEY_HEX).await;
- let revision_plan = buyer_sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- root.clone(),
- previous.clone(),
- fixture_revision_decision(&proposal, &previous_event_id),
- ))
- .expect("revision decision plan");
- let revision_missing = buyer_sdk
- .trades()
- .enqueue_prepared_revision_decision(
- &buyer,
- revision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await
- .expect_err("configured missing revision decision evidence");
- assert!(matches!(
- revision_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-
- let cancellation_plan = buyer_sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- root,
- previous,
- fixture_cancellation("order-configured-preflight"),
- ))
- .expect("cancellation plan");
- let cancellation_missing = buyer_sdk
- .trades()
- .enqueue_prepared_cancellation(
- &buyer,
- cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await
- .expect_err("configured missing cancellation evidence");
- assert!(matches!(
- cancellation_missing,
- RadrootsSdkError::InvalidRequest { .. }
- ));
-}
-
-#[tokio::test]
-async fn configured_prepared_lifecycle_enqueues_report_existing_event_lookup_errors() {
- let buyer = fixture_buyer_actor();
- let seller = fixture_seller_actor();
- let root_event_id = event_id('e');
- let previous_event_id = event_id('f');
- let root = fixture_order_event_ptr(&root_event_id);
- let previous = fixture_order_event_ptr(&previous_event_id);
- let proposal = fixture_revision_proposal(
- "order-configured-existing-lookup",
- &root_event_id,
- &previous_event_id,
- );
-
- let seller_sdk = configured_order_sdk(SELLER_SECRET_KEY_HEX).await;
- let decision_plan = seller_sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- root.clone(),
- fixture_order_decision("order-configured-existing-lookup"),
- ))
- .expect("decision plan");
- let proposal_plan = seller_sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- root.clone(),
- previous.clone(),
- proposal.clone(),
- ))
- .expect("proposal plan");
- seller_sdk._event_store.pool().close().await;
- assert!(matches!(
- seller_sdk
- .trades()
- .enqueue_prepared_decision(
- &seller,
- decision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::EventStore { .. })
- ));
- assert!(matches!(
- seller_sdk
- .trades()
- .enqueue_prepared_revision_proposal(
- &seller,
- proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await,
- Err(RadrootsSdkError::EventStore { .. })
- ));
-
- let buyer_sdk = configured_order_sdk(BUYER_SECRET_KEY_HEX).await;
- let revision_plan = buyer_sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- root.clone(),
- previous.clone(),
- fixture_revision_decision(&proposal, &previous_event_id),
- ))
- .expect("revision decision plan");
- let cancellation_plan = buyer_sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- root,
- previous,
- fixture_cancellation("order-configured-existing-lookup"),
- ))
- .expect("cancellation plan");
- buyer_sdk._event_store.pool().close().await;
- assert!(matches!(
- buyer_sdk
- .trades()
- .enqueue_prepared_revision_decision(
- &buyer,
- revision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::EventStore { .. })
- ));
- assert!(matches!(
- buyer_sdk
- .trades()
- .enqueue_prepared_cancellation(
- &buyer,
- cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::EventStore { .. })
- ));
-}
-
-#[tokio::test]
-async fn configured_enqueue_wrappers_report_prepare_errors_before_signing() {
- let seller_sdk = configured_order_sdk(SELLER_SECRET_KEY_HEX).await;
- let buyer_sdk = configured_order_sdk(BUYER_SECRET_KEY_HEX).await;
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let proposal = fixture_revision_proposal(
- "order-configured-prepare-errors",
- &root_event_id,
- &previous_event_id,
- );
-
- assert!(matches!(
- buyer_sdk
- .trades()
- .enqueue_submit(TradeSubmitEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_event_ptr('a'),
- fixture_order_request("order-configured-prepare-submit"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await,
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- seller_sdk
- .trades()
- .enqueue_decision(TradeDecisionEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_event_ptr('a'),
- fixture_order_decision("order-configured-prepare-decision"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await,
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- seller_sdk
- .trades()
- .enqueue_revision_proposal(TradeRevisionProposalEnqueueRequest::new(
- fixture_buyer_actor(),
- fixture_order_event_ptr(&root_event_id),
- fixture_order_event_ptr(&previous_event_id),
- proposal.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await,
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- buyer_sdk
- .trades()
- .enqueue_revision_decision(TradeRevisionDecisionEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_order_event_ptr(&root_event_id),
- fixture_order_event_ptr(&previous_event_id),
- fixture_revision_decision(&proposal, &previous_event_id),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await,
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- buyer_sdk
- .trades()
- .enqueue_cancellation(TradeCancellationEnqueueRequest::new(
- fixture_seller_actor(),
- fixture_order_event_ptr(&root_event_id),
- fixture_order_event_ptr(&previous_event_id),
- fixture_cancellation("order-configured-prepare-cancel"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- ))
- .await,
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
-}
-
-#[tokio::test]
-async fn configured_prepared_methods_report_missing_configured_signer_after_preflight() {
- let sdk = prepared_order_sdk().await;
- let buyer = fixture_buyer_actor();
- let seller = fixture_seller_actor();
+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 submit_plan = sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer.clone(),
- fixture_event_ptr('a'),
- fixture_order_request("order-missing-configured-submit"),
- ))
- .expect("submit plan");
- assert!(matches!(
- sdk.trades()
- .enqueue_prepared_submit(
- &buyer,
- submit_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::SignerUnavailable { .. })
- ));
-
- let decision_submit = enqueue_fixture_submit(&sdk, "order-missing-configured-decision").await;
- let decision_plan = sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&decision_submit.signed_event_id),
- fixture_order_decision("order-missing-configured-decision"),
- ))
- .expect("decision plan");
- assert!(matches!(
- sdk.trades()
- .enqueue_prepared_decision(
- &seller,
- decision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::SignerUnavailable { .. })
- ));
-
- let proposal_submit = enqueue_fixture_submit(&sdk, "order-missing-configured-proposal").await;
- let proposal_payload = fixture_revision_proposal(
- "order-missing-configured-proposal",
- &proposal_submit.signed_event_id,
- &proposal_submit.signed_event_id,
- );
- let proposal_plan = sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- proposal_payload.clone(),
- ))
- .expect("proposal plan");
- assert!(matches!(
- sdk.trades()
- .enqueue_prepared_revision_proposal(
- &seller,
- proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::SignerUnavailable { .. })
- ));
-
- let revision_submit = enqueue_fixture_submit(&sdk, "order-missing-configured-revision").await;
- let revision_proposal_payload = fixture_revision_proposal(
- "order-missing-configured-revision",
- &revision_submit.signed_event_id,
- &revision_submit.signed_event_id,
- );
- let revision_proposal_plan = sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- revision_proposal_payload.clone(),
- ))
- .expect("revision proposal plan");
- let revision_proposal = sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- revision_proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("revision proposal evidence");
- let revision_plan = sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&revision_proposal.signed_event_id),
- fixture_revision_decision(
- &revision_proposal_payload,
- &revision_proposal.signed_event_id,
- ),
- ))
- .expect("revision plan");
- assert!(matches!(
- sdk.trades()
- .enqueue_prepared_revision_decision(
- &buyer,
- revision_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::SignerUnavailable { .. })
- ));
-
- let cancellation_submit = enqueue_fixture_submit(&sdk, "order-missing-configured-cancel").await;
- let cancellation_plan = sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_cancellation("order-missing-configured-cancel"),
- ))
- .expect("cancellation plan");
- assert!(matches!(
- sdk.trades()
- .enqueue_prepared_cancellation(
- &buyer,
- cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None
- )
- .await,
- Err(RadrootsSdkError::SignerUnavailable { .. })
- ));
-}
-
-#[tokio::test]
-async fn lifecycle_preflight_helpers_map_projection_query_failures() {
- let sdk = prepared_order_sdk().await;
- let client = sdk.trades();
- let buyer = fixture_buyer_actor();
- let seller = fixture_seller_actor();
- let root_event_id = event_id('e');
- let previous_event_id = event_id('f');
- let root = fixture_order_event_ptr(&root_event_id);
- let previous = fixture_order_event_ptr(&previous_event_id);
- let proposal = fixture_revision_proposal(
- "order-preflight-query-failure",
- &root_event_id,
- &previous_event_id,
- );
-
- let decision_plan = client
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- root.clone(),
- fixture_order_decision("order-preflight-query-failure"),
- ))
- .expect("decision plan");
- let proposal_plan = client
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller,
- root.clone(),
- previous.clone(),
- proposal.clone(),
- ))
- .expect("proposal plan");
- let revision_plan = client
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- root.clone(),
- previous.clone(),
- fixture_revision_decision(&proposal, &previous_event_id),
- ))
- .expect("revision plan");
- let cancellation_plan = client
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer,
- root,
- previous,
- fixture_cancellation("order-preflight-query-failure"),
- ))
- .expect("cancellation plan");
-
- sdk._event_store.pool().close().await;
- assert!(matches!(
- client.require_decision_preflight(&decision_plan).await,
- Err(RadrootsSdkError::Projection { .. })
- ));
- assert!(matches!(
- client
- .require_revision_proposal_preflight(&proposal_plan)
- .await,
- Err(RadrootsSdkError::Projection { .. })
- ));
- assert!(matches!(
- client
- .require_revision_decision_preflight(&revision_plan)
- .await,
- Err(RadrootsSdkError::Projection { .. })
- ));
- assert!(matches!(
- client
- .require_cancellation_preflight(&cancellation_plan)
- .await,
- Err(RadrootsSdkError::Projection { .. })
- ));
-}
-
-#[tokio::test]
-async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() {
- let proposal_sdk = prepared_order_sdk().await;
- let proposal_submit =
- enqueue_fixture_submit(&proposal_sdk, "order-closed-outbox-proposal").await;
- let seller = fixture_seller_actor();
- let proposal_payload = fixture_revision_proposal(
- "order-closed-outbox-proposal",
- &proposal_submit.signed_event_id,
- &proposal_submit.signed_event_id,
- );
- let proposal_plan = proposal_sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- proposal_payload,
- ))
- .expect("proposal plan");
- let proposal_events_before = local_event_count(&proposal_sdk).await;
- proposal_sdk._outbox.pool().close().await;
- let proposal_error = proposal_sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("closed outbox proposal");
- assert_outbox_preflight_error(proposal_error);
- assert_eq!(
- local_event_count(&proposal_sdk).await,
- proposal_events_before
- );
-
- let revision_sdk = prepared_order_sdk().await;
- let revision_submit =
- enqueue_fixture_submit(&revision_sdk, "order-closed-outbox-revision").await;
- let proposal_payload = fixture_revision_proposal(
- "order-closed-outbox-revision",
- &revision_submit.signed_event_id,
- &revision_submit.signed_event_id,
- );
- let proposal_plan = revision_sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- proposal_payload.clone(),
- ))
- .expect("revision proposal plan");
- let proposal = revision_sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue proposal");
- let buyer = fixture_buyer_actor();
- let revision_plan = revision_sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&proposal.signed_event_id),
- fixture_revision_decision(&proposal_payload, &proposal.signed_event_id),
- ))
- .expect("revision plan");
- let revision_events_before = local_event_count(&revision_sdk).await;
- revision_sdk._outbox.pool().close().await;
- let revision_error = revision_sdk
- .trades()
- .enqueue_prepared_revision_decision_with_explicit_signer(
- &buyer,
- revision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("closed outbox revision");
- assert_outbox_preflight_error(revision_error);
- assert_eq!(
- local_event_count(&revision_sdk).await,
- revision_events_before
- );
-
- let cancellation_sdk = prepared_order_sdk().await;
- let cancellation_submit =
- enqueue_fixture_submit(&cancellation_sdk, "order-closed-outbox-cancellation").await;
- let cancellation_plan = cancellation_sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_cancellation("order-closed-outbox-cancellation"),
- ))
- .expect("cancellation plan");
- let cancellation_events_before = local_event_count(&cancellation_sdk).await;
- cancellation_sdk._outbox.pool().close().await;
- let cancellation_error = cancellation_sdk
- .trades()
- .enqueue_prepared_cancellation_with_explicit_signer(
- &buyer,
- cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect_err("closed outbox cancellation");
- assert_outbox_preflight_error(cancellation_error);
- assert_eq!(
- local_event_count(&cancellation_sdk).await,
- cancellation_events_before
- );
-}
-
-#[tokio::test]
-async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() {
- let decision_sdk = configured_order_sdk(SELLER_SECRET_KEY_HEX).await;
- let decision_submit = enqueue_fixture_submit(&decision_sdk, "order-existing-decision").await;
- let seller = fixture_seller_actor();
- let decision_plan = decision_sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&decision_submit.signed_event_id),
- fixture_order_decision("order-existing-decision"),
- ))
- .expect("decision plan");
- decision_sdk
- .trades()
- .enqueue_prepared_decision_with_explicit_signer(
- &seller,
- decision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue decision");
- let decision_repeat = decision_sdk
- .trades()
- .enqueue_prepared_decision_with_explicit_signer(
- &seller,
- decision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("repeat decision replay");
+ let zero = validate_validation_receipt_limit(0).expect_err("zero limit");
assert_eq!(
- decision_repeat.workflow.state,
- SdkMutationState::AlreadyQueued
- );
- assert!(
- decision_repeat
- .workflow
- .idempotency
- .replayed_existing_operation
- );
- let configured_decision_repeat = decision_sdk
- .trades()
- .enqueue_prepared_decision(
- &seller,
- decision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await
- .expect("configured decision replay");
- assert_eq!(
- configured_decision_repeat.workflow.state,
- SdkMutationState::AlreadyQueued
- );
- assert!(
- configured_decision_repeat
- .workflow
- .idempotency
- .replayed_existing_operation
- );
-
- let proposal_sdk = configured_order_sdk(SELLER_SECRET_KEY_HEX).await;
- let proposal_submit = enqueue_fixture_submit(&proposal_sdk, "order-existing-proposal").await;
- let proposal_payload = fixture_revision_proposal(
- "order-existing-proposal",
- &proposal_submit.signed_event_id,
- &proposal_submit.signed_event_id,
- );
- let proposal_plan = proposal_sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- fixture_order_event_ptr(&proposal_submit.signed_event_id),
- proposal_payload,
- ))
- .expect("proposal plan");
- proposal_sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- proposal_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue proposal");
- let proposal_repeat = proposal_sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- proposal_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("repeat proposal replay");
- assert_eq!(
- proposal_repeat.workflow.state,
- SdkMutationState::AlreadyQueued
- );
- assert!(
- proposal_repeat
- .workflow
- .idempotency
- .replayed_existing_operation
- );
- let configured_proposal_repeat = proposal_sdk
- .trades()
- .enqueue_prepared_revision_proposal(
- &seller,
- proposal_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await
- .expect("configured proposal replay");
- assert_eq!(
- configured_proposal_repeat.workflow.state,
- SdkMutationState::AlreadyQueued
- );
- assert!(
- configured_proposal_repeat
- .workflow
- .idempotency
- .replayed_existing_operation
+ zero.to_string(),
+ format!("sdk order status limit invalid: limit=0, min=1, max={TRADE_STATUS_MAX_LIMIT}")
);
- let revision_sdk = configured_order_sdk(BUYER_SECRET_KEY_HEX).await;
- let revision_submit = enqueue_fixture_submit(&revision_sdk, "order-existing-revision").await;
- let proposal_payload = fixture_revision_proposal(
- "order-existing-revision",
- &revision_submit.signed_event_id,
- &revision_submit.signed_event_id,
- );
- let proposal_plan = revision_sdk
- .trades()
- .prepare_revision_proposal(TradeRevisionProposalPrepareRequest::new(
- seller.clone(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- proposal_payload.clone(),
- ))
- .expect("revision proposal plan");
- let proposal = revision_sdk
- .trades()
- .enqueue_prepared_revision_proposal_with_explicit_signer(
- &seller,
- proposal_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue proposal");
- let buyer = fixture_buyer_actor();
- let revision_plan = revision_sdk
- .trades()
- .prepare_revision_decision(TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&revision_submit.signed_event_id),
- fixture_order_event_ptr(&proposal.signed_event_id),
- fixture_revision_decision(&proposal_payload, &proposal.signed_event_id),
- ))
- .expect("revision plan");
- revision_sdk
- .trades()
- .enqueue_prepared_revision_decision_with_explicit_signer(
- &buyer,
- revision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue revision");
- let revision_repeat = revision_sdk
- .trades()
- .enqueue_prepared_revision_decision_with_explicit_signer(
- &buyer,
- revision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("repeat revision replay");
- assert_eq!(
- revision_repeat.workflow.state,
- SdkMutationState::AlreadyQueued
- );
- assert!(
- revision_repeat
- .workflow
- .idempotency
- .replayed_existing_operation
- );
- let configured_revision_repeat = revision_sdk
- .trades()
- .enqueue_prepared_revision_decision(
- &buyer,
- revision_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- )
- .await
- .expect("configured revision replay");
+ let over_max =
+ validate_validation_receipt_limit(TRADE_STATUS_MAX_LIMIT + 1).expect_err("over max");
assert_eq!(
- configured_revision_repeat.workflow.state,
- SdkMutationState::AlreadyQueued
- );
- assert!(
- configured_revision_repeat
- .workflow
- .idempotency
- .replayed_existing_operation
- );
-
- let cancellation_sdk = configured_order_sdk(BUYER_SECRET_KEY_HEX).await;
- let cancellation_submit =
- enqueue_fixture_submit(&cancellation_sdk, "order-existing-cancellation").await;
- let cancellation_plan = cancellation_sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer.clone(),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_order_event_ptr(&cancellation_submit.signed_event_id),
- fixture_cancellation("order-existing-cancellation"),
- ))
- .expect("cancellation plan");
- cancellation_sdk
- .trades()
- .enqueue_prepared_cancellation_with_explicit_signer(
- &buyer,
- cancellation_plan.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await
- .expect("enqueue cancellation");
- let cancellation_repeat = cancellation_sdk
- .trades()
- .enqueue_prepared_cancellation(
- &buyer,
- cancellation_plan,
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- None,
+ over_max.to_string(),
+ format!(
+ "sdk order status limit invalid: limit={}, min=1, max={TRADE_STATUS_MAX_LIMIT}",
+ TRADE_STATUS_MAX_LIMIT + 1
)
- .await
- .expect("configured cancellation replay");
- assert_eq!(
- cancellation_repeat.workflow.state,
- SdkMutationState::AlreadyQueued
- );
- assert!(
- cancellation_repeat
- .workflow
- .idempotency
- .replayed_existing_operation
- );
-}
-
-#[tokio::test]
-async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() {
- let sdk = prepared_order_sdk().await;
- let out_of_range = RadrootsSdkTimestamp::from_unix_seconds(u64::MAX);
- let buyer = fixture_buyer_actor();
- let seller = fixture_seller_actor();
-
- assert!(matches!(
- sdk.trades()
- .ingest_evidence(
- TradeEvidenceIngestRequest::new(request_signed_event())
- .with_observed_at(out_of_range,)
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk.trades()
- .ingest_request_evidence(
- TradeRequestEvidenceIngestRequest::new(request_signed_event())
- .with_observed_at(out_of_range,),
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk.trades()
- .enqueue_submit_with_explicit_signer(
- TradeSubmitEnqueueRequest::new(
- buyer.clone(),
- fixture_event_ptr('a'),
- fixture_order_request("order-wrapper-submit-error"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_created_at(out_of_range),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk.trades()
- .enqueue_decision_with_explicit_signer(
- TradeDecisionEnqueueRequest::new(
- seller.clone(),
- fixture_event_ptr('b'),
- fixture_order_decision("order-wrapper-decision-error"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_created_at(out_of_range),
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
-
- let root_event_id = event_id('c');
- let previous_event_id = event_id('d');
- let proposal_payload = fixture_revision_proposal(
- "order-wrapper-proposal-error",
- &root_event_id,
- &previous_event_id,
- );
- assert!(matches!(
- sdk.trades()
- .enqueue_revision_proposal_with_explicit_signer(
- TradeRevisionProposalEnqueueRequest::new(
- seller.clone(),
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- proposal_payload.clone(),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_created_at(out_of_range),
- &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX),
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk.trades()
- .enqueue_revision_decision_with_explicit_signer(
- TradeRevisionDecisionEnqueueRequest::new(
- buyer.clone(),
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- fixture_revision_decision(&proposal_payload, &previous_event_id),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_created_at(out_of_range),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk.trades()
- .enqueue_cancellation_with_explicit_signer(
- TradeCancellationEnqueueRequest::new(
- buyer,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- fixture_cancellation("order-wrapper-cancellation-error"),
- fixture_target_policy(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .with_created_at(out_of_range),
- &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX),
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
-}
-
-#[tokio::test]
-async fn order_default_timestamp_paths_report_clock_errors() {
- let clock_error_sdk = crate::RadrootsClient::builder()
- .clock(crate::RadrootsSdkClock::BeforeUnixEpoch)
- .build()
- .await
- .expect("sdk");
- let buyer = buyer_actor();
- let seller = seller_actor();
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let proposal = revision_proposal_payload(&root_event_id, &previous_event_id);
-
- assert!(matches!(
- clock_error_sdk
- .trades()
- .ingest_evidence(TradeEvidenceIngestRequest::new(request_signed_event()))
- .await,
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
- assert!(matches!(
- clock_error_sdk
- .trades()
- .ingest_request_evidence(TradeRequestEvidenceIngestRequest::new(
- request_signed_event()
- ))
- .await,
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
- assert!(matches!(
- clock_error_sdk
- .trades()
- .prepare_submit(TradeSubmitPrepareRequest::new(
- buyer.clone(),
- ptr(root_event_id.as_str().to_owned()),
- order_request_payload(),
- )),
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
- assert!(matches!(
- clock_error_sdk
- .trades()
- .prepare_decision(TradeDecisionPrepareRequest::new(
- seller.clone(),
- ptr(root_event_id.as_str().to_owned()),
- order_decision_payload(),
- )),
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
- assert!(matches!(
- clock_error_sdk.trades().prepare_revision_proposal(
- TradeRevisionProposalPrepareRequest::new(
- seller,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- proposal.clone(),
- ),
- ),
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
- assert!(matches!(
- clock_error_sdk.trades().prepare_revision_decision(
- TradeRevisionDecisionPrepareRequest::new(
- buyer.clone(),
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- revision_decision_payload(
- &proposal,
- &previous_event_id,
- RadrootsOrderRevisionOutcome::Accepted,
- ),
- ),
- ),
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
- assert!(matches!(
- clock_error_sdk
- .trades()
- .prepare_cancellation(TradeCancellationPrepareRequest::new(
- buyer,
- ptr(root_event_id.as_str().to_owned()),
- ptr(previous_event_id.as_str().to_owned()),
- cancellation_payload(),
- )),
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
-}
-
-#[test]
-fn order_runtime_request_builders_and_serializers_cover_source_attached_paths() {
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_321);
- let root_event_id = event_id('a');
- let previous_event_id = event_id('b');
- let root_event = ptr(root_event_id.as_str().to_owned());
- let previous_event = ptr(previous_event_id.as_str().to_owned());
- let proposal = revision_proposal_payload(&root_event_id, &previous_event_id);
- let revision_decision = revision_decision_payload(
- &proposal,
- &event_id('c'),
- RadrootsOrderRevisionOutcome::Declined {
- reason: "not workable".to_owned(),
- },
- );
- let policy = TargetPolicy::default_profile();
-
- let submit_prepare =
- TradeSubmitPrepareRequest::new(buyer_actor(), root_event.clone(), order_request_payload())
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&submit_prepare, 4);
- assert_eq!(
- serde_json::to_value(&submit_prepare).expect("submit prepare json")["created_at"],
- 1_700_000_321
- );
-
- let submit_enqueue = TradeSubmitEnqueueRequest::new(
- buyer_actor(),
- root_event.clone(),
- order_request_payload(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(["wss://relay-a.radroots.test"], NostrRelayUrlPolicy::Public)
- .expect("submit relays")
- .with_idempotency_key(SdkIdempotencyKey::new("submit-unit-key").expect("key"))
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&submit_enqueue, 6);
-
- let request_ingest =
- TradeRequestEvidenceIngestRequest::new(request_signed_event()).with_observed_at(created_at);
- assert_struct_serialize_error_paths(&request_ingest, 2);
- let evidence_ingest =
- TradeEvidenceIngestRequest::new(request_signed_event()).with_observed_at(created_at);
- assert_struct_serialize_error_paths(&evidence_ingest, 2);
-
- let decision_prepare = TradeDecisionPrepareRequest::new(
- seller_actor(),
- root_event.clone(),
- order_decision_payload(),
- )
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&decision_prepare, 4);
-
- let decision_enqueue = TradeDecisionEnqueueRequest::new(
- seller_actor(),
- root_event.clone(),
- order_decision_payload(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(["wss://relay-b.radroots.test"], NostrRelayUrlPolicy::Public)
- .expect("decision relays")
- .with_idempotency_key(SdkIdempotencyKey::new("decision-unit-key").expect("key"))
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&decision_enqueue, 6);
-
- let proposal_prepare = TradeRevisionProposalPrepareRequest::new(
- seller_actor(),
- root_event.clone(),
- previous_event.clone(),
- proposal.clone(),
- )
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&proposal_prepare, 5);
-
- let proposal_enqueue = TradeRevisionProposalEnqueueRequest::new(
- seller_actor(),
- root_event.clone(),
- previous_event.clone(),
- proposal.clone(),
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(["wss://relay-c.radroots.test"], NostrRelayUrlPolicy::Public)
- .expect("proposal relays")
- .with_idempotency_key(SdkIdempotencyKey::new("proposal-unit-key").expect("key"))
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&proposal_enqueue, 7);
-
- let revision_decision_prepare = TradeRevisionDecisionPrepareRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- revision_decision.clone(),
- )
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&revision_decision_prepare, 5);
-
- let revision_decision_enqueue = TradeRevisionDecisionEnqueueRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- revision_decision,
- policy.clone(),
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(["wss://relay-d.radroots.test"], NostrRelayUrlPolicy::Public)
- .expect("revision decision relays")
- .with_idempotency_key(SdkIdempotencyKey::new("revision-decision-unit-key").expect("key"))
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&revision_decision_enqueue, 7);
-
- let cancellation_prepare = TradeCancellationPrepareRequest::new(
- buyer_actor(),
- root_event.clone(),
- previous_event.clone(),
- cancellation_payload(),
- )
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&cancellation_prepare, 5);
-
- let cancellation_enqueue = TradeCancellationEnqueueRequest::new(
- buyer_actor(),
- root_event,
- previous_event,
- cancellation_payload(),
- policy,
- PublishMode::EnqueueOnly,
- SatisfactionPolicy::NoWait,
- )
- .try_with_nostr_targets(["wss://relay-e.radroots.test"], NostrRelayUrlPolicy::Public)
- .expect("cancellation relays")
- .with_idempotency_key(SdkIdempotencyKey::new("cancellation-unit-key").expect("key"))
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&cancellation_enqueue, 7);
-
- let parsed_status = TradeStatusRequest::parse(order_id().as_str())
- .expect("status request")
- .with_limit(TRADE_STATUS_DEFAULT_LIMIT);
- parsed_status.validate().expect("status validates");
- assert_eq!(
- serde_json::to_value(&parsed_status).expect("status json")["limit"],
- TRADE_STATUS_DEFAULT_LIMIT
);
-
- let issue =
- SdkTradeStatusIssue::single(SdkTradeStatusIssueKind::ForkedLifecycle, event_id('f'));
- assert_eq!(issue.code(), "forked_lifecycle");
- assert_struct_serialize_error_paths(&issue, 3);
}
#[test]
-fn relay_outcome_labels_cover_resync_and_validation_receipt_variants() {
+fn relay_outcome_labels_cover_current_variants() {
for (kind, label) in [
(TradeResyncNostrRelayOutcomeKind::Eose, "eose"),
(TradeResyncNostrRelayOutcomeKind::Closed, "closed"),
@@ -4242,6 +33,20 @@ fn relay_outcome_labels_cover_resync_and_validation_receipt_variants() {
}
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",
@@ -4309,20 +114,6 @@ fn relay_outcome_labels_cover_resync_and_validation_receipt_variants() {
}
for (kind, label) in [
- (TradeValidationReceiptNostrRelayOutcomeKind::Eose, "eose"),
- (
- TradeValidationReceiptNostrRelayOutcomeKind::Closed,
- "closed",
- ),
- (
- TradeValidationReceiptNostrRelayOutcomeKind::Notice,
- "notice",
- ),
- ] {
- assert_eq!(kind.as_str(), label);
- }
-
- for (kind, label) in [
(
TradeValidationReceiptNostrRelayTransportOutcomeKind::Accepted,
"accepted",
@@ -4396,72 +187,12 @@ fn relay_outcome_labels_cover_resync_and_validation_receipt_variants() {
}
}
-#[tokio::test]
-async fn closed_event_store_errors_are_mapped_for_ingest_and_prepared_lookup() {
- let sdk = crate::RadrootsClient::builder().build().await.expect("sdk");
- sdk._event_store.pool().close().await;
- let ingest_error = sdk
- .trades()
- .ingest_evidence(TradeEvidenceIngestRequest::new(request_signed_event()))
- .await
- .expect_err("closed ingest evidence");
- assert!(matches!(ingest_error, RadrootsSdkError::EventStore { .. }));
-
- let sdk = crate::RadrootsClient::builder().build().await.expect("sdk");
- sdk._event_store.pool().close().await;
- let request_ingest_error = sdk
- .trades()
- .ingest_request_evidence(TradeRequestEvidenceIngestRequest::new(
- request_signed_event(),
- ))
- .await
- .expect_err("closed request evidence ingest");
- assert!(matches!(
- request_ingest_error,
- RadrootsSdkError::EventStore { .. }
- ));
-
- let sdk = crate::RadrootsClient::builder().build().await.expect("sdk");
- sdk._event_store.pool().close().await;
- let lookup_error = sdk
- .trades()
- .prepared_order_event_exists(&event_id('a'))
- .await
- .expect_err("closed prepared lookup");
- assert!(matches!(lookup_error, RadrootsSdkError::EventStore { .. }));
-}
-
-#[tokio::test]
-async fn order_status_and_evidence_ingest_cover_source_attached_success_paths() {
- let sdk = prepared_order_sdk().await;
- let request_event = request_signed_event();
- let request_receipt = sdk
- .trades()
- .ingest_request_evidence(TradeRequestEvidenceIngestRequest::new(
- request_event.clone(),
- ))
- .await
- .expect("request evidence ingest");
- assert!(request_receipt.inserted);
- assert_eq!(request_receipt.order_id, order_id());
-
- let submit = enqueue_fixture_submit(&sdk, "order-status-source-attached").await;
- let status = sdk
- .trades()
- .status(TradeStatusRequest::parse(submit.order_id.as_str()).expect("status request"))
- .await
- .expect("order status");
- assert_eq!(status.status, TradeStatusKind::Requested);
- assert!(status.evidence.has_request);
-
- let duplicate_receipt = sdk
- .trades()
- .ingest_evidence(TradeEvidenceIngestRequest::new(request_event))
- .await
- .expect("order evidence ingest");
- assert!(!duplicate_receipt.inserted);
+#[test]
+fn camel_to_snake_converts_debug_case_labels() {
+ assert_eq!(camel_to_snake("AwaitValidation"), "await_validation");
assert_eq!(
- duplicate_receipt.local_event_seq,
- request_receipt.local_event_seq
+ camel_to_snake("InspectEvidenceIssues"),
+ "inspect_evidence_issues"
);
+ assert_eq!(camel_to_snake("Terminal"), "terminal");
}
diff --git a/crates/sdk/tests/unit/private_store_tests.rs b/crates/sdk/tests/unit/private_store_tests.rs
@@ -194,7 +194,7 @@ async fn private_store_status_update_delete_and_pragmas_round_trip() {
}
#[tokio::test]
-async fn private_store_file_open_materializes_label_column_for_existing_stores() {
+async fn private_store_file_open_rejects_pre_label_schema_without_repair() {
let tempdir = tempfile::tempdir().expect("tempdir");
let path = tempdir.path().join("private.sqlite");
let options = SqliteConnectOptions::new()
@@ -231,18 +231,8 @@ async fn private_store_file_open_materializes_label_column_for_existing_stores()
let store = SdkPrivateStore::open_file(&path).await.expect("open store");
let record = private_location_record();
- store
- .upsert_farm_location(&record)
- .await
- .expect("upsert labeled location");
- assert_eq!(
- store
- .farm_location(&record.farm_addr)
- .await
- .expect("read labeled location")
- .expect("stored location")
- .label
- .as_deref(),
- Some("Main pickup point")
- );
+ assert!(matches!(
+ store.upsert_farm_location(&record).await,
+ Err(RadrootsSdkError::PrivateStore { .. })
+ ));
}
diff --git a/crates/sdk/tests/unit/runtime_tests.rs b/crates/sdk/tests/unit/runtime_tests.rs
@@ -39,6 +39,21 @@ fn assert_private_store_error<T>(result: Result<T, RadrootsSdkError>) {
}
}
+fn assert_studio_store_error<T>(result: Result<T, RadrootsSdkError>) {
+ match result {
+ Err(RadrootsSdkError::StudioStore { .. }) => {}
+ Err(other) => panic!("expected studio store error, got {other:?}"),
+ Ok(_) => panic!("expected studio store error"),
+ }
+}
+
+fn assert_unsupported_profile_error<T>(result: Result<T, RadrootsSdkError>) -> PathBuf {
+ match result.err().expect("expected unsupported profile error") {
+ RadrootsSdkError::UnsupportedProfileSchema { path, .. } => path,
+ other => panic!("expected unsupported profile error, got {other:?}"),
+ }
+}
+
fn sqlite_status() -> SdkSqliteStoreStatus {
SdkSqliteStoreStatus {
schema_version: 1,
@@ -108,6 +123,10 @@ fn storage_status() -> StorageStatusReceipt {
store: private_sqlite_status(),
farm_private_locations: 0,
},
+ studio_store: SdkStudioStoreStorageStatus {
+ store: sqlite_status(),
+ studio_state_records: 0,
+ },
}
}
@@ -116,9 +135,11 @@ fn verification(event_store_ok: bool, outbox_ok: bool) -> SdkBackupVerification
event_store_ok,
outbox_ok,
private_store_ok: true,
+ studio_store_ok: true,
event_store_events: 0,
outbox_events: 0,
private_farm_locations: 0,
+ studio_state_records: 0,
}
}
@@ -154,9 +175,9 @@ fn manifest() -> SdkBackupManifest {
source_storage: SdkStorageKind::Memory,
source_paths: None,
backup_paths: RadrootsSdkStoragePaths {
- event_store_path: PathBuf::from(EVENT_STORE_BACKUP_FILE),
- outbox_path: PathBuf::from(OUTBOX_BACKUP_FILE),
- private_store_path: PathBuf::from(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: PathBuf::from(RUNTIME_SQLITE_FILE),
+ studio_path: PathBuf::from(STUDIO_SQLITE_FILE),
+ private_path: PathBuf::from(PRIVATE_SQLITE_FILE),
},
source_status: storage_status(),
backup_verification: verification(true, true),
@@ -294,6 +315,7 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
_event_store: memory.event_store,
_outbox: memory.outbox,
_private_store: memory.private_store,
+ _studio_store: memory.studio_store,
storage_paths: None,
geonames: None,
clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)),
@@ -309,13 +331,14 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
.await
.expect("directory storage");
let directory_paths = directory_storage.paths.expect("directory paths");
- assert!(directory_paths.event_store_path.exists());
- assert!(directory_paths.outbox_path.exists());
- assert!(directory_paths.private_store_path.exists());
+ assert!(directory_paths.runtime_path.exists());
+ assert!(directory_paths.private_path.exists());
+ assert!(directory_paths.studio_path.exists());
let directory_sdk = RadrootsClient {
_event_store: directory_storage.event_store,
_outbox: directory_storage.outbox,
_private_store: directory_storage.private_store,
+ _studio_store: directory_storage.studio_store,
storage_paths: Some(directory_paths),
geonames: None,
clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)),
@@ -331,18 +354,18 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
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(EVENT_STORE_BACKUP_FILE))
+ 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);
- let outbox_directory = tempdir.path().join("outbox-directory");
- fs::create_dir(&outbox_directory).expect("outbox dir");
- fs::create_dir(outbox_directory.join(OUTBOX_BACKUP_FILE)).expect("outbox file slot dir");
- assert_outbox_error(open_directory_storage(&outbox_directory).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);
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_STORE_BACKUP_FILE))
+ 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);
}
@@ -565,6 +588,74 @@ fn sqlite_wal_checkpoint_receipt_mapping_covers_edge_states() {
}
#[tokio::test]
+async fn storage_status_inspection_is_read_only_and_never_creates_missing_profiles() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let missing = tempdir.path().join("missing-profile");
+ assert_event_store_error(
+ RadrootsClient::inspect_storage_status(&missing, StorageStatusRequest::new()).await,
+ );
+ assert!(!missing.exists());
+
+ let pre_v1_profile = tempdir.path().join("pre-v1");
+ fs::create_dir(&pre_v1_profile).expect("pre-v1 profile");
+ fs::write(pre_v1_profile.join("event_store.sqlite"), b"old runtime").expect("old event store");
+ let unsupported = assert_unsupported_profile_error(
+ RadrootsClient::inspect_storage_status(&pre_v1_profile, StorageStatusRequest::new()).await,
+ );
+ assert_eq!(unsupported, pre_v1_profile.join("event_store.sqlite"));
+ assert!(!pre_v1_profile.join(RUNTIME_SQLITE_FILE).exists());
+ assert!(!pre_v1_profile.join(PRIVATE_SQLITE_FILE).exists());
+ assert!(!pre_v1_profile.join(STUDIO_SQLITE_FILE).exists());
+}
+
+#[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;
@@ -595,13 +686,13 @@ async fn storage_status_integrity_and_backup_map_closed_pool_errors() {
outbox_closed
.storage_status(StorageStatusRequest::new())
.await,
- Err(RadrootsSdkError::Outbox { .. })
+ Err(RadrootsSdkError::EventStore { .. })
));
assert_outbox_error(outbox_sqlite_status(&outbox_closed._outbox).await);
assert_outbox_error(outbox_status_summary(&outbox_closed._outbox, 1).await);
assert!(matches!(
outbox_closed.integrity(IntegrityRequest::new()).await,
- Err(RadrootsSdkError::Outbox { .. })
+ Err(RadrootsSdkError::EventStore { .. })
));
let private_store_closed = RadrootsClient::builder().build().await.expect("sdk");
@@ -709,19 +800,19 @@ async fn storage_status_integrity_and_backup_map_closed_pool_errors() {
let event_store = RadrootsEventStore::open_memory()
.await
.expect("event store");
- let outbox = RadrootsOutbox::open_memory().await.expect("outbox");
let private_store = SdkPrivateStore::open_memory().await.expect("private store");
+ let studio_store = SdkStudioStore::open_memory().await.expect("studio store");
private_store.pool().close().await;
let tempdir = tempfile::tempdir().expect("tempdir");
assert_private_store_error(
backup_sqlite_stores(
event_store.pool(),
- outbox.pool(),
private_store.pool(),
+ studio_store.pool(),
&RadrootsSdkStoragePaths {
- event_store_path: tempdir.path().join(EVENT_STORE_BACKUP_FILE),
- outbox_path: tempdir.path().join(OUTBOX_BACKUP_FILE),
- private_store_path: tempdir.path().join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: tempdir.path().join(RUNTIME_SQLITE_FILE),
+ studio_path: tempdir.path().join(STUDIO_SQLITE_FILE),
+ private_path: tempdir.path().join(PRIVATE_SQLITE_FILE),
},
)
.await,
@@ -742,26 +833,26 @@ async fn verify_backup_paths_reports_each_store_member_failure() {
.await
.expect("backup");
- let bad_event_store_path = backup_destination.join("bad-event-store.sqlite");
- fs::create_dir(&bad_event_store_path).expect("bad event store dir");
- let bad_outbox_path = backup_destination.join("bad-outbox.sqlite");
- fs::create_dir(&bad_outbox_path).expect("bad outbox dir");
- let bad_private_store_path = backup_destination.join("bad-private.sqlite");
- fs::create_dir(&bad_private_store_path).expect("bad private store dir");
+ let bad_runtime_path = backup_destination.join("bad-event-store.sqlite");
+ fs::create_dir(&bad_runtime_path).expect("bad event store dir");
+ let bad_studio_path = backup_destination.join("bad-studio.sqlite");
+ fs::create_dir(&bad_studio_path).expect("bad studio dir");
+ let bad_private_path = backup_destination.join("bad-private.sqlite");
+ fs::create_dir(&bad_private_path).expect("bad private store dir");
let mut invalid_member = RadrootsSdkStoragePaths {
- event_store_path: bad_event_store_path,
- outbox_path: backup_destination.join(OUTBOX_BACKUP_FILE),
- private_store_path: backup_destination.join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: bad_runtime_path,
+ studio_path: backup_destination.join(STUDIO_SQLITE_FILE),
+ private_path: backup_destination.join(PRIVATE_SQLITE_FILE),
};
assert_event_store_error(verify_backup_paths(&invalid_member).await);
- invalid_member.event_store_path = backup_destination.join(EVENT_STORE_BACKUP_FILE);
- invalid_member.outbox_path = bad_outbox_path;
- assert_outbox_error(verify_backup_paths(&invalid_member).await);
+ invalid_member.runtime_path = backup_destination.join(RUNTIME_SQLITE_FILE);
+ invalid_member.studio_path = bad_studio_path;
+ assert_studio_store_error(verify_backup_paths(&invalid_member).await);
- invalid_member.outbox_path = backup_destination.join(OUTBOX_BACKUP_FILE);
- invalid_member.private_store_path = bad_private_store_path;
+ invalid_member.studio_path = backup_destination.join(STUDIO_SQLITE_FILE);
+ invalid_member.private_path = bad_private_path;
assert_private_store_error(verify_backup_paths(&invalid_member).await);
}
@@ -771,16 +862,14 @@ fn restore_archive_path_validators_cover_missing_outside_and_manifest_edges() {
let source = tempdir.path().join("source");
fs::create_dir(&source).expect("source");
let source_root = canonical_restore_directory(&source).expect("canonical source");
- let file_member = source.join(EVENT_STORE_BACKUP_FILE);
+ let file_member = source.join(RUNTIME_SQLITE_FILE);
fs::write(&file_member, b"sqlite").expect("file member");
let outside_file = tempdir.path().join("outside.sqlite");
fs::write(&outside_file, b"sqlite").expect("outside file");
let dir_member = source.join("dir-member");
fs::create_dir(&dir_member).expect("dir member");
- assert!(
- validate_relative_archive_path(Path::new(EVENT_STORE_BACKUP_FILE), "event store").is_ok()
- );
+ assert!(validate_relative_archive_path(Path::new(RUNTIME_SQLITE_FILE), "event store").is_ok());
assert!(
invalid_request_message(validate_relative_archive_path(Path::new(""), "event store"))
.contains("must not be empty")
@@ -818,12 +907,8 @@ fn restore_archive_path_validators_cover_missing_outside_and_manifest_edges() {
.contains("No such")
);
assert!(
- restore_archive_member_path(
- &source_root,
- Path::new(EVENT_STORE_BACKUP_FILE),
- "event store",
- )
- .is_ok()
+ restore_archive_member_path(&source_root, Path::new(RUNTIME_SQLITE_FILE), "event store",)
+ .is_ok()
);
assert!(
invalid_request_message(restore_archive_member_path(
@@ -886,16 +971,16 @@ fn restore_destination_preflight_covers_empty_existing_new_and_overlap_paths() {
let paths =
preflight_restore_destination(&source, &new_destination, false).expect("new preflight");
assert_eq!(
- paths.event_store_path,
- new_destination.join(EVENT_STORE_BACKUP_FILE)
+ paths.runtime_path,
+ new_destination.join(RUNTIME_SQLITE_FILE)
);
let nested_new_destination = parent.join("nested").join("new-destination");
fs::create_dir(nested_new_destination.parent().expect("nested parent")).expect("nested parent");
let nested_paths = preflight_restore_destination(&source, &nested_new_destination, false)
.expect("nested new preflight");
assert_eq!(
- nested_paths.private_store_path,
- nested_new_destination.join(PRIVATE_STORE_BACKUP_FILE)
+ nested_paths.private_path,
+ nested_new_destination.join(PRIVATE_SQLITE_FILE)
);
let relative_destination = PathBuf::from(format!(
"relative-restore-{}",
@@ -904,8 +989,8 @@ fn restore_destination_preflight_covers_empty_existing_new_and_overlap_paths() {
let relative_paths = preflight_restore_destination(&source, &relative_destination, false)
.expect("relative preflight");
assert_eq!(
- relative_paths.event_store_path,
- relative_destination.join(EVENT_STORE_BACKUP_FILE)
+ relative_paths.runtime_path,
+ relative_destination.join(RUNTIME_SQLITE_FILE)
);
let file_source = tempdir.path().join("file-source");
@@ -1064,9 +1149,9 @@ fn backup_destination_and_restore_file_helpers_cover_cleanup_and_io_edges() {
io_message(copy_restore_file(
&tempdir.path().join("missing-source"),
&tempdir.path().join("copy-destination"),
- "event store",
+ "runtime store",
))
- .contains("restore event store copy failed")
+ .contains("restore runtime store copy failed")
);
assert!(
io_message(rename_restore_path(
@@ -1310,42 +1395,41 @@ async fn sqlite_backup_errors_cover_invalid_paths_and_execute_failures() {
SqliteStoreRole::EventStore,
)
.await
- .err()
- .expect("sqlite error");
+ .expect_err("sqlite error");
assert!(matches!(
error,
RadrootsSdkError::EventStore { message } if message.contains("backup failed")
));
let backup_paths = RadrootsSdkStoragePaths {
- event_store_path: tempdir.path().join("closed-event-store-backup.sqlite"),
- outbox_path: tempdir.path().join("closed-event-store-outbox.sqlite"),
- private_store_path: tempdir.path().join("closed-event-store-private.sqlite"),
+ runtime_path: tempdir.path().join("closed-event-store-backup.sqlite"),
+ studio_path: tempdir.path().join("closed-event-store-outbox.sqlite"),
+ private_path: tempdir.path().join("closed-event-store-private.sqlite"),
};
assert_event_store_error(
backup_sqlite_stores(
storage.event_store.pool(),
- storage.outbox.pool(),
storage.private_store.pool(),
+ storage.studio_store.pool(),
&backup_paths,
)
.await,
);
- let outbox_closed_storage = open_storage(&RadrootsSdkStorageConfig::Memory)
+ let studio_closed_storage = open_storage(&RadrootsSdkStorageConfig::Memory)
.await
- .expect("outbox closed storage");
- outbox_closed_storage.outbox.pool().close().await;
- let outbox_closed_paths = RadrootsSdkStoragePaths {
- event_store_path: tempdir.path().join("open-event-store-backup.sqlite"),
- outbox_path: tempdir.path().join("closed-outbox-backup.sqlite"),
- private_store_path: tempdir.path().join("outbox-closed-private.sqlite"),
+ .expect("studio closed storage");
+ studio_closed_storage.studio_store.pool().close().await;
+ let studio_closed_paths = RadrootsSdkStoragePaths {
+ runtime_path: tempdir.path().join("open-runtime-backup.sqlite"),
+ studio_path: tempdir.path().join("closed-studio-backup.sqlite"),
+ private_path: tempdir.path().join("studio-closed-private.sqlite"),
};
- assert_outbox_error(
+ assert_studio_store_error(
backup_sqlite_stores(
- outbox_closed_storage.event_store.pool(),
- outbox_closed_storage.outbox.pool(),
- outbox_closed_storage.private_store.pool(),
- &outbox_closed_paths,
+ studio_closed_storage.event_store.pool(),
+ studio_closed_storage.private_store.pool(),
+ studio_closed_storage.studio_store.pool(),
+ &studio_closed_paths,
)
.await,
);
@@ -1353,9 +1437,9 @@ async fn sqlite_backup_errors_cover_invalid_paths_and_execute_failures() {
!io_message(write_backup_receipt(
tempdir.path().join("receipt-destination"),
RadrootsSdkStoragePaths {
- event_store_path: tempdir.path().join(EVENT_STORE_BACKUP_FILE),
- outbox_path: tempdir.path().join(OUTBOX_BACKUP_FILE),
- private_store_path: tempdir.path().join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: tempdir.path().join(RUNTIME_SQLITE_FILE),
+ studio_path: tempdir.path().join(STUDIO_SQLITE_FILE),
+ private_path: tempdir.path().join(PRIVATE_SQLITE_FILE),
},
tempdir.path().to_path_buf(),
manifest(),
@@ -1401,43 +1485,42 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
let missing_archive = RestoreArchive {
source: tempdir.path().join("missing-archive"),
- event_store_path: tempdir.path().join("missing-event-store.sqlite"),
- outbox_path: tempdir.path().join("missing-outbox.sqlite"),
- private_store_path: tempdir.path().join("missing-private.sqlite"),
+ runtime_path: tempdir.path().join("missing-event-store.sqlite"),
+ studio_path: tempdir.path().join("missing-outbox.sqlite"),
+ private_path: tempdir.path().join("missing-private.sqlite"),
manifest_path: tempdir.path().join(BACKUP_MANIFEST_FILE),
manifest: manifest(),
verification: verification(true, true),
};
let staging_paths = RadrootsSdkStoragePaths {
- event_store_path: tempdir.path().join("staging-event-store.sqlite"),
- outbox_path: tempdir.path().join("staging-outbox.sqlite"),
- private_store_path: tempdir.path().join("staging-private.sqlite"),
+ runtime_path: tempdir.path().join("staging-event-store.sqlite"),
+ studio_path: tempdir.path().join("staging-outbox.sqlite"),
+ private_path: tempdir.path().join("staging-private.sqlite"),
};
assert!(
io_message(copy_restore_archive_to_staging(&missing_archive, &staging_paths).await)
- .contains("restore event store copy failed")
+ .contains("restore runtime store copy failed")
);
let partial_archive = RestoreArchive {
- event_store_path: staging_paths.event_store_path.clone(),
+ runtime_path: staging_paths.runtime_path.clone(),
..missing_archive.clone()
};
- fs::write(&partial_archive.event_store_path, b"not sqlite").expect("partial event store");
+ fs::write(&partial_archive.runtime_path, b"not sqlite").expect("partial event store");
assert!(
io_message(copy_restore_archive_to_staging(&partial_archive, &staging_paths).await)
- .contains("restore outbox copy failed")
+ .contains("restore studio copy failed")
);
let private_partial_archive = RestoreArchive {
- event_store_path: tempdir.path().join("private-partial-event-store.sqlite"),
- outbox_path: tempdir.path().join("private-partial-outbox.sqlite"),
+ runtime_path: tempdir.path().join("private-partial-event-store.sqlite"),
+ studio_path: tempdir.path().join("private-partial-outbox.sqlite"),
..missing_archive.clone()
};
- fs::write(&private_partial_archive.event_store_path, b"event store")
- .expect("private partial event store");
- fs::write(&private_partial_archive.outbox_path, b"outbox").expect("private partial outbox");
+ fs::write(&private_partial_archive.runtime_path, b"runtime").expect("private partial runtime");
+ fs::write(&private_partial_archive.studio_path, b"studio").expect("private partial studio");
let private_staging_paths = RadrootsSdkStoragePaths {
- event_store_path: tempdir.path().join("private-staging-event-store.sqlite"),
- outbox_path: tempdir.path().join("private-staging-outbox.sqlite"),
- private_store_path: tempdir.path().join("private-staging-missing.sqlite"),
+ runtime_path: tempdir.path().join("private-staging-event-store.sqlite"),
+ studio_path: tempdir.path().join("private-staging-outbox.sqlite"),
+ private_path: tempdir.path().join("private-staging-missing.sqlite"),
};
assert!(
io_message(
@@ -1447,14 +1530,14 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
.contains("restore private store copy failed")
);
let corrupt_archive = RestoreArchive {
- event_store_path: tempdir.path().join("corrupt-event-store.sqlite"),
- outbox_path: tempdir.path().join("corrupt-outbox.sqlite"),
- private_store_path: tempdir.path().join("corrupt-private.sqlite"),
+ runtime_path: tempdir.path().join("corrupt-event-store.sqlite"),
+ studio_path: tempdir.path().join("corrupt-outbox.sqlite"),
+ private_path: tempdir.path().join("corrupt-private.sqlite"),
..missing_archive.clone()
};
- fs::write(&corrupt_archive.event_store_path, b"not sqlite").expect("corrupt event store");
- fs::write(&corrupt_archive.outbox_path, b"not sqlite").expect("corrupt outbox");
- fs::write(&corrupt_archive.private_store_path, b"not sqlite").expect("corrupt private store");
+ fs::write(&corrupt_archive.runtime_path, b"not sqlite").expect("corrupt runtime");
+ fs::write(&corrupt_archive.studio_path, b"not sqlite").expect("corrupt studio");
+ fs::write(&corrupt_archive.private_path, b"not sqlite").expect("corrupt private store");
assert_event_store_error(
copy_restore_archive_to_staging(&corrupt_archive, &staging_paths).await,
);
@@ -1465,38 +1548,38 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
.contains("parent is required")
);
- let invalid_outbox_member_source = tempdir.path().join("invalid-outbox-member");
- fs::create_dir(&invalid_outbox_member_source).expect("invalid outbox source");
+ let invalid_studio_member_source = tempdir.path().join("invalid-studio-member");
+ fs::create_dir(&invalid_studio_member_source).expect("invalid studio source");
fs::write(
- invalid_outbox_member_source.join(EVENT_STORE_BACKUP_FILE),
+ invalid_studio_member_source.join(RUNTIME_SQLITE_FILE),
b"not sqlite",
)
- .expect("event store member");
- let mut invalid_outbox_manifest = manifest();
- invalid_outbox_manifest.backup_paths.outbox_path = PathBuf::from("../outside.sqlite");
+ .expect("runtime member");
+ let mut invalid_studio_manifest = manifest();
+ invalid_studio_manifest.backup_paths.studio_path = PathBuf::from("../outside.sqlite");
write_backup_manifest(
- &invalid_outbox_member_source.join(BACKUP_MANIFEST_FILE),
- &invalid_outbox_manifest,
+ &invalid_studio_member_source.join(BACKUP_MANIFEST_FILE),
+ &invalid_studio_manifest,
)
- .expect("invalid outbox manifest");
+ .expect("invalid studio manifest");
assert!(
- invalid_request_message(inspect_restore_archive(invalid_outbox_member_source).await)
- .contains("outbox archive path")
+ invalid_request_message(inspect_restore_archive(invalid_studio_member_source).await)
+ .contains("studio archive path")
);
let invalid_private_member_source = tempdir.path().join("invalid-private-member");
fs::create_dir(&invalid_private_member_source).expect("invalid private source");
fs::write(
- invalid_private_member_source.join(EVENT_STORE_BACKUP_FILE),
+ invalid_private_member_source.join(RUNTIME_SQLITE_FILE),
b"not sqlite",
)
- .expect("invalid private event store member");
+ .expect("invalid private runtime member");
fs::write(
- invalid_private_member_source.join(OUTBOX_BACKUP_FILE),
+ invalid_private_member_source.join(STUDIO_SQLITE_FILE),
b"not sqlite",
)
- .expect("invalid private outbox member");
+ .expect("invalid private studio member");
let mut invalid_private_manifest = manifest();
- invalid_private_manifest.backup_paths.private_store_path = PathBuf::from("../outside.sqlite");
+ invalid_private_manifest.backup_paths.private_path = PathBuf::from("../outside.sqlite");
write_backup_manifest(
&invalid_private_member_source.join(BACKUP_MANIFEST_FILE),
&invalid_private_manifest,
@@ -1511,9 +1594,9 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
fs::create_dir(&protected_parent).expect("protected parent");
let protected_destination = protected_parent.join("restore");
let protected_paths = RadrootsSdkStoragePaths {
- event_store_path: protected_destination.join(EVENT_STORE_BACKUP_FILE),
- outbox_path: protected_destination.join(OUTBOX_BACKUP_FILE),
- private_store_path: protected_destination.join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: protected_destination.join(RUNTIME_SQLITE_FILE),
+ studio_path: protected_destination.join(STUDIO_SQLITE_FILE),
+ private_path: protected_destination.join(PRIVATE_SQLITE_FILE),
};
set_mode(&protected_parent, 0o500);
let protected_result =
@@ -1545,18 +1628,18 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
.await;
set_mode(&public_protected_parent, 0o700);
assert!(!io_message(public_protected_result).is_empty());
- let missing_outbox_paths = RadrootsSdkStoragePaths {
- event_store_path: archive.event_store_path.clone(),
- outbox_path: tempdir.path().to_path_buf(),
- private_store_path: archive.private_store_path.clone(),
+ let missing_studio_paths = RadrootsSdkStoragePaths {
+ runtime_path: archive.runtime_path.clone(),
+ studio_path: tempdir.path().to_path_buf(),
+ private_path: archive.private_path.clone(),
};
- assert!(verify_backup_paths(&missing_outbox_paths).await.is_err());
+ assert!(verify_backup_paths(&missing_studio_paths).await.is_err());
let invalid_destination = tempdir.path().join(nul_path());
let invalid_paths = RadrootsSdkStoragePaths {
- event_store_path: invalid_destination.join(EVENT_STORE_BACKUP_FILE),
- outbox_path: invalid_destination.join(OUTBOX_BACKUP_FILE),
- private_store_path: invalid_destination.join(PRIVATE_STORE_BACKUP_FILE),
+ runtime_path: invalid_destination.join(RUNTIME_SQLITE_FILE),
+ studio_path: invalid_destination.join(STUDIO_SQLITE_FILE),
+ private_path: invalid_destination.join(PRIVATE_SQLITE_FILE),
};
let invalid_restore_message = io_message(
restore_archive_to_destination(&archive, &invalid_destination, &invalid_paths).await,
@@ -1572,13 +1655,9 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
restore_archive_to_destination(&archive, &existing_destination, &existing_paths)
.await
.expect("overwrite existing restore");
- assert!(existing_destination.join(EVENT_STORE_BACKUP_FILE).exists());
- assert!(existing_destination.join(OUTBOX_BACKUP_FILE).exists());
- assert!(
- existing_destination
- .join(PRIVATE_STORE_BACKUP_FILE)
- .exists()
- );
+ assert!(existing_destination.join(RUNTIME_SQLITE_FILE).exists());
+ assert!(existing_destination.join(STUDIO_SQLITE_FILE).exists());
+ assert!(existing_destination.join(PRIVATE_SQLITE_FILE).exists());
let mut mismatch_archive = archive.clone();
mismatch_archive.verification.event_store_events += 1;
@@ -1651,9 +1730,9 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
assert_ne!(archive.verification, populated_archive.verification);
let verification_mismatch_destination = tempdir.path().join("verification-mismatch-restore");
let wrong_destination_paths = RadrootsSdkStoragePaths {
- event_store_path: populated_archive.event_store_path.clone(),
- outbox_path: populated_archive.outbox_path.clone(),
- private_store_path: populated_archive.private_store_path.clone(),
+ runtime_path: populated_archive.runtime_path.clone(),
+ studio_path: populated_archive.studio_path.clone(),
+ private_path: populated_archive.private_path.clone(),
};
assert!(
invalid_request_message(
@@ -1673,8 +1752,8 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
preflight_restore_destination(&archive.source, &bad_verify_destination, false)
.expect("bad verify preflight");
let mut mismatched_verify_paths = bad_verify_paths.clone();
- mismatched_verify_paths.event_store_path = bad_verify_destination.clone();
- mismatched_verify_paths.outbox_path = bad_verify_destination.join(OUTBOX_BACKUP_FILE);
+ mismatched_verify_paths.runtime_path = bad_verify_destination.clone();
+ mismatched_verify_paths.studio_path = bad_verify_destination.join(STUDIO_SQLITE_FILE);
assert!(
restore_archive_to_destination(
&archive,
diff --git a/crates/sdk/tests/unit/signer_provider_tests.rs b/crates/sdk/tests/unit/signer_provider_tests.rs
@@ -369,7 +369,7 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() {
transport.clone(),
)
.expect("signer");
- let provider = RadrootsSdkSignerProvider::MycNip46(signer);
+ let provider = RadrootsSdkSignerProvider::MycNip46(Box::new(signer));
assert_eq!(provider.mode(), RadrootsSdkSignerMode::MycNip46);
assert_eq!(
@@ -542,7 +542,7 @@ async fn myc_nip46_provider_signs_and_validates_remote_event() {
let signer =
RadrootsSdkMycNip46Signer::new(client_keys, target, USER_PUBLIC_KEY_HEX, transport.clone())
.expect("signer");
- let provider = RadrootsSdkSignerProvider::MycNip46(signer);
+ let provider = RadrootsSdkSignerProvider::MycNip46(Box::new(signer));
assert_eq!(
provider.capability().nip46_permissions,
radroots_sdk_myc_nip46_product_permission_strings()
diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs
@@ -770,7 +770,7 @@ async fn sync_status_maps_closed_store_errors() {
.sync()
.status(super::SyncStatusRequest::new())
.await,
- Err(RadrootsSdkError::Outbox { .. })
+ Err(RadrootsSdkError::EventStore { .. })
));
}
diff --git a/crates/sdk/tests/unit/transport_tests.rs b/crates/sdk/tests/unit/transport_tests.rs
@@ -9,10 +9,7 @@ use radroots_transport::{
RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetUri,
};
-#[path = "../support/serializer_failure.rs"]
-mod serializer_failure;
-
-use serializer_failure::assert_struct_serialize_error_paths;
+use crate::serializer_failure::assert_struct_serialize_error_paths;
#[test]
fn publish_mode_and_ack_policy_serialize_explicit_product_contracts() {
diff --git a/crates/sdk/tests/unit/workflow_runtime_tests.rs b/crates/sdk/tests/unit/workflow_runtime_tests.rs
@@ -14,6 +14,11 @@ const FARMER_SECRET_KEY_HEX: &str =
const FARMER_PUBLIC_KEY_HEX: &str =
"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
+fn workflow_idempotency_key(index: u16) -> SdkIdempotencyKey {
+ SdkIdempotencyKey::new(format!("01890f0e-6c00-7000-8000-00000000{index:04x}"))
+ .expect("workflow idempotency")
+}
+
struct WorkflowSigner {
identity: RadrootsSignerIdentity,
keys: RadrootsNostrKeys,
@@ -135,7 +140,8 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() {
assert_eq!(event.id(), signed.id());
assert_eq!(event.author(), signed.pubkey());
- let idempotency_key = SdkIdempotencyKey::new("workflow-idempotency").expect("idempotency");
+ let idempotency_key =
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237").expect("idempotency");
let input = signed_outbox_input(
"workflow.test.v1",
&draft,
@@ -185,7 +191,7 @@ async fn default_operation_idempotency_ignores_target_policy() {
frozen_draft: &draft,
target_policy: first_target_policy,
satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x240)),
},
&signer,
)
@@ -199,7 +205,7 @@ async fn default_operation_idempotency_ignores_target_policy() {
frozen_draft: &draft,
target_policy: second_target_policy,
satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x240)),
},
&signer,
)
@@ -270,7 +276,7 @@ async fn enqueue_signed_workflow_maps_no_wait_directly_and_allows_local_only_pro
frozen_draft: &draft,
target_policy: TargetPolicy::default_profile(),
satisfaction_policy: SatisfactionPolicy::NoWait,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x241)),
},
&signer,
)
@@ -323,6 +329,56 @@ async fn enqueue_signed_workflow_maps_no_wait_directly_and_allows_local_only_pro
}
#[tokio::test]
+async fn enqueue_signed_workflow_rejects_missing_explicit_idempotency_key_without_mutation() {
+ let sdk = crate::RadrootsClient::builder()
+ .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
+ 1_700_000_013,
+ ))
+ .build()
+ .await
+ .expect("sdk");
+ let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
+ .expect("actor");
+ let draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-missing-idempotency");
+
+ let error = match enqueue_signed_workflow(
+ &sdk,
+ SdkWorkflowEnqueueRequest {
+ operation_kind: "workflow.test.v1",
+ actor: &actor,
+ frozen_draft: &draft,
+ target_policy: TargetPolicy::default_profile(),
+ satisfaction_policy: SatisfactionPolicy::NoWait,
+ idempotency_key: None,
+ },
+ &WorkflowSigner::new(),
+ )
+ .await
+ {
+ Err(error) => error,
+ Ok(_) => panic!("expected missing idempotency error"),
+ };
+
+ assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
+ assert_eq!(
+ sdk._event_store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 0
+ );
+ assert_eq!(
+ sdk._outbox
+ .status_summary(0)
+ .await
+ .expect("outbox status")
+ .total_events,
+ 0
+ );
+}
+
+#[tokio::test]
async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_conflicts() {
let sdk = crate::RadrootsClient::builder()
.transport_profile(nostr_profile("wss://relay.example.com"))
@@ -336,7 +392,8 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con
.expect("actor");
let signer = WorkflowSigner::new();
let first_draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-success");
- let idempotency_key = SdkIdempotencyKey::new("workflow-idempotency").expect("idempotency");
+ let idempotency_key =
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237").expect("idempotency");
let receipt = enqueue_signed_workflow(
&sdk,
SdkWorkflowEnqueueRequest {
@@ -449,7 +506,7 @@ async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() {
frozen_draft: &draft,
target_policy: TargetPolicy::default_profile(),
satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x242)),
},
)
.await
@@ -463,7 +520,7 @@ async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() {
}
#[tokio::test]
-async fn enqueue_signed_workflow_reports_outbox_preflight_failure_without_mutation() {
+async fn enqueue_signed_workflow_reports_runtime_pool_failure_before_mutation() {
let sdk = crate::RadrootsClient::builder()
.transport_profile(nostr_profile("wss://relay.example.com"))
.build()
@@ -487,7 +544,7 @@ async fn enqueue_signed_workflow_reports_outbox_preflight_failure_without_mutati
frozen_draft: &draft,
target_policy: TargetPolicy::default_profile(),
satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x243)),
};
let error = match enqueue_signed_workflow(&sdk, request, &WorkflowSigner::new()).await {
@@ -495,15 +552,7 @@ async fn enqueue_signed_workflow_reports_outbox_preflight_failure_without_mutati
Ok(_) => panic!("expected closed outbox error"),
};
- assert!(matches!(error, RadrootsSdkError::Outbox { .. }));
- assert_eq!(
- sdk._event_store
- .status_summary()
- .await
- .expect("event store summary")
- .total_events,
- 0
- );
+ assert!(matches!(error, RadrootsSdkError::EventStore { .. }));
}
#[tokio::test]
@@ -523,7 +572,7 @@ async fn enqueue_signed_workflow_reports_store_failures() {
frozen_draft: &draft,
target_policy: TargetPolicy::default_profile(),
satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x244)),
};
assert!(matches!(
enqueue_signed_workflow(
@@ -553,7 +602,7 @@ async fn enqueue_signed_workflow_reports_clock_failures() {
frozen_draft: &draft,
target_policy: TargetPolicy::default_profile(),
satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x245)),
};
assert!(matches!(
enqueue_signed_workflow(&sdk, request, &WorkflowSigner::new()).await,
@@ -573,7 +622,7 @@ async fn enqueue_signed_workflow_rejects_transport_profile_targets_without_proxy
frozen_draft: &draft,
target_policy: TargetPolicy::DefaultProfile,
satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
+ idempotency_key: Some(workflow_idempotency_key(0x246)),
};
assert!(matches!(
diff --git a/packages/event-bindings/src/generated/kinds.ts b/packages/event-bindings/src/generated/kinds.ts
@@ -108,40 +108,14 @@ export const KIND_LISTING = 30402;
export const KIND_APPLICATION_HANDLER = 31990;
-export const KIND_TRADE_LISTING_VALIDATION_REQUEST = 5321;
-
-export const KIND_TRADE_LISTING_VALIDATION_RESULT = 6321;
-
-export const KIND_TRADE_TRANSITION_PROOF_REQUEST = 5322;
-
-export const KIND_TRADE_TRANSITION_PROOF_RESULT = 6322;
-
export const KIND_ORDER_REQUEST = 3422;
export const KIND_ORDER_DECISION = 3423;
-export const KIND_ORDER_REVISION_PROPOSAL = 3424;
-
-export const KIND_ORDER_REVISION_DECISION = 3425;
-
export const KIND_ORDER_CANCELLATION = 3432;
-export const KIND_TRADE_QUESTION = 3426;
-
-export const KIND_TRADE_ANSWER = 3427;
-
-export const KIND_TRADE_DISCOUNT_REQUEST = 3428;
-
-export const KIND_TRADE_DISCOUNT_OFFER = 3429;
-
-export const KIND_TRADE_DISCOUNT_ACCEPT = 3430;
-
export const KIND_TRADE_FORBIDDEN_3431 = 3431;
-export const KIND_TRADE_FULFILLMENT_UPDATE = 3433;
-
-export const KIND_TRADE_RECEIPT = 3434;
-
export const KIND_TRADE_VALIDATION_RECEIPT = 3440;
export const KIND_KNOWLEDGE_CLAIM = 3460;
diff --git a/packages/event-bindings/src/generated/types.ts b/packages/event-bindings/src/generated/types.ts
@@ -160,7 +160,7 @@ export type RadrootsOrderEconomicTotals = { subtotal: RadrootsCoreMoney, discoun
export type RadrootsOrderEconomics = { quote_id: string, quote_version: number, pricing_basis: RadrootsOrderPricingBasis, currency: string, items: Array<RadrootsOrderEconomicItem>, discounts: Array<RadrootsOrderEconomicLine>, adjustments: Array<RadrootsOrderEconomicLine>, subtotal: RadrootsCoreMoney, discount_total: RadrootsCoreMoney, adjustment_total: RadrootsCoreMoney, total: RadrootsCoreMoney, };
-export type RadrootsOrderEventType = "TradeOrderRequested" | "TradeOrderDecision" | "TradeOrderRevisionProposed" | "TradeOrderRevisionDecision" | "TradeOrderCancelled";
+export type RadrootsOrderEventType = "TradeOrderRequested" | "TradeOrderDecision" | "TradeOrderCancelled";
export type RadrootsOrderInventoryCommitment = { bin_id: string, bin_count: number, };
@@ -170,12 +170,6 @@ export type RadrootsOrderPricingBasis = "listing_event";
export type RadrootsOrderRequest = { order_id: string, listing_addr: string, buyer_pubkey: string, seller_pubkey: string, items: Array<RadrootsOrderItem>, economics: RadrootsOrderEconomics, };
-export type RadrootsOrderRevisionDecision = { revision_id: string, order_id: string, listing_addr: string, buyer_pubkey: string, seller_pubkey: string, root_event_id: string, prev_event_id: string, decision: RadrootsOrderRevisionOutcome, };
-
-export type RadrootsOrderRevisionOutcome = { decision: "accepted", } | { decision: "declined", reason: string, };
-
-export type RadrootsOrderRevisionProposal = { revision_id: string, order_id: string, listing_addr: string, buyer_pubkey: string, seller_pubkey: string, root_event_id: string, prev_event_id: string, items: Array<RadrootsOrderItem>, economics: RadrootsOrderEconomics, reason: string, };
-
export type RadrootsPlot = { d_tag: string, farm: RadrootsFarmRef, name: string, about?: string | null, location?: RadrootsPlotLocation | null, tags?: Array<string> | null, };
export type RadrootsPlotLocation = { primary?: string | null, city?: string | null, region?: string | null, country?: string | null, gcs: RadrootsGcsLocation, };
@@ -222,10 +216,6 @@ export type RadrootsSocialMediaThumbnail = { url: string, dimensions?: RadrootsS
export type RadrootsSocialTarget = { kind: "event", id: string, author?: string | null, event_kind?: number | null, relays?: Array<string> | null, } | { kind: "address", address: string, author?: string | null, event_kind?: number | null, relays?: Array<string> | null, } | { kind: "external", id: string, external_kind: string, hint?: string | null, };
-export type RadrootsTradeListingValidateRequest = { listing_event?: RadrootsEventPtr | null, };
-
-export type RadrootsTradeListingValidateResult = { valid: boolean, errors: Array<RadrootsTradeValidationListingError>, };
-
export type RadrootsTradeValidationListingError = { kind: "invalid_kind", amount: { kind: number, }, } | { kind: "missing_listing_id", } | { kind: "listing_event_not_found", amount: { listing_addr: string, }, } | { kind: "listing_event_fetch_failed", amount: { listing_addr: string, }, } | { kind: "parse_error", amount: { error: RadrootsListingParseError, }, } | { kind: "invalid_seller", } | { kind: "missing_farm_profile", } | { kind: "missing_farm_record", } | { kind: "missing_title", } | { kind: "missing_description", } | { kind: "missing_product_type", } | { kind: "missing_bins", } | { kind: "missing_primary_bin", } | { kind: "invalid_bin", } | { kind: "missing_price", } | { kind: "invalid_price", } | { kind: "missing_inventory", } | { kind: "invalid_inventory", } | { kind: "missing_availability", } | { kind: "missing_location", } | { kind: "missing_location_locality", } | { kind: "missing_location_geohash", } | { kind: "invalid_location_geohash", } | { kind: "missing_delivery_method", };
export type RadrootsVerifiedSignedEventDto = { state: RadrootsVerifiedSignedEventVerificationStateDto, signed_event: RadrootsSignedEventDto, };
diff --git a/packages/trade-bindings/src/generated/types.ts b/packages/trade-bindings/src/generated/types.ts
@@ -3,9 +3,9 @@
import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreUnit } from "@radroots/core-bindings";
import type { RadrootsFarmRef, RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingProduct, RadrootsListingPublicLocation, RadrootsOrderEconomics, RadrootsOrderEventType, RadrootsOrderInventoryCommitment, RadrootsPlotRef, RadrootsResourceAreaRef } from "@radroots/event-bindings";
-export type RadrootsOrderIssue = "missing_request" | { multiple_requests: { event_ids: Array<string>, }, } | { request_payload_invalid: { event_id: string, }, } | { request_order_id_mismatch: { event_id: string, }, } | { request_author_mismatch: { event_id: string, }, } | { request_listing_address_invalid: { event_id: string, }, } | { request_seller_listing_mismatch: { event_id: string, }, } | { decision_payload_invalid: { event_id: string, }, } | { decision_order_id_mismatch: { event_id: string, }, } | { decision_author_mismatch: { event_id: string, }, } | { decision_counterparty_mismatch: { event_id: string, }, } | { decision_buyer_mismatch: { event_id: string, }, } | { decision_seller_mismatch: { event_id: string, }, } | { decision_listing_address_invalid: { event_id: string, }, } | { decision_listing_mismatch: { event_id: string, }, } | { decision_root_mismatch: { event_id: string, }, } | { decision_previous_mismatch: { event_id: string, }, } | { decision_missing_inventory_commitments: { event_id: string, }, } | { decision_inventory_commitment_mismatch: { event_id: string, }, } | { decision_missing_reason: { event_id: string, }, } | { conflicting_decisions: { event_ids: Array<string>, }, } | { revision_proposal_payload_invalid: { event_id: string, }, } | { revision_proposal_order_id_mismatch: { event_id: string, }, } | { revision_proposal_author_mismatch: { event_id: string, }, } | { revision_proposal_counterparty_mismatch: { event_id: string, }, } | { revision_proposal_buyer_mismatch: { event_id: string, }, } | { revision_proposal_seller_mismatch: { event_id: string, }, } | { revision_proposal_listing_address_invalid: { event_id: string, }, } | { revision_proposal_listing_mismatch: { event_id: string, }, } | { revision_proposal_root_mismatch: { event_id: string, }, } | { revision_proposal_previous_mismatch: { event_id: string, }, } | { revision_decision_without_proposal: { event_id: string, }, } | { revision_decision_payload_invalid: { event_id: string, }, } | { revision_decision_order_id_mismatch: { event_id: string, }, } | { revision_decision_author_mismatch: { event_id: string, }, } | { revision_decision_counterparty_mismatch: { event_id: string, }, } | { revision_decision_buyer_mismatch: { event_id: string, }, } | { revision_decision_seller_mismatch: { event_id: string, }, } | { revision_decision_listing_address_invalid: { event_id: string, }, } | { revision_decision_listing_mismatch: { event_id: string, }, } | { revision_decision_root_mismatch: { event_id: string, }, } | { revision_decision_previous_mismatch: { event_id: string, }, } | { revision_decision_revision_id_mismatch: { event_id: string, }, } | { cancellation_without_cancellable_order: { event_id: string, }, } | { cancellation_payload_invalid: { event_id: string, }, } | { cancellation_order_id_mismatch: { event_id: string, }, } | { cancellation_author_mismatch: { event_id: string, }, } | { cancellation_counterparty_mismatch: { event_id: string, }, } | { cancellation_buyer_mismatch: { event_id: string, }, } | { cancellation_seller_mismatch: { event_id: string, }, } | { cancellation_listing_address_invalid: { event_id: string, }, } | { cancellation_listing_mismatch: { event_id: string, }, } | { cancellation_root_mismatch: { event_id: string, }, } | { cancellation_previous_mismatch: { event_id: string, }, } | { forked_lifecycle: { event_ids: Array<string>, }, } | { validation_receipt_without_pending_agreement: { event_id: string, }, } | { validation_receipt_order_id_mismatch: { event_id: string, }, } | { validation_receipt_type_mismatch: { event_id: string, }, } | { validation_receipt_root_mismatch: { event_id: string, }, } | { validation_receipt_target_mismatch: { event_id: string, }, } | { validation_receipt_listing_mismatch: { event_id: string, }, } | { conflicting_validation_receipts: { event_ids: Array<string>, }, } | { deterministic_validation_failure: { event_id: string, reason: string, }, } | { stale_listing_event: { expected_event_id: string, current_event_id: string, }, };
+export type RadrootsOrderIssue = "missing_request" | { multiple_requests: { event_ids: Array<string>, }, } | { request_payload_invalid: { event_id: string, }, } | { request_order_id_mismatch: { event_id: string, }, } | { request_author_mismatch: { event_id: string, }, } | { request_listing_address_invalid: { event_id: string, }, } | { request_seller_listing_mismatch: { event_id: string, }, } | { decision_payload_invalid: { event_id: string, }, } | { decision_order_id_mismatch: { event_id: string, }, } | { decision_author_mismatch: { event_id: string, }, } | { decision_counterparty_mismatch: { event_id: string, }, } | { decision_buyer_mismatch: { event_id: string, }, } | { decision_seller_mismatch: { event_id: string, }, } | { decision_listing_address_invalid: { event_id: string, }, } | { decision_listing_mismatch: { event_id: string, }, } | { decision_root_mismatch: { event_id: string, }, } | { decision_previous_mismatch: { event_id: string, }, } | { decision_missing_inventory_commitments: { event_id: string, }, } | { decision_inventory_commitment_mismatch: { event_id: string, }, } | { decision_missing_reason: { event_id: string, }, } | { conflicting_decisions: { event_ids: Array<string>, }, } | { cancellation_without_cancellable_order: { event_id: string, }, } | { cancellation_payload_invalid: { event_id: string, }, } | { cancellation_order_id_mismatch: { event_id: string, }, } | { cancellation_author_mismatch: { event_id: string, }, } | { cancellation_counterparty_mismatch: { event_id: string, }, } | { cancellation_buyer_mismatch: { event_id: string, }, } | { cancellation_seller_mismatch: { event_id: string, }, } | { cancellation_listing_address_invalid: { event_id: string, }, } | { cancellation_listing_mismatch: { event_id: string, }, } | { cancellation_root_mismatch: { event_id: string, }, } | { cancellation_previous_mismatch: { event_id: string, }, } | { forked_lifecycle: { event_ids: Array<string>, }, } | { validation_receipt_without_pending_agreement: { event_id: string, }, } | { validation_receipt_order_id_mismatch: { event_id: string, }, } | { validation_receipt_type_mismatch: { event_id: string, }, } | { validation_receipt_root_mismatch: { event_id: string, }, } | { validation_receipt_target_mismatch: { event_id: string, }, } | { validation_receipt_listing_mismatch: { event_id: string, }, } | { conflicting_validation_receipts: { event_ids: Array<string>, }, } | { deterministic_validation_failure: { event_id: string, reason: string, }, } | { stale_listing_event: { expected_event_id: string, current_event_id: string, }, };
-export type RadrootsOrderWorkflowProjection = { order_id: string, status: RadrootsTradeWorkflowState, request_event_id?: string | null, decision_event_id?: string | null, cancellation_event_id?: string | null, validation_receipt_event_id?: string | null, lifecycle_terminal: boolean, economics?: RadrootsOrderEconomics | null, agreement_event_id?: string | null, pending_revision_event_id?: string | null, pending_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, committed_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, listing_addr?: string | null, buyer_pubkey?: string | null, seller_pubkey?: string | null, last_event_id?: string | null, issues: Array<RadrootsOrderIssue>, };
+export type RadrootsOrderWorkflowProjection = { order_id: string, status: RadrootsTradeWorkflowState, request_event_id?: string | null, decision_event_id?: string | null, cancellation_event_id?: string | null, validation_receipt_event_id?: string | null, lifecycle_terminal: boolean, economics?: RadrootsOrderEconomics | null, agreement_event_id?: string | null, pending_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, committed_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, listing_addr?: string | null, buyer_pubkey?: string | null, seller_pubkey?: string | null, last_event_id?: string | null, issues: Array<RadrootsOrderIssue>, };
export type RadrootsTradeFacetCount = { key: string, count: number, };
@@ -67,4 +67,4 @@ export type RadrootsTradeReviewStatus = "queued" | "in_progress" | "blocked" | "
export type RadrootsTradeSortDirection = "asc" | "desc";
-export type RadrootsTradeWorkflowState = "missing" | "requested" | "revision_proposed" | "agreed_pending_rhi" | "committed" | "declined" | "cancelled" | "invalid";
+export type RadrootsTradeWorkflowState = "missing" | "requested" | "agreed_pending_validation" | "committed" | "declined" | "cancelled" | "validation_expired" | "invalid";