commit db6d423678cad10c2de2323e68a8ce1fbbc36c19
parent ccd8d8d54a95bd57a77c95775b08d0c6e693e4b4
Author: triesap <tyson@radroots.org>
Date: Thu, 18 Jun 2026 22:58:32 -0700
cli: route order decisions through sdk
- ingest seller request evidence before decision enqueue
- enqueue accept and decline decisions through the SDK runtime
- push the SDK outbox instead of directly building decision events
- tighten migrated order decision source-guard requirements
Diffstat:
2 files changed, 233 insertions(+), 43 deletions(-)
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -12,7 +12,6 @@ use radroots_core::{
RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCoreUnit,
convert_unit_decimal,
};
-use radroots_events::RadrootsNostrEventPtr;
use radroots_events::contract::RadrootsActorRole;
use radroots_events::ids::{
RadrootsEconomicsDigest, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress,
@@ -36,11 +35,12 @@ use radroots_events::order::{
RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome,
RadrootsOrderRevisionProposal, RadrootsOrderSettlementDecision, RadrootsOrderSettlementOutcome,
};
+use radroots_events::{RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr};
use radroots_events_codec::d_tag::is_d_tag_base64url;
use radroots_events_codec::listing::decode::listing_from_event;
use radroots_events_codec::order::{
- order_cancellation_event_build, order_cancellation_from_event, order_decision_event_build,
- order_envelope_from_event, order_event_context_from_tags, order_fulfillment_update_event_build,
+ order_cancellation_event_build, order_cancellation_from_event, order_envelope_from_event,
+ order_event_context_from_tags, order_fulfillment_update_event_build,
order_fulfillment_update_from_event, order_payment_record_event_build,
order_payment_record_from_event, order_receipt_event_build, order_receipt_from_event,
order_request_from_event, order_revision_decision_event_build,
@@ -68,11 +68,12 @@ use radroots_replica_db_schema::trade_product::{
ITradeProductFieldsFilter, ITradeProductFindMany, TradeProduct,
};
use radroots_sdk::{
- OrderFulfillmentStatusKind, OrderPaymentStateKind, OrderSettlementStateKind, OrderStatusKind,
- OrderStatusReceipt, OrderStatusRequest, OrderSubmitEnqueueRequest, OrderSubmitPlan,
- OrderSubmitPrepareRequest, OrderSubmitReceipt, PushOutboxEventReceipt, PushOutboxEventState,
- PushOutboxReceipt, PushOutboxRelayOutcomeKind, PushOutboxRequest, SdkMutationState,
- SdkOrderStatusIssue, SdkRelayTargetPolicy, SdkRelayUrlPolicy,
+ OrderDecisionEnqueueRequest, OrderDecisionReceipt, OrderFulfillmentStatusKind,
+ OrderPaymentStateKind, OrderRequestEvidenceIngestRequest, OrderSettlementStateKind,
+ OrderStatusKind, OrderStatusReceipt, OrderStatusRequest, OrderSubmitEnqueueRequest,
+ OrderSubmitPlan, OrderSubmitPrepareRequest, OrderSubmitReceipt, PushOutboxEventReceipt,
+ PushOutboxEventState, PushOutboxReceipt, PushOutboxRelayOutcomeKind, PushOutboxRequest,
+ SdkMutationState, SdkOrderStatusIssue, SdkRelayTargetPolicy, SdkRelayUrlPolicy,
};
use radroots_sql_core::SqliteExecutor;
use radroots_trade::order::{
@@ -129,7 +130,7 @@ const ORDER_DRAFT_KIND: &str = "order_draft_v1";
const ORDER_SOURCE: &str = "local order drafts · local first";
const ORDER_APP_RECORD_SOURCE: &str = "app-authored shared local order records";
const ORDER_SUBMIT_SOURCE: &str = "SDK order submit · local key";
-const ORDER_DECISION_SOURCE: &str = "direct Nostr relay decision publish · local key";
+const ORDER_DECISION_SOURCE: &str = "SDK order decision · local key";
const ORDER_REVISION_PROPOSAL_SOURCE: &str =
"direct Nostr relay revision proposal publish · local key";
const ORDER_REVISION_DECISION_SOURCE: &str =
@@ -350,6 +351,7 @@ struct ResolvedTradeProductNotes {
#[derive(Debug, Clone)]
struct ResolvedSellerOrderRequest {
+ request_event: SdkRadrootsNostrEvent,
request_event_id: RadrootsEventId,
listing_event_id: Option<String>,
order_id: RadrootsOrderId,
@@ -8837,13 +8839,13 @@ fn seller_order_request_from_event(
)));
}
- let event = radroots_event_from_nostr(event);
- let event_id = protocol_event_id(event.id.as_str(), "request_event_id")?;
+ let request_event = radroots_event_from_nostr(event);
+ let event_id = protocol_event_id(request_event.id.as_str(), "request_event_id")?;
let seller_protocol_pubkey = protocol_pubkey(seller_pubkey, "seller_pubkey")?;
- let envelope = order_request_from_event(&event)
+ let envelope = order_request_from_event(&request_event)
.map_err(|error| RuntimeError::Config(format!("decode order request event: {error}")))?;
let context =
- order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags)
+ order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &request_event.tags)
.map_err(|error| RuntimeError::Config(format!("decode order request tags: {error}")))?;
if envelope.order_id.to_string() != order_id
@@ -8872,6 +8874,7 @@ fn seller_order_request_from_event(
let listing_event_id = context.listing_event.as_ref().map(|event| event.id.clone());
Ok(ResolvedSellerOrderRequest {
+ request_event,
request_event_id: event_id,
listing_event_id,
order_id: envelope.payload.order_id,
@@ -8892,21 +8895,139 @@ fn publish_order_decision(
payload: RadrootsOrderDecision,
inventory: Option<OrderInventoryView>,
) -> Result<OrderDecisionView, RuntimeError> {
- let parts = order_decision_event_build(
- &request.request_event_id,
- &request.request_event_id,
- &payload,
+ let input = sdk_order_decision_input(config, &request, &signing, payload)
+ .map_err(cli_sdk_error_to_runtime)?;
+ enqueue_order_decision_via_sdk(config, args, request, resolution, signing, input, inventory)
+ .map_err(cli_sdk_error_to_runtime)
+}
+
+fn sdk_order_decision_input(
+ config: &RuntimeConfig,
+ request: &ResolvedSellerOrderRequest,
+ signing: &account::AccountSigningIdentity,
+ payload: RadrootsOrderDecision,
+) -> Result<SdkOrderDecisionInput, CliSdkAdapterError> {
+ let actor = RadrootsActorContext::local_account(
+ signing
+ .account
+ .record
+ .public_identity
+ .public_key_hex
+ .as_str(),
+ signing.account.record.account_id.to_string(),
+ [RadrootsActorRole::Seller],
)
- .map_err(|error| RuntimeError::Config(format!("encode order decision event: {error}")))?;
- let event_kind = parts.kind;
- let receipt = publish_parts_with_identity(&signing.identity, &config.relay.urls, parts)
- .map_err(|error| RuntimeError::Network(error.to_string()))?;
+ .map_err(|error| RuntimeError::Config(format!("invalid order decision SDK actor: {error}")))?;
+ let target_relays = order_decision_target_relays(config)?;
+ Ok(SdkOrderDecisionInput {
+ actor,
+ request_event: request.request_event.clone(),
+ request_event_ptr: order_decision_request_event_ptr(request, target_relays.as_slice()),
+ decision: payload,
+ target_relays,
+ })
+}
+
+#[derive(Debug, Clone)]
+struct SdkOrderDecisionInput {
+ actor: RadrootsActorContext,
+ request_event: SdkRadrootsNostrEvent,
+ request_event_ptr: RadrootsNostrEventPtr,
+ decision: RadrootsOrderDecision,
+ target_relays: Vec<String>,
+}
+
+fn order_decision_request_event_ptr(
+ request: &ResolvedSellerOrderRequest,
+ target_relays: &[String],
+) -> RadrootsNostrEventPtr {
+ RadrootsNostrEventPtr {
+ id: request.request_event_id.as_str().to_owned(),
+ relays: target_relays.first().cloned(),
+ }
+}
+
+fn order_decision_target_relays(config: &RuntimeConfig) -> Result<Vec<String>, RuntimeError> {
+ let target_relays = normalize_listing_relay_set(config.relay.urls.iter())
+ .map_err(|error| RuntimeError::Config(format!("configured relay target: {error}")))?;
+ if target_relays.is_empty() {
+ return Err(RuntimeError::Config(
+ "order decision requires at least one configured relay".to_owned(),
+ ));
+ }
+ Ok(target_relays)
+}
+
+fn order_decision_relay_url_policy(target_relays: &[String]) -> SdkRelayUrlPolicy {
+ if target_relays
+ .iter()
+ .any(|relay_url| relay_url.starts_with("ws://"))
+ {
+ SdkRelayUrlPolicy::Localhost
+ } else {
+ SdkRelayUrlPolicy::Public
+ }
+}
+
+fn enqueue_order_decision_via_sdk(
+ config: &RuntimeConfig,
+ args: &OrderDecisionArgs,
+ request_context: ResolvedSellerOrderRequest,
+ resolution: SellerOrderRequestResolution,
+ signing: account::AccountSigningIdentity,
+ input: SdkOrderDecisionInput,
+ inventory: Option<OrderInventoryView>,
+) -> Result<OrderDecisionView, CliSdkAdapterError> {
+ let target_relays = input.target_relays.clone();
+ let policy = order_decision_relay_url_policy(target_relays.as_slice());
+ let target_policy = SdkRelayTargetPolicy::try_explicit(target_relays.clone(), policy)?;
+ let mut request = OrderDecisionEnqueueRequest::new(
+ input.actor,
+ input.request_event_ptr,
+ input.decision,
+ target_policy,
+ );
+ if let Some(idempotency_key) = args.idempotency_key.as_deref() {
+ request = request.try_with_idempotency_key(idempotency_key)?;
+ }
- Ok(published_order_decision_view(
- config, args, request, resolution, event_kind, receipt, inventory,
+ let session = CliSdkSession::connect(config)?;
+ session.block_on(
+ session
+ .sdk()
+ .orders()
+ .ingest_request_evidence(OrderRequestEvidenceIngestRequest::new(input.request_event)),
+ )?;
+ let keys: RadrootsNostrKeys = signing.identity.into_keys();
+ let signer = RadrootsLocalEventSigner::new(keys)
+ .map_err(|error| RuntimeError::Config(error.to_string()))?;
+ let enqueue = session.block_on(session.sdk().orders().enqueue_decision(request, &signer))?;
+ let push = session.block_on(
+ session.sdk().sync().push_outbox(
+ PushOutboxRequest::new()
+ .with_limit(1)
+ .with_relay_url_policy(policy),
+ ),
+ )?;
+ Ok(sdk_enqueued_order_decision_view(
+ config,
+ args,
+ request_context,
+ resolution,
+ enqueue,
+ push,
+ target_relays,
+ inventory,
))
}
+fn cli_sdk_error_to_runtime(error: CliSdkAdapterError) -> RuntimeError {
+ match error {
+ CliSdkAdapterError::Runtime(error) => error,
+ CliSdkAdapterError::Sdk(error) => RuntimeError::Config(error.to_string()),
+ }
+}
+
fn canonical_order_decision_payload(
args: &OrderDecisionArgs,
request: &ResolvedSellerOrderRequest,
@@ -8975,40 +9096,95 @@ fn declined_order_decision_payload_from_request(
}
}
-fn published_order_decision_view(
+fn sdk_enqueued_order_decision_view(
config: &RuntimeConfig,
args: &OrderDecisionArgs,
request: ResolvedSellerOrderRequest,
resolution: SellerOrderRequestResolution,
- event_kind: u32,
- receipt: DirectRelayPublishReceipt,
+ enqueue: OrderDecisionReceipt,
+ push: PushOutboxReceipt,
+ target_relays: Vec<String>,
inventory: Option<OrderInventoryView>,
) -> OrderDecisionView {
- let DirectRelayPublishReceipt {
- event: _,
- event_id,
- created_at: _,
- signature: _,
- target_relays,
- connected_relays: _,
- acknowledged_relays,
- failed_relays,
- } = receipt;
- let mut view = order_decision_base_view(config, args, args.decision.as_str(), false);
+ let push_event = sdk_push_event_for_order_decision(&enqueue, &push);
+ let mut view = order_decision_base_view(
+ config,
+ args,
+ sdk_order_decision_state(args.decision, push_event).as_str(),
+ false,
+ );
apply_order_decision_request(&mut view, &request);
- view.event_id = Some(event_id);
- view.event_kind = Some(event_kind);
- view.target_relays = target_relays;
- view.connected_relays = resolution.connected_relays;
- view.acknowledged_relays = acknowledged_relays;
- view.failed_relays = relay_failures(failed_relays);
+ view.event_id = Some(enqueue.signed_event_id.as_str().to_owned());
+ view.event_kind = Some(KIND_ORDER_DECISION);
+ view.target_relays = push_event
+ .map(sdk_push_target_relays)
+ .unwrap_or(target_relays);
+ view.connected_relays = push_event
+ .map(sdk_push_connected_relays)
+ .unwrap_or_default();
+ view.acknowledged_relays = push_event
+ .map(sdk_push_acknowledged_relays)
+ .unwrap_or_default();
+ view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default();
view.fetched_count = resolution.fetched_count;
view.decoded_count = resolution.decoded_count;
view.skipped_count = resolution.skipped_count;
view.inventory = order_decision_inventory_for_view(args, &request, inventory);
+ view.reason = sdk_order_decision_reason(push_event);
+ view.actions = sdk_order_decision_actions(push_event);
view
}
+fn sdk_push_event_for_order_decision<'a>(
+ enqueue: &OrderDecisionReceipt,
+ push: &'a PushOutboxReceipt,
+) -> Option<&'a PushOutboxEventReceipt> {
+ push.events
+ .iter()
+ .find(|event| event.event_id == enqueue.signed_event_id)
+}
+
+fn sdk_order_decision_state(
+ decision: OrderDecisionArg,
+ push_event: Option<&PushOutboxEventReceipt>,
+) -> String {
+ match push_event.map(|event| event.final_state) {
+ Some(PushOutboxEventState::Published) => decision.as_str(),
+ Some(PushOutboxEventState::PublishRetryable | PushOutboxEventState::FailedTerminal) => {
+ "unavailable"
+ }
+ Some(_) | None => "queued",
+ }
+ .to_owned()
+}
+
+fn sdk_order_decision_reason(push_event: Option<&PushOutboxEventReceipt>) -> Option<String> {
+ match push_event.map(|event| event.final_state) {
+ Some(PushOutboxEventState::Published) => None,
+ Some(PushOutboxEventState::PublishRetryable) => Some(
+ "SDK relay publish did not reach accepted quorum; outbox event remains retryable"
+ .to_owned(),
+ ),
+ Some(PushOutboxEventState::FailedTerminal) => {
+ Some("SDK relay publish failed terminally".to_owned())
+ }
+ Some(state) => Some(format!("SDK relay push left event in state `{state:?}`")),
+ None => Some(
+ "order decision queued in SDK outbox; no ready SDK outbox event was pushed".to_owned(),
+ ),
+ }
+}
+
+fn sdk_order_decision_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<String> {
+ if !matches!(
+ push_event.map(|event| event.final_state),
+ Some(PushOutboxEventState::Published)
+ ) {
+ return vec!["radroots sync push".to_owned()];
+ }
+ Vec::new()
+}
+
fn order_decision_binding_error_view(
config: &RuntimeConfig,
args: &OrderDecisionArgs,
@@ -18522,6 +18698,7 @@ mod tests {
fixture.listing_event_id.as_str(),
);
let existing_request = ResolvedSellerOrderRequest {
+ request_event: radroots_event_from_nostr(&existing_request_event),
request_event_id: test_event_id(existing_request_event.id.to_string().as_str()),
listing_event_id: Some(fixture.listing_event_id.clone()),
order_id: test_order_id(existing_order_id),
@@ -18622,6 +18799,7 @@ mod tests {
fixture.listing_event_id.as_str(),
);
let existing_request = ResolvedSellerOrderRequest {
+ request_event: radroots_event_from_nostr(&existing_request_event),
request_event_id: test_event_id(existing_request_event.id.to_string().as_str()),
listing_event_id: Some(fixture.listing_event_id.clone()),
order_id: test_order_id(existing_order_id),
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -458,6 +458,18 @@ mod tests {
],
},
MigratedCliPathGuard {
+ label: "order decision",
+ path: "src/runtime/order.rs",
+ start: "fn publish_order_decision(",
+ end: "fn canonical_order_decision_payload(",
+ required_tokens: &[
+ "OrderDecisionEnqueueRequest::new",
+ "ingest_request_evidence(OrderRequestEvidenceIngestRequest::new",
+ "enqueue_decision(request, &signer)",
+ "push_outbox(",
+ ],
+ },
+ MigratedCliPathGuard {
label: "store status",
path: "src/runtime/store.rs",
start: "pub fn status(config: &RuntimeConfig) -> Result<LocalStatusView, CliSdkAdapterError>",