commit 40f83f485fb5af7c4763ad4e8662ae601e1277ab
parent c146bca18a56a55d6364a008e4914e07ae4665dc
Author: triesap <tyson@radroots.org>
Date: Thu, 18 Jun 2026 23:22:46 -0700
sdk: add order lifecycle runtimes
- add typed revision, cancellation, fulfillment, and receipt runtime APIs
- enforce local projection preflight before lifecycle mutation
- preserve idempotent replay for already stored prepared events
- cover lifecycle enqueue, status projection, and invalid-state rejection
Diffstat:
3 files changed, 2990 insertions(+), 392 deletions(-)
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -85,13 +85,24 @@ pub use crate::listings_runtime::{
};
#[cfg(feature = "runtime")]
pub use crate::orders_runtime::{
- ORDER_DECISION_OPERATION_KIND, ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT,
- ORDER_SUBMIT_OPERATION_KIND, OrderDecisionEnqueueRequest, OrderDecisionPlan,
+ ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND,
+ ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND,
+ ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND,
+ ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, ORDER_SUBMIT_OPERATION_KIND,
+ OrderCancellationEnqueueRequest, OrderCancellationPlan, OrderCancellationPrepareRequest,
+ OrderCancellationReceipt, OrderDecisionEnqueueRequest, OrderDecisionPlan,
OrderDecisionPrepareRequest, OrderDecisionReceipt, OrderFulfillmentStatusKind,
- OrderPaymentStateKind, OrderRequestEvidenceIngestReceipt, OrderRequestEvidenceIngestRequest,
- OrderSettlementStateKind, OrderStatusKind, OrderStatusReceipt, OrderStatusRequest,
- OrderSubmitEnqueueRequest, OrderSubmitPlan, OrderSubmitPrepareRequest, OrderSubmitReceipt,
- SdkOrderStatusIssue, SdkOrderStatusIssueKind, SdkOrderStatusSource,
+ OrderFulfillmentUpdateEnqueueRequest, OrderFulfillmentUpdatePlan,
+ OrderFulfillmentUpdatePrepareRequest, OrderFulfillmentUpdateReceipt, OrderPaymentStateKind,
+ OrderReceiptRecordEnqueueRequest, OrderReceiptRecordPlan, OrderReceiptRecordPrepareRequest,
+ OrderReceiptRecordReceipt, OrderRequestEvidenceIngestReceipt,
+ OrderRequestEvidenceIngestRequest, OrderRevisionDecisionEnqueueRequest,
+ OrderRevisionDecisionPlan, OrderRevisionDecisionPrepareRequest, OrderRevisionDecisionReceipt,
+ OrderRevisionProposalEnqueueRequest, OrderRevisionProposalPlan,
+ OrderRevisionProposalPrepareRequest, OrderRevisionProposalReceipt, OrderSettlementStateKind,
+ OrderStatusKind, OrderStatusReceipt, OrderStatusRequest, OrderSubmitEnqueueRequest,
+ OrderSubmitPlan, OrderSubmitPrepareRequest, OrderSubmitReceipt, SdkOrderStatusIssue,
+ SdkOrderStatusIssueKind, SdkOrderStatusSource,
};
#[cfg(feature = "runtime")]
pub use crate::product_clients::{FarmsClient, ListingsClient, OrdersClient, SyncClient};
diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs
@@ -16,10 +16,14 @@ use radroots_events::{
contract::RadrootsActorRole,
draft::RadrootsFrozenEventDraft,
ids::{RadrootsEventId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey},
- order::{RadrootsOrderDecision, RadrootsOrderFulfillmentState, RadrootsOrderRequest},
+ order::{
+ RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderFulfillmentState,
+ RadrootsOrderFulfillmentUpdate, RadrootsOrderReceipt, RadrootsOrderRequest,
+ RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal,
+ },
};
#[cfg(feature = "runtime")]
-use radroots_events_codec::wire::to_frozen_draft;
+use radroots_events_codec::wire::{WireEventParts, to_frozen_draft};
#[cfg(feature = "runtime")]
use radroots_trade::order::{
RadrootsOrderCanonicalizationError, RadrootsOrderIssue, RadrootsOrderPaymentState,
@@ -38,11 +42,31 @@ pub const ORDER_STATUS_MAX_LIMIT: u32 = 1_000;
pub const ORDER_SUBMIT_OPERATION_KIND: &str = "order.submit.v1";
#[cfg(feature = "runtime")]
pub const ORDER_DECISION_OPERATION_KIND: &str = "order.decision.v1";
+#[cfg(feature = "runtime")]
+pub const ORDER_REVISION_PROPOSAL_OPERATION_KIND: &str = "order.revision.proposal.v1";
+#[cfg(feature = "runtime")]
+pub const ORDER_REVISION_DECISION_OPERATION_KIND: &str = "order.revision.decision.v1";
+#[cfg(feature = "runtime")]
+pub const ORDER_CANCELLATION_OPERATION_KIND: &str = "order.cancellation.v1";
+#[cfg(feature = "runtime")]
+pub const ORDER_FULFILLMENT_UPDATE_OPERATION_KIND: &str = "order.fulfillment.update.v1";
+#[cfg(feature = "runtime")]
+pub const ORDER_RECEIPT_RECORD_OPERATION_KIND: &str = "order.receipt.record.v1";
#[cfg(feature = "runtime")]
const ORDER_REQUEST_CONTRACT_ID: &str = "radroots.order.request.v1";
#[cfg(feature = "runtime")]
const ORDER_DECISION_CONTRACT_ID: &str = "radroots.order.decision.v1";
+#[cfg(feature = "runtime")]
+const ORDER_REVISION_PROPOSAL_CONTRACT_ID: &str = "radroots.order.revision_proposal.v1";
+#[cfg(feature = "runtime")]
+const ORDER_REVISION_DECISION_CONTRACT_ID: &str = "radroots.order.revision_decision.v1";
+#[cfg(feature = "runtime")]
+const ORDER_CANCELLATION_CONTRACT_ID: &str = "radroots.order.cancellation.v1";
+#[cfg(feature = "runtime")]
+const ORDER_FULFILLMENT_UPDATE_CONTRACT_ID: &str = "radroots.order.fulfillment_update.v1";
+#[cfg(feature = "runtime")]
+const ORDER_RECEIPT_CONTRACT_ID: &str = "radroots.order.receipt.v1";
#[cfg(feature = "runtime")]
#[derive(Clone, Debug)]
@@ -399,382 +423,1663 @@ pub struct OrderDecisionReceipt {
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+#[derive(Clone, Debug)]
#[non_exhaustive]
-pub struct OrderStatusRequest {
- pub order_id: RadrootsOrderId,
- pub limit: u32,
+pub struct OrderRevisionProposalPrepareRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub proposal: RadrootsOrderRevisionProposal,
+ pub created_at: Option<RadrootsSdkTimestamp>,
}
#[cfg(feature = "runtime")]
-impl OrderStatusRequest {
- pub fn new(order_id: RadrootsOrderId) -> Self {
- Self {
- order_id,
- limit: ORDER_STATUS_DEFAULT_LIMIT,
- }
+impl serde::Serialize for OrderRevisionProposalPrepareRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderRevisionProposalPrepareRequest", 5)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("proposal", &self.proposal)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
}
+}
- pub fn parse(order_id: &str) -> Result<Self, RadrootsSdkError> {
- RadrootsOrderId::parse(order_id)
- .map(Self::new)
- .map_err(|error| RadrootsSdkError::invalid_order_id(order_id, error.to_string()))
+#[cfg(feature = "runtime")]
+impl OrderRevisionProposalPrepareRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ proposal: RadrootsOrderRevisionProposal,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ proposal,
+ created_at: None,
+ }
}
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = limit;
+ pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
+ self.created_at = Some(created_at);
self
}
-
- fn validate(&self) -> Result<(), RadrootsSdkError> {
- if self.limit == 0 || self.limit > ORDER_STATUS_MAX_LIMIT {
- return Err(RadrootsSdkError::order_status_limit_invalid(
- self.limit,
- 1,
- ORDER_STATUS_MAX_LIMIT,
- ));
- }
- Ok(())
- }
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct OrderStatusReceipt {
- pub order_id: RadrootsOrderId,
- pub source: SdkOrderStatusSource,
- pub found: bool,
- pub event_count: usize,
- pub limit_applied: u32,
- pub status: OrderStatusKind,
- pub fulfillment_status: Option<OrderFulfillmentStatusKind>,
- pub payment_state: OrderPaymentStateKind,
- pub settlement_state: OrderSettlementStateKind,
- pub lifecycle_terminal: bool,
- pub event_ids: Vec<RadrootsEventId>,
- pub request_event_id: Option<RadrootsEventId>,
- pub decision_event_id: Option<RadrootsEventId>,
- pub fulfillment_event_id: Option<RadrootsEventId>,
- pub cancellation_event_id: Option<RadrootsEventId>,
- pub receipt_event_id: Option<RadrootsEventId>,
- pub last_event_id: Option<RadrootsEventId>,
- pub issues: Vec<SdkOrderStatusIssue>,
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderRevisionProposalEnqueueRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub proposal: RadrootsOrderRevisionProposal,
+ pub target_relays: SdkRelayTargetPolicy,
+ pub idempotency_key: Option<SdkIdempotencyKey>,
+ pub created_at: Option<RadrootsSdkTimestamp>,
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum SdkOrderStatusSource {
- LocalEventStore,
+impl serde::Serialize for OrderRevisionProposalEnqueueRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderRevisionProposalEnqueueRequest", 7)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("proposal", &self.proposal)?;
+ state.serialize_field("target_relays", &self.target_relays)?;
+ state.serialize_field("idempotency_key", &self.idempotency_key)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
+ }
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum OrderStatusKind {
- Missing,
- Requested,
- Accepted,
- Declined,
- Cancelled,
- Completed,
- Disputed,
- Invalid,
+impl OrderRevisionProposalEnqueueRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ proposal: RadrootsOrderRevisionProposal,
+ target_relays: SdkRelayTargetPolicy,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ proposal,
+ target_relays,
+ idempotency_key: None,
+ created_at: None,
+ }
+ }
+
+ pub fn try_with_target_relays<I, S>(
+ mut self,
+ target_relays: I,
+ policy: SdkRelayUrlPolicy,
+ ) -> Result<Self, RadrootsSdkError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ {
+ self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, 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(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum OrderFulfillmentStatusKind {
- AcceptedNotFulfilled,
- Preparing,
- ReadyForPickup,
- OutForDelivery,
- Delivered,
- SellerCancelled,
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderRevisionProposalPlan {
+ 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: RadrootsFrozenEventDraft,
+ pub created_at: RadrootsSdkTimestamp,
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum OrderPaymentStateKind {
- NotRecorded,
- Recorded,
- Settled,
- Rejected,
- Invalid,
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderRevisionProposalReceipt {
+ 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")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
+#[derive(Clone, Debug)]
#[non_exhaustive]
-pub enum OrderSettlementStateKind {
- NotRequired,
- Pending,
- Accepted,
- Rejected,
- Invalid,
+pub struct OrderRevisionDecisionPrepareRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub decision: RadrootsOrderRevisionDecision,
+ pub created_at: Option<RadrootsSdkTimestamp>,
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct SdkOrderStatusIssue {
- pub kind: SdkOrderStatusIssueKind,
- pub event_ids: Vec<RadrootsEventId>,
+impl serde::Serialize for OrderRevisionDecisionPrepareRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderRevisionDecisionPrepareRequest", 5)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("decision", &self.decision)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
+ }
}
#[cfg(feature = "runtime")]
-impl SdkOrderStatusIssue {
- fn new(kind: SdkOrderStatusIssueKind, event_ids: Vec<RadrootsEventId>) -> Self {
- Self { kind, event_ids }
+impl OrderRevisionDecisionPrepareRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ decision: RadrootsOrderRevisionDecision,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ decision,
+ created_at: None,
+ }
}
- fn single(kind: SdkOrderStatusIssueKind, event_id: RadrootsEventId) -> Self {
- Self::new(kind, vec![event_id])
+ pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
+ self.created_at = Some(created_at);
+ self
}
+}
- pub fn code(&self) -> String {
- self.kind.code()
- }
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderRevisionDecisionEnqueueRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub decision: RadrootsOrderRevisionDecision,
+ pub target_relays: SdkRelayTargetPolicy,
+ pub idempotency_key: Option<SdkIdempotencyKey>,
+ pub created_at: Option<RadrootsSdkTimestamp>,
}
#[cfg(feature = "runtime")]
-impl serde::Serialize for SdkOrderStatusIssue {
+impl serde::Serialize for OrderRevisionDecisionEnqueueRequest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
- let mut state = serializer.serialize_struct("SdkOrderStatusIssue", 3)?;
- state.serialize_field("code", &self.code())?;
- state.serialize_field("kind", &self.kind)?;
- state.serialize_field("event_ids", &self.event_ids)?;
+ let mut state = serializer.serialize_struct("OrderRevisionDecisionEnqueueRequest", 7)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("decision", &self.decision)?;
+ state.serialize_field("target_relays", &self.target_relays)?;
+ state.serialize_field("idempotency_key", &self.idempotency_key)?;
+ state.serialize_field("created_at", &self.created_at)?;
state.end()
}
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum SdkOrderStatusIssueKind {
- MissingRequest,
- MultipleRequests,
- RequestPayloadInvalid,
- RequestOrderIdMismatch,
- RequestAuthorMismatch,
- RequestListingAddressInvalid,
- RequestSellerListingMismatch,
- DecisionPayloadInvalid,
- DecisionOrderIdMismatch,
- DecisionAuthorMismatch,
- DecisionCounterpartyMismatch,
- DecisionBuyerMismatch,
- DecisionSellerMismatch,
- DecisionListingAddressInvalid,
- DecisionListingMismatch,
- DecisionRootMismatch,
- DecisionPreviousMismatch,
- DecisionMissingInventoryCommitments,
- DecisionInventoryCommitmentMismatch,
- DecisionMissingReason,
- ConflictingDecisions,
- RevisionProposalWithoutAcceptedDecision,
- RevisionProposalPayloadInvalid,
- RevisionProposalOrderIdMismatch,
- RevisionProposalAuthorMismatch,
- RevisionProposalCounterpartyMismatch,
- RevisionProposalBuyerMismatch,
- RevisionProposalSellerMismatch,
- RevisionProposalListingAddressInvalid,
- RevisionProposalListingMismatch,
- RevisionProposalRootMismatch,
- RevisionProposalPreviousMismatch,
- RevisionDecisionWithoutProposal,
- RevisionDecisionPayloadInvalid,
- RevisionDecisionOrderIdMismatch,
- RevisionDecisionAuthorMismatch,
- RevisionDecisionCounterpartyMismatch,
- RevisionDecisionBuyerMismatch,
- RevisionDecisionSellerMismatch,
- RevisionDecisionListingAddressInvalid,
- RevisionDecisionListingMismatch,
- RevisionDecisionRootMismatch,
- RevisionDecisionPreviousMismatch,
- RevisionDecisionRevisionIdMismatch,
- FulfillmentWithoutAcceptedDecision,
- FulfillmentPayloadInvalid,
- FulfillmentOrderIdMismatch,
- FulfillmentAuthorMismatch,
- FulfillmentCounterpartyMismatch,
- FulfillmentBuyerMismatch,
- FulfillmentSellerMismatch,
- FulfillmentListingAddressInvalid,
- FulfillmentListingMismatch,
- FulfillmentRootMismatch,
- FulfillmentPreviousMismatch,
- FulfillmentStatusNotPublishable,
- FulfillmentUnsupportedTransition,
- ForkedFulfillments,
- CancellationWithoutCancellableOrder,
- CancellationPayloadInvalid,
- CancellationOrderIdMismatch,
- CancellationAuthorMismatch,
- CancellationCounterpartyMismatch,
- CancellationBuyerMismatch,
- CancellationSellerMismatch,
- CancellationListingAddressInvalid,
- CancellationListingMismatch,
- CancellationRootMismatch,
- CancellationPreviousMismatch,
- CancellationAfterFulfillment,
- ReceiptWithoutEligibleFulfillment,
- ReceiptPayloadInvalid,
- ReceiptOrderIdMismatch,
- ReceiptAuthorMismatch,
- ReceiptCounterpartyMismatch,
- ReceiptBuyerMismatch,
- ReceiptSellerMismatch,
- ReceiptListingAddressInvalid,
- ReceiptListingMismatch,
- ReceiptRootMismatch,
- ReceiptPreviousMismatch,
- PaymentWithoutAcceptedAgreement,
- PaymentPayloadInvalid,
- PaymentOrderIdMismatch,
- PaymentAuthorMismatch,
- PaymentCounterpartyMismatch,
- PaymentBuyerMismatch,
- PaymentSellerMismatch,
- PaymentListingAddressInvalid,
- PaymentListingMismatch,
- PaymentRootMismatch,
- PaymentPreviousMismatch,
- PaymentAgreementMismatch,
- PaymentQuoteMismatch,
- PaymentQuoteVersionMismatch,
- PaymentEconomicsDigestMismatch,
- PaymentAmountMismatch,
- PaymentCurrencyMismatch,
- PaymentAfterCancellation,
- RevisionAfterPayment,
- DuplicatePayments,
- SettlementWithoutValidPayment,
- SettlementPayloadInvalid,
- SettlementOrderIdMismatch,
- SettlementAuthorMismatch,
- SettlementCounterpartyMismatch,
- SettlementBuyerMismatch,
- SettlementSellerMismatch,
- SettlementListingAddressInvalid,
- SettlementListingMismatch,
- SettlementRootMismatch,
- SettlementPreviousMismatch,
- SettlementPaymentEventMismatch,
- SettlementAgreementMismatch,
- SettlementQuoteMismatch,
- SettlementQuoteVersionMismatch,
- SettlementEconomicsDigestMismatch,
- SettlementAmountMismatch,
- SettlementCurrencyMismatch,
- DuplicateSettlements,
- ForkedLifecycle,
+impl OrderRevisionDecisionEnqueueRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ decision: RadrootsOrderRevisionDecision,
+ target_relays: SdkRelayTargetPolicy,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ decision,
+ target_relays,
+ idempotency_key: None,
+ created_at: None,
+ }
+ }
+
+ pub fn try_with_target_relays<I, S>(
+ mut self,
+ target_relays: I,
+ policy: SdkRelayUrlPolicy,
+ ) -> Result<Self, RadrootsSdkError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ {
+ self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, 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(feature = "runtime")]
-impl SdkOrderStatusIssueKind {
- pub fn code(self) -> String {
- camel_to_snake(format!("{self:?}").as_str())
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderRevisionDecisionPlan {
+ 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: RadrootsFrozenEventDraft,
+ pub created_at: RadrootsSdkTimestamp,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderRevisionDecisionReceipt {
+ 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")]
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderCancellationPrepareRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub cancellation: RadrootsOrderCancellation,
+ pub created_at: Option<RadrootsSdkTimestamp>,
+}
+
+#[cfg(feature = "runtime")]
+impl serde::Serialize for OrderCancellationPrepareRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderCancellationPrepareRequest", 5)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("cancellation", &self.cancellation)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
}
}
#[cfg(feature = "runtime")]
-impl<'sdk> OrdersClient<'sdk> {
- pub async fn ingest_request_evidence(
- &self,
- request: OrderRequestEvidenceIngestRequest,
- ) -> Result<OrderRequestEvidenceIngestReceipt, RadrootsSdkError> {
- let evidence = parse_order_request_evidence(&request.event)?;
- let observed_at = self.resolved_created_at(request.observed_at)?;
- let observed_at_ms = sdk_timestamp_ms(observed_at)?;
- let receipt = self
- .sdk
- ._event_store
- .ingest_event(RadrootsEventIngest::new(request.event, observed_at_ms))
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Ok(OrderRequestEvidenceIngestReceipt {
- order_id: evidence.order_id,
- listing_addr: evidence.listing_addr,
- buyer_pubkey: evidence.buyer_pubkey,
- seller_pubkey: evidence.seller_pubkey,
- request_event_id: evidence.request_event_id,
- local_event_seq: receipt.seq,
- inserted: receipt.inserted,
+impl OrderCancellationPrepareRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ 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")]
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderCancellationEnqueueRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub cancellation: RadrootsOrderCancellation,
+ pub target_relays: SdkRelayTargetPolicy,
+ pub idempotency_key: Option<SdkIdempotencyKey>,
+ pub created_at: Option<RadrootsSdkTimestamp>,
+}
+
+#[cfg(feature = "runtime")]
+impl serde::Serialize for OrderCancellationEnqueueRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderCancellationEnqueueRequest", 7)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("cancellation", &self.cancellation)?;
+ state.serialize_field("target_relays", &self.target_relays)?;
+ state.serialize_field("idempotency_key", &self.idempotency_key)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
+ }
+}
+
+#[cfg(feature = "runtime")]
+impl OrderCancellationEnqueueRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ cancellation: RadrootsOrderCancellation,
+ target_relays: SdkRelayTargetPolicy,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ cancellation,
+ target_relays,
+ idempotency_key: None,
+ created_at: None,
+ }
+ }
+
+ pub fn try_with_target_relays<I, S>(
+ mut self,
+ target_relays: I,
+ policy: SdkRelayUrlPolicy,
+ ) -> Result<Self, RadrootsSdkError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ {
+ self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, 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(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderCancellationPlan {
+ 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: RadrootsFrozenEventDraft,
+ pub created_at: RadrootsSdkTimestamp,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderCancellationReceipt {
+ 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")]
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderFulfillmentUpdatePrepareRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub fulfillment: RadrootsOrderFulfillmentUpdate,
+ pub created_at: Option<RadrootsSdkTimestamp>,
+}
+
+#[cfg(feature = "runtime")]
+impl serde::Serialize for OrderFulfillmentUpdatePrepareRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderFulfillmentUpdatePrepareRequest", 5)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("fulfillment", &self.fulfillment)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
+ }
+}
+
+#[cfg(feature = "runtime")]
+impl OrderFulfillmentUpdatePrepareRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ fulfillment: RadrootsOrderFulfillmentUpdate,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ fulfillment,
+ created_at: None,
+ }
+ }
+
+ pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
+ self.created_at = Some(created_at);
+ self
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderFulfillmentUpdateEnqueueRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub fulfillment: RadrootsOrderFulfillmentUpdate,
+ pub target_relays: SdkRelayTargetPolicy,
+ pub idempotency_key: Option<SdkIdempotencyKey>,
+ pub created_at: Option<RadrootsSdkTimestamp>,
+}
+
+#[cfg(feature = "runtime")]
+impl serde::Serialize for OrderFulfillmentUpdateEnqueueRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderFulfillmentUpdateEnqueueRequest", 7)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("fulfillment", &self.fulfillment)?;
+ state.serialize_field("target_relays", &self.target_relays)?;
+ state.serialize_field("idempotency_key", &self.idempotency_key)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
+ }
+}
+
+#[cfg(feature = "runtime")]
+impl OrderFulfillmentUpdateEnqueueRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ fulfillment: RadrootsOrderFulfillmentUpdate,
+ target_relays: SdkRelayTargetPolicy,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ fulfillment,
+ target_relays,
+ idempotency_key: None,
+ created_at: None,
+ }
+ }
+
+ pub fn try_with_target_relays<I, S>(
+ mut self,
+ target_relays: I,
+ policy: SdkRelayUrlPolicy,
+ ) -> Result<Self, RadrootsSdkError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ {
+ self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, 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(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderFulfillmentUpdatePlan {
+ 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: RadrootsFrozenEventDraft,
+ pub created_at: RadrootsSdkTimestamp,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderFulfillmentUpdateReceipt {
+ 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")]
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderReceiptRecordPrepareRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub receipt: RadrootsOrderReceipt,
+ pub created_at: Option<RadrootsSdkTimestamp>,
+}
+
+#[cfg(feature = "runtime")]
+impl serde::Serialize for OrderReceiptRecordPrepareRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderReceiptRecordPrepareRequest", 5)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("receipt", &self.receipt)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
+ }
+}
+
+#[cfg(feature = "runtime")]
+impl OrderReceiptRecordPrepareRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ receipt: RadrootsOrderReceipt,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ receipt,
+ created_at: None,
+ }
+ }
+
+ pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
+ self.created_at = Some(created_at);
+ self
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct OrderReceiptRecordEnqueueRequest {
+ pub actor: RadrootsActorContext,
+ pub root_event: RadrootsNostrEventPtr,
+ pub previous_event: RadrootsNostrEventPtr,
+ pub receipt: RadrootsOrderReceipt,
+ pub target_relays: SdkRelayTargetPolicy,
+ pub idempotency_key: Option<SdkIdempotencyKey>,
+ pub created_at: Option<RadrootsSdkTimestamp>,
+}
+
+#[cfg(feature = "runtime")]
+impl serde::Serialize for OrderReceiptRecordEnqueueRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("OrderReceiptRecordEnqueueRequest", 7)?;
+ state.serialize_field("actor", &SdkActorContextJson(&self.actor))?;
+ state.serialize_field("root_event", &self.root_event)?;
+ state.serialize_field("previous_event", &self.previous_event)?;
+ state.serialize_field("receipt", &self.receipt)?;
+ state.serialize_field("target_relays", &self.target_relays)?;
+ state.serialize_field("idempotency_key", &self.idempotency_key)?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.end()
+ }
+}
+
+#[cfg(feature = "runtime")]
+impl OrderReceiptRecordEnqueueRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ receipt: RadrootsOrderReceipt,
+ target_relays: SdkRelayTargetPolicy,
+ ) -> Self {
+ Self {
+ actor,
+ root_event,
+ previous_event,
+ receipt,
+ target_relays,
+ idempotency_key: None,
+ created_at: None,
+ }
+ }
+
+ pub fn try_with_target_relays<I, S>(
+ mut self,
+ target_relays: I,
+ policy: SdkRelayUrlPolicy,
+ ) -> Result<Self, RadrootsSdkError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ {
+ self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, 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(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderReceiptRecordPlan {
+ 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: RadrootsFrozenEventDraft,
+ pub created_at: RadrootsSdkTimestamp,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderReceiptRecordReceipt {
+ 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")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+#[non_exhaustive]
+pub struct OrderStatusRequest {
+ pub order_id: RadrootsOrderId,
+ pub limit: u32,
+}
+
+#[cfg(feature = "runtime")]
+impl OrderStatusRequest {
+ pub fn new(order_id: RadrootsOrderId) -> Self {
+ Self {
+ order_id,
+ limit: ORDER_STATUS_DEFAULT_LIMIT,
+ }
+ }
+
+ pub fn parse(order_id: &str) -> Result<Self, RadrootsSdkError> {
+ RadrootsOrderId::parse(order_id)
+ .map(Self::new)
+ .map_err(|error| RadrootsSdkError::invalid_order_id(order_id, error.to_string()))
+ }
+
+ pub fn with_limit(mut self, limit: u32) -> Self {
+ self.limit = limit;
+ self
+ }
+
+ fn validate(&self) -> Result<(), RadrootsSdkError> {
+ if self.limit == 0 || self.limit > ORDER_STATUS_MAX_LIMIT {
+ return Err(RadrootsSdkError::order_status_limit_invalid(
+ self.limit,
+ 1,
+ ORDER_STATUS_MAX_LIMIT,
+ ));
+ }
+ Ok(())
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+pub struct OrderStatusReceipt {
+ pub order_id: RadrootsOrderId,
+ pub source: SdkOrderStatusSource,
+ pub found: bool,
+ pub event_count: usize,
+ pub limit_applied: u32,
+ pub status: OrderStatusKind,
+ pub fulfillment_status: Option<OrderFulfillmentStatusKind>,
+ pub payment_state: OrderPaymentStateKind,
+ pub settlement_state: OrderSettlementStateKind,
+ pub lifecycle_terminal: bool,
+ pub event_ids: Vec<RadrootsEventId>,
+ pub request_event_id: Option<RadrootsEventId>,
+ pub decision_event_id: Option<RadrootsEventId>,
+ pub fulfillment_event_id: Option<RadrootsEventId>,
+ pub cancellation_event_id: Option<RadrootsEventId>,
+ pub receipt_event_id: Option<RadrootsEventId>,
+ pub last_event_id: Option<RadrootsEventId>,
+ pub issues: Vec<SdkOrderStatusIssue>,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "snake_case")]
+#[non_exhaustive]
+pub enum SdkOrderStatusSource {
+ LocalEventStore,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "snake_case")]
+#[non_exhaustive]
+pub enum OrderStatusKind {
+ Missing,
+ Requested,
+ Accepted,
+ Declined,
+ Cancelled,
+ Completed,
+ Disputed,
+ Invalid,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "snake_case")]
+#[non_exhaustive]
+pub enum OrderFulfillmentStatusKind {
+ AcceptedNotFulfilled,
+ Preparing,
+ ReadyForPickup,
+ OutForDelivery,
+ Delivered,
+ SellerCancelled,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "snake_case")]
+#[non_exhaustive]
+pub enum OrderPaymentStateKind {
+ NotRecorded,
+ Recorded,
+ Settled,
+ Rejected,
+ Invalid,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "snake_case")]
+#[non_exhaustive]
+pub enum OrderSettlementStateKind {
+ NotRequired,
+ Pending,
+ Accepted,
+ Rejected,
+ Invalid,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SdkOrderStatusIssue {
+ pub kind: SdkOrderStatusIssueKind,
+ pub event_ids: Vec<RadrootsEventId>,
+}
+
+#[cfg(feature = "runtime")]
+impl SdkOrderStatusIssue {
+ fn new(kind: SdkOrderStatusIssueKind, event_ids: Vec<RadrootsEventId>) -> Self {
+ Self { kind, event_ids }
+ }
+
+ fn single(kind: SdkOrderStatusIssueKind, event_id: RadrootsEventId) -> Self {
+ Self::new(kind, vec![event_id])
+ }
+
+ pub fn code(&self) -> String {
+ self.kind.code()
+ }
+}
+
+#[cfg(feature = "runtime")]
+impl serde::Serialize for SdkOrderStatusIssue {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("SdkOrderStatusIssue", 3)?;
+ state.serialize_field("code", &self.code())?;
+ state.serialize_field("kind", &self.kind)?;
+ state.serialize_field("event_ids", &self.event_ids)?;
+ state.end()
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "snake_case")]
+#[non_exhaustive]
+pub enum SdkOrderStatusIssueKind {
+ MissingRequest,
+ MultipleRequests,
+ RequestPayloadInvalid,
+ RequestOrderIdMismatch,
+ RequestAuthorMismatch,
+ RequestListingAddressInvalid,
+ RequestSellerListingMismatch,
+ DecisionPayloadInvalid,
+ DecisionOrderIdMismatch,
+ DecisionAuthorMismatch,
+ DecisionCounterpartyMismatch,
+ DecisionBuyerMismatch,
+ DecisionSellerMismatch,
+ DecisionListingAddressInvalid,
+ DecisionListingMismatch,
+ DecisionRootMismatch,
+ DecisionPreviousMismatch,
+ DecisionMissingInventoryCommitments,
+ DecisionInventoryCommitmentMismatch,
+ DecisionMissingReason,
+ ConflictingDecisions,
+ RevisionProposalWithoutAcceptedDecision,
+ RevisionProposalPayloadInvalid,
+ RevisionProposalOrderIdMismatch,
+ RevisionProposalAuthorMismatch,
+ RevisionProposalCounterpartyMismatch,
+ RevisionProposalBuyerMismatch,
+ RevisionProposalSellerMismatch,
+ RevisionProposalListingAddressInvalid,
+ RevisionProposalListingMismatch,
+ RevisionProposalRootMismatch,
+ RevisionProposalPreviousMismatch,
+ RevisionDecisionWithoutProposal,
+ RevisionDecisionPayloadInvalid,
+ RevisionDecisionOrderIdMismatch,
+ RevisionDecisionAuthorMismatch,
+ RevisionDecisionCounterpartyMismatch,
+ RevisionDecisionBuyerMismatch,
+ RevisionDecisionSellerMismatch,
+ RevisionDecisionListingAddressInvalid,
+ RevisionDecisionListingMismatch,
+ RevisionDecisionRootMismatch,
+ RevisionDecisionPreviousMismatch,
+ RevisionDecisionRevisionIdMismatch,
+ FulfillmentWithoutAcceptedDecision,
+ FulfillmentPayloadInvalid,
+ FulfillmentOrderIdMismatch,
+ FulfillmentAuthorMismatch,
+ FulfillmentCounterpartyMismatch,
+ FulfillmentBuyerMismatch,
+ FulfillmentSellerMismatch,
+ FulfillmentListingAddressInvalid,
+ FulfillmentListingMismatch,
+ FulfillmentRootMismatch,
+ FulfillmentPreviousMismatch,
+ FulfillmentStatusNotPublishable,
+ FulfillmentUnsupportedTransition,
+ ForkedFulfillments,
+ CancellationWithoutCancellableOrder,
+ CancellationPayloadInvalid,
+ CancellationOrderIdMismatch,
+ CancellationAuthorMismatch,
+ CancellationCounterpartyMismatch,
+ CancellationBuyerMismatch,
+ CancellationSellerMismatch,
+ CancellationListingAddressInvalid,
+ CancellationListingMismatch,
+ CancellationRootMismatch,
+ CancellationPreviousMismatch,
+ CancellationAfterFulfillment,
+ ReceiptWithoutEligibleFulfillment,
+ ReceiptPayloadInvalid,
+ ReceiptOrderIdMismatch,
+ ReceiptAuthorMismatch,
+ ReceiptCounterpartyMismatch,
+ ReceiptBuyerMismatch,
+ ReceiptSellerMismatch,
+ ReceiptListingAddressInvalid,
+ ReceiptListingMismatch,
+ ReceiptRootMismatch,
+ ReceiptPreviousMismatch,
+ PaymentWithoutAcceptedAgreement,
+ PaymentPayloadInvalid,
+ PaymentOrderIdMismatch,
+ PaymentAuthorMismatch,
+ PaymentCounterpartyMismatch,
+ PaymentBuyerMismatch,
+ PaymentSellerMismatch,
+ PaymentListingAddressInvalid,
+ PaymentListingMismatch,
+ PaymentRootMismatch,
+ PaymentPreviousMismatch,
+ PaymentAgreementMismatch,
+ PaymentQuoteMismatch,
+ PaymentQuoteVersionMismatch,
+ PaymentEconomicsDigestMismatch,
+ PaymentAmountMismatch,
+ PaymentCurrencyMismatch,
+ PaymentAfterCancellation,
+ RevisionAfterPayment,
+ DuplicatePayments,
+ SettlementWithoutValidPayment,
+ SettlementPayloadInvalid,
+ SettlementOrderIdMismatch,
+ SettlementAuthorMismatch,
+ SettlementCounterpartyMismatch,
+ SettlementBuyerMismatch,
+ SettlementSellerMismatch,
+ SettlementListingAddressInvalid,
+ SettlementListingMismatch,
+ SettlementRootMismatch,
+ SettlementPreviousMismatch,
+ SettlementPaymentEventMismatch,
+ SettlementAgreementMismatch,
+ SettlementQuoteMismatch,
+ SettlementQuoteVersionMismatch,
+ SettlementEconomicsDigestMismatch,
+ SettlementAmountMismatch,
+ SettlementCurrencyMismatch,
+ DuplicateSettlements,
+ ForkedLifecycle,
+}
+
+#[cfg(feature = "runtime")]
+impl SdkOrderStatusIssueKind {
+ pub fn code(self) -> String {
+ camel_to_snake(format!("{self:?}").as_str())
+ }
+}
+
+#[cfg(feature = "runtime")]
+impl<'sdk> OrdersClient<'sdk> {
+ pub async fn ingest_request_evidence(
+ &self,
+ request: OrderRequestEvidenceIngestRequest,
+ ) -> Result<OrderRequestEvidenceIngestReceipt, RadrootsSdkError> {
+ let evidence = parse_order_request_evidence(&request.event)?;
+ let observed_at = self.resolved_created_at(request.observed_at)?;
+ let observed_at_ms = sdk_timestamp_ms(observed_at)?;
+ let receipt = self
+ .sdk
+ ._event_store
+ .ingest_event(RadrootsEventIngest::new(request.event, observed_at_ms))
+ .await
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })?;
+ Ok(OrderRequestEvidenceIngestReceipt {
+ order_id: evidence.order_id,
+ listing_addr: evidence.listing_addr,
+ buyer_pubkey: evidence.buyer_pubkey,
+ seller_pubkey: evidence.seller_pubkey,
+ request_event_id: evidence.request_event_id,
+ local_event_seq: receipt.seq,
+ inserted: receipt.inserted,
+ })
+ }
+
+ pub fn prepare_submit(
+ &self,
+ request: OrderSubmitPrepareRequest,
+ ) -> Result<OrderSubmitPlan, RadrootsSdkError> {
+ let created_at = self.resolved_created_at(request.created_at)?;
+ order_submit_plan(
+ &request.actor,
+ request.listing_event,
+ request.order,
+ created_at,
+ )
+ }
+
+ pub async fn enqueue_submit<S>(
+ &self,
+ request: OrderSubmitEnqueueRequest,
+ signer: &S,
+ ) -> Result<OrderSubmitReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ let OrderSubmitEnqueueRequest {
+ actor,
+ listing_event,
+ order,
+ target_relays,
+ idempotency_key,
+ created_at,
+ } = request;
+ let prepare_request = OrderSubmitPrepareRequest {
+ actor: actor.clone(),
+ listing_event,
+ order,
+ created_at,
+ };
+ let plan = self.prepare_submit(prepare_request)?;
+ self.enqueue_prepared_submit(&actor, plan, target_relays, idempotency_key, signer)
+ .await
+ }
+
+ pub async fn enqueue_prepared_submit<S>(
+ &self,
+ actor: &RadrootsActorContext,
+ plan: OrderSubmitPlan,
+ target_relays: SdkRelayTargetPolicy,
+ idempotency_key: Option<SdkIdempotencyKey>,
+ signer: &S,
+ ) -> Result<OrderSubmitReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ let enqueue = enqueue_signed_workflow(
+ self.sdk,
+ SdkWorkflowEnqueueRequest {
+ operation_kind: ORDER_SUBMIT_OPERATION_KIND,
+ actor,
+ frozen_draft: &plan.frozen_draft,
+ target_relays,
+ idempotency_key,
+ },
+ signer,
+ )
+ .await?;
+ Ok(OrderSubmitReceipt {
+ order_id: plan.order_id,
+ listing_addr: plan.listing_addr,
+ listing_event_id: plan.listing_event_id,
+ expected_event_id: plan.expected_event_id,
+ signed_event_id: enqueue.signed_event_id,
+ local_event_seq: enqueue.local_event_seq,
+ outbox_operation_id: enqueue.outbox_operation_id,
+ outbox_event_id: enqueue.outbox_event_id,
+ state: enqueue.state.into(),
+ idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix),
+ })
+ }
+
+ pub fn prepare_decision(
+ &self,
+ request: OrderDecisionPrepareRequest,
+ ) -> Result<OrderDecisionPlan, RadrootsSdkError> {
+ let created_at = self.resolved_created_at(request.created_at)?;
+ order_decision_plan(
+ &request.actor,
+ request.request_event,
+ request.decision,
+ created_at,
+ )
+ }
+
+ pub async fn enqueue_decision<S>(
+ &self,
+ request: OrderDecisionEnqueueRequest,
+ signer: &S,
+ ) -> Result<OrderDecisionReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ let OrderDecisionEnqueueRequest {
+ actor,
+ request_event,
+ decision,
+ target_relays,
+ idempotency_key,
+ created_at,
+ } = request;
+ let prepare_request = OrderDecisionPrepareRequest {
+ actor: actor.clone(),
+ request_event,
+ decision,
+ created_at,
+ };
+ let plan = self.prepare_decision(prepare_request)?;
+ self.enqueue_prepared_decision(&actor, plan, target_relays, idempotency_key, signer)
+ .await
+ }
+
+ pub async fn enqueue_prepared_decision<S>(
+ &self,
+ actor: &RadrootsActorContext,
+ plan: OrderDecisionPlan,
+ target_relays: SdkRelayTargetPolicy,
+ idempotency_key: Option<SdkIdempotencyKey>,
+ signer: &S,
+ ) -> Result<OrderDecisionReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ 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: ORDER_DECISION_OPERATION_KIND,
+ actor,
+ frozen_draft: &plan.frozen_draft,
+ target_relays,
+ idempotency_key,
+ },
+ signer,
+ )
+ .await?;
+ Ok(OrderDecisionReceipt {
+ order_id: plan.order_id,
+ listing_addr: plan.listing_addr,
+ buyer_pubkey: plan.buyer_pubkey,
+ seller_pubkey: plan.seller_pubkey,
+ request_event_id: plan.request_event_id,
+ expected_event_id: plan.expected_event_id,
+ signed_event_id: enqueue.signed_event_id,
+ local_event_seq: enqueue.local_event_seq,
+ outbox_operation_id: enqueue.outbox_operation_id,
+ outbox_event_id: enqueue.outbox_event_id,
+ state: enqueue.state.into(),
+ idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix),
+ })
+ }
+
+ pub fn prepare_revision_proposal(
+ &self,
+ request: OrderRevisionProposalPrepareRequest,
+ ) -> Result<OrderRevisionProposalPlan, 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,
+ )
+ }
+
+ pub async fn enqueue_revision_proposal<S>(
+ &self,
+ request: OrderRevisionProposalEnqueueRequest,
+ signer: &S,
+ ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ let OrderRevisionProposalEnqueueRequest {
+ actor,
+ root_event,
+ previous_event,
+ proposal,
+ target_relays,
+ idempotency_key,
+ created_at,
+ } = request;
+ let prepare_request = OrderRevisionProposalPrepareRequest {
+ 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_relays,
+ idempotency_key,
+ signer,
+ )
+ .await
+ }
+
+ pub async fn enqueue_prepared_revision_proposal<S>(
+ &self,
+ actor: &RadrootsActorContext,
+ plan: OrderRevisionProposalPlan,
+ target_relays: SdkRelayTargetPolicy,
+ idempotency_key: Option<SdkIdempotencyKey>,
+ signer: &S,
+ ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ 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: ORDER_REVISION_PROPOSAL_OPERATION_KIND,
+ actor,
+ frozen_draft: &plan.frozen_draft,
+ target_relays,
+ idempotency_key,
+ },
+ signer,
+ )
+ .await?;
+ Ok(OrderRevisionProposalReceipt {
+ 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),
+ })
+ }
+
+ pub fn prepare_revision_decision(
+ &self,
+ request: OrderRevisionDecisionPrepareRequest,
+ ) -> Result<OrderRevisionDecisionPlan, 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,
+ )
+ }
+
+ pub async fn enqueue_revision_decision<S>(
+ &self,
+ request: OrderRevisionDecisionEnqueueRequest,
+ signer: &S,
+ ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ let OrderRevisionDecisionEnqueueRequest {
+ actor,
+ root_event,
+ previous_event,
+ decision,
+ target_relays,
+ idempotency_key,
+ created_at,
+ } = request;
+ let prepare_request = OrderRevisionDecisionPrepareRequest {
+ 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_relays,
+ idempotency_key,
+ signer,
+ )
+ .await
+ }
+
+ pub async fn enqueue_prepared_revision_decision<S>(
+ &self,
+ actor: &RadrootsActorContext,
+ plan: OrderRevisionDecisionPlan,
+ target_relays: SdkRelayTargetPolicy,
+ idempotency_key: Option<SdkIdempotencyKey>,
+ signer: &S,
+ ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ 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: ORDER_REVISION_DECISION_OPERATION_KIND,
+ actor,
+ frozen_draft: &plan.frozen_draft,
+ target_relays,
+ idempotency_key,
+ },
+ signer,
+ )
+ .await?;
+ Ok(OrderRevisionDecisionReceipt {
+ 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),
+ })
+ }
+
+ pub fn prepare_cancellation(
+ &self,
+ request: OrderCancellationPrepareRequest,
+ ) -> Result<OrderCancellationPlan, 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,
+ )
+ }
+
+ pub async fn enqueue_cancellation<S>(
+ &self,
+ request: OrderCancellationEnqueueRequest,
+ signer: &S,
+ ) -> Result<OrderCancellationReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ let OrderCancellationEnqueueRequest {
+ actor,
+ root_event,
+ previous_event,
+ cancellation,
+ target_relays,
+ idempotency_key,
+ created_at,
+ } = request;
+ let prepare_request = OrderCancellationPrepareRequest {
+ actor: actor.clone(),
+ root_event,
+ previous_event,
+ cancellation,
+ created_at,
+ };
+ let plan = self.prepare_cancellation(prepare_request)?;
+ self.enqueue_prepared_cancellation(&actor, plan, target_relays, idempotency_key, signer)
+ .await
+ }
+
+ pub async fn enqueue_prepared_cancellation<S>(
+ &self,
+ actor: &RadrootsActorContext,
+ plan: OrderCancellationPlan,
+ target_relays: SdkRelayTargetPolicy,
+ idempotency_key: Option<SdkIdempotencyKey>,
+ signer: &S,
+ ) -> Result<OrderCancellationReceipt, RadrootsSdkError>
+ where
+ S: RadrootsEventSigner + ?Sized,
+ {
+ if !self
+ .prepared_order_event_exists(&plan.expected_event_id)
+ .await?
+ {
+ self.require_cancellation_preflight(&plan).await?;
+ }
+ let enqueue = enqueue_signed_workflow(
+ self.sdk,
+ SdkWorkflowEnqueueRequest {
+ operation_kind: ORDER_CANCELLATION_OPERATION_KIND,
+ actor,
+ frozen_draft: &plan.frozen_draft,
+ target_relays,
+ idempotency_key,
+ },
+ signer,
+ )
+ .await?;
+ Ok(OrderCancellationReceipt {
+ 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),
})
}
- pub fn prepare_submit(
+ pub fn prepare_fulfillment_update(
&self,
- request: OrderSubmitPrepareRequest,
- ) -> Result<OrderSubmitPlan, RadrootsSdkError> {
+ request: OrderFulfillmentUpdatePrepareRequest,
+ ) -> Result<OrderFulfillmentUpdatePlan, RadrootsSdkError> {
let created_at = self.resolved_created_at(request.created_at)?;
- order_submit_plan(
+ order_fulfillment_update_plan(
&request.actor,
- request.listing_event,
- request.order,
+ request.root_event,
+ request.previous_event,
+ request.fulfillment,
created_at,
)
}
- pub async fn enqueue_submit<S>(
+ pub async fn enqueue_fulfillment_update<S>(
&self,
- request: OrderSubmitEnqueueRequest,
+ request: OrderFulfillmentUpdateEnqueueRequest,
signer: &S,
- ) -> Result<OrderSubmitReceipt, RadrootsSdkError>
+ ) -> Result<OrderFulfillmentUpdateReceipt, RadrootsSdkError>
where
S: RadrootsEventSigner + ?Sized,
{
- let OrderSubmitEnqueueRequest {
+ let OrderFulfillmentUpdateEnqueueRequest {
actor,
- listing_event,
- order,
+ root_event,
+ previous_event,
+ fulfillment,
target_relays,
idempotency_key,
created_at,
} = request;
- let prepare_request = OrderSubmitPrepareRequest {
+ let prepare_request = OrderFulfillmentUpdatePrepareRequest {
actor: actor.clone(),
- listing_event,
- order,
+ root_event,
+ previous_event,
+ fulfillment,
created_at,
};
- let plan = self.prepare_submit(prepare_request)?;
- self.enqueue_prepared_submit(&actor, plan, target_relays, idempotency_key, signer)
- .await
+ let plan = self.prepare_fulfillment_update(prepare_request)?;
+ self.enqueue_prepared_fulfillment_update(
+ &actor,
+ plan,
+ target_relays,
+ idempotency_key,
+ signer,
+ )
+ .await
}
- pub async fn enqueue_prepared_submit<S>(
+ pub async fn enqueue_prepared_fulfillment_update<S>(
&self,
actor: &RadrootsActorContext,
- plan: OrderSubmitPlan,
+ plan: OrderFulfillmentUpdatePlan,
target_relays: SdkRelayTargetPolicy,
idempotency_key: Option<SdkIdempotencyKey>,
signer: &S,
- ) -> Result<OrderSubmitReceipt, RadrootsSdkError>
+ ) -> Result<OrderFulfillmentUpdateReceipt, RadrootsSdkError>
where
S: RadrootsEventSigner + ?Sized,
{
+ if !self
+ .prepared_order_event_exists(&plan.expected_event_id)
+ .await?
+ {
+ self.require_fulfillment_update_preflight(&plan).await?;
+ }
let enqueue = enqueue_signed_workflow(
self.sdk,
SdkWorkflowEnqueueRequest {
- operation_kind: ORDER_SUBMIT_OPERATION_KIND,
+ operation_kind: ORDER_FULFILLMENT_UPDATE_OPERATION_KIND,
actor,
frozen_draft: &plan.frozen_draft,
target_relays,
@@ -783,10 +2088,13 @@ impl<'sdk> OrdersClient<'sdk> {
signer,
)
.await?;
- Ok(OrderSubmitReceipt {
+ Ok(OrderFulfillmentUpdateReceipt {
order_id: plan.order_id,
listing_addr: plan.listing_addr,
- listing_event_id: plan.listing_event_id,
+ 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,
@@ -797,62 +2105,70 @@ impl<'sdk> OrdersClient<'sdk> {
})
}
- pub fn prepare_decision(
+ pub fn prepare_receipt_record(
&self,
- request: OrderDecisionPrepareRequest,
- ) -> Result<OrderDecisionPlan, RadrootsSdkError> {
+ request: OrderReceiptRecordPrepareRequest,
+ ) -> Result<OrderReceiptRecordPlan, RadrootsSdkError> {
let created_at = self.resolved_created_at(request.created_at)?;
- order_decision_plan(
+ order_receipt_record_plan(
&request.actor,
- request.request_event,
- request.decision,
+ request.root_event,
+ request.previous_event,
+ request.receipt,
created_at,
)
}
- pub async fn enqueue_decision<S>(
+ pub async fn enqueue_receipt_record<S>(
&self,
- request: OrderDecisionEnqueueRequest,
+ request: OrderReceiptRecordEnqueueRequest,
signer: &S,
- ) -> Result<OrderDecisionReceipt, RadrootsSdkError>
+ ) -> Result<OrderReceiptRecordReceipt, RadrootsSdkError>
where
S: RadrootsEventSigner + ?Sized,
{
- let OrderDecisionEnqueueRequest {
+ let OrderReceiptRecordEnqueueRequest {
actor,
- request_event,
- decision,
+ root_event,
+ previous_event,
+ receipt,
target_relays,
idempotency_key,
created_at,
} = request;
- let prepare_request = OrderDecisionPrepareRequest {
+ let prepare_request = OrderReceiptRecordPrepareRequest {
actor: actor.clone(),
- request_event,
- decision,
+ root_event,
+ previous_event,
+ receipt,
created_at,
};
- let plan = self.prepare_decision(prepare_request)?;
- self.enqueue_prepared_decision(&actor, plan, target_relays, idempotency_key, signer)
+ let plan = self.prepare_receipt_record(prepare_request)?;
+ self.enqueue_prepared_receipt_record(&actor, plan, target_relays, idempotency_key, signer)
.await
}
- pub async fn enqueue_prepared_decision<S>(
+ pub async fn enqueue_prepared_receipt_record<S>(
&self,
actor: &RadrootsActorContext,
- plan: OrderDecisionPlan,
+ plan: OrderReceiptRecordPlan,
target_relays: SdkRelayTargetPolicy,
idempotency_key: Option<SdkIdempotencyKey>,
signer: &S,
- ) -> Result<OrderDecisionReceipt, RadrootsSdkError>
+ ) -> Result<OrderReceiptRecordReceipt, RadrootsSdkError>
where
S: RadrootsEventSigner + ?Sized,
{
- self.require_decision_preflight(&plan).await?;
+ if !self
+ .prepared_order_event_exists(&plan.expected_event_id)
+ .await?
+ {
+ self.require_receipt_record_preflight(&plan).await?;
+ }
let enqueue = enqueue_signed_workflow(
self.sdk,
SdkWorkflowEnqueueRequest {
- operation_kind: ORDER_DECISION_OPERATION_KIND,
+ operation_kind: ORDER_RECEIPT_RECORD_OPERATION_KIND,
actor,
frozen_draft: &plan.frozen_draft,
target_relays,
@@ -861,12 +2177,13 @@ impl<'sdk> OrdersClient<'sdk> {
signer,
)
.await?;
- Ok(OrderDecisionReceipt {
+ Ok(OrderReceiptRecordReceipt {
order_id: plan.order_id,
listing_addr: plan.listing_addr,
buyer_pubkey: plan.buyer_pubkey,
seller_pubkey: plan.seller_pubkey,
- request_event_id: plan.request_event_id,
+ 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,
@@ -906,14 +2223,75 @@ impl<'sdk> OrdersClient<'sdk> {
&self,
plan: &OrderDecisionPlan,
) -> Result<(), RadrootsSdkError> {
- let query_result = order_projection_query_for_order_id(
+ let query_result = self.query_order_projection(&plan.order_id).await?;
+ require_decision_request_evidence(plan, &query_result.projection)
+ }
+
+ async fn require_revision_proposal_preflight(
+ &self,
+ plan: &OrderRevisionProposalPlan,
+ ) -> Result<(), RadrootsSdkError> {
+ let query_result = self.query_order_projection(&plan.order_id).await?;
+ require_revision_proposal_state(plan, &query_result.projection)
+ }
+
+ async fn require_revision_decision_preflight(
+ &self,
+ plan: &OrderRevisionDecisionPlan,
+ ) -> Result<(), RadrootsSdkError> {
+ let query_result = self.query_order_projection(&plan.order_id).await?;
+ require_revision_decision_state(plan, &query_result.projection)
+ }
+
+ async fn require_cancellation_preflight(
+ &self,
+ plan: &OrderCancellationPlan,
+ ) -> Result<(), RadrootsSdkError> {
+ let query_result = self.query_order_projection(&plan.order_id).await?;
+ require_cancellation_state(plan, &query_result.projection)
+ }
+
+ async fn require_fulfillment_update_preflight(
+ &self,
+ plan: &OrderFulfillmentUpdatePlan,
+ ) -> Result<(), RadrootsSdkError> {
+ let query_result = self.query_order_projection(&plan.order_id).await?;
+ require_fulfillment_update_state(plan, &query_result.projection)
+ }
+
+ async fn require_receipt_record_preflight(
+ &self,
+ plan: &OrderReceiptRecordPlan,
+ ) -> Result<(), RadrootsSdkError> {
+ let query_result = self.query_order_projection(&plan.order_id).await?;
+ require_receipt_record_state(plan, &query_result.projection)
+ }
+
+ async fn query_order_projection(
+ &self,
+ order_id: &RadrootsOrderId,
+ ) -> Result<RadrootsOrderProjectionQueryResult, RadrootsSdkError> {
+ order_projection_query_for_order_id(
&self.sdk._event_store,
- &plan.order_id,
+ order_id,
ORDER_STATUS_MAX_LIMIT,
)
.await
- .map_err(projection_error)?;
- require_decision_request_evidence(plan, &query_result.projection)
+ .map_err(projection_error)
+ }
+
+ async fn prepared_order_event_exists(
+ &self,
+ expected_event_id: &RadrootsEventId,
+ ) -> Result<bool, RadrootsSdkError> {
+ self.sdk
+ ._event_store
+ .get_event(expected_event_id.as_str())
+ .await
+ .map(|event| event.is_some())
+ .map_err(|error| RadrootsSdkError::EventStore {
+ message: error.to_string(),
+ })
}
}
@@ -946,43 +2324,249 @@ impl OrderStatusReceipt {
}
#[cfg(feature = "runtime")]
-fn order_submit_plan(
+fn order_submit_plan(
+ actor: &RadrootsActorContext,
+ listing_event: RadrootsNostrEventPtr,
+ order_request: RadrootsOrderRequest,
+ created_at: RadrootsSdkTimestamp,
+) -> Result<OrderSubmitPlan, RadrootsSdkError> {
+ require_buyer_actor(actor, "order.prepare_submit")?;
+ let listing_event_id = listing_event_id(&listing_event)?;
+ let order_request =
+ canonicalize_order_request_for_signer(order_request, actor.pubkey().as_str())
+ .map_err(order_canonicalization_error)?;
+ let created_at_nostr = created_at.try_into_nostr_created_at()?;
+ let order_id = order_request.order_id.clone();
+ let listing_addr = order_request.listing_addr.clone();
+ let draft =
+ order::build_order_request_draft(&listing_event, &order_request).map_err(|error| {
+ RadrootsSdkError::InvalidRequest {
+ message: format!("order submit draft encode failed: {error}"),
+ }
+ })?;
+ let frozen_draft = to_frozen_draft(
+ draft.into_wire_parts(),
+ ORDER_REQUEST_CONTRACT_ID,
+ order_request.buyer_pubkey.as_str(),
+ created_at_nostr,
+ )
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order submit draft freeze failed: {error}"),
+ })?;
+ let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id.as_str())
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order submit draft produced invalid event id: {error}"),
+ })?;
+ Ok(OrderSubmitPlan {
+ order_id,
+ listing_addr,
+ listing_event_id,
+ expected_event_id,
+ frozen_draft,
+ created_at,
+ })
+}
+
+#[cfg(feature = "runtime")]
+fn order_decision_plan(
+ actor: &RadrootsActorContext,
+ request_event: RadrootsNostrEventPtr,
+ decision: RadrootsOrderDecision,
+ created_at: RadrootsSdkTimestamp,
+) -> Result<OrderDecisionPlan, RadrootsSdkError> {
+ require_seller_actor(actor, "order.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: "order.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();
+ let draft = order::build_order_decision_draft(&request_event_id, &request_event_id, &decision)
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order decision draft encode failed: {error}"),
+ })?;
+ let frozen_draft = to_frozen_draft(
+ draft.into_wire_parts(),
+ ORDER_DECISION_CONTRACT_ID,
+ decision.seller_pubkey.as_str(),
+ created_at_nostr,
+ )
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order decision draft freeze failed: {error}"),
+ })?;
+ let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id.as_str())
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order decision draft produced invalid event id: {error}"),
+ })?;
+ Ok(OrderDecisionPlan {
+ order_id,
+ listing_addr,
+ buyer_pubkey,
+ seller_pubkey,
+ request_event_id,
+ expected_event_id,
+ frozen_draft,
+ created_at,
+ })
+}
+
+#[cfg(feature = "runtime")]
+fn order_revision_proposal_plan(
+ actor: &RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ proposal: RadrootsOrderRevisionProposal,
+ created_at: RadrootsSdkTimestamp,
+) -> Result<OrderRevisionProposalPlan, RadrootsSdkError> {
+ require_seller_actor(actor, "order.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: "order.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();
+ let draft =
+ order::build_order_revision_proposal_draft(&root_event_id, &previous_event_id, &proposal)
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order revision proposal draft encode failed: {error}"),
+ })?;
+ let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
+ draft.into_wire_parts(),
+ ORDER_REVISION_PROPOSAL_CONTRACT_ID,
+ seller_pubkey.as_str(),
+ created_at_nostr,
+ "order revision proposal",
+ )?;
+ Ok(OrderRevisionProposalPlan {
+ order_id,
+ listing_addr,
+ buyer_pubkey,
+ seller_pubkey,
+ root_event_id,
+ previous_event_id,
+ expected_event_id,
+ frozen_draft,
+ created_at,
+ })
+}
+
+#[cfg(feature = "runtime")]
+fn order_revision_decision_plan(
+ actor: &RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ decision: RadrootsOrderRevisionDecision,
+ created_at: RadrootsSdkTimestamp,
+) -> Result<OrderRevisionDecisionPlan, RadrootsSdkError> {
+ require_buyer_actor(actor, "order.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() {
+ return Err(RadrootsSdkError::UnauthorizedActor {
+ operation: "order.prepare_revision_decision".to_owned(),
+ reason: "actor pubkey must match order buyer_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 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();
+ let draft =
+ order::build_order_revision_decision_draft(&root_event_id, &previous_event_id, &decision)
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order revision decision draft encode failed: {error}"),
+ })?;
+ let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
+ draft.into_wire_parts(),
+ ORDER_REVISION_DECISION_CONTRACT_ID,
+ buyer_pubkey.as_str(),
+ created_at_nostr,
+ "order revision decision",
+ )?;
+ Ok(OrderRevisionDecisionPlan {
+ order_id,
+ listing_addr,
+ buyer_pubkey,
+ seller_pubkey,
+ root_event_id,
+ previous_event_id,
+ expected_event_id,
+ frozen_draft,
+ created_at,
+ })
+}
+
+#[cfg(feature = "runtime")]
+fn order_cancellation_plan(
actor: &RadrootsActorContext,
- listing_event: RadrootsNostrEventPtr,
- order_request: RadrootsOrderRequest,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ cancellation: RadrootsOrderCancellation,
created_at: RadrootsSdkTimestamp,
-) -> Result<OrderSubmitPlan, RadrootsSdkError> {
- require_buyer_actor(actor, "order.prepare_submit")?;
- let listing_event_id = listing_event_id(&listing_event)?;
- let order_request =
- canonicalize_order_request_for_signer(order_request, actor.pubkey().as_str())
- .map_err(order_canonicalization_error)?;
+) -> Result<OrderCancellationPlan, RadrootsSdkError> {
+ require_buyer_actor(actor, "order.prepare_cancellation")?;
+ let root_event_id = order_reference_event_id(&root_event, "root")?;
+ let previous_event_id = order_reference_event_id(&previous_event, "previous")?;
+ if cancellation.buyer_pubkey.as_str() != actor.pubkey().as_str() {
+ return Err(RadrootsSdkError::UnauthorizedActor {
+ operation: "order.prepare_cancellation".to_owned(),
+ reason: "actor pubkey must match order buyer_pubkey".to_owned(),
+ });
+ }
let created_at_nostr = created_at.try_into_nostr_created_at()?;
- let order_id = order_request.order_id.clone();
- let listing_addr = order_request.listing_addr.clone();
+ let order_id = cancellation.order_id.clone();
+ let listing_addr = cancellation.listing_addr.clone();
+ let buyer_pubkey = cancellation.buyer_pubkey.clone();
+ let seller_pubkey = cancellation.seller_pubkey.clone();
let draft =
- order::build_order_request_draft(&listing_event, &order_request).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("order submit draft encode failed: {error}"),
- }
- })?;
- let frozen_draft = to_frozen_draft(
+ order::build_order_cancellation_draft(&root_event_id, &previous_event_id, &cancellation)
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order cancellation draft encode failed: {error}"),
+ })?;
+ let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
draft.into_wire_parts(),
- ORDER_REQUEST_CONTRACT_ID,
- order_request.buyer_pubkey.as_str(),
+ ORDER_CANCELLATION_CONTRACT_ID,
+ buyer_pubkey.as_str(),
created_at_nostr,
- )
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("order submit draft freeze failed: {error}"),
- })?;
- let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id.as_str())
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("order submit draft produced invalid event id: {error}"),
- })?;
- Ok(OrderSubmitPlan {
+ "order cancellation",
+ )?;
+ Ok(OrderCancellationPlan {
order_id,
listing_addr,
- listing_event_id,
+ buyer_pubkey,
+ seller_pubkey,
+ root_event_id,
+ previous_event_id,
expected_event_id,
frozen_draft,
created_at,
@@ -990,50 +2574,92 @@ fn order_submit_plan(
}
#[cfg(feature = "runtime")]
-fn order_decision_plan(
+fn order_fulfillment_update_plan(
actor: &RadrootsActorContext,
- request_event: RadrootsNostrEventPtr,
- decision: RadrootsOrderDecision,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ fulfillment: RadrootsOrderFulfillmentUpdate,
created_at: RadrootsSdkTimestamp,
-) -> Result<OrderDecisionPlan, RadrootsSdkError> {
- require_seller_actor(actor, "order.prepare_decision")?;
- let request_event_id = request_event_id(&request_event)?;
- if decision.seller_pubkey.as_str() != actor.pubkey().as_str() {
+) -> Result<OrderFulfillmentUpdatePlan, RadrootsSdkError> {
+ require_seller_actor(actor, "order.prepare_fulfillment_update")?;
+ let root_event_id = order_reference_event_id(&root_event, "root")?;
+ let previous_event_id = order_reference_event_id(&previous_event, "previous")?;
+ if fulfillment.seller_pubkey.as_str() != actor.pubkey().as_str() {
return Err(RadrootsSdkError::UnauthorizedActor {
- operation: "order.prepare_decision".to_owned(),
+ operation: "order.prepare_fulfillment_update".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();
- let draft = order::build_order_decision_draft(&request_event_id, &request_event_id, &decision)
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("order decision draft encode failed: {error}"),
- })?;
- let frozen_draft = to_frozen_draft(
+ let order_id = fulfillment.order_id.clone();
+ let listing_addr = fulfillment.listing_addr.clone();
+ let buyer_pubkey = fulfillment.buyer_pubkey.clone();
+ let seller_pubkey = fulfillment.seller_pubkey.clone();
+ let draft =
+ order::build_fulfillment_update_draft(&root_event_id, &previous_event_id, &fulfillment)
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order fulfillment update draft encode failed: {error}"),
+ })?;
+ let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
draft.into_wire_parts(),
- ORDER_DECISION_CONTRACT_ID,
- decision.seller_pubkey.as_str(),
+ ORDER_FULFILLMENT_UPDATE_CONTRACT_ID,
+ seller_pubkey.as_str(),
created_at_nostr,
- )
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("order decision draft freeze failed: {error}"),
- })?;
- let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id.as_str())
+ "order fulfillment update",
+ )?;
+ Ok(OrderFulfillmentUpdatePlan {
+ order_id,
+ listing_addr,
+ buyer_pubkey,
+ seller_pubkey,
+ root_event_id,
+ previous_event_id,
+ expected_event_id,
+ frozen_draft,
+ created_at,
+ })
+}
+
+#[cfg(feature = "runtime")]
+fn order_receipt_record_plan(
+ actor: &RadrootsActorContext,
+ root_event: RadrootsNostrEventPtr,
+ previous_event: RadrootsNostrEventPtr,
+ receipt: RadrootsOrderReceipt,
+ created_at: RadrootsSdkTimestamp,
+) -> Result<OrderReceiptRecordPlan, RadrootsSdkError> {
+ require_buyer_actor(actor, "order.prepare_receipt_record")?;
+ let root_event_id = order_reference_event_id(&root_event, "root")?;
+ let previous_event_id = order_reference_event_id(&previous_event, "previous")?;
+ if receipt.buyer_pubkey.as_str() != actor.pubkey().as_str() {
+ return Err(RadrootsSdkError::UnauthorizedActor {
+ operation: "order.prepare_receipt_record".to_owned(),
+ reason: "actor pubkey must match order buyer_pubkey".to_owned(),
+ });
+ }
+ let created_at_nostr = created_at.try_into_nostr_created_at()?;
+ let order_id = receipt.order_id.clone();
+ let listing_addr = receipt.listing_addr.clone();
+ let buyer_pubkey = receipt.buyer_pubkey.clone();
+ let seller_pubkey = receipt.seller_pubkey.clone();
+ let draft = order::build_buyer_receipt_draft(&root_event_id, &previous_event_id, &receipt)
.map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("order decision draft produced invalid event id: {error}"),
+ message: format!("order receipt record draft encode failed: {error}"),
})?;
- Ok(OrderDecisionPlan {
+ let (frozen_draft, expected_event_id) = freeze_order_workflow_draft(
+ draft.into_wire_parts(),
+ ORDER_RECEIPT_CONTRACT_ID,
+ buyer_pubkey.as_str(),
+ created_at_nostr,
+ "order receipt record",
+ )?;
+ Ok(OrderReceiptRecordPlan {
order_id,
listing_addr,
buyer_pubkey,
seller_pubkey,
- request_event_id,
+ root_event_id,
+ previous_event_id,
expected_event_id,
frozen_draft,
created_at,
@@ -1041,6 +2667,27 @@ fn order_decision_plan(
}
#[cfg(feature = "runtime")]
+fn freeze_order_workflow_draft(
+ parts: WireEventParts,
+ contract_id: &str,
+ expected_pubkey: &str,
+ created_at: u32,
+ operation: &'static str,
+) -> Result<(RadrootsFrozenEventDraft, RadrootsEventId), RadrootsSdkError> {
+ let frozen_draft =
+ to_frozen_draft(parts, contract_id, expected_pubkey, created_at).map_err(|error| {
+ RadrootsSdkError::InvalidRequest {
+ message: format!("{operation} draft freeze failed: {error}"),
+ }
+ })?;
+ let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id.as_str())
+ .map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("{operation} draft produced invalid event id: {error}"),
+ })?;
+ Ok((frozen_draft, expected_event_id))
+}
+
+#[cfg(feature = "runtime")]
struct OrderRequestEvidence {
order_id: RadrootsOrderId,
listing_addr: RadrootsListingAddress,
@@ -1140,18 +2787,21 @@ fn require_decision_request_evidence(
});
}
require_projection_match(
+ "order decision",
"listing_addr",
projection.listing_addr.as_ref(),
&plan.listing_addr,
&plan.order_id,
)?;
require_projection_match(
+ "order decision",
"buyer_pubkey",
projection.buyer_pubkey.as_ref(),
&plan.buyer_pubkey,
&plan.order_id,
)?;
require_projection_match(
+ "order decision",
"seller_pubkey",
projection.seller_pubkey.as_ref(),
&plan.seller_pubkey,
@@ -1161,7 +2811,366 @@ fn require_decision_request_evidence(
}
#[cfg(feature = "runtime")]
+#[derive(Clone, Copy)]
+struct OrderLifecycleReferences<'a> {
+ operation: &'static str,
+ order_id: &'a RadrootsOrderId,
+ listing_addr: &'a RadrootsListingAddress,
+ buyer_pubkey: &'a RadrootsPublicKey,
+ seller_pubkey: &'a RadrootsPublicKey,
+ root_event_id: &'a RadrootsEventId,
+ previous_event_id: &'a RadrootsEventId,
+}
+
+#[cfg(feature = "runtime")]
+fn require_revision_proposal_state(
+ plan: &OrderRevisionProposalPlan,
+ 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, RadrootsOrderStatus::Accepted)?;
+ require_no_lifecycle_terminal(&refs, projection)?;
+ require_no_payment_for_revision(&refs, projection)?;
+ require_no_pending_revision(&refs, projection)?;
+ if projection.fulfillment_event_id.is_some() {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "revision proposal requires order before fulfillment",
+ ));
+ }
+ require_lifecycle_previous_is_current(&refs, projection)
+}
+
+#[cfg(feature = "runtime")]
+fn require_revision_decision_state(
+ plan: &OrderRevisionDecisionPlan,
+ 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, RadrootsOrderStatus::Accepted)?;
+ require_no_lifecycle_terminal(&refs, projection)?;
+ require_pending_revision(&refs, projection)?;
+ require_lifecycle_previous_is_current(&refs, projection)
+}
+
+#[cfg(feature = "runtime")]
+fn require_cancellation_state(
+ plan: &OrderCancellationPlan,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ let refs = OrderLifecycleReferences {
+ operation: "order cancellation",
+ order_id: &plan.order_id,
+ listing_addr: &plan.listing_addr,
+ buyer_pubkey: &plan.buyer_pubkey,
+ seller_pubkey: &plan.seller_pubkey,
+ root_event_id: &plan.root_event_id,
+ previous_event_id: &plan.previous_event_id,
+ };
+ require_clean_lifecycle_projection(refs, projection)?;
+ if !matches!(
+ projection.status,
+ RadrootsOrderStatus::Requested | RadrootsOrderStatus::Accepted
+ ) {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ format!(
+ "cancellation requires requested or accepted local state; current state is {:?}",
+ projection.status
+ ),
+ ));
+ }
+ require_no_lifecycle_terminal(&refs, projection)?;
+ require_no_pending_revision(&refs, projection)?;
+ if projection.fulfillment_event_id.is_some() {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "cancellation requires order before fulfillment",
+ ));
+ }
+ require_lifecycle_previous_is_current(&refs, projection)
+}
+
+#[cfg(feature = "runtime")]
+fn require_fulfillment_update_state(
+ plan: &OrderFulfillmentUpdatePlan,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ let refs = OrderLifecycleReferences {
+ operation: "order fulfillment update",
+ 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, RadrootsOrderStatus::Accepted)?;
+ require_no_lifecycle_terminal(&refs, projection)?;
+ require_no_pending_revision(&refs, projection)?;
+ if matches!(
+ projection.fulfillment_status,
+ Some(
+ RadrootsOrderFulfillmentState::Delivered
+ | RadrootsOrderFulfillmentState::SellerCancelled
+ )
+ ) {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "fulfillment update cannot follow terminal fulfillment status",
+ ));
+ }
+ require_lifecycle_previous_is_current(&refs, projection)
+}
+
+#[cfg(feature = "runtime")]
+fn require_receipt_record_state(
+ plan: &OrderReceiptRecordPlan,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ let refs = OrderLifecycleReferences {
+ operation: "order receipt record",
+ 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, RadrootsOrderStatus::Accepted)?;
+ require_no_lifecycle_terminal(&refs, projection)?;
+ if projection.fulfillment_event_id.as_ref() != Some(refs.previous_event_id) {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "receipt record requires previous event to be the current fulfillment event",
+ ));
+ }
+ if !matches!(
+ projection.fulfillment_status,
+ Some(
+ RadrootsOrderFulfillmentState::ReadyForPickup
+ | RadrootsOrderFulfillmentState::Delivered
+ )
+ ) {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "receipt record requires ready-for-pickup or delivered fulfillment state",
+ ));
+ }
+ require_lifecycle_previous_is_current(&refs, projection)
+}
+
+#[cfg(feature = "runtime")]
+fn require_clean_lifecycle_projection(
+ refs: OrderLifecycleReferences<'_>,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ let Some(request_event_id) = &projection.request_event_id else {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "requires local order request evidence",
+ ));
+ };
+ if request_event_id != refs.root_event_id {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ format!(
+ "root event {} does not match local request {}",
+ refs.root_event_id, request_event_id
+ ),
+ ));
+ }
+ if !projection.issues.is_empty() {
+ return Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ format!(
+ "local order evidence has {} reducer issue(s)",
+ projection.issues.len()
+ ),
+ ));
+ }
+ require_projection_match(
+ refs.operation,
+ "listing_addr",
+ projection.listing_addr.as_ref(),
+ refs.listing_addr,
+ refs.order_id,
+ )?;
+ require_projection_match(
+ refs.operation,
+ "buyer_pubkey",
+ projection.buyer_pubkey.as_ref(),
+ refs.buyer_pubkey,
+ refs.order_id,
+ )?;
+ require_projection_match(
+ refs.operation,
+ "seller_pubkey",
+ projection.seller_pubkey.as_ref(),
+ refs.seller_pubkey,
+ refs.order_id,
+ )
+}
+
+#[cfg(feature = "runtime")]
+fn require_lifecycle_status(
+ refs: &OrderLifecycleReferences<'_>,
+ projection: &RadrootsOrderProjection,
+ expected: RadrootsOrderStatus,
+) -> 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(feature = "runtime")]
+fn require_no_lifecycle_terminal(
+ refs: &OrderLifecycleReferences<'_>,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ if projection.lifecycle_terminal {
+ Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "requires non-terminal local order state",
+ ))
+ } else {
+ Ok(())
+ }
+}
+
+#[cfg(feature = "runtime")]
+fn require_no_payment_for_revision(
+ refs: &OrderLifecycleReferences<'_>,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ if projection.payment.state == RadrootsOrderPaymentState::NotRecorded {
+ Ok(())
+ } else {
+ Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "revision proposal cannot follow recorded payment state",
+ ))
+ }
+}
+
+#[cfg(feature = "runtime")]
+fn require_pending_revision(
+ refs: &OrderLifecycleReferences<'_>,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ if has_pending_revision(projection) {
+ Ok(())
+ } else {
+ Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "requires pending revision proposal local state",
+ ))
+ }
+}
+
+#[cfg(feature = "runtime")]
+fn require_no_pending_revision(
+ refs: &OrderLifecycleReferences<'_>,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ if has_pending_revision(projection) {
+ Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "cannot follow pending revision proposal local state",
+ ))
+ } else {
+ Ok(())
+ }
+}
+
+#[cfg(feature = "runtime")]
+fn has_pending_revision(projection: &RadrootsOrderProjection) -> bool {
+ matches!(projection.status, RadrootsOrderStatus::Accepted)
+ && projection.fulfillment_event_id.is_none()
+ && projection.agreement_event_id.is_some()
+ && projection.last_event_id.is_some()
+ && projection.agreement_event_id != projection.last_event_id
+}
+
+#[cfg(feature = "runtime")]
+fn require_lifecycle_previous_is_current(
+ refs: &OrderLifecycleReferences<'_>,
+ projection: &RadrootsOrderProjection,
+) -> Result<(), RadrootsSdkError> {
+ match projection.last_event_id.as_ref() {
+ Some(last_event_id) if last_event_id == refs.previous_event_id => Ok(()),
+ Some(last_event_id) => Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ format!(
+ "previous event {} does not match current lifecycle event {}",
+ refs.previous_event_id, last_event_id
+ ),
+ )),
+ None => Err(lifecycle_invalid(
+ refs.operation,
+ refs.order_id,
+ "requires current lifecycle event evidence",
+ )),
+ }
+}
+
+#[cfg(feature = "runtime")]
+fn lifecycle_invalid(
+ operation: &'static str,
+ order_id: &RadrootsOrderId,
+ reason: impl Into<String>,
+) -> RadrootsSdkError {
+ RadrootsSdkError::InvalidRequest {
+ message: format!("{operation} for order {order_id} {}", reason.into()),
+ }
+}
+
+#[cfg(feature = "runtime")]
fn require_projection_match<T>(
+ operation: &'static str,
field: &'static str,
actual: Option<&T>,
expected: &T,
@@ -1174,12 +3183,12 @@ where
Some(actual) if actual == expected => Ok(()),
Some(actual) => Err(RadrootsSdkError::InvalidRequest {
message: format!(
- "order decision {field} {expected} does not match local request {actual} for order {order_id}"
+ "{operation} {field} {expected} does not match local request {actual} for order {order_id}"
),
}),
None => Err(RadrootsSdkError::InvalidRequest {
message: format!(
- "order decision request evidence is missing {field} for order {order_id}"
+ "{operation} request evidence is missing {field} for order {order_id}"
),
}),
}
@@ -1238,6 +3247,43 @@ fn request_event_id(
}
#[cfg(feature = "runtime")]
+fn order_reference_event_id(
+ event: &RadrootsNostrEventPtr,
+ label: &'static str,
+) -> Result<RadrootsEventId, RadrootsSdkError> {
+ RadrootsEventId::parse(event.id.as_str()).map_err(|error| RadrootsSdkError::InvalidRequest {
+ message: format!("order {label} evidence event id is invalid: {error}"),
+ })
+}
+
+#[cfg(feature = "runtime")]
+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(feature = "runtime")]
fn order_canonicalization_error(error: RadrootsOrderCanonicalizationError) -> RadrootsSdkError {
match error {
RadrootsOrderCanonicalizationError::InvalidBuyerSigner => {
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -12,7 +12,11 @@ use radroots_events::{
contract::RadrootsActorRole,
draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent},
ids::{RadrootsEventId, RadrootsOrderId},
- kinds::{KIND_LISTING, KIND_ORDER_DECISION, KIND_ORDER_REQUEST},
+ kinds::{
+ KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_FULFILLMENT_UPDATE,
+ KIND_ORDER_RECEIPT, KIND_ORDER_REQUEST, KIND_ORDER_REVISION_DECISION,
+ KIND_ORDER_REVISION_PROPOSAL,
+ },
};
use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp, radroots_event_from_nostr,
@@ -22,21 +26,29 @@ use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState};
use radroots_relay_transport::RadrootsMockRelayPublishAdapter;
use radroots_sdk::protocol::events::RadrootsNostrEventPtr;
use radroots_sdk::protocol::order::{
- RadrootsListingAddress, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
- RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics,
+ RadrootsListingAddress, RadrootsOrderCancellation, RadrootsOrderDecision,
+ RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem, RadrootsOrderEconomicLine,
+ RadrootsOrderEconomics, RadrootsOrderFulfillmentState, RadrootsOrderFulfillmentUpdate,
RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis,
- RadrootsOrderRequest,
+ RadrootsOrderReceipt, RadrootsOrderRequest, RadrootsOrderRevisionDecision,
+ RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal,
};
use radroots_sdk::protocol::wire::WireEventParts;
use radroots_sdk::{
- ORDER_DECISION_OPERATION_KIND, ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT,
- ORDER_SUBMIT_OPERATION_KIND, OrderDecisionEnqueueRequest, OrderDecisionPrepareRequest,
- OrderPaymentStateKind, OrderRequestEvidenceIngestRequest, OrderSettlementStateKind,
- OrderStatusKind, OrderStatusRequest, OrderSubmitEnqueueRequest, OrderSubmitPrepareRequest,
- PushOutboxEventState, PushOutboxRelayOutcomeKind, PushOutboxRequest, RadrootsSdk,
- RadrootsSdkError, RadrootsSdkPartialLocalMutationFailure, RadrootsSdkRecoveryAction,
- RadrootsSdkTimestamp, SdkMutationState, SdkOrderStatusIssue, SdkOrderStatusIssueKind,
- SdkOrderStatusSource, SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy,
+ ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND,
+ ORDER_FULFILLMENT_UPDATE_OPERATION_KIND, ORDER_RECEIPT_RECORD_OPERATION_KIND,
+ ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND,
+ ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, ORDER_SUBMIT_OPERATION_KIND,
+ OrderCancellationEnqueueRequest, OrderDecisionEnqueueRequest, OrderDecisionPrepareRequest,
+ OrderFulfillmentStatusKind, OrderFulfillmentUpdateEnqueueRequest, OrderPaymentStateKind,
+ OrderReceiptRecordEnqueueRequest, OrderRequestEvidenceIngestRequest,
+ OrderRevisionDecisionEnqueueRequest, OrderRevisionProposalEnqueueRequest,
+ OrderSettlementStateKind, OrderStatusKind, OrderStatusRequest, OrderSubmitEnqueueRequest,
+ OrderSubmitPrepareRequest, PushOutboxEventState, PushOutboxRelayOutcomeKind, PushOutboxRequest,
+ RadrootsSdk, RadrootsSdkError, RadrootsSdkPartialLocalMutationFailure,
+ RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, SdkMutationState, SdkOrderStatusIssue,
+ SdkOrderStatusIssueKind, SdkOrderStatusSource, SdkRelayTargetPolicy, SdkRelayTargetSet,
+ SdkRelayUrlPolicy,
};
const BUYER_SECRET_KEY_HEX: &str =
@@ -780,6 +792,110 @@ fn order_decision(raw_order_id: &str) -> RadrootsOrderDecision {
}
}
+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(),
+ }
+}
+
+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,
+ }
+}
+
+fn order_fulfillment_update(
+ raw_order_id: &str,
+ status: RadrootsOrderFulfillmentState,
+) -> RadrootsOrderFulfillmentUpdate {
+ RadrootsOrderFulfillmentUpdate {
+ 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"),
+ status,
+ }
+}
+
+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 order_receipt_record(raw_order_id: &str, received: bool) -> RadrootsOrderReceipt {
+ RadrootsOrderReceipt {
+ 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"),
+ received,
+ issue: if received {
+ None
+ } else {
+ Some("missing one item".to_owned())
+ },
+ received_at: 1_785_000_000,
+ }
+}
+
+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"),
+ }
+}
+
fn signed_event(
secret_key_hex: &str,
created_at: u32,
@@ -811,6 +927,26 @@ fn request_event_ptr(event: &RadrootsNostrEvent) -> RadrootsNostrEventPtr {
}
}
+fn order_event_ptr(event_id: &RadrootsEventId) -> RadrootsNostrEventPtr {
+ RadrootsNostrEventPtr {
+ id: event_id.as_str().to_owned(),
+ relays: Some(RELAY.to_owned()),
+ }
+}
+
+async fn outbox_operation_kind(sdk: &RadrootsSdk, 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,
@@ -1447,6 +1583,411 @@ async fn order_decision_enqueue_rejects_existing_decision_state_before_mutation(
}
#[tokio::test]
+async fn order_revision_order_fulfillment_order_receipt_lifecycle_enqueue_updates_status() {
+ let (_tempdir, sdk, store) = directory_sdk_and_store().await;
+ let request_event = signed_order_request_event("order-lifecycle-complete", 50);
+ let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id");
+ store
+ .ingest_event(RadrootsEventIngest::new(request_event.clone(), 5_000))
+ .await
+ .expect("ingest request");
+ let decision_receipt = sdk
+ .orders()
+ .enqueue_decision(
+ OrderDecisionEnqueueRequest::new(
+ seller_actor(),
+ request_event_ptr(&request_event),
+ order_decision("order-lifecycle-complete"),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("decision target relays"),
+ &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect("enqueue decision");
+ let proposal = order_revision_proposal(
+ "order-lifecycle-complete",
+ &request_event_id,
+ &decision_receipt.signed_event_id,
+ );
+ let proposal_receipt = sdk
+ .orders()
+ .enqueue_revision_proposal(
+ OrderRevisionProposalEnqueueRequest::new(
+ seller_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&decision_receipt.signed_event_id),
+ proposal.clone(),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("proposal target relays")
+ .try_with_idempotency_key("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,
+ ORDER_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_receipt = sdk
+ .orders()
+ .enqueue_revision_decision(
+ OrderRevisionDecisionEnqueueRequest::new(
+ buyer_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&proposal_receipt.signed_event_id),
+ revision_decision,
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("revision decision target relays"),
+ &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect("enqueue revision decision");
+ assert_eq!(
+ outbox_operation_kind(&sdk, revision_decision_receipt.outbox_operation_id).await,
+ ORDER_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 fulfillment_receipt = sdk
+ .orders()
+ .enqueue_fulfillment_update(
+ OrderFulfillmentUpdateEnqueueRequest::new(
+ seller_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&revision_decision_receipt.signed_event_id),
+ order_fulfillment_update(
+ "order-lifecycle-complete",
+ RadrootsOrderFulfillmentState::ReadyForPickup,
+ ),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("fulfillment target relays"),
+ &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect("enqueue fulfillment");
+ assert_eq!(
+ outbox_operation_kind(&sdk, fulfillment_receipt.outbox_operation_id).await,
+ ORDER_FULFILLMENT_UPDATE_OPERATION_KIND
+ );
+ assert_eq!(
+ store
+ .get_event(fulfillment_receipt.signed_event_id.as_str())
+ .await
+ .expect("fulfillment lookup")
+ .expect("fulfillment")
+ .kind,
+ KIND_ORDER_FULFILLMENT_UPDATE
+ );
+
+ let receipt = sdk
+ .orders()
+ .enqueue_receipt_record(
+ OrderReceiptRecordEnqueueRequest::new(
+ buyer_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&fulfillment_receipt.signed_event_id),
+ order_receipt_record("order-lifecycle-complete", true),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("receipt target relays"),
+ &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect("enqueue receipt");
+ assert_eq!(
+ outbox_operation_kind(&sdk, receipt.outbox_operation_id).await,
+ ORDER_RECEIPT_RECORD_OPERATION_KIND
+ );
+ assert_eq!(
+ store
+ .get_event(receipt.signed_event_id.as_str())
+ .await
+ .expect("receipt lookup")
+ .expect("receipt")
+ .kind,
+ KIND_ORDER_RECEIPT
+ );
+
+ let status = sdk
+ .orders()
+ .status(status_request("order-lifecycle-complete"))
+ .await
+ .expect("status");
+ assert_eq!(status.status, OrderStatusKind::Completed);
+ assert_eq!(status.event_count, 6);
+ assert_eq!(
+ status
+ .fulfillment_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
+ Some(fulfillment_receipt.signed_event_id.as_str())
+ );
+ assert_eq!(
+ status
+ .receipt_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
+ Some(receipt.signed_event_id.as_str())
+ );
+ assert_eq!(
+ status.fulfillment_status,
+ Some(OrderFulfillmentStatusKind::ReadyForPickup)
+ );
+ assert!(status.lifecycle_terminal);
+ assert!(status.issues.is_empty());
+}
+
+#[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.as_str()).expect("request id");
+ store
+ .ingest_event(RadrootsEventIngest::new(request_event.clone(), 6_000))
+ .await
+ .expect("ingest request");
+ let decision_receipt = sdk
+ .orders()
+ .enqueue_decision(
+ OrderDecisionEnqueueRequest::new(
+ seller_actor(),
+ request_event_ptr(&request_event),
+ order_decision("order-lifecycle-cancel"),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("decision target relays"),
+ &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect("enqueue decision");
+ let cancellation = sdk
+ .orders()
+ .enqueue_cancellation(
+ OrderCancellationEnqueueRequest::new(
+ buyer_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&decision_receipt.signed_event_id),
+ order_cancellation("order-lifecycle-cancel"),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("cancellation target relays")
+ .try_with_idempotency_key("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,
+ decision_receipt.signed_event_id
+ );
+ assert_eq!(
+ outbox_operation_kind(&sdk, cancellation.outbox_operation_id).await,
+ ORDER_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
+ .orders()
+ .enqueue_cancellation(
+ OrderCancellationEnqueueRequest::new(
+ buyer_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&decision_receipt.signed_event_id),
+ order_cancellation("order-lifecycle-cancel"),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("replay target relays")
+ .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
+ .orders()
+ .status(status_request("order-lifecycle-cancel"))
+ .await
+ .expect("status");
+ assert_eq!(status.status, OrderStatusKind::Cancelled);
+ assert_eq!(
+ status
+ .cancellation_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
+ Some(cancellation.signed_event_id.as_str())
+ );
+ assert!(status.lifecycle_terminal);
+ assert!(status.issues.is_empty());
+}
+
+#[tokio::test]
+async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() {
+ 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.as_str()).expect("request id");
+ let missing = sdk
+ .orders()
+ .enqueue_fulfillment_update(
+ OrderFulfillmentUpdateEnqueueRequest::new(
+ seller_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&request_event_id),
+ order_fulfillment_update(
+ "order-lifecycle-invalid",
+ RadrootsOrderFulfillmentState::ReadyForPickup,
+ ),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("missing target relays"),
+ &FixtureSigner::new(SELLER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect_err("missing local evidence");
+ assert!(matches!(missing, RadrootsSdkError::InvalidRequest { .. }));
+ assert_eq!(
+ store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 0
+ );
+
+ store
+ .ingest_event(RadrootsEventIngest::new(request_event.clone(), 7_000))
+ .await
+ .expect("ingest request");
+ let decision_receipt = sdk
+ .orders()
+ .enqueue_decision(
+ OrderDecisionEnqueueRequest::new(
+ seller_actor(),
+ request_event_ptr(&request_event),
+ order_decision("order-lifecycle-invalid"),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("decision target relays"),
+ &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
+ .orders()
+ .enqueue_revision_decision(
+ OrderRevisionDecisionEnqueueRequest::new(
+ buyer_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&decision_receipt.signed_event_id),
+ revision_without_proposal,
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("revision decision target relays"),
+ &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect_err("revision decision without proposal");
+ assert!(matches!(
+ revision_error,
+ RadrootsSdkError::InvalidRequest { .. }
+ ));
+
+ let receipt_error = sdk
+ .orders()
+ .enqueue_receipt_record(
+ OrderReceiptRecordEnqueueRequest::new(
+ buyer_actor(),
+ request_event_ptr(&request_event),
+ order_event_ptr(&decision_receipt.signed_event_id),
+ order_receipt_record("order-lifecycle-invalid", true),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("receipt target relays"),
+ &FixtureSigner::new(BUYER_SECRET_KEY_HEX),
+ )
+ .await
+ .expect_err("receipt without fulfillment");
+ assert!(matches!(
+ receipt_error,
+ RadrootsSdkError::InvalidRequest { .. }
+ ));
+ assert_eq!(
+ store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 2
+ );
+}
+
+#[tokio::test]
async fn order_status_returns_not_found_for_missing_local_order() {
let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
let request = status_request("order-1");