commit 023faeb5a806a3494ebc08b42b0bda441dac41f7
parent 7b48b68af0eaa118340c4e1ef128e4bc2972f06b
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 11:40:53 +0000
sdk: refactor trade commands and queries
- freeze every trade command through canonical workflow and event contracts
- delegate commit pagination and private metadata to shared sync and storage
- return lower projections evidence pages receipts and canonical trade identity
- remove the retired SQL-backed duplicate trade runtime and tests
Diffstat:
5 files changed, 853 insertions(+), 2550 deletions(-)
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -251,6 +251,15 @@ impl Client {
Ok(self.sync()?.map(crate::listing::Operations::new))
}
+ /// Returns trade operations when canonical synchronization is configured.
+ #[cfg(feature = "sync")]
+ pub fn trade(&self) -> Result<Option<crate::trade::Operations<'_>>> {
+ let storage = self.storage()?;
+ Ok(self
+ .sync()?
+ .map(|sync| crate::trade::Operations::new(storage, sync)))
+ }
+
/// Returns whether explicit close completed successfully or reached the
/// lower storage commit point.
#[must_use]
diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs
@@ -1 +1,803 @@
-//! Trade commands and queries.
+//! Canonical trade planning, commit, query, and private-evidence operations.
+
+use std::{error, fmt};
+
+use radroots_event::{
+ EventDraft,
+ contract::AuthorRole,
+ trade::{TradeMutationEnvelopeV1, TradeProtocolError, canonical_trade_mutation_content},
+};
+use radroots_event_codec::{encode::EventEncodeError, encode::trade::trade_mutation_event_build};
+use radroots_signing::Actor;
+use radroots_trade::{Projection, ReductionInput, WorkflowPlan, reducer::reduce_trade_records};
+
+/// Pure inputs for one frozen trade command.
+#[derive(Clone, Debug)]
+pub struct PrepareRequest {
+ actor: Actor,
+ mutation: TradeMutationEnvelopeV1,
+}
+
+impl PrepareRequest {
+ /// Creates explicit inputs for any canonical proposal, revision, decision,
+ /// cancellation, or resumable mutation.
+ #[must_use]
+ pub const fn new(actor: Actor, mutation: TradeMutationEnvelopeV1) -> Self {
+ Self { actor, mutation }
+ }
+}
+
+/// Frozen, replay-stable trade workflow plan.
+#[derive(Clone, Debug)]
+pub struct Plan {
+ actor: Actor,
+ workflow: WorkflowPlan,
+ draft: EventDraft,
+}
+
+impl Plan {
+ /// Returns the exact authorized actor carried into signing.
+ #[must_use]
+ pub const fn actor(&self) -> &Actor {
+ &self.actor
+ }
+
+ /// Returns the lower-owned validated workflow and required host actions.
+ pub const fn workflow(&self) -> &WorkflowPlan {
+ &self.workflow
+ }
+
+ /// Returns the frozen canonical event draft.
+ #[must_use]
+ pub const fn draft(&self) -> &EventDraft {
+ &self.draft
+ }
+}
+
+/// Trade planning failure stage.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[non_exhaustive]
+pub enum PrepareErrorKind {
+ /// The actor identity or role cannot author the supplied mutation.
+ UnauthorizedActor,
+ /// Event-domain canonicalization rejected the mutation.
+ CanonicalMutation,
+ /// The lower trade workflow rejected the canonical mutation.
+ Workflow,
+ /// The canonical event codec rejected the mutation.
+ Encode,
+ /// The canonical event draft rejected the encoded mutation.
+ Draft,
+}
+
+/// One secret-safe trade planning failure retaining its lower source.
+pub struct PrepareError {
+ kind: PrepareErrorKind,
+ source: Option<Box<dyn error::Error + Send + Sync>>,
+}
+
+impl PrepareError {
+ /// Returns the stable client-level planning stage.
+ #[must_use]
+ pub const fn kind(&self) -> PrepareErrorKind {
+ self.kind
+ }
+
+ fn unauthorized_actor() -> Self {
+ Self {
+ kind: PrepareErrorKind::UnauthorizedActor,
+ source: None,
+ }
+ }
+
+ fn canonical(source: TradeProtocolError) -> Self {
+ Self::with_source(PrepareErrorKind::CanonicalMutation, source)
+ }
+
+ fn workflow(source: radroots_trade::Error) -> Self {
+ Self::with_source(PrepareErrorKind::Workflow, source)
+ }
+
+ fn encode(source: EventEncodeError) -> Self {
+ Self::with_source(PrepareErrorKind::Encode, source)
+ }
+
+ fn draft(source: radroots_event::draft::DraftError) -> Self {
+ Self::with_source(PrepareErrorKind::Draft, source)
+ }
+
+ fn with_source(
+ kind: PrepareErrorKind,
+ source: impl error::Error + Send + Sync + 'static,
+ ) -> Self {
+ Self {
+ kind,
+ source: Some(Box::new(source)),
+ }
+ }
+}
+
+impl fmt::Display for PrepareError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ PrepareErrorKind::UnauthorizedActor => "trade actor is not authorized",
+ PrepareErrorKind::CanonicalMutation => "trade mutation is not canonical",
+ PrepareErrorKind::Workflow => "trade workflow is invalid",
+ PrepareErrorKind::Encode => "trade event encoding failed",
+ PrepareErrorKind::Draft => "trade event draft is invalid",
+ })
+ }
+}
+
+impl fmt::Debug for PrepareError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("PrepareError")
+ .field("kind", &self.kind)
+ .finish_non_exhaustive()
+ }
+}
+
+impl error::Error for PrepareError {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ self.source
+ .as_deref()
+ .map(|source| source as &(dyn error::Error + 'static))
+ }
+}
+
+/// Canonicalizes, authorizes, validates, and freezes one trade command.
+///
+/// This operation performs no signing, persistence, private-artifact access,
+/// scheduling, or delivery. Every proposal, revision, decision, cancellation,
+/// and resumed command uses the same lower-owned `TradeId` and workflow law.
+pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> {
+ let canonical = canonical_trade_mutation_content(request.mutation)
+ .map_err(PrepareError::canonical)?
+ .envelope;
+ let required_role = if canonical.author_pubkey == canonical.buyer_pubkey {
+ AuthorRole::Buyer
+ } else if canonical.author_pubkey == canonical.seller_pubkey {
+ AuthorRole::Seller
+ } else {
+ return Err(PrepareError::unauthorized_actor());
+ };
+ if request.actor.public_key() != canonical.author_pubkey
+ || !request.actor.satisfies(required_role)
+ {
+ return Err(PrepareError::unauthorized_actor());
+ }
+ let workflow = WorkflowPlan::prepare(canonical.clone()).map_err(PrepareError::workflow)?;
+ let parts = trade_mutation_event_build(canonical.clone()).map_err(PrepareError::encode)?;
+ let draft = EventDraft::new(
+ canonical.contract_id.clone(),
+ parts.kind,
+ canonical.authored_at_unix_s,
+ parts.tags,
+ parts.content,
+ canonical.author_pubkey.to_hex(),
+ )
+ .map_err(PrepareError::draft)?;
+ Ok(Plan {
+ actor: request.actor,
+ workflow,
+ draft,
+ })
+}
+
+/// Deterministically reduces caller-supplied canonical evidence.
+#[must_use]
+pub fn project(input: ReductionInput) -> Projection {
+ reduce_trade_records(input)
+}
+
+#[cfg(feature = "sync")]
+use radroots_signing::request::CancellationPolicy;
+#[cfg(feature = "sync")]
+use radroots_storage::{
+ event::{EventPage, EventQuery, StoredVisibleEvent},
+ journal::IdempotencyKey,
+ private_artifact::{PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactStage},
+};
+#[cfg(feature = "sync")]
+use radroots_sync::{
+ PushReceipt,
+ policy::{Error as SyncError, SyncId},
+};
+
+/// Explicit commit inputs for one prepared trade command.
+#[cfg(feature = "sync")]
+#[derive(Clone, Debug)]
+pub struct EnqueueRequest {
+ operation_id: SyncId,
+ idempotency_key: IdempotencyKey,
+ plan: Plan,
+ profile: crate::transport::Profile,
+ cancellation: CancellationPolicy,
+}
+
+#[cfg(feature = "sync")]
+impl EnqueueRequest {
+ /// Creates a command request whose transport selection has no fallback.
+ #[must_use]
+ pub const fn new(
+ operation_id: SyncId,
+ idempotency_key: IdempotencyKey,
+ plan: Plan,
+ profile: crate::transport::Profile,
+ cancellation: CancellationPolicy,
+ ) -> Self {
+ Self {
+ operation_id,
+ idempotency_key,
+ plan,
+ profile,
+ cancellation,
+ }
+ }
+}
+
+/// Private-term metadata verification failure.
+#[cfg(feature = "sync")]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[non_exhaustive]
+pub enum PrivateTermsError {
+ /// Canonical storage failed to inspect the requested metadata.
+ Storage,
+ /// The workflow does not require private terms.
+ NotRequired,
+ /// Metadata is absent, inactive, or does not match the public commitment.
+ EvidenceMismatch,
+}
+
+/// Borrowed trade operations over canonical storage and sync capabilities.
+#[cfg(feature = "sync")]
+#[derive(Clone, Copy)]
+pub struct Operations<'a> {
+ storage: &'a dyn radroots_storage::Storage,
+ sync: crate::sync::Operations<'a>,
+}
+
+#[cfg(feature = "sync")]
+impl fmt::Debug for Operations<'_> {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("Operations")
+ .field("storage", &"<borrowed canonical storage>")
+ .field("sync", &self.sync)
+ .finish()
+ }
+}
+
+#[cfg(feature = "sync")]
+impl<'a> Operations<'a> {
+ pub(crate) const fn new(
+ storage: &'a dyn radroots_storage::Storage,
+ sync: crate::sync::Operations<'a>,
+ ) -> Self {
+ Self { storage, sync }
+ }
+
+ /// Signs and atomically enqueues a prepared command. Reusing the same
+ /// idempotency input is the canonical resume/replay operation.
+ pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushReceipt, SyncError> {
+ let targets = request
+ .profile
+ .targets()
+ .cloned()
+ .ok_or(SyncError::InvalidPushRequest)?;
+ let satisfaction = request
+ .profile
+ .satisfaction()
+ .cloned()
+ .ok_or(SyncError::InvalidPushRequest)?;
+ self.sync
+ .sign_and_enqueue(radroots_sync::PushRequest::new(
+ request.operation_id,
+ request.idempotency_key,
+ request.plan.actor,
+ request.plan.draft,
+ targets,
+ satisfaction,
+ request.cancellation,
+ )?)
+ .await
+ }
+
+ /// Returns one native, bounded, generation-bound page of visible evidence.
+ pub async fn query_visible(
+ &self,
+ query: EventQuery,
+ ) -> Result<EventPage<StoredVisibleEvent>, radroots_storage::Error> {
+ radroots_storage::event::EventStore::query_visible(self.storage, query).await
+ }
+
+ /// Returns native private-artifact metadata without reading secret material.
+ pub async fn private_artifact(
+ &self,
+ artifact_id: PrivateArtifactId,
+ ) -> Result<Option<PrivateArtifactMetadata>, radroots_storage::Error> {
+ radroots_storage::private_artifact::PrivateArtifactStore::metadata(
+ self.storage,
+ artifact_id,
+ )
+ .await
+ }
+
+ /// Verifies that canonical active metadata matches a plan's public schema
+ /// and ciphertext commitment. Plaintext, ciphertext, and keys never cross
+ /// the SDK boundary.
+ pub async fn verify_private_terms(
+ &self,
+ plan: &Plan,
+ artifact_id: PrivateArtifactId,
+ ) -> Result<PrivateArtifactMetadata, PrivateTermsError> {
+ let expected = plan
+ .workflow
+ .private_terms()
+ .ok_or(PrivateTermsError::NotRequired)?;
+ let metadata = self
+ .private_artifact(artifact_id)
+ .await
+ .map_err(|_| PrivateTermsError::Storage)?
+ .ok_or(PrivateTermsError::EvidenceMismatch)?;
+ let commitment = hex_lower(metadata.commitment().as_bytes());
+ if metadata.stage() != PrivateArtifactStage::Active
+ || metadata.schema_id().as_str() != expected.schema_id()
+ || commitment != expected.ciphertext_commitment()
+ {
+ return Err(PrivateTermsError::EvidenceMismatch);
+ }
+ Ok(metadata)
+ }
+}
+
+#[cfg(feature = "sync")]
+fn hex_lower(bytes: &[u8]) -> String {
+ const HEX: &[u8; 16] = b"0123456789abcdef";
+ let mut encoded = String::with_capacity(bytes.len() * 2);
+ for byte in bytes {
+ encoded.push(char::from(HEX[usize::from(byte >> 4)]));
+ encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
+ }
+ encoded
+}
+
+#[cfg(test)]
+mod tests {
+ use radroots_event::{
+ id::{ClassifiedListingAddress, DTag, EventId, InventoryBinId, MutationId, TradeId},
+ trade::{
+ FulfillmentProfileV1, RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
+ RADROOTS_TRADE_DECISION_CONTRACT_ID, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
+ RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID,
+ RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION,
+ TradeCancellationProfileV1, TradeCandidateLineV1, TradeCandidateTermsV1,
+ TradeDecisionV1, TradeEconomicAdjustmentV1, TradeEconomicsProfileV1,
+ TradeLineTombstoneV1, TradeMutationBodyV1, TradeMutationKindV1, TradePrivateTermsRefV1,
+ },
+ };
+ use radroots_identity::PublicKey;
+ use radroots_signing::actor::ActorSource;
+ use radroots_trade::workflow::WorkflowAction;
+
+ use super::*;
+
+ const BUYER: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
+ const SELLER: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
+
+ fn pubkey(value: &str) -> PublicKey {
+ PublicKey::from_hex(value).expect("public key")
+ }
+
+ fn actor(public_key: &str, role: AuthorRole) -> Actor {
+ Actor::from_public_key_hex(public_key, ActorSource::ExplicitPublicKey, [role])
+ .expect("actor")
+ }
+
+ fn mutation_id(marker: char) -> MutationId {
+ MutationId::parse(std::iter::repeat_n(marker, 64).collect::<String>()).expect("mutation id")
+ }
+
+ fn candidate(suffix: &str) -> TradeCandidateTermsV1 {
+ TradeCandidateTermsV1 {
+ candidate_id: None,
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ base_candidate_id: None,
+ supersession_intent: None,
+ buyer_pubkey: pubkey(BUYER),
+ seller_pubkey: pubkey(SELLER),
+ farm_id: DTag::parse("farm-1").expect("farm id"),
+ lines: vec![TradeCandidateLineV1 {
+ line_id: DTag::parse(format!("line-{suffix}")).expect("line id"),
+ listing_addr: ClassifiedListingAddress::parse(format!(
+ "30402:{SELLER}:listing-{suffix}"
+ ))
+ .expect("listing address"),
+ listing_event_id: EventId::parse("cc".repeat(32)).expect("event id"),
+ listing_snapshot_sha256: "dd".repeat(32),
+ product_id: format!("carrots-{suffix}"),
+ option_id: None,
+ bin_id: InventoryBinId::parse(format!("bin-{suffix}")).expect("bin id"),
+ quantity_mantissa: "2".into(),
+ quantity_scale: 0,
+ unit_code: "count".into(),
+ unit_profile: "mvp-count".into(),
+ unit_price_mantissa: "500".into(),
+ currency_code: "USD".into(),
+ line_subtotal_mantissa: "1000".into(),
+ replaces_line_id: None,
+ }],
+ line_tombstones: Vec::<TradeLineTombstoneV1>::new(),
+ economics: TradeEconomicsProfileV1 {
+ profile_id: "mvp-fixed".into(),
+ currency_code: "USD".into(),
+ currency_exponent: 2,
+ rounding_profile: "half-even".into(),
+ subtotal_mantissa: "1000".into(),
+ discount_total_mantissa: "0".into(),
+ adjustment_total_mantissa: "0".into(),
+ total_mantissa: "1000".into(),
+ adjustments: Vec::<TradeEconomicAdjustmentV1>::new(),
+ },
+ fulfillment: FulfillmentProfileV1 {
+ profile_id: "market-pickup".into(),
+ method: "pickup".into(),
+ starts_at_unix_s: 1_800_000_000,
+ ends_at_unix_s: 1_800_003_600,
+ timezone: "America/New_York".into(),
+ utc_offset_seconds: -18_000,
+ fold: 0,
+ location_class: "farmstand".into(),
+ requires_private_terms: true,
+ },
+ cancellation: TradeCancellationProfileV1 {
+ profile_id: "buyer-pre-agreement".into(),
+ buyer_pre_agreement: true,
+ post_agreement_cutoff_unix_s: None,
+ },
+ private_terms: Some(TradePrivateTermsRefV1 {
+ artifact_id: "artifact-1".into(),
+ schema_id: "radroots.private.fulfillment.v1".into(),
+ ciphertext_commitment: "ee".repeat(32),
+ required_acknowledgement: true,
+ }),
+ proposal_expires_at_unix_s: 1_800_010_000,
+ }
+ }
+
+ fn envelope(contract_id: &str, body: TradeMutationBodyV1) -> TradeMutationEnvelopeV1 {
+ let initial = body.mutation_kind() == TradeMutationKindV1::Proposal;
+ TradeMutationEnvelopeV1 {
+ mutation_id: None,
+ contract_id: contract_id.into(),
+ schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
+ trade_id: TradeId::parse("11".repeat(16)).expect("trade id"),
+ root_mutation_id: (!initial).then(|| mutation_id('1')),
+ buyer_pubkey: pubkey(BUYER),
+ seller_pubkey: pubkey(SELLER),
+ farm_id: DTag::parse("farm-1").expect("farm id"),
+ parent_mutation_ids: if initial {
+ vec![]
+ } else {
+ vec![mutation_id('1')]
+ },
+ author_pubkey: pubkey(BUYER),
+ counterparty_pubkey: pubkey(SELLER),
+ authored_at_unix_s: 1_800_000_000,
+ body,
+ }
+ }
+
+ fn all_commands() -> Vec<TradeMutationEnvelopeV1> {
+ let proposal = canonical_trade_mutation_content(envelope(
+ RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
+ TradeMutationBodyV1::Proposal {
+ candidate: candidate("1"),
+ },
+ ))
+ .expect("proposal")
+ .envelope;
+ let proposal_id = proposal.mutation_id.expect("proposal id");
+ let candidate_id = match &proposal.body {
+ TradeMutationBodyV1::Proposal { candidate } => {
+ candidate.candidate_id.expect("candidate id")
+ }
+ _ => unreachable!(),
+ };
+ vec![
+ proposal,
+ envelope(
+ RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID,
+ TradeMutationBodyV1::RevisionProposal {
+ candidate: candidate("2"),
+ },
+ ),
+ envelope(
+ RADROOTS_TRADE_DECISION_CONTRACT_ID,
+ TradeMutationBodyV1::Decision {
+ proposal_mutation_id: proposal_id,
+ candidate_id,
+ decision: TradeDecisionV1::Declined {
+ reason: "unavailable".into(),
+ },
+ },
+ ),
+ envelope(
+ RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID,
+ TradeMutationBodyV1::RevisionDecision {
+ proposal_mutation_id: mutation_id('2'),
+ candidate_id,
+ decision: TradeDecisionV1::Declined {
+ reason: "unavailable".into(),
+ },
+ },
+ ),
+ envelope(
+ RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
+ TradeMutationBodyV1::Cancellation {
+ target_candidate_id: Some(candidate_id),
+ target_claim_mutation_id: None,
+ reason: "cancelled".into(),
+ },
+ ),
+ ]
+ }
+
+ #[test]
+ fn prepare_covers_every_command_with_one_trade_identity_and_private_plan() {
+ let plans = all_commands()
+ .into_iter()
+ .map(|mutation| {
+ prepare(PrepareRequest::new(
+ actor(BUYER, AuthorRole::Buyer),
+ mutation,
+ ))
+ })
+ .collect::<Result<Vec<_>, _>>()
+ .expect("plans");
+
+ assert_eq!(
+ plans
+ .iter()
+ .map(|plan| plan.workflow().kind())
+ .collect::<Vec<_>>(),
+ [
+ TradeMutationKindV1::Proposal,
+ TradeMutationKindV1::RevisionProposal,
+ TradeMutationKindV1::Decision,
+ TradeMutationKindV1::RevisionDecision,
+ TradeMutationKindV1::Cancellation,
+ ]
+ );
+ assert!(
+ plans
+ .iter()
+ .all(|plan| plan.workflow().trade_id() == plans[0].workflow().trade_id())
+ );
+ assert_eq!(
+ plans[0].workflow().required_actions()[0],
+ WorkflowAction::VerifyPrivateTerms
+ );
+ assert_eq!(
+ plans[0]
+ .workflow()
+ .private_terms()
+ .expect("private terms")
+ .artifact_id(),
+ "artifact-1"
+ );
+ for plan in plans {
+ assert_eq!(
+ plan.draft().contract_id(),
+ plan.workflow().kind().contract_id()
+ );
+ assert_eq!(plan.draft().kind_u32(), plan.workflow().kind().nostr_kind());
+ }
+ }
+
+ #[test]
+ fn prepare_rejects_wrong_identity_role_and_invalid_protocol_once() {
+ let mutation = all_commands().remove(0);
+ let wrong_role = prepare(PrepareRequest::new(
+ actor(BUYER, AuthorRole::Seller),
+ mutation.clone(),
+ ))
+ .expect_err("wrong role");
+ assert_eq!(wrong_role.kind(), PrepareErrorKind::UnauthorizedActor);
+ assert!(std::error::Error::source(&wrong_role).is_none());
+
+ let wrong_identity = prepare(PrepareRequest::new(
+ actor(SELLER, AuthorRole::Buyer),
+ mutation,
+ ))
+ .expect_err("wrong identity");
+ assert_eq!(wrong_identity.kind(), PrepareErrorKind::UnauthorizedActor);
+
+ let mut invalid = all_commands().remove(0);
+ invalid.contract_id = "radroots.trade.cancellation.v1".into();
+ let canonical = prepare(PrepareRequest::new(
+ actor(BUYER, AuthorRole::Buyer),
+ invalid,
+ ))
+ .expect_err("invalid contract");
+ assert_eq!(canonical.kind(), PrepareErrorKind::CanonicalMutation);
+ assert!(std::error::Error::source(&canonical).is_some());
+ assert!(!format!("{canonical:?}").contains("artifact-1"));
+ }
+
+ #[test]
+ fn query_projection_returns_the_lower_projection_and_conflict_evidence_types() {
+ let trade_id = TradeId::parse("22".repeat(16)).expect("trade id");
+ let projection = project(ReductionInput::new(trade_id));
+ assert_eq!(projection.trade_id(), &trade_id);
+ assert!(projection.candidate_heads().is_empty());
+ assert!(!projection.projection_digest().is_empty());
+ }
+
+ #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))]
+ mod operations {
+ use std::sync::{
+ Arc,
+ atomic::{AtomicU8, Ordering},
+ };
+
+ use radroots_nostr::key::SecretKey;
+ use radroots_signing::request::CancellationPolicy;
+ use radroots_storage::{
+ Outbox,
+ event::{EventQuery, EventQueryBounds, SourceGeneration},
+ journal::IdempotencyKey,
+ memory::MemoryStorage,
+ private_artifact::{
+ ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DurableSecretReference,
+ PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactStore, RetentionPolicy,
+ },
+ };
+ use radroots_sync::{
+ Engine,
+ policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage},
+ };
+ use radroots_transport::{
+ DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkStatus,
+ Target, TargetSet, TransportId,
+ capability::{Availability, Maturity, SinkCapabilities},
+ policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
+ };
+
+ use super::*;
+ use crate::{ClientBuilder, transport::Profile};
+
+ const BUYER_SECRET: &str =
+ "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
+
+ struct FixedClock;
+ struct SequenceIds(AtomicU8);
+ struct NoopSink;
+
+ impl Clock for FixedClock {
+ fn now_unix_ms(&self) -> Result<u64, Error> {
+ Ok(2_000_000_000_000)
+ }
+ }
+ impl IdSource for SequenceIds {
+ fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> {
+ SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16])
+ }
+ }
+ impl EventSink for NoopSink {
+ fn status(
+ &self,
+ ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> {
+ Box::pin(async {
+ Ok(SinkStatus::new(
+ TransportId::NOSTR,
+ true,
+ Maturity::Stable,
+ Availability::Available,
+ SinkCapabilities::DELIVER,
+ "ready",
+ ))
+ })
+ }
+ fn deliver(
+ &self,
+ _request: DeliveryRequest,
+ ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, TransportError>>
+ {
+ Box::pin(async { Err(TransportError::UnsupportedOperation) })
+ }
+ }
+
+ #[tokio::test]
+ async fn operations_cover_private_evidence_pagination_commit_replay_and_cancellation() {
+ let storage = Arc::new(MemoryStorage::new(
+ SourceGeneration::new([6; 32]).expect("generation"),
+ ));
+ let artifact_id = PrivateArtifactId::new([7; 16]).expect("artifact id");
+ let metadata = PrivateArtifactMetadata::new(
+ artifact_id,
+ ArtifactKind::parse("trade_private_terms").expect("kind"),
+ ArtifactSchemaId::parse("radroots.private.fulfillment.v1").expect("schema"),
+ ArtifactCommitment::new([0xee; 32]),
+ 64,
+ DurableSecretReference::new("memory", "trade-artifact-1", 1).expect("reference"),
+ RetentionPolicy::indefinite(),
+ 1_800_000_000_000,
+ )
+ .expect("metadata");
+ PrivateArtifactStore::put_metadata(storage.as_ref(), metadata)
+ .await
+ .expect("store metadata");
+
+ let signer = Arc::new(
+ radroots_nostr::signing::LocalSigner::new(
+ SecretKey::parse(BUYER_SECRET).expect("secret"),
+ )
+ .expect("signer"),
+ );
+ let capability: Arc<dyn SyncStorage> = storage.clone();
+ let engine = Engine::builder(
+ capability,
+ Arc::new(FixedClock),
+ Arc::new(SequenceIds(AtomicU8::new(1))),
+ DeadlinePolicy::new(1_000, 1_000, 1_000).expect("deadlines"),
+ )
+ .sink(Arc::new(NoopSink))
+ .signer(signer)
+ .build()
+ .expect("engine");
+ let client = ClientBuilder::new()
+ .storage(storage.clone())
+ .sync_engine(engine)
+ .build()
+ .expect("client");
+ let operations = client.trade().expect("open").expect("trade operations");
+ let plan = prepare(PrepareRequest::new(
+ actor(BUYER, AuthorRole::Buyer),
+ all_commands().remove(0),
+ ))
+ .expect("plan");
+
+ let verified = operations
+ .verify_private_terms(&plan, artifact_id)
+ .await
+ .expect("private evidence");
+ assert_eq!(verified.artifact_id(), artifact_id);
+ let page = operations
+ .query_visible(EventQuery::all(EventQueryBounds::first(1).expect("bounds")))
+ .await
+ .expect("page");
+ assert!(page.items().is_empty());
+ assert!(page.next_cursor().is_none());
+
+ let targets = TargetSet::new(vec![
+ Target::nostr_relay("wss://trade.example").expect("target"),
+ ])
+ .expect("targets");
+ let satisfaction =
+ SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all());
+ let request = EnqueueRequest::new(
+ SyncId::new([11; 16]).expect("operation id"),
+ IdempotencyKey::parse("trade-proposal-a").expect("idempotency"),
+ plan,
+ Profile::delivery(targets.clone(), satisfaction.clone()).expect("profile"),
+ CancellationPolicy::PreservePublishedRequest,
+ );
+ drop(operations.enqueue(request.clone()));
+ assert_eq!(
+ Outbox::status(storage.as_ref())
+ .await
+ .expect("status")
+ .pending,
+ 0
+ );
+ let committed = operations.enqueue(request.clone()).await.expect("commit");
+ assert!(!committed.is_replay());
+ assert_eq!(committed.outbox().request().target_set(), &targets);
+ let replay = operations.enqueue(request).await.expect("resume replay");
+ assert!(replay.is_replay());
+ assert_eq!(replay.outbox().item_id(), committed.outbox().item_id());
+ }
+ }
+}
diff --git a/crates/sdk/src/trade_runtime.rs b/crates/sdk/src/trade_runtime.rs
@@ -1,2174 +0,0 @@
-#[cfg(feature = "signer-adapters")]
-use crate::workflow_runtime::enqueue_configured_signed_workflow;
-#[cfg(feature = "runtime")]
-use crate::{
- RadrootsClient, RadrootsSdkError, RadrootsSdkRecoveryAction, RadrootsSdkTradeErrorKind,
- SatisfactionPolicy, SdkIdempotencyKey, SdkMutationState, TargetPolicy, TradesClient,
- private_store::{
- SDK_PRIVATE_STORE_SCHEMA_VERSION, SdkPrivateTradeArtifactInput,
- SdkPrivateTradeArtifactKind, SdkPrivateTradeArtifactMetadata,
- },
- runtime::sdk_now_ms,
- workflow_runtime::{
- SdkWorkflowEnqueueReceipt, SdkWorkflowEnqueueRequest, enqueue_signed_workflow,
- },
-};
-#[cfg(feature = "runtime")]
-use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
-#[cfg(feature = "runtime")]
-use radroots_event::{
- draft::EventDraft,
- envelope::kind::TRADE_MUTATION_EVENT_KINDS,
- id::{CandidateId, EventId, MutationId, TradeId},
- trade::{
- RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES, RADROOTS_TRADE_MUTATION_CONTRACT_IDS,
- RADROOTS_TRADE_SCHEMA_VERSION, TradeDecisionV1, TradeMutationBodyV1,
- TradeMutationEnvelopeV1, TradePrivateTermsRefV1, trade_mutation_from_canonical_content,
- },
-};
-#[cfg(feature = "runtime")]
-use radroots_event_codec::encode::trade::trade_mutation_event_build;
-#[cfg(feature = "runtime")]
-use radroots_event_store::{RadrootsStoredTradeMutation, RadrootsTradeProjectionCheckpoint};
-#[cfg(feature = "runtime")]
-use radroots_signing::{Actor, Signer};
-#[cfg(feature = "runtime")]
-use radroots_trade::evidence::{
- RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1,
- RadrootsTradePrivateTermsEvidenceV1,
-};
-#[cfg(feature = "runtime")]
-use radroots_trade::model::{
- RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, RadrootsTradeConflictStateV1,
- RadrootsTradeFulfillmentStateV1, RadrootsTradeNegotiationStateV1, RadrootsTradePaymentStateV1,
- RadrootsTradePrivateTermsStateV1,
-};
-#[cfg(feature = "runtime")]
-use radroots_trade::reducer::{
- RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION, reduce_trade_records,
-};
-#[cfg(feature = "runtime")]
-use radroots_trade::{Projection, ReductionInput};
-#[cfg(feature = "runtime")]
-use serde::{Deserialize, Serialize};
-#[cfg(feature = "runtime")]
-use sha2::{Digest, Sha256};
-#[cfg(feature = "runtime")]
-use sqlx::{QueryBuilder, Row, Sqlite};
-#[cfg(feature = "runtime")]
-use std::collections::{BTreeMap, BTreeSet};
-
-#[cfg(feature = "runtime")]
-pub const TRADE_SUBMIT_PROPOSAL_OPERATION_KIND: &str = "trade.submit_proposal.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_PROPOSE_REVISION_OPERATION_KIND: &str = "trade.propose_revision.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_DECIDE_CANDIDATE_OPERATION_KIND: &str = "trade.decide_candidate.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_CANCEL_OPERATION_KIND: &str = "trade.cancel.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_RESUME_OPERATION_KIND: &str = "trade.resume_operation.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_QUERY_DEFAULT_LIMIT: u32 = 50;
-#[cfg(feature = "runtime")]
-pub const TRADE_QUERY_MAX_LIMIT: u32 = 100;
-#[cfg(feature = "runtime")]
-pub const TRADE_RUNTIME_CAPABILITY_API_VERSION: u16 = 1;
-#[cfg(feature = "runtime")]
-pub const TRADE_RUNTIME_PROTOCOL_PROFILE_ID: &str = "radroots.trade.protocol.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_RUNTIME_WIRE_PROFILE_ID: &str = "radroots.trade.nostr_regular_immutable_jcs.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_RUNTIME_STORAGE_PROFILE_ID: &str = "radroots.sdk.trade.sqlite.v1";
-#[cfg(feature = "runtime")]
-pub const TRADE_RUNTIME_PRIVATE_STORAGE_PROFILE_ID: &str =
- "radroots.sdk.trade.private_artifacts.v1";
-#[cfg(feature = "runtime")]
-const TRADE_MUTATION_QUERY_LIMIT: u32 = 1_000;
-#[cfg(feature = "runtime")]
-const TRADE_LIST_CURSOR_VERSION: u8 = 1;
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct TradeCommandService<'client> {
- sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> TradeCommandService<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn submit_proposal(
- &self,
- request: SubmitProposalRequest,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::SubmitProposal(request);
- enqueue_configured_trade_command(self.sdk, command).await
- }
-
- pub async fn submit_proposal_with_explicit_signer(
- &self,
- request: SubmitProposalRequest,
- signer: &dyn Signer,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::SubmitProposal(request);
- enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn propose_revision(
- &self,
- request: ProposeRevisionRequest,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::ProposeRevision(request);
- enqueue_configured_trade_command(self.sdk, command).await
- }
-
- pub async fn propose_revision_with_explicit_signer(
- &self,
- request: ProposeRevisionRequest,
- signer: &dyn Signer,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::ProposeRevision(request);
- enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn decide_candidate(
- &self,
- request: DecideCandidateRequest,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::DecideCandidate(request);
- enqueue_configured_trade_command(self.sdk, command).await
- }
-
- pub async fn decide_candidate_with_explicit_signer(
- &self,
- request: DecideCandidateRequest,
- signer: &dyn Signer,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::DecideCandidate(request);
- enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn cancel_trade(
- &self,
- request: CancelTradeRequest,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::CancelTrade(request);
- enqueue_configured_trade_command(self.sdk, command).await
- }
-
- pub async fn cancel_trade_with_explicit_signer(
- &self,
- request: CancelTradeRequest,
- signer: &dyn Signer,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::CancelTrade(request);
- enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn resume_operation(
- &self,
- request: ResumeOperationRequest,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::ResumeOperation(request);
- enqueue_configured_trade_command(self.sdk, command).await
- }
-
- pub async fn resume_operation_with_explicit_signer(
- &self,
- request: ResumeOperationRequest,
- signer: &dyn Signer,
- ) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let command = TradeCommandRequest::ResumeOperation(request);
- enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct TradeQueryService<'client> {
- sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> TradeQueryService<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-
- pub async fn get_trade(
- &self,
- request: GetTradeRequest,
- ) -> Result<TradeStatusView, RadrootsSdkError> {
- trade_status_view(self.sdk, &request.trade_id).await
- }
-
- pub async fn list_trades(
- &self,
- request: ListTradesRequest,
- ) -> Result<Page<TradeSummaryView>, RadrootsSdkError> {
- list_trade_views(self.sdk, request).await
- }
-
- pub async fn refresh_evidence(
- &self,
- request: RefreshTradeEvidenceRequest,
- ) -> Result<EvidenceRefreshReceipt, RadrootsSdkError> {
- let view = trade_status_view(self.sdk, &request.trade_id).await?;
- let last = last_trade_mutation_snapshot(self.sdk, &request.trade_id).await?;
- let checkpoint = RadrootsTradeProjectionCheckpoint {
- trade_id: view.trade_id,
- reducer_contract_id: RADROOTS_TRADE_REDUCER_CONTRACT_ID.to_owned(),
- reducer_version: RADROOTS_TRADE_REDUCER_VERSION,
- projection_digest: view.projection.projection_digest().to_owned(),
- root_mutation_id: view.projection.root_mutation_id().copied(),
- negotiation_state: enum_label(&view.projection.negotiation_state())?,
- agreement_state: enum_label(&view.projection.agreement_state())?,
- evidence_state: enum_label(&view.projection.evidence_state())?,
- conflict_state: enum_label(&view.projection.conflict_state())?,
- private_terms_state: enum_label(&view.projection.private_terms_state())?,
- attestation_state: enum_label(&view.projection.attestation_state())?,
- fulfillment_state: enum_label(&view.projection.fulfillment_state())?,
- payment_state: enum_label(&view.projection.payment_state())?,
- projection_json: serde_json::to_string(&view.projection)
- .map_err(trade_query_store_error)?,
- last_mutation_id: last.mutation_id,
- last_transport_event_seq: last.event_seq,
- updated_at_ms: sdk_now_ms(self.sdk)?,
- };
- self.sdk
- ._event_store
- .update_trade_projection_checkpoint(&checkpoint)
- .await?;
- Ok(EvidenceRefreshReceipt {
- api_version: 1,
- trade_id: view.trade_id,
- evidence_count: view.private_terms.len(),
- projection_digest: view.projection.projection_digest().to_owned(),
- projection_state: view.projection.private_terms_state(),
- })
- }
-
- pub async fn inspect_evidence(
- &self,
- request: InspectEvidenceRequest,
- ) -> Result<Page<EvidenceView>, RadrootsSdkError> {
- inspect_evidence_views(self.sdk, request).await
- }
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> TradesClient<'client> {
- pub fn capabilities(&self) -> TradeRuntimeCapabilityReport {
- trade_runtime_capabilities()
- }
-
- pub fn commands(&self) -> TradeCommandService<'client> {
- TradeCommandService::new(self.sdk)
- }
-
- pub fn queries(&self) -> TradeQueryService<'client> {
- TradeQueryService::new(self.sdk)
- }
-
- pub async fn seal_private_artifact(
- &self,
- request: TradePrivateArtifactSealRequest,
- ) -> Result<TradePrivateArtifactSealReceipt, RadrootsSdkError> {
- seal_private_artifact(self.sdk, request).await
- }
-
- pub async fn open_private_artifact(
- &self,
- request: TradePrivateArtifactOpenRequest,
- ) -> Result<Option<TradePrivateArtifactOpenReceipt>, RadrootsSdkError> {
- open_private_artifact(self.sdk, request).await
- }
-
- pub async fn delete_private_artifact(
- &self,
- request: TradePrivateArtifactDeleteRequest,
- ) -> Result<TradePrivateArtifactDeleteReceipt, RadrootsSdkError> {
- delete_private_artifact(self.sdk, request).await
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct SubmitProposalRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub envelope: TradeMutationEnvelopeV1,
- pub target_policy: TargetPolicy,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
-}
-
-#[cfg(feature = "runtime")]
-impl SubmitProposalRequest {
- pub fn new(
- actor: Actor,
- envelope: TradeMutationEnvelopeV1,
- target_policy: TargetPolicy,
- ) -> Self {
- Self {
- actor,
- envelope,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
- }
- }
-
- pub fn with_satisfaction_policy(mut self, policy: SatisfactionPolicy) -> Self {
- self.satisfaction_policy = policy;
- self
- }
-
- pub fn with_idempotency_key(mut self, key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(key);
- self
- }
-
- pub fn try_with_idempotency_key(
- mut self,
- key: impl AsRef<str>,
- ) -> Result<Self, RadrootsSdkError> {
- self.idempotency_key = Some(SdkIdempotencyKey::new(key)?);
- Ok(self)
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct ProposeRevisionRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub envelope: TradeMutationEnvelopeV1,
- pub target_policy: TargetPolicy,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
-}
-
-#[cfg(feature = "runtime")]
-impl ProposeRevisionRequest {
- pub fn new(
- actor: Actor,
- envelope: TradeMutationEnvelopeV1,
- target_policy: TargetPolicy,
- ) -> Self {
- Self {
- actor,
- envelope,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
- }
- }
-
- pub fn with_satisfaction_policy(mut self, policy: SatisfactionPolicy) -> Self {
- self.satisfaction_policy = policy;
- self
- }
-
- pub fn with_idempotency_key(mut self, key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(key);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct DecideCandidateRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub envelope: TradeMutationEnvelopeV1,
- pub target_policy: TargetPolicy,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub private_terms_acknowledged: bool,
-}
-
-#[cfg(feature = "runtime")]
-impl DecideCandidateRequest {
- pub fn new(
- actor: Actor,
- envelope: TradeMutationEnvelopeV1,
- target_policy: TargetPolicy,
- ) -> Self {
- Self {
- actor,
- envelope,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
- private_terms_acknowledged: false,
- }
- }
-
- pub fn with_satisfaction_policy(mut self, policy: SatisfactionPolicy) -> Self {
- self.satisfaction_policy = policy;
- self
- }
-
- pub fn with_idempotency_key(mut self, key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(key);
- self
- }
-
- pub fn acknowledge_private_terms(mut self) -> Self {
- self.private_terms_acknowledged = true;
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct CancelTradeRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub envelope: TradeMutationEnvelopeV1,
- pub target_policy: TargetPolicy,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
-}
-
-#[cfg(feature = "runtime")]
-impl CancelTradeRequest {
- pub fn new(
- actor: Actor,
- envelope: TradeMutationEnvelopeV1,
- target_policy: TargetPolicy,
- ) -> Self {
- Self {
- actor,
- envelope,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
- }
- }
-
- pub fn with_idempotency_key(mut self, key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(key);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct ResumeOperationRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub envelope: TradeMutationEnvelopeV1,
- pub operation_kind: &'static str,
- pub target_policy: TargetPolicy,
- pub satisfaction_policy: SatisfactionPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub private_terms_acknowledged: bool,
-}
-
-#[cfg(feature = "runtime")]
-impl ResumeOperationRequest {
- pub fn new(
- actor: Actor,
- envelope: TradeMutationEnvelopeV1,
- operation_kind: &'static str,
- target_policy: TargetPolicy,
- ) -> Self {
- Self {
- actor,
- envelope,
- operation_kind,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: None,
- private_terms_acknowledged: false,
- }
- }
-
- pub fn with_idempotency_key(mut self, key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(key);
- self
- }
-
- pub fn acknowledge_private_terms(mut self) -> Self {
- self.private_terms_acknowledged = true;
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum TradeCommandLifecycleState {
- Committed,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct TradeCommandReceipt {
- pub api_version: u16,
- pub operation_kind: String,
- pub operation_state: TradeCommandLifecycleState,
- pub trade_id: TradeId,
- pub mutation_id: MutationId,
- pub expected_event_id: EventId,
- pub signed_event_id: EventId,
- pub local_event_seq: i64,
- pub outbox_operation_id: i64,
- pub outbox_event_id: i64,
- pub delivery_state: SdkMutationState,
- pub projection_state: Option<Projection>,
- pub idempotency_digest_prefix: String,
- pub recovery_actions: Vec<RadrootsSdkRecoveryAction>,
- pub warnings: Vec<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum TradePrivateArtifactKind {
- BindingTerms,
- Message,
- ContactBundle,
- DeliveryInstruction,
-}
-
-#[cfg(feature = "runtime")]
-impl From<TradePrivateArtifactKind> for SdkPrivateTradeArtifactKind {
- fn from(value: TradePrivateArtifactKind) -> Self {
- match value {
- TradePrivateArtifactKind::BindingTerms => Self::BindingTerms,
- TradePrivateArtifactKind::Message => Self::Message,
- TradePrivateArtifactKind::ContactBundle => Self::ContactBundle,
- TradePrivateArtifactKind::DeliveryInstruction => Self::DeliveryInstruction,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-impl From<SdkPrivateTradeArtifactKind> for TradePrivateArtifactKind {
- fn from(value: SdkPrivateTradeArtifactKind) -> Self {
- match value {
- SdkPrivateTradeArtifactKind::BindingTerms => Self::BindingTerms,
- SdkPrivateTradeArtifactKind::Message => Self::Message,
- SdkPrivateTradeArtifactKind::ContactBundle => Self::ContactBundle,
- SdkPrivateTradeArtifactKind::DeliveryInstruction => Self::DeliveryInstruction,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct TradePrivateArtifactSealRequest {
- pub artifact_id: String,
- pub trade_id: TradeId,
- pub candidate_id: Option<CandidateId>,
- pub artifact_kind: TradePrivateArtifactKind,
- pub schema_id: String,
- pub plaintext: Vec<u8>,
- pub retention_class: String,
- pub expires_at_ms: Option<i64>,
-}
-
-#[cfg(feature = "runtime")]
-impl TradePrivateArtifactSealRequest {
- pub fn binding_terms(
- artifact_id: impl Into<String>,
- trade_id: TradeId,
- schema_id: impl Into<String>,
- plaintext: impl Into<Vec<u8>>,
- ) -> Self {
- Self {
- artifact_id: artifact_id.into(),
- trade_id,
- candidate_id: None,
- artifact_kind: TradePrivateArtifactKind::BindingTerms,
- schema_id: schema_id.into(),
- plaintext: plaintext.into(),
- retention_class: "trade_private_terms".to_owned(),
- expires_at_ms: None,
- }
- }
-
- pub fn with_retention_class(mut self, retention_class: impl Into<String>) -> Self {
- self.retention_class = retention_class.into();
- self
- }
-
- pub fn with_candidate_id(mut self, candidate_id: CandidateId) -> Self {
- self.candidate_id = Some(candidate_id);
- self
- }
-
- pub fn with_expires_at_ms(mut self, expires_at_ms: i64) -> Self {
- self.expires_at_ms = Some(expires_at_ms);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct TradePrivateArtifactSealReceipt {
- pub artifact_id: String,
- pub trade_id: TradeId,
- pub candidate_id: Option<CandidateId>,
- pub artifact_kind: TradePrivateArtifactKind,
- pub schema_id: String,
- pub ciphertext_commitment: String,
- pub private_terms_ref: Option<TradePrivateTermsRefV1>,
- pub retention_class: String,
- pub created_at_ms: i64,
- pub expires_at_ms: Option<i64>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct TradePrivateArtifactOpenRequest {
- pub artifact_id: String,
-}
-
-#[cfg(feature = "runtime")]
-impl TradePrivateArtifactOpenRequest {
- pub fn new(artifact_id: impl Into<String>) -> Self {
- Self {
- artifact_id: artifact_id.into(),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct TradePrivateArtifactOpenReceipt {
- pub artifact_id: String,
- pub trade_id: TradeId,
- pub candidate_id: Option<CandidateId>,
- pub artifact_kind: TradePrivateArtifactKind,
- pub schema_id: String,
- pub plaintext: Vec<u8>,
- pub retention_class: String,
- pub created_at_ms: i64,
- pub expires_at_ms: Option<i64>,
- pub deleted_at_ms: Option<i64>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct TradePrivateArtifactDeleteRequest {
- pub artifact_id: String,
-}
-
-#[cfg(feature = "runtime")]
-impl TradePrivateArtifactDeleteRequest {
- pub fn new(artifact_id: impl Into<String>) -> Self {
- Self {
- artifact_id: artifact_id.into(),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct TradePrivateArtifactDeleteReceipt {
- pub artifact_id: String,
- pub deleted: bool,
- pub deleted_at_ms: i64,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-#[non_exhaustive]
-pub struct TradeRuntimeCapabilityReport {
- pub api_version: u16,
- pub protocol: TradeProtocolCapabilityReport,
- pub storage: TradeStorageCapabilityReport,
- pub core_mvp: TradeCoreMvpCapabilityReport,
- pub optional_integrations: TradeOptionalIntegrationCapabilityReport,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-#[non_exhaustive]
-pub struct TradeProtocolCapabilityReport {
- pub protocol_profile_id: &'static str,
- pub wire_profile_id: &'static str,
- pub schema_version: u16,
- pub mutation_contract_ids: Vec<&'static str>,
- pub mutation_event_kinds: Vec<u32>,
- pub reducer_contract_id: &'static str,
- pub reducer_version: u16,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-#[non_exhaustive]
-pub struct TradeStorageCapabilityReport {
- pub storage_profile_id: &'static str,
- pub private_storage_profile_id: &'static str,
- pub private_store_schema_version: i64,
- pub max_private_artifact_bytes: usize,
- pub private_artifact_kinds: Vec<TradePrivateArtifactKind>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-#[non_exhaustive]
-pub struct TradeCoreMvpCapabilityReport {
- pub commands: bool,
- pub queries: bool,
- pub local_event_store: bool,
- pub semantic_outbox: bool,
- pub protected_private_artifacts: bool,
- pub backup_restore: bool,
- pub local_signer: bool,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-#[non_exhaustive]
-pub struct TradeOptionalIntegrationCapabilityReport {
- pub myc_nip46_signer: bool,
- pub radrootsd_execution: bool,
- pub rhi_attestation: bool,
- pub tangle_transport: bool,
- pub reticulum_transport: bool,
-}
-
-#[cfg(feature = "runtime")]
-fn trade_runtime_capabilities() -> TradeRuntimeCapabilityReport {
- TradeRuntimeCapabilityReport {
- api_version: TRADE_RUNTIME_CAPABILITY_API_VERSION,
- protocol: TradeProtocolCapabilityReport {
- protocol_profile_id: TRADE_RUNTIME_PROTOCOL_PROFILE_ID,
- wire_profile_id: TRADE_RUNTIME_WIRE_PROFILE_ID,
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- mutation_contract_ids: RADROOTS_TRADE_MUTATION_CONTRACT_IDS.to_vec(),
- mutation_event_kinds: TRADE_MUTATION_EVENT_KINDS.to_vec(),
- reducer_contract_id: RADROOTS_TRADE_REDUCER_CONTRACT_ID,
- reducer_version: RADROOTS_TRADE_REDUCER_VERSION,
- },
- storage: TradeStorageCapabilityReport {
- storage_profile_id: TRADE_RUNTIME_STORAGE_PROFILE_ID,
- private_storage_profile_id: TRADE_RUNTIME_PRIVATE_STORAGE_PROFILE_ID,
- private_store_schema_version: SDK_PRIVATE_STORE_SCHEMA_VERSION,
- max_private_artifact_bytes: RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES,
- private_artifact_kinds: vec![
- TradePrivateArtifactKind::BindingTerms,
- TradePrivateArtifactKind::Message,
- TradePrivateArtifactKind::ContactBundle,
- TradePrivateArtifactKind::DeliveryInstruction,
- ],
- },
- core_mvp: TradeCoreMvpCapabilityReport {
- commands: true,
- queries: true,
- local_event_store: true,
- semantic_outbox: true,
- protected_private_artifacts: true,
- backup_restore: true,
- local_signer: cfg!(feature = "local-signer"),
- },
- optional_integrations: TradeOptionalIntegrationCapabilityReport {
- myc_nip46_signer: cfg!(feature = "signer-adapters"),
- radrootsd_execution: cfg!(feature = "radrootsd-execution"),
- rhi_attestation: false,
- tangle_transport: false,
- reticulum_transport: false,
- },
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct GetTradeRequest {
- pub trade_id: TradeId,
-}
-
-#[cfg(feature = "runtime")]
-impl GetTradeRequest {
- pub fn new(trade_id: TradeId) -> Self {
- Self { trade_id }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct RefreshTradeEvidenceRequest {
- pub trade_id: TradeId,
-}
-
-#[cfg(feature = "runtime")]
-impl RefreshTradeEvidenceRequest {
- pub fn new(trade_id: TradeId) -> Self {
- Self { trade_id }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct InspectEvidenceRequest {
- pub trade_id: TradeId,
- pub limit: Option<u32>,
- pub cursor: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-impl InspectEvidenceRequest {
- pub fn new(trade_id: TradeId) -> Self {
- Self {
- trade_id,
- limit: None,
- cursor: None,
- }
- }
-
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = Some(limit);
- self
- }
-
- pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
- self.cursor = Some(cursor.into());
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
-#[non_exhaustive]
-pub struct TradeListFilter {
- pub buyer_pubkey: Option<String>,
- pub seller_pubkey: Option<String>,
- pub participant_pubkeys_any_of: Vec<String>,
- pub agreement_states_any_of: Vec<RadrootsTradeAgreementStateV1>,
- pub any_of: Vec<TradeListAnyOf>,
-}
-
-#[cfg(feature = "runtime")]
-impl TradeListFilter {
- pub fn buyer(mut self, pubkey: impl Into<String>) -> Self {
- self.buyer_pubkey = Some(pubkey.into());
- self
- }
-
- pub fn seller(mut self, pubkey: impl Into<String>) -> Self {
- self.seller_pubkey = Some(pubkey.into());
- self
- }
-
- pub fn participant_any_of<I, S>(mut self, pubkeys: I) -> Self
- where
- I: IntoIterator<Item = S>,
- S: Into<String>,
- {
- self.participant_pubkeys_any_of = pubkeys.into_iter().map(Into::into).collect();
- self
- }
-
- pub fn agreement_states_any_of<I>(mut self, states: I) -> Self
- where
- I: IntoIterator<Item = RadrootsTradeAgreementStateV1>,
- {
- self.agreement_states_any_of = states.into_iter().collect();
- self
- }
-
- pub fn any_of<I>(mut self, clauses: I) -> Self
- where
- I: IntoIterator<Item = TradeListAnyOf>,
- {
- self.any_of = clauses.into_iter().collect();
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
-pub enum TradeListAnyOf {
- TradeId(String),
- BuyerPubkey(String),
- SellerPubkey(String),
- ParticipantPubkey(String),
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum TradeListSort {
- #[default]
- UpdatedDesc,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize)]
-#[non_exhaustive]
-pub struct ListTradesRequest {
- pub filter: TradeListFilter,
- pub sort: TradeListSort,
- pub limit: Option<u32>,
- pub cursor: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-impl ListTradesRequest {
- pub fn new() -> Self {
- Self {
- filter: TradeListFilter::default(),
- sort: TradeListSort::UpdatedDesc,
- limit: None,
- cursor: None,
- }
- }
-
- pub fn with_filter(mut self, filter: TradeListFilter) -> Self {
- self.filter = filter;
- self
- }
-
- pub fn with_limit(mut self, limit: u32) -> Self {
- self.limit = Some(limit);
- self
- }
-
- pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
- self.cursor = Some(cursor.into());
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-impl Default for ListTradesRequest {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct Page<T> {
- pub items: Vec<T>,
- pub next_cursor: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct TradeStatusView {
- pub trade_id: TradeId,
- pub projection: Projection,
- pub source_event_count: usize,
- pub private_terms: Vec<TradePrivateTermsAvailabilityView>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct TradeSummaryView {
- pub trade_id: TradeId,
- pub root_mutation_id: Option<MutationId>,
- pub buyer_pubkey: Option<String>,
- pub seller_pubkey: Option<String>,
- pub farm_id: Option<String>,
- pub negotiation_state: RadrootsTradeNegotiationStateV1,
- pub agreement_state: RadrootsTradeAgreementStateV1,
- pub evidence_state: RadrootsTradeEvidenceStateV1,
- pub conflict_state: RadrootsTradeConflictStateV1,
- pub private_terms_state: RadrootsTradePrivateTermsStateV1,
- pub attestation_state: RadrootsTradeAttestationStateV1,
- pub fulfillment_state: RadrootsTradeFulfillmentStateV1,
- pub payment_state: RadrootsTradePaymentStateV1,
- pub projection_digest: String,
- pub source_event_count: usize,
- pub updated_event_seq: i64,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct TradePrivateTermsAvailabilityView {
- pub candidate_id: CandidateId,
- pub artifact_id: Option<String>,
- pub schema_id: Option<String>,
- pub ciphertext_commitment: Option<String>,
- pub state: RadrootsTradePrivateTermsStateV1,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct EvidenceRefreshReceipt {
- pub api_version: u16,
- pub trade_id: TradeId,
- pub evidence_count: usize,
- pub projection_digest: String,
- pub projection_state: RadrootsTradePrivateTermsStateV1,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
-pub struct EvidenceView {
- pub artifact_id: String,
- pub trade_id: TradeId,
- pub candidate_id: Option<CandidateId>,
- pub artifact_kind: TradePrivateArtifactKind,
- pub schema_id: String,
- pub ciphertext_commitment: String,
- pub retention_class: String,
- pub state: RadrootsTradePrivateTermsStateV1,
- pub created_at_ms: i64,
- pub expires_at_ms: Option<i64>,
- pub deleted_at_ms: Option<i64>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug)]
-struct TradeCommandPlan {
- operation_kind: &'static str,
- actor: Actor,
- frozen_draft: EventDraft,
- trade_id: TradeId,
- mutation_id: MutationId,
- target_policy: TargetPolicy,
- satisfaction_policy: SatisfactionPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
-}
-
-#[cfg(feature = "runtime")]
-enum TradeCommandRequest {
- SubmitProposal(SubmitProposalRequest),
- ProposeRevision(ProposeRevisionRequest),
- DecideCandidate(DecideCandidateRequest),
- CancelTrade(CancelTradeRequest),
- ResumeOperation(ResumeOperationRequest),
-}
-
-#[cfg(feature = "runtime")]
-impl TradeCommandRequest {
- fn operation_kind(&self) -> &'static str {
- match self {
- Self::SubmitProposal(_) => TRADE_SUBMIT_PROPOSAL_OPERATION_KIND,
- Self::ProposeRevision(_) => TRADE_PROPOSE_REVISION_OPERATION_KIND,
- Self::DecideCandidate(_) => TRADE_DECIDE_CANDIDATE_OPERATION_KIND,
- Self::CancelTrade(_) => TRADE_CANCEL_OPERATION_KIND,
- Self::ResumeOperation(request) => request.operation_kind,
- }
- }
-
- fn into_parts(
- self,
- ) -> (
- Actor,
- TradeMutationEnvelopeV1,
- TargetPolicy,
- SatisfactionPolicy,
- Option<SdkIdempotencyKey>,
- bool,
- ) {
- match self {
- Self::SubmitProposal(request) => (
- request.actor,
- request.envelope,
- request.target_policy,
- request.satisfaction_policy,
- request.idempotency_key,
- false,
- ),
- Self::ProposeRevision(request) => (
- request.actor,
- request.envelope,
- request.target_policy,
- request.satisfaction_policy,
- request.idempotency_key,
- false,
- ),
- Self::DecideCandidate(request) => (
- request.actor,
- request.envelope,
- request.target_policy,
- request.satisfaction_policy,
- request.idempotency_key,
- request.private_terms_acknowledged,
- ),
- Self::CancelTrade(request) => (
- request.actor,
- request.envelope,
- request.target_policy,
- request.satisfaction_policy,
- request.idempotency_key,
- false,
- ),
- Self::ResumeOperation(request) => (
- request.actor,
- request.envelope,
- request.target_policy,
- request.satisfaction_policy,
- request.idempotency_key,
- request.private_terms_acknowledged,
- ),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Serialize, Deserialize)]
-struct TradeListCursorPayload {
- version: u8,
- sort: TradeListSort,
- filter_digest: String,
- updated_event_seq: i64,
- trade_id: String,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug)]
-struct TradeListRow {
- trade_id: TradeId,
- updated_event_seq: i64,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug)]
-struct LastTradeMutationSnapshot {
- mutation_id: Option<MutationId>,
- event_seq: Option<i64>,
-}
-
-#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
-async fn enqueue_configured_trade_command(
- sdk: &RadrootsClient,
- request: TradeCommandRequest,
-) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let plan = trade_command_plan(sdk, request).await?;
- let enqueue = enqueue_configured_signed_workflow(sdk, workflow_request(&plan)).await?;
- trade_command_receipt(sdk, plan, enqueue).await
-}
-
-#[cfg(feature = "runtime")]
-async fn enqueue_trade_command_with_explicit_signer(
- sdk: &RadrootsClient,
- request: TradeCommandRequest,
- signer: &dyn Signer,
-) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let plan = trade_command_plan(sdk, request).await?;
- let enqueue = enqueue_signed_workflow(sdk, workflow_request(&plan), signer).await?;
- trade_command_receipt(sdk, plan, enqueue).await
-}
-
-#[cfg(feature = "runtime")]
-async fn trade_command_plan(
- sdk: &RadrootsClient,
- request: TradeCommandRequest,
-) -> Result<TradeCommandPlan, RadrootsSdkError> {
- let operation_kind = request.operation_kind();
- let (actor, envelope, target_policy, satisfaction_policy, idempotency_key, acknowledged) =
- request.into_parts();
- validate_operation_body(operation_kind, &envelope)?;
- validate_actor_matches_envelope(operation_kind, &actor, &envelope)?;
- let wire = trade_mutation_event_build(envelope.clone()).map_err(|error| {
- trade_command_error(
- RadrootsSdkTradeErrorKind::InvalidEnvelope,
- operation_kind,
- error.to_string(),
- )
- })?;
- let canonical =
- trade_mutation_from_canonical_content(wire.content.as_str()).map_err(|error| {
- trade_command_error(
- RadrootsSdkTradeErrorKind::InvalidEnvelope,
- operation_kind,
- error.to_string(),
- )
- })?;
- validate_command_private_terms(sdk, operation_kind, &canonical, acknowledged).await?;
- let mutation_id = canonical.mutation_id.ok_or_else(|| {
- trade_command_error(
- RadrootsSdkTradeErrorKind::InvalidEnvelope,
- operation_kind,
- "canonical trade mutation is missing mutation id",
- )
- })?;
- let frozen_draft = EventDraft::new(
- canonical.contract_id.as_str(),
- wire.kind,
- canonical.authored_at_unix_s,
- wire.tags,
- wire.content,
- actor.public_key().to_hex(),
- )
- .map_err(|error| {
- trade_command_error(
- RadrootsSdkTradeErrorKind::InvalidEnvelope,
- operation_kind,
- error.to_string(),
- )
- })?;
- Ok(TradeCommandPlan {
- operation_kind,
- actor,
- frozen_draft,
- trade_id: canonical.trade_id,
- mutation_id,
- target_policy,
- satisfaction_policy,
- idempotency_key,
- })
-}
-
-#[cfg(feature = "runtime")]
-fn workflow_request(plan: &TradeCommandPlan) -> SdkWorkflowEnqueueRequest<'_> {
- SdkWorkflowEnqueueRequest {
- operation_kind: plan.operation_kind,
- actor: &plan.actor,
- frozen_draft: &plan.frozen_draft,
- target_policy: plan.target_policy.clone(),
- satisfaction_policy: plan.satisfaction_policy.clone(),
- idempotency_key: plan.idempotency_key.clone(),
- }
-}
-
-#[cfg(feature = "runtime")]
-async fn trade_command_receipt(
- sdk: &RadrootsClient,
- plan: TradeCommandPlan,
- enqueue: SdkWorkflowEnqueueReceipt,
-) -> Result<TradeCommandReceipt, RadrootsSdkError> {
- let projection_state = trade_projection_for_trade(sdk, &plan.trade_id).await.ok();
- Ok(TradeCommandReceipt {
- api_version: 1,
- operation_kind: plan.operation_kind.to_owned(),
- operation_state: TradeCommandLifecycleState::Committed,
- trade_id: plan.trade_id,
- mutation_id: plan.mutation_id,
- expected_event_id: EventId::parse(plan.frozen_draft.expected_event_id_hex())
- .expect("trade workflow draft has a valid 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,
- delivery_state: enqueue.state.into(),
- projection_state,
- idempotency_digest_prefix: enqueue.idempotency_digest_prefix,
- recovery_actions: Vec::new(),
- warnings: Vec::new(),
- })
-}
-
-#[cfg(feature = "runtime")]
-fn validate_operation_body(
- operation_kind: &'static str,
- envelope: &TradeMutationEnvelopeV1,
-) -> Result<(), RadrootsSdkError> {
- let valid = match operation_kind {
- TRADE_SUBMIT_PROPOSAL_OPERATION_KIND => {
- matches!(envelope.body, TradeMutationBodyV1::Proposal { .. })
- }
- TRADE_PROPOSE_REVISION_OPERATION_KIND => {
- matches!(envelope.body, TradeMutationBodyV1::RevisionProposal { .. })
- }
- TRADE_DECIDE_CANDIDATE_OPERATION_KIND | TRADE_RESUME_OPERATION_KIND => matches!(
- envelope.body,
- TradeMutationBodyV1::Decision { .. } | TradeMutationBodyV1::RevisionDecision { .. }
- ),
- TRADE_CANCEL_OPERATION_KIND => {
- matches!(envelope.body, TradeMutationBodyV1::Cancellation { .. })
- }
- _ => true,
- };
- if valid {
- Ok(())
- } else {
- Err(trade_command_error(
- RadrootsSdkTradeErrorKind::InvalidCommandBody,
- operation_kind,
- "trade command operation kind does not match mutation body",
- ))
- }
-}
-
-#[cfg(feature = "runtime")]
-fn validate_actor_matches_envelope(
- operation_kind: &'static str,
- actor: &Actor,
- envelope: &TradeMutationEnvelopeV1,
-) -> Result<(), RadrootsSdkError> {
- if actor.public_key() == envelope.author_pubkey {
- Ok(())
- } else {
- Err(RadrootsSdkError::UnauthorizedActor {
- operation: operation_kind.to_owned(),
- reason: "actor pubkey must match trade mutation author_pubkey".to_owned(),
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-async fn validate_command_private_terms(
- sdk: &RadrootsClient,
- operation_kind: &'static str,
- envelope: &TradeMutationEnvelopeV1,
- private_terms_acknowledged: bool,
-) -> Result<(), RadrootsSdkError> {
- match &envelope.body {
- TradeMutationBodyV1::Proposal { candidate }
- | TradeMutationBodyV1::RevisionProposal { candidate } => {
- let Some(candidate_id) = &candidate.candidate_id else {
- return Ok(());
- };
- if candidate.fulfillment.requires_private_terms && candidate.private_terms.is_none() {
- return Err(trade_command_error(
- RadrootsSdkTradeErrorKind::PrivateArtifactMissing,
- operation_kind,
- "candidate requires private terms but no private terms reference is present",
- ));
- }
- if let Some(private_ref) = &candidate.private_terms {
- ensure_private_terms_ref_available(
- sdk,
- operation_kind,
- &envelope.trade_id,
- candidate_id,
- private_ref,
- )
- .await?;
- }
- Ok(())
- }
- TradeMutationBodyV1::Decision {
- proposal_mutation_id,
- candidate_id,
- decision,
- }
- | TradeMutationBodyV1::RevisionDecision {
- proposal_mutation_id,
- candidate_id,
- decision,
- } => {
- if !matches!(decision, TradeDecisionV1::Accepted { .. }) {
- return Ok(());
- }
- let Some(candidate_ref) = referenced_candidate_for_decision(
- sdk,
- operation_kind,
- proposal_mutation_id,
- candidate_id,
- )
- .await?
- else {
- return Ok(());
- };
- let requires_private_terms = candidate_ref.fulfillment.requires_private_terms
- || candidate_ref.private_terms.is_some();
- if !requires_private_terms {
- return Ok(());
- }
- let Some(private_ref) = &candidate_ref.private_terms else {
- return Err(trade_command_error(
- RadrootsSdkTradeErrorKind::PrivateArtifactMissing,
- operation_kind,
- "accepted candidate requires private terms but no private terms reference is present",
- ));
- };
- if private_ref.required_acknowledgement && !private_terms_acknowledged {
- return Err(trade_command_error(
- RadrootsSdkTradeErrorKind::PrivateArtifactAcknowledgementMissing,
- operation_kind,
- "accepted candidate private terms require explicit acknowledgement",
- ));
- }
- ensure_private_terms_ref_available(
- sdk,
- operation_kind,
- &envelope.trade_id,
- candidate_id,
- private_ref,
- )
- .await
- }
- TradeMutationBodyV1::Cancellation { .. } => Ok(()),
- }
-}
-
-#[cfg(feature = "runtime")]
-async fn referenced_candidate_for_decision(
- sdk: &RadrootsClient,
- operation_kind: &'static str,
- proposal_mutation_id: &MutationId,
- candidate_id: &CandidateId,
-) -> Result<Option<radroots_event::trade::TradeCandidateTermsV1>, RadrootsSdkError> {
- let Some(stored) = sdk
- ._event_store
- .get_trade_mutation(proposal_mutation_id)
- .await?
- else {
- return Err(trade_command_error(
- RadrootsSdkTradeErrorKind::TradeNotFound,
- operation_kind,
- "referenced proposal mutation is not present in the local event store",
- ));
- };
- let envelope = stored_trade_envelope(&stored)?;
- let candidate = match envelope.body {
- TradeMutationBodyV1::Proposal { candidate }
- | TradeMutationBodyV1::RevisionProposal { candidate } => candidate,
- _ => {
- return Err(trade_command_error(
- RadrootsSdkTradeErrorKind::InvalidCommandBody,
- operation_kind,
- "referenced mutation is not a candidate proposal",
- ));
- }
- };
- if candidate.candidate_id.as_ref() != Some(candidate_id) {
- return Err(trade_command_error(
- RadrootsSdkTradeErrorKind::InvalidCommandBody,
- operation_kind,
- "decision candidate_id does not match referenced proposal candidate_id",
- ));
- }
- Ok(Some(candidate))
-}
-
-#[cfg(feature = "runtime")]
-async fn ensure_private_terms_ref_available(
- sdk: &RadrootsClient,
- operation_kind: &'static str,
- trade_id: &TradeId,
- candidate_id: &CandidateId,
- private_ref: &TradePrivateTermsRefV1,
-) -> Result<(), RadrootsSdkError> {
- let trade_id_hex = trade_id.to_hex();
- let candidate_id_hex = candidate_id.to_hex();
- let evidence = sdk
- ._private_store
- .private_terms_evidence(
- trade_id_hex.as_str(),
- candidate_id_hex.as_str(),
- private_ref.artifact_id.as_str(),
- private_ref.schema_id.as_str(),
- private_ref.ciphertext_commitment.as_str(),
- )
- .await?;
- match evidence.state() {
- RadrootsTradePrivateTermsStateV1::AvailableVerified => Ok(()),
- RadrootsTradePrivateTermsStateV1::CommitmentMismatch => Err(trade_command_error(
- RadrootsSdkTradeErrorKind::PrivateArtifactCommitmentMismatch,
- operation_kind,
- "private artifact commitment does not match candidate private terms reference",
- )),
- _ => Err(trade_command_error(
- RadrootsSdkTradeErrorKind::PrivateArtifactMissing,
- operation_kind,
- "private artifact is unavailable for candidate private terms reference",
- )),
- }
-}
-
-#[cfg(feature = "runtime")]
-async fn seal_private_artifact(
- sdk: &RadrootsClient,
- request: TradePrivateArtifactSealRequest,
-) -> Result<TradePrivateArtifactSealReceipt, RadrootsSdkError> {
- let now_ms = sdk_now_ms(sdk)?;
- let input = SdkPrivateTradeArtifactInput {
- artifact_id: request.artifact_id,
- trade_id: request.trade_id.to_hex(),
- candidate_id: request.candidate_id.as_ref().map(CandidateId::to_hex),
- artifact_kind: request.artifact_kind.into(),
- schema_id: request.schema_id,
- plaintext: request.plaintext,
- retention_class: request.retention_class,
- created_at_ms: now_ms,
- expires_at_ms: request.expires_at_ms,
- };
- let metadata = sdk._private_store.upsert_trade_artifact(&input).await?;
- Ok(seal_receipt_from_metadata(metadata))
-}
-
-#[cfg(feature = "runtime")]
-fn seal_receipt_from_metadata(
- metadata: SdkPrivateTradeArtifactMetadata,
-) -> TradePrivateArtifactSealReceipt {
- let artifact_kind = TradePrivateArtifactKind::from(metadata.artifact_kind);
- let private_terms_ref = if artifact_kind == TradePrivateArtifactKind::BindingTerms {
- Some(TradePrivateTermsRefV1 {
- artifact_id: metadata.artifact_id.clone(),
- schema_id: metadata.schema_id.clone(),
- ciphertext_commitment: metadata.ciphertext_commitment.clone(),
- required_acknowledgement: true,
- })
- } else {
- None
- };
- TradePrivateArtifactSealReceipt {
- artifact_id: metadata.artifact_id,
- trade_id: TradeId::parse(metadata.trade_id).expect("stored trade id is valid"),
- candidate_id: parse_optional_candidate_id(metadata.candidate_id),
- artifact_kind,
- schema_id: metadata.schema_id,
- ciphertext_commitment: metadata.ciphertext_commitment,
- private_terms_ref,
- retention_class: metadata.retention_class,
- created_at_ms: metadata.created_at_ms,
- expires_at_ms: metadata.expires_at_ms,
- }
-}
-
-#[cfg(feature = "runtime")]
-async fn open_private_artifact(
- sdk: &RadrootsClient,
- request: TradePrivateArtifactOpenRequest,
-) -> Result<Option<TradePrivateArtifactOpenReceipt>, RadrootsSdkError> {
- let Some(record) = sdk
- ._private_store
- .trade_artifact(request.artifact_id.as_str())
- .await?
- else {
- return Ok(None);
- };
- Ok(Some(TradePrivateArtifactOpenReceipt {
- artifact_id: record.artifact_id,
- trade_id: TradeId::parse(record.trade_id).expect("stored trade id is valid"),
- candidate_id: parse_optional_candidate_id(record.candidate_id),
- artifact_kind: record.artifact_kind.into(),
- schema_id: record.schema_id,
- plaintext: record.plaintext,
- retention_class: record.retention_class,
- created_at_ms: record.created_at_ms,
- expires_at_ms: record.expires_at_ms,
- deleted_at_ms: record.deleted_at_ms,
- }))
-}
-
-#[cfg(feature = "runtime")]
-async fn delete_private_artifact(
- sdk: &RadrootsClient,
- request: TradePrivateArtifactDeleteRequest,
-) -> Result<TradePrivateArtifactDeleteReceipt, RadrootsSdkError> {
- let deleted_at_ms = sdk_now_ms(sdk)?;
- let deleted = sdk
- ._private_store
- .delete_trade_artifact(request.artifact_id.as_str(), deleted_at_ms)
- .await?;
- Ok(TradePrivateArtifactDeleteReceipt {
- artifact_id: request.artifact_id,
- deleted,
- deleted_at_ms,
- })
-}
-
-#[cfg(feature = "runtime")]
-async fn trade_status_view(
- sdk: &RadrootsClient,
- trade_id: &TradeId,
-) -> Result<TradeStatusView, RadrootsSdkError> {
- let projection = trade_projection_for_trade(sdk, trade_id).await?;
- let private_terms = private_terms_views_for_trade(sdk, trade_id).await?;
- let source_event_count = sdk
- ._event_store
- .trade_mutations_for_trade(trade_id, TRADE_MUTATION_QUERY_LIMIT)
- .await?
- .len();
- Ok(TradeStatusView {
- trade_id: *trade_id,
- projection,
- source_event_count,
- private_terms,
- })
-}
-
-#[cfg(feature = "runtime")]
-async fn trade_projection_for_trade(
- sdk: &RadrootsClient,
- trade_id: &TradeId,
-) -> Result<Projection, RadrootsSdkError> {
- let stored = sdk
- ._event_store
- .trade_mutations_for_trade(trade_id, TRADE_MUTATION_QUERY_LIMIT)
- .await?;
- if stored.is_empty() {
- return Err(trade_query_error(
- RadrootsSdkTradeErrorKind::TradeNotFound,
- "trade.get",
- "trade is not present in the local event store",
- ));
- }
- let mutations = stored
- .iter()
- .map(stored_trade_mutation_record)
- .collect::<Result<Vec<_>, _>>()?;
- let private_terms = private_terms_evidence_for_mutations(sdk, trade_id, &mutations).await?;
- let input = ReductionInput::new(*trade_id)
- .with_mutations(mutations)
- .with_private_terms(private_terms)
- .with_observed_at_unix_s(Some(sdk.now()?.unix_seconds()));
- Ok(reduce_trade_records(input))
-}
-
-#[cfg(feature = "runtime")]
-async fn private_terms_views_for_trade(
- sdk: &RadrootsClient,
- trade_id: &TradeId,
-) -> Result<Vec<TradePrivateTermsAvailabilityView>, RadrootsSdkError> {
- let stored = sdk
- ._event_store
- .trade_mutations_for_trade(trade_id, TRADE_MUTATION_QUERY_LIMIT)
- .await?;
- let records = stored
- .iter()
- .map(stored_trade_mutation_record)
- .collect::<Result<Vec<_>, _>>()?;
- let evidence = private_terms_evidence_for_mutations(sdk, trade_id, &records).await?;
- let mut evidence_by_candidate = evidence
- .into_iter()
- .map(|item| (*item.candidate_id(), item.state()))
- .collect::<BTreeMap<_, _>>();
- let mut views = BTreeMap::<CandidateId, TradePrivateTermsAvailabilityView>::new();
- for record in records {
- if let Some((candidate_id, private_ref)) = candidate_private_ref(record.mutation()) {
- let state = evidence_by_candidate
- .remove(&candidate_id)
- .unwrap_or(RadrootsTradePrivateTermsStateV1::Missing);
- views.insert(
- candidate_id,
- TradePrivateTermsAvailabilityView {
- candidate_id,
- artifact_id: Some(private_ref.artifact_id.clone()),
- schema_id: Some(private_ref.schema_id.clone()),
- ciphertext_commitment: Some(private_ref.ciphertext_commitment.clone()),
- state,
- },
- );
- }
- }
- Ok(views.into_values().collect())
-}
-
-#[cfg(feature = "runtime")]
-async fn private_terms_evidence_for_mutations(
- sdk: &RadrootsClient,
- trade_id: &TradeId,
- mutations: &[RadrootsTradeMutationRecordV1],
-) -> Result<Vec<RadrootsTradePrivateTermsEvidenceV1>, RadrootsSdkError> {
- let mut evidence = Vec::new();
- let mut seen = BTreeSet::new();
- let trade_id_hex = trade_id.to_hex();
- for record in mutations {
- if let Some((candidate_id, private_ref)) = candidate_private_ref(record.mutation()) {
- if !seen.insert(candidate_id) {
- continue;
- }
- let candidate_id_hex = candidate_id.to_hex();
- evidence.push(
- sdk._private_store
- .private_terms_evidence(
- trade_id_hex.as_str(),
- candidate_id_hex.as_str(),
- private_ref.artifact_id.as_str(),
- private_ref.schema_id.as_str(),
- private_ref.ciphertext_commitment.as_str(),
- )
- .await?,
- );
- }
- }
- Ok(evidence)
-}
-
-#[cfg(feature = "runtime")]
-fn candidate_private_ref(
- envelope: &TradeMutationEnvelopeV1,
-) -> Option<(CandidateId, TradePrivateTermsRefV1)> {
- match &envelope.body {
- TradeMutationBodyV1::Proposal { candidate }
- | TradeMutationBodyV1::RevisionProposal { candidate } => {
- let candidate_id = candidate.candidate_id?;
- let private_ref = candidate.private_terms.clone()?;
- Some((candidate_id, private_ref))
- }
- _ => None,
- }
-}
-
-#[cfg(feature = "runtime")]
-fn stored_trade_mutation_record(
- stored: &RadrootsStoredTradeMutation,
-) -> Result<RadrootsTradeMutationRecordV1, RadrootsSdkError> {
- Ok(RadrootsTradeMutationRecordV1::new(
- Some(stored.first_transport_event_id),
- stored_trade_envelope(stored)?,
- ))
-}
-
-#[cfg(feature = "runtime")]
-fn stored_trade_envelope(
- stored: &RadrootsStoredTradeMutation,
-) -> Result<TradeMutationEnvelopeV1, RadrootsSdkError> {
- let content =
- std::str::from_utf8(stored.canonical_payload_bytes.as_slice()).map_err(|error| {
- RadrootsSdkError::Projection {
- message: error.to_string(),
- }
- })?;
- trade_mutation_from_canonical_content(content).map_err(|error| RadrootsSdkError::Projection {
- message: error.to_string(),
- })
-}
-
-#[cfg(feature = "runtime")]
-async fn list_trade_views(
- sdk: &RadrootsClient,
- request: ListTradesRequest,
-) -> Result<Page<TradeSummaryView>, RadrootsSdkError> {
- let limit = bounded_limit(request.limit, "trade.list")?;
- let filter_digest = filter_digest(&request)?;
- let cursor = request
- .cursor
- .as_deref()
- .map(|cursor| decode_trade_list_cursor(cursor, &filter_digest, request.sort))
- .transpose()?;
- let rows = list_trade_rows(sdk, &request, cursor.as_ref(), limit + 1).await?;
- let mut items = Vec::new();
- for row in rows
- .iter()
- .take(usize::try_from(limit).expect("limit fits usize"))
- {
- let view = trade_status_view(sdk, &row.trade_id).await?;
- items.push(TradeSummaryView {
- trade_id: view.trade_id,
- root_mutation_id: view.projection.root_mutation_id().copied(),
- buyer_pubkey: view.projection.buyer_pubkey().map(ToString::to_string),
- seller_pubkey: view.projection.seller_pubkey().map(ToString::to_string),
- farm_id: view.projection.farm_id().map(ToString::to_string),
- negotiation_state: view.projection.negotiation_state(),
- agreement_state: view.projection.agreement_state(),
- evidence_state: view.projection.evidence_state(),
- conflict_state: view.projection.conflict_state(),
- private_terms_state: view.projection.private_terms_state(),
- attestation_state: view.projection.attestation_state(),
- fulfillment_state: view.projection.fulfillment_state(),
- payment_state: view.projection.payment_state(),
- projection_digest: view.projection.projection_digest().to_owned(),
- source_event_count: view.source_event_count,
- updated_event_seq: row.updated_event_seq,
- });
- }
- let next_cursor = if rows.len() > usize::try_from(limit).expect("limit fits usize") {
- rows.get(usize::try_from(limit - 1).expect("limit fits usize"))
- .map(|row| encode_trade_list_cursor(row, &filter_digest, request.sort))
- .transpose()?
- .flatten()
- } else {
- None
- };
- Ok(Page { items, next_cursor })
-}
-
-#[cfg(feature = "runtime")]
-async fn list_trade_rows(
- sdk: &RadrootsClient,
- request: &ListTradesRequest,
- cursor: Option<&TradeListCursorPayload>,
- limit: u32,
-) -> Result<Vec<TradeListRow>, RadrootsSdkError> {
- let mut query: QueryBuilder<Sqlite> = QueryBuilder::new(
- "SELECT m.trade_id, MAX(m.first_event_seq) AS updated_event_seq FROM trade_mutation m",
- );
- if !request.filter.agreement_states_any_of.is_empty() {
- query.push(" JOIN trade_projection_checkpoint c ON c.trade_id = m.trade_id");
- }
- query.push(" WHERE 1 = 1");
- if let Some(buyer_pubkey) = &request.filter.buyer_pubkey {
- query.push(" AND m.buyer_pubkey = ");
- query.push_bind(buyer_pubkey);
- }
- if let Some(seller_pubkey) = &request.filter.seller_pubkey {
- query.push(" AND m.seller_pubkey = ");
- query.push_bind(seller_pubkey);
- }
- if !request.filter.participant_pubkeys_any_of.is_empty() {
- query.push(" AND (m.buyer_pubkey IN (");
- push_string_list(&mut query, &request.filter.participant_pubkeys_any_of);
- query.push(") OR m.seller_pubkey IN (");
- push_string_list(&mut query, &request.filter.participant_pubkeys_any_of);
- query.push("))");
- }
- if !request.filter.agreement_states_any_of.is_empty() {
- let labels = request
- .filter
- .agreement_states_any_of
- .iter()
- .map(enum_label)
- .collect::<Result<Vec<_>, _>>()?;
- query.push(" AND c.agreement_state IN (");
- push_string_list(&mut query, &labels);
- query.push(")");
- }
- if !request.filter.any_of.is_empty() {
- query.push(" AND (");
- for (index, clause) in request.filter.any_of.iter().enumerate() {
- if index > 0 {
- query.push(" OR ");
- }
- match clause {
- TradeListAnyOf::TradeId(value) => {
- query.push("m.trade_id = ");
- query.push_bind(value);
- }
- TradeListAnyOf::BuyerPubkey(value) => {
- query.push("m.buyer_pubkey = ");
- query.push_bind(value);
- }
- TradeListAnyOf::SellerPubkey(value) => {
- query.push("m.seller_pubkey = ");
- query.push_bind(value);
- }
- TradeListAnyOf::ParticipantPubkey(value) => {
- query.push("(m.buyer_pubkey = ");
- query.push_bind(value);
- query.push(" OR m.seller_pubkey = ");
- query.push_bind(value);
- query.push(")");
- }
- }
- }
- query.push(")");
- }
- query.push(" GROUP BY m.trade_id");
- if let Some(cursor) = cursor {
- query.push(" HAVING (updated_event_seq < ");
- query.push_bind(cursor.updated_event_seq);
- query.push(" OR (updated_event_seq = ");
- query.push_bind(cursor.updated_event_seq);
- query.push(" AND m.trade_id > ");
- query.push_bind(cursor.trade_id.as_str());
- query.push("))");
- }
- query.push(" ORDER BY updated_event_seq DESC, m.trade_id ASC LIMIT ");
- query.push_bind(i64::from(limit));
- let rows = query
- .build()
- .fetch_all(sdk._event_store.pool())
- .await
- .map_err(trade_query_store_error)?;
- rows.into_iter()
- .map(|row| {
- Ok(TradeListRow {
- trade_id: TradeId::parse(
- row.try_get::<String, _>("trade_id")
- .map_err(trade_query_store_error)?,
- )
- .map_err(|error| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::InvalidEnvelope,
- "trade.list",
- format!("stored trade id is invalid: {error}"),
- )
- })?,
- updated_event_seq: row
- .try_get("updated_event_seq")
- .map_err(trade_query_store_error)?,
- })
- })
- .collect()
-}
-
-#[cfg(feature = "runtime")]
-fn push_string_list(query: &mut QueryBuilder<Sqlite>, values: &[String]) {
- let mut separated = query.separated(", ");
- for value in values {
- separated.push_bind(value.as_str());
- }
-}
-
-#[cfg(feature = "runtime")]
-async fn inspect_evidence_views(
- sdk: &RadrootsClient,
- request: InspectEvidenceRequest,
-) -> Result<Page<EvidenceView>, RadrootsSdkError> {
- let limit = bounded_limit(request.limit, "trade.inspect_evidence")?;
- let offset = evidence_cursor_offset(request.cursor.as_deref())?;
- let metadata = sdk
- ._private_store
- .trade_artifact_metadata_for_trade(&request.trade_id.to_hex())
- .await?;
- let mut items = Vec::new();
- for item in metadata
- .iter()
- .skip(offset)
- .take(usize::try_from(limit).expect("limit fits usize"))
- {
- let state = if item.deleted_at_ms.is_some() {
- RadrootsTradePrivateTermsStateV1::Missing
- } else {
- RadrootsTradePrivateTermsStateV1::AvailableVerified
- };
- items.push(EvidenceView {
- artifact_id: item.artifact_id.clone(),
- trade_id: TradeId::parse(item.trade_id.clone()).expect("stored trade id is valid"),
- candidate_id: parse_optional_candidate_id(item.candidate_id.clone()),
- artifact_kind: item.artifact_kind.into(),
- schema_id: item.schema_id.clone(),
- ciphertext_commitment: item.ciphertext_commitment.clone(),
- retention_class: item.retention_class.clone(),
- state,
- created_at_ms: item.created_at_ms,
- expires_at_ms: item.expires_at_ms,
- deleted_at_ms: item.deleted_at_ms,
- });
- }
- let next_offset = offset + items.len();
- let next_cursor = if metadata.len() > next_offset {
- Some(encode_offset_cursor(next_offset)?)
- } else {
- None
- };
- Ok(Page { items, next_cursor })
-}
-
-#[cfg(feature = "runtime")]
-async fn last_trade_mutation_snapshot(
- sdk: &RadrootsClient,
- trade_id: &TradeId,
-) -> Result<LastTradeMutationSnapshot, RadrootsSdkError> {
- let row = sqlx::query(
- "SELECT mutation_id, first_event_seq FROM trade_mutation WHERE trade_id = ? ORDER BY first_event_seq DESC, mutation_id DESC LIMIT 1",
- )
- .bind(trade_id.to_hex())
- .fetch_optional(sdk._event_store.pool())
- .await
- .map_err(trade_query_store_error)?;
- match row {
- Some(row) => Ok(LastTradeMutationSnapshot {
- mutation_id: Some(
- MutationId::parse(
- row.try_get::<String, _>("mutation_id")
- .map_err(trade_query_store_error)?,
- )
- .map_err(|error| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::InvalidEnvelope,
- "trade.refresh_evidence",
- format!("stored mutation id is invalid: {error}"),
- )
- })?,
- ),
- event_seq: Some(
- row.try_get("first_event_seq")
- .map_err(trade_query_store_error)?,
- ),
- }),
- None => Ok(LastTradeMutationSnapshot {
- mutation_id: None,
- event_seq: None,
- }),
- }
-}
-
-#[cfg(feature = "runtime")]
-fn bounded_limit(limit: Option<u32>, operation: &'static str) -> Result<u32, RadrootsSdkError> {
- let limit = limit.unwrap_or(TRADE_QUERY_DEFAULT_LIMIT);
- if !(1..=TRADE_QUERY_MAX_LIMIT).contains(&limit) {
- return Err(trade_query_error(
- RadrootsSdkTradeErrorKind::QueryLimitInvalid,
- operation,
- format!("trade query limit must be between 1 and {TRADE_QUERY_MAX_LIMIT}"),
- ));
- }
- Ok(limit)
-}
-
-#[cfg(feature = "runtime")]
-fn filter_digest(request: &ListTradesRequest) -> Result<String, RadrootsSdkError> {
- let bytes = serde_json::to_vec(&serde_json::json!({
- "filter": request.filter,
- "sort": request.sort
- }))
- .map_err(trade_query_store_error)?;
- Ok(hex::encode(Sha256::digest(bytes)))
-}
-
-#[cfg(feature = "runtime")]
-fn encode_trade_list_cursor(
- row: &TradeListRow,
- filter_digest: &str,
- sort: TradeListSort,
-) -> Result<Option<String>, RadrootsSdkError> {
- let payload = TradeListCursorPayload {
- version: TRADE_LIST_CURSOR_VERSION,
- sort,
- filter_digest: filter_digest.to_owned(),
- updated_event_seq: row.updated_event_seq,
- trade_id: row.trade_id.to_string(),
- };
- let bytes = serde_json::to_vec(&payload).map_err(trade_query_store_error)?;
- Ok(Some(URL_SAFE_NO_PAD.encode(bytes)))
-}
-
-#[cfg(feature = "runtime")]
-fn decode_trade_list_cursor(
- cursor: &str,
- filter_digest: &str,
- sort: TradeListSort,
-) -> Result<TradeListCursorPayload, RadrootsSdkError> {
- let bytes = URL_SAFE_NO_PAD.decode(cursor).map_err(|error| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.list",
- error.to_string(),
- )
- })?;
- let payload: TradeListCursorPayload =
- serde_json::from_slice(bytes.as_slice()).map_err(|error| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.list",
- error.to_string(),
- )
- })?;
- if payload.version != TRADE_LIST_CURSOR_VERSION
- || payload.filter_digest != filter_digest
- || payload.sort != sort
- {
- return Err(trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.list",
- "trade list cursor does not match request filter or sort",
- ));
- }
- Ok(payload)
-}
-
-#[cfg(feature = "runtime")]
-fn evidence_cursor_offset(cursor: Option<&str>) -> Result<usize, RadrootsSdkError> {
- let Some(cursor) = cursor else {
- return Ok(0);
- };
- let bytes = URL_SAFE_NO_PAD.decode(cursor).map_err(|error| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.inspect_evidence",
- error.to_string(),
- )
- })?;
- let value: serde_json::Value = serde_json::from_slice(bytes.as_slice()).map_err(|error| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.inspect_evidence",
- error.to_string(),
- )
- })?;
- let version = value
- .get("version")
- .and_then(serde_json::Value::as_u64)
- .ok_or_else(|| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.inspect_evidence",
- "evidence cursor is missing version",
- )
- })?;
- let offset = value
- .get("offset")
- .and_then(serde_json::Value::as_u64)
- .ok_or_else(|| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.inspect_evidence",
- "evidence cursor is missing offset",
- )
- })?;
- if version != 1 {
- return Err(trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.inspect_evidence",
- "evidence cursor version is unsupported",
- ));
- }
- usize::try_from(offset).map_err(|_| {
- trade_query_error(
- RadrootsSdkTradeErrorKind::CursorInvalid,
- "trade.inspect_evidence",
- "evidence cursor offset is too large",
- )
- })
-}
-
-#[cfg(feature = "runtime")]
-fn encode_offset_cursor(offset: usize) -> Result<String, RadrootsSdkError> {
- let bytes = serde_json::to_vec(&serde_json::json!({
- "version": 1,
- "offset": offset
- }))
- .map_err(trade_query_store_error)?;
- Ok(URL_SAFE_NO_PAD.encode(bytes))
-}
-
-#[cfg(feature = "runtime")]
-fn enum_label<T: Serialize>(value: &T) -> Result<String, RadrootsSdkError> {
- serde_json::to_value(value)
- .map_err(trade_query_store_error)?
- .as_str()
- .map(ToOwned::to_owned)
- .ok_or_else(|| RadrootsSdkError::Projection {
- message: "projection state label did not serialize to string".to_owned(),
- })
-}
-
-#[cfg(feature = "runtime")]
-fn parse_optional_candidate_id(value: Option<String>) -> Option<CandidateId> {
- value
- .map(|candidate_id| CandidateId::parse(candidate_id).expect("stored candidate id is valid"))
-}
-
-#[cfg(feature = "runtime")]
-fn trade_command_error(
- kind: RadrootsSdkTradeErrorKind,
- operation: &'static str,
- message: impl Into<String>,
-) -> RadrootsSdkError {
- RadrootsSdkError::Trade {
- kind,
- operation: operation.to_owned(),
- message: message.into(),
- }
-}
-
-#[cfg(feature = "runtime")]
-fn trade_query_error(
- kind: RadrootsSdkTradeErrorKind,
- operation: &'static str,
- message: impl Into<String>,
-) -> RadrootsSdkError {
- RadrootsSdkError::Trade {
- kind,
- operation: operation.to_owned(),
- message: message.into(),
- }
-}
-
-#[cfg(feature = "runtime")]
-fn trade_query_store_error(error: impl ToString) -> RadrootsSdkError {
- RadrootsSdkError::Projection {
- message: error.to_string(),
- }
-}
-
-#[cfg(all(test, feature = "runtime", feature = "signer-adapters"))]
-#[path = "../tests/unit/trade_runtime_tests.rs"]
-mod tests;
diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs
@@ -5,6 +5,7 @@ const ROOT: &str = include_str!("../src/lib.rs");
const CLIENT: &str = include_str!("../src/client.rs");
const FARM: &str = include_str!("../src/farm.rs");
const LISTING: &str = include_str!("../src/listing.rs");
+const TRADE: &str = include_str!("../src/trade.rs");
const SYNC: &str = include_str!("../src/sync.rs");
const TRANSPORT: &str = include_str!("../src/transport.rs");
@@ -267,6 +268,46 @@ fn listing_operations_reuse_trade_event_sync_and_privacy_boundaries() {
);
}
+#[test]
+fn trade_operations_use_canonical_workflow_storage_sync_and_projection_types() {
+ for required in [
+ "WorkflowPlan",
+ "TradeMutationEnvelopeV1",
+ "ReductionInput",
+ "Projection",
+ "reduce_trade_records",
+ "EventQuery",
+ "EventPage<StoredVisibleEvent>",
+ "PrivateArtifactMetadata",
+ "radroots_sync::PushRequest::new",
+ ] {
+ assert!(
+ TRADE.contains(required),
+ "missing trade boundary `{required}`"
+ );
+ }
+ for forbidden in [
+ "sqlx::",
+ "QueryBuilder",
+ "TradeStatusView",
+ "SdkPrivateTradeArtifact",
+ "SdkMutationState",
+ "TradeCommandReceipt",
+ "struct Page",
+ "struct TradeId",
+ ] {
+ assert!(
+ !TRADE.contains(forbidden),
+ "trade source contains retired duplicate `{forbidden}`"
+ );
+ }
+ assert!(
+ !std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("src/trade_runtime.rs")
+ .exists()
+ );
+}
+
fn dependency_names(manifest: &str) -> BTreeSet<&str> {
let dependencies = manifest
.split_once("[dependencies]")
diff --git a/crates/sdk/tests/unit/trade_runtime_tests.rs b/crates/sdk/tests/unit/trade_runtime_tests.rs
@@ -1,375 +0,0 @@
-use super::*;
-use crate::{
- RadrootsClient, RadrootsSdkError, RadrootsSdkTimestamp, RadrootsSdkTradeErrorKind,
- SatisfactionPolicy, SdkIdempotencyKey, TargetPolicy,
-};
-use radroots_event::{
- contract::AuthorRole,
- envelope::kind::TRADE_MUTATION_EVENT_KINDS,
- id::{ClassifiedListingAddress, DTag, EventId, InventoryBinId, TradeId},
- trade::{
- FulfillmentProfileV1, RADROOTS_TRADE_DECISION_CONTRACT_ID,
- RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES, RADROOTS_TRADE_MUTATION_CONTRACT_IDS,
- RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION,
- SellerReservationAssertionV1, SellerReservationLineV1, TradeCancellationProfileV1,
- TradeCandidateLineV1, TradeCandidateTermsV1, TradeDecisionV1, TradeEconomicAdjustmentV1,
- TradeEconomicsProfileV1, TradeMutationBodyV1, TradeMutationEnvelopeV1,
- canonical_trade_mutation_content,
- },
-};
-use radroots_identity::PublicKey;
-use radroots_nostr::signing::LocalSigner;
-use radroots_signing::{Actor, actor::ActorSource};
-use radroots_trade::model::RadrootsTradePrivateTermsStateV1;
-use radroots_trade::reducer::{RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION};
-
-fn pubkey(value: &str) -> PublicKey {
- PublicKey::from_hex(value).expect("pubkey")
-}
-
-fn event_id(marker: char) -> EventId {
- EventId::parse(std::iter::repeat_n(marker, 64).collect::<String>()).expect("event id")
-}
-
-fn trade_id() -> TradeId {
- TradeId::parse("11111111111111111111111111111111").expect("trade id")
-}
-
-fn local_signer() -> (String, LocalSigner) {
- let signer = LocalSigner::generate().expect("local signer");
- let pubkey = signer.public_key().to_hex();
- (pubkey, signer)
-}
-
-#[tokio::test]
-async fn trade_capabilities_report_canonical_release_product_surface() {
- let sdk = RadrootsClient::builder().build().await.expect("sdk");
- let capabilities = sdk.trades().capabilities();
-
- assert_eq!(
- capabilities.api_version,
- TRADE_RUNTIME_CAPABILITY_API_VERSION
- );
- assert_eq!(
- capabilities.protocol.protocol_profile_id,
- TRADE_RUNTIME_PROTOCOL_PROFILE_ID
- );
- assert_eq!(
- capabilities.protocol.wire_profile_id,
- TRADE_RUNTIME_WIRE_PROFILE_ID
- );
- assert_eq!(
- capabilities.protocol.schema_version,
- RADROOTS_TRADE_SCHEMA_VERSION
- );
- assert_eq!(
- capabilities.protocol.mutation_contract_ids,
- RADROOTS_TRADE_MUTATION_CONTRACT_IDS.to_vec()
- );
- assert_eq!(
- capabilities.protocol.mutation_event_kinds,
- TRADE_MUTATION_EVENT_KINDS.to_vec()
- );
- assert!(!capabilities.protocol.mutation_event_kinds.contains(&3422));
- assert_eq!(
- capabilities.protocol.reducer_contract_id,
- RADROOTS_TRADE_REDUCER_CONTRACT_ID
- );
- assert_eq!(
- capabilities.protocol.reducer_version,
- RADROOTS_TRADE_REDUCER_VERSION
- );
- assert_eq!(
- capabilities.storage.storage_profile_id,
- TRADE_RUNTIME_STORAGE_PROFILE_ID
- );
- assert_eq!(
- capabilities.storage.private_storage_profile_id,
- TRADE_RUNTIME_PRIVATE_STORAGE_PROFILE_ID
- );
- assert_eq!(
- capabilities.storage.max_private_artifact_bytes,
- RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES
- );
- assert!(capabilities.core_mvp.commands);
- assert!(capabilities.core_mvp.queries);
- assert!(capabilities.core_mvp.local_event_store);
- assert!(capabilities.core_mvp.semantic_outbox);
- assert!(capabilities.core_mvp.protected_private_artifacts);
- assert!(capabilities.core_mvp.backup_restore);
- assert!(!capabilities.optional_integrations.rhi_attestation);
- assert!(!capabilities.optional_integrations.tangle_transport);
- assert!(!capabilities.optional_integrations.reticulum_transport);
-}
-
-fn buyer_actor(buyer_pubkey: &str) -> Actor {
- Actor::from_public_key_hex(
- buyer_pubkey,
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Buyer],
- )
- .expect("buyer")
-}
-
-fn seller_actor(seller_pubkey: &str) -> Actor {
- Actor::from_public_key_hex(
- seller_pubkey,
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Seller],
- )
- .expect("seller")
-}
-
-fn candidate(buyer_pubkey: &str, seller_pubkey: &str) -> TradeCandidateTermsV1 {
- TradeCandidateTermsV1 {
- candidate_id: None,
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- base_candidate_id: None,
- supersession_intent: None,
- buyer_pubkey: pubkey(buyer_pubkey),
- seller_pubkey: pubkey(seller_pubkey),
- farm_id: DTag::parse("farm-1").expect("farm id"),
- lines: vec![TradeCandidateLineV1 {
- line_id: DTag::parse("line-1").expect("line id"),
- listing_addr: ClassifiedListingAddress::parse(format!(
- "30402:{seller_pubkey}:listing-1"
- ))
- .expect("listing address"),
- listing_event_id: event_id('c'),
- listing_snapshot_sha256: "d".repeat(64),
- product_id: "carrots".to_owned(),
- option_id: None,
- bin_id: InventoryBinId::parse("bin-1").expect("bin id"),
- quantity_mantissa: "2".to_owned(),
- quantity_scale: 0,
- unit_code: "count".to_owned(),
- unit_profile: "mvp-count".to_owned(),
- unit_price_mantissa: "500".to_owned(),
- currency_code: "USD".to_owned(),
- line_subtotal_mantissa: "1000".to_owned(),
- replaces_line_id: None,
- }],
- line_tombstones: Vec::new(),
- economics: TradeEconomicsProfileV1 {
- profile_id: "mvp-fixed".to_owned(),
- currency_code: "USD".to_owned(),
- currency_exponent: 2,
- rounding_profile: "half-even".to_owned(),
- subtotal_mantissa: "1000".to_owned(),
- discount_total_mantissa: "0".to_owned(),
- adjustment_total_mantissa: "0".to_owned(),
- total_mantissa: "1000".to_owned(),
- adjustments: Vec::<TradeEconomicAdjustmentV1>::new(),
- },
- fulfillment: FulfillmentProfileV1 {
- profile_id: "market-pickup".to_owned(),
- method: "pickup".to_owned(),
- starts_at_unix_s: 1_800_000_000,
- ends_at_unix_s: 1_800_003_600,
- timezone: "America/New_York".to_owned(),
- utc_offset_seconds: -18_000,
- fold: 0,
- location_class: "farmstand".to_owned(),
- requires_private_terms: true,
- },
- cancellation: TradeCancellationProfileV1 {
- profile_id: "buyer-pre-agreement".to_owned(),
- buyer_pre_agreement: true,
- post_agreement_cutoff_unix_s: None,
- },
- private_terms: None,
- proposal_expires_at_unix_s: 1_799_999_000,
- }
-}
-
-fn proposal(
- candidate: TradeCandidateTermsV1,
- buyer_pubkey: &str,
- seller_pubkey: &str,
-) -> TradeMutationEnvelopeV1 {
- TradeMutationEnvelopeV1 {
- mutation_id: None,
- contract_id: RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_owned(),
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- trade_id: trade_id(),
- root_mutation_id: None,
- buyer_pubkey: pubkey(buyer_pubkey),
- seller_pubkey: pubkey(seller_pubkey),
- farm_id: DTag::parse("farm-1").expect("farm id"),
- parent_mutation_ids: Vec::new(),
- author_pubkey: pubkey(buyer_pubkey),
- counterparty_pubkey: pubkey(seller_pubkey),
- authored_at_unix_s: 1_799_000_000,
- body: TradeMutationBodyV1::Proposal { candidate },
- }
-}
-
-fn reservation(
- candidate: &TradeCandidateTermsV1,
- seller_pubkey: &str,
-) -> SellerReservationAssertionV1 {
- SellerReservationAssertionV1 {
- reservation_id: DTag::parse("reservation-1").expect("reservation id"),
- inventory_authority_id: pubkey(seller_pubkey),
- inventory_epoch: 42,
- candidate_id: candidate.candidate_id.expect("candidate id"),
- commitments: candidate
- .lines
- .iter()
- .map(|line| SellerReservationLineV1 {
- line_id: line.line_id.clone(),
- bin_id: line.bin_id.clone(),
- quantity_mantissa: line.quantity_mantissa.clone(),
- quantity_scale: line.quantity_scale,
- unit_code: line.unit_code.clone(),
- })
- .collect(),
- reservation_expires_at_unix_s: 1_800_000_000,
- assertion_commitment: "e".repeat(64),
- }
-}
-
-fn accepted_decision(
- proposal: &TradeMutationEnvelopeV1,
- buyer_pubkey: &str,
- seller_pubkey: &str,
-) -> TradeMutationEnvelopeV1 {
- let proposal_id = proposal.mutation_id.expect("proposal id");
- let candidate = match &proposal.body {
- TradeMutationBodyV1::Proposal { candidate } => candidate.clone(),
- _ => unreachable!(),
- };
- TradeMutationEnvelopeV1 {
- mutation_id: None,
- contract_id: RADROOTS_TRADE_DECISION_CONTRACT_ID.to_owned(),
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- trade_id: proposal.trade_id,
- root_mutation_id: Some(proposal_id),
- buyer_pubkey: pubkey(buyer_pubkey),
- seller_pubkey: pubkey(seller_pubkey),
- farm_id: DTag::parse("farm-1").expect("farm id"),
- parent_mutation_ids: vec![proposal_id],
- author_pubkey: pubkey(seller_pubkey),
- counterparty_pubkey: pubkey(buyer_pubkey),
- authored_at_unix_s: 1_799_000_060,
- body: TradeMutationBodyV1::Decision {
- proposal_mutation_id: proposal_id,
- candidate_id: candidate.candidate_id.expect("candidate id"),
- decision: TradeDecisionV1::Accepted {
- reservation_assertion: Some(reservation(&candidate, seller_pubkey)),
- },
- },
- }
-}
-
-#[tokio::test]
-async fn trade_commands_query_and_private_terms_are_release_product_v1() {
- let (buyer_pubkey, buyer_signer) = local_signer();
- let (seller_pubkey, seller_signer) = local_signer();
- let sdk = RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_799_000_100))
- .build()
- .await
- .expect("sdk");
- let sealed = sdk
- .trades()
- .seal_private_artifact(TradePrivateArtifactSealRequest::binding_terms(
- "terms-1",
- trade_id(),
- "radroots.trade.binding_terms.v1",
- b"{\"pickup\":\"south gate\"}".to_vec(),
- ))
- .await
- .expect("seal private terms");
- let mut candidate = candidate(&buyer_pubkey, &seller_pubkey);
- candidate.private_terms = sealed.private_terms_ref.clone();
- let proposal = proposal(candidate, &buyer_pubkey, &seller_pubkey);
- let submit = SubmitProposalRequest::new(
- buyer_actor(&buyer_pubkey),
- proposal.clone(),
- TargetPolicy::LocalOnly,
- )
- .with_satisfaction_policy(SatisfactionPolicy::NoWait)
- .with_idempotency_key(
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000501").expect("idempotency key"),
- );
- let receipt = sdk
- .trades()
- .commands()
- .submit_proposal_with_explicit_signer(submit, &buyer_signer)
- .await
- .expect("submit proposal");
- let canonical_proposal = canonical_trade_mutation_content(proposal)
- .expect("canonical proposal")
- .envelope;
-
- assert_eq!(receipt.operation_kind, TRADE_SUBMIT_PROPOSAL_OPERATION_KIND);
- assert_eq!(receipt.trade_id, trade_id());
- assert_eq!(
- sdk.trades()
- .open_private_artifact(TradePrivateArtifactOpenRequest::new("terms-1"))
- .await
- .expect("open private artifact")
- .expect("private artifact")
- .plaintext,
- b"{\"pickup\":\"south gate\"}".to_vec()
- );
-
- let status = sdk
- .trades()
- .queries()
- .get_trade(GetTradeRequest::new(trade_id()))
- .await
- .expect("trade status");
- assert_eq!(status.source_event_count, 1);
- assert_eq!(status.private_terms.len(), 1);
- assert_eq!(
- status.private_terms[0].state,
- RadrootsTradePrivateTermsStateV1::AvailableVerified
- );
-
- let evidence = sdk
- .trades()
- .queries()
- .inspect_evidence(InspectEvidenceRequest::new(trade_id()))
- .await
- .expect("evidence");
- assert_eq!(evidence.items.len(), 1);
- assert_eq!(evidence.items[0].artifact_id, "terms-1");
-
- let listed = sdk
- .trades()
- .queries()
- .list_trades(ListTradesRequest::new())
- .await
- .expect("list trades");
- assert_eq!(listed.items.len(), 1);
- assert_eq!(listed.items[0].trade_id, trade_id());
-
- let decision = accepted_decision(&canonical_proposal, &buyer_pubkey, &seller_pubkey);
- let error = sdk
- .trades()
- .commands()
- .decide_candidate_with_explicit_signer(
- DecideCandidateRequest::new(
- seller_actor(&seller_pubkey),
- decision,
- TargetPolicy::LocalOnly,
- )
- .with_satisfaction_policy(SatisfactionPolicy::NoWait)
- .with_idempotency_key(
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000502")
- .expect("idempotency key"),
- ),
- &seller_signer,
- )
- .await
- .expect_err("private terms acknowledgement required");
- assert!(matches!(
- error,
- RadrootsSdkError::Trade {
- kind: RadrootsSdkTradeErrorKind::PrivateArtifactAcknowledgementMissing,
- ..
- }
- ));
-}