commit 26e03efae10d928d57a0a979be1f39327ade52d4
parent e2a22515e1ff15baca756071ec5a0df082cc9c19
Author: triesap <tyson@radroots.org>
Date: Tue, 30 Jun 2026 04:09:16 +0000
sdk: cover product trade lifecycle mutations
Diffstat:
2 files changed, 183 insertions(+), 5 deletions(-)
diff --git a/crates/sdk/src/privacy.rs b/crates/sdk/src/privacy.rs
@@ -97,6 +97,7 @@ impl PrivacyPreflightReceipt {
.fields
.iter()
.copied()
+ .filter(|field| privacy_field_requires_confirmation(*field))
.filter(|field| !confirmation.confirms(*field))
.collect::<Vec<_>>();
if missing_fields.is_empty() {
@@ -113,6 +114,11 @@ impl PrivacyPreflightReceipt {
}
}
+#[cfg(feature = "runtime")]
+fn privacy_field_requires_confirmation(field: ProductSensitivityField) -> bool {
+ matches!(field, ProductSensitivityField::PublicButSensitiveNotes)
+}
+
#[cfg(test)]
#[cfg(feature = "runtime")]
mod tests {
@@ -152,6 +158,12 @@ mod tests {
PrivacyPreflightReceipt::evaluate([ProductSensitivityField::PublicButSensitiveNotes])
.require_public_publish_allowed("trade.test", &confirmation)
.expect("confirmed public note");
+ PrivacyPreflightReceipt::evaluate([
+ ProductSensitivityField::PublicButSensitiveNotes,
+ ProductSensitivityField::ProtocolMinimizedInventoryFields,
+ ])
+ .require_public_publish_allowed("trade.test", &confirmation)
+ .expect("confirmed public note with protocol inventory");
let missing =
PrivacyPreflightReceipt::evaluate([ProductSensitivityField::PublicButSensitiveNotes])
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -11,7 +11,7 @@ use radroots_event_store::{RadrootsEventIngest, RadrootsEventStore};
use radroots_events::{
RadrootsNostrEvent,
contract::RadrootsActorRole,
- ids::{RadrootsEventId, RadrootsOrderId, RadrootsPublicKey},
+ ids::{RadrootsEventId, RadrootsOrderId, RadrootsOrderRevisionId, RadrootsPublicKey},
kinds::{KIND_LISTING, KIND_ORDER_DECISION, KIND_ORDER_REQUEST},
};
use radroots_nostr::prelude::{
@@ -24,7 +24,7 @@ use radroots_sdk::protocol::order::{
RadrootsListingAddress, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics,
RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis,
- RadrootsOrderRequest,
+ RadrootsOrderRequest, RadrootsOrderRevisionOutcome,
};
use radroots_sdk::protocol::wire::WireEventParts;
use radroots_sdk::{
@@ -33,8 +33,9 @@ use radroots_sdk::{
SdkTradeStatusIssue, SdkTradeStatusIssueKind, SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT,
TRADE_STATUS_MAX_LIMIT, TradeAcceptRequest, TradeCancelRequest, TradeDeclineRequest,
TradeEvidenceIngestRequest, TradeMutationOutcome, TradeProposeRequest,
- TradeRequestEvidenceIngestRequest, TradeResyncRequest, TradeSellerInboxRequest,
- TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest,
+ TradeRequestEvidenceIngestRequest, TradeResyncRequest, TradeRevisionDecisionRequest,
+ TradeRevisionProposalRequest, TradeSellerInboxRequest, TradeStatusKind,
+ TradeStatusNextActionKind, TradeStatusRequest,
};
use radroots_sdk::{PrivacyPreflightConfirmation, PrivacyPreflightStatus, ProductSensitivityField};
#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
@@ -1129,6 +1130,172 @@ async fn trade_product_cancel_blocks_sensitive_fulfillment_reason_before_mutatio
);
}
+#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
+#[tokio::test]
+async fn trade_product_cancel_enqueues_with_locator_and_updates_status() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let storage_root = tempdir.path().join("sdk");
+ let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
+ let propose_receipt = expect_enqueued(
+ buyer_sdk
+ .trades()
+ .buyer()
+ .propose_trade(
+ TradeProposeRequest::new(
+ buyer_actor(),
+ listing_event_ptr(),
+ order_request("trade-product-cancel"),
+ explicit_trade_relays(),
+ PublishMode::EnqueueOnly,
+ AckPolicy::NoWait,
+ )
+ .try_with_idempotency_key("trade-product-cancel-propose")
+ .expect("propose idempotency"),
+ )
+ .await
+ .expect("propose trade"),
+ );
+
+ let cancellation = expect_enqueued(
+ buyer_sdk
+ .trades()
+ .buyer()
+ .cancel_trade(
+ TradeCancelRequest::new(
+ buyer_actor(),
+ propose_receipt.locator.clone(),
+ "changed plan",
+ explicit_trade_relays(),
+ PublishMode::EnqueueOnly,
+ AckPolicy::NoWait,
+ )
+ .with_privacy_confirmation(public_note_confirmation())
+ .try_with_idempotency_key("trade-product-cancel")
+ .expect("cancel idempotency"),
+ )
+ .await
+ .expect("cancel trade"),
+ );
+
+ assert_eq!(cancellation.locator, propose_receipt.locator);
+ assert_eq!(cancellation.root_event_id, propose_receipt.signed_event_id);
+ assert_eq!(
+ cancellation.previous_event_id,
+ propose_receipt.signed_event_id
+ );
+ let status = buyer_sdk
+ .trades()
+ .status(TradeStatusRequest::new(propose_receipt.locator))
+ .await
+ .expect("status");
+ assert_eq!(status.status, TradeStatusKind::Cancelled);
+ assert_eq!(
+ status.cancellation_event_id,
+ Some(cancellation.signed_event_id)
+ );
+ assert_eq!(status.next_action, TradeStatusNextActionKind::Terminal);
+}
+
+#[cfg(all(feature = "signer-adapters", feature = "local-signer"))]
+#[tokio::test]
+async fn trade_product_revision_lifecycle_uses_locator_and_updates_status() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let storage_root = tempdir.path().join("sdk");
+ let buyer_sdk = directory_sdk_with_signer(storage_root.as_path(), BUYER_SECRET_KEY_HEX).await;
+ let seller_sdk = directory_sdk_with_signer(storage_root.as_path(), SELLER_SECRET_KEY_HEX).await;
+ let propose_receipt = expect_enqueued(
+ buyer_sdk
+ .trades()
+ .buyer()
+ .propose_trade(
+ TradeProposeRequest::new(
+ buyer_actor(),
+ listing_event_ptr(),
+ order_request("trade-product-revision"),
+ explicit_trade_relays(),
+ PublishMode::EnqueueOnly,
+ AckPolicy::NoWait,
+ )
+ .try_with_idempotency_key("trade-product-revision-propose")
+ .expect("propose idempotency"),
+ )
+ .await
+ .expect("propose trade"),
+ );
+ let revision_id: RadrootsOrderRevisionId =
+ "revision-product-revision".parse().expect("revision id");
+ let proposal = expect_enqueued(
+ seller_sdk
+ .trades()
+ .seller()
+ .propose_revision(
+ TradeRevisionProposalRequest::new(
+ seller_actor(),
+ propose_receipt.locator.clone(),
+ revision_id.clone(),
+ vec![RadrootsOrderItem {
+ bin_id: "bin-1".parse().expect("bin id"),
+ bin_count: 3,
+ }],
+ revision_economics(),
+ "increase quantity",
+ explicit_trade_relays(),
+ PublishMode::EnqueueOnly,
+ AckPolicy::NoWait,
+ )
+ .with_privacy_confirmation(public_note_confirmation())
+ .try_with_idempotency_key("trade-product-revision-proposal")
+ .expect("revision proposal idempotency"),
+ )
+ .await
+ .expect("propose revision"),
+ );
+ let pending = buyer_sdk
+ .trades()
+ .status(TradeStatusRequest::new(propose_receipt.locator.clone()))
+ .await
+ .expect("pending revision status");
+ assert_eq!(pending.status, TradeStatusKind::RevisionProposed);
+ assert_eq!(
+ pending.pending_revision_event_id,
+ Some(proposal.signed_event_id.clone())
+ );
+ assert!(pending.eligibility.can_decide_revision);
+
+ let decision = expect_enqueued(
+ buyer_sdk
+ .trades()
+ .buyer()
+ .accept_revision(
+ TradeRevisionDecisionRequest::new(
+ buyer_actor(),
+ propose_receipt.locator.clone(),
+ revision_id,
+ RadrootsOrderRevisionOutcome::Accepted,
+ explicit_trade_relays(),
+ PublishMode::EnqueueOnly,
+ AckPolicy::NoWait,
+ )
+ .try_with_idempotency_key("trade-product-revision-decision")
+ .expect("revision decision idempotency"),
+ )
+ .await
+ .expect("accept revision"),
+ );
+ assert_eq!(decision.locator, propose_receipt.locator);
+ assert_eq!(decision.root_event_id, propose_receipt.signed_event_id);
+ assert_eq!(decision.previous_event_id, proposal.signed_event_id);
+ let status = buyer_sdk
+ .trades()
+ .status(TradeStatusRequest::new(propose_receipt.locator))
+ .await
+ .expect("status");
+ assert_eq!(status.status, TradeStatusKind::AgreedPendingRhi);
+ assert_eq!(status.last_event_id, Some(decision.signed_event_id));
+ assert_eq!(status.pending_revision_event_id, None);
+ assert_eq!(status.economics, Some(revision_economics()));
+}
+
#[cfg(feature = "signer-adapters")]
#[tokio::test]
async fn trade_product_propose_dry_run_returns_plan_without_local_side_effects() {
@@ -1668,7 +1835,6 @@ fn order_cancellation(
}
}
-#[cfg(any())]
fn revision_economics() -> RadrootsOrderEconomics {
RadrootsOrderEconomics {
quote_id: "revision-quote-1".parse().expect("revision quote id"),