cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 0dbc924a4c885cf60de932b72d19a725483acccb
parent 2894c333555a208a0c4449d1ea952c1a8bf33c59
Author: triesap <tyson@radroots.org>
Date:   Thu, 30 Apr 2026 01:16:15 +0000

order: fix lifecycle preflight accounting

Diffstat:
Msrc/runtime/order.rs | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 220 insertions(+), 4 deletions(-)

diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -905,7 +905,7 @@ pub fn fulfillment_update( let reduction = order_status_reduction_from_receipt_with_context( OrderStatusContext { order_id: args.key.as_str(), - selected_account_pubkey: None, + selected_account_pubkey: Some(selected_pubkey.as_str()), }, receipt, ); @@ -2537,6 +2537,10 @@ fn order_accept_inventory_preflight_view( .filter(|record| request_order_ids.contains(&record.payload.order_id)) .collect::<Vec<_>>(); decisions.push(proposed_accept_decision_record(request)?); + let fulfillments = fetch_listing_accounting_fulfillments(config, request)? + .into_iter() + .filter(|record| request_order_ids.contains(&record.payload.order_id)) + .collect::<Vec<_>>(); let projection = reduce_listing_inventory_accounting( request.listing_addr.as_str(), @@ -2544,7 +2548,7 @@ fn order_accept_inventory_preflight_view( listing.bins, requests, decisions, - [], + fulfillments, ); Ok(order_accept_inventory_preflight_view_from_projection( config, args, request, resolution, status, projection, @@ -2821,6 +2825,27 @@ fn fetch_listing_accounting_decisions( Ok(records) } +fn fetch_listing_accounting_fulfillments( + config: &RuntimeConfig, + request: &ResolvedSellerOrderRequest, +) -> Result<Vec<RadrootsActiveOrderFulfillmentRecord>, RuntimeError> { + let filter = order_listing_fulfillment_filter(request.listing_addr.as_str())?; + let receipt = fetch_events_from_relays(&config.relay.urls, filter) + .map_err(|error| RuntimeError::Network(error.to_string()))?; + let mut records = Vec::new(); + for event in receipt.events { + if event_kind_u32(&event) != KIND_TRADE_FULFILLMENT_UPDATE + || !event_matches_tag_value(&event, "a", request.listing_addr.as_str()) + { + continue; + } + if let Ok(OrderStatusRecord::Fulfillment(record)) = order_status_record_from_event(&event) { + records.push(record); + } + } + Ok(records) +} + fn listing_accounting_request_from_event( event: &RadrootsNostrEvent, ) -> Result<ResolvedAccountingRequest, RuntimeError> { @@ -5013,8 +5038,9 @@ mod tests { use radroots_runtime_paths::RadrootsMigrationReport; use radroots_secret_vault::RadrootsSecretBackend; use radroots_trade::order::{ - RadrootsActiveOrderDecisionRecord, RadrootsListingInventoryBinAvailability, - canonicalize_active_order_decision_for_signer, reduce_listing_inventory_accounting, + RadrootsActiveOrderDecisionRecord, RadrootsActiveOrderFulfillmentRecord, + RadrootsListingInventoryBinAvailability, canonicalize_active_order_decision_for_signer, + reduce_listing_inventory_accounting, }; use tempfile::tempdir; @@ -5787,6 +5813,87 @@ mod tests { } #[test] + fn order_fulfillment_preflight_uses_selected_seller_context() { + let dir = tempdir().expect("tempdir"); + let mut config = sample_config(dir.path()); + config.relay.urls = vec!["ws://relay.test".to_owned()]; + let fixture = order_status_fixture(); + let other_seller = RadrootsIdentity::generate(); + let other_seller_pubkey = other_seller.public_key_hex(); + let other_listing_addr = format!("30402:{other_seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAw"); + let other_request_event = signed_order_request_event( + &fixture.buyer, + fixture.order_id.as_str(), + other_listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + other_seller_pubkey.as_str(), + "2".repeat(64).as_str(), + ); + let decision_event = signed_order_decision_event( + &fixture.seller, + &fixture.request_event, + fixture.order_id.as_str(), + fixture.listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + fixture.seller_pubkey.as_str(), + RadrootsTradeOrderDecision::Accepted { + inventory_commitments: vec![RadrootsTradeInventoryCommitment { + bin_id: "bin-1".to_owned(), + bin_count: 2, + }], + }, + ); + let unscoped_reduction = order_status_reduction_from_receipt_with_context( + OrderStatusContext { + order_id: fixture.order_id.as_str(), + selected_account_pubkey: None, + }, + DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![ + fixture.request_event.clone(), + other_request_event.clone(), + decision_event.clone(), + ], + }, + ); + let scoped_reduction = order_status_reduction_from_receipt_with_context( + OrderStatusContext { + order_id: fixture.order_id.as_str(), + selected_account_pubkey: Some(fixture.seller_pubkey.as_str()), + }, + DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![ + fixture.request_event.clone(), + other_request_event, + decision_event, + ], + }, + ); + let args = fulfillment_args_for_fixture(&fixture, "ready_for_pickup"); + + assert_eq!(unscoped_reduction.view.state, "invalid"); + assert_eq!(scoped_reduction.view.state, "accepted"); + assert_eq!(scoped_reduction.view.decoded_count, 2); + assert_eq!(scoped_reduction.view.skipped_count, 1); + assert!( + order_fulfillment_preflight_view_from_status( + &config, + &args, + &scoped_reduction.view, + scoped_reduction.fulfillment_status, + scoped_reduction.fulfillment_event_id.as_deref(), + ) + .is_none() + ); + } + + #[test] fn order_decision_dry_run_view_preserves_ready_preflight_without_publish_fields() { let dir = tempdir().expect("tempdir"); let mut config = sample_config(dir.path()); @@ -6726,6 +6833,115 @@ mod tests { } #[test] + fn order_accept_inventory_preflight_counts_seller_cancelled_release() { + let dir = tempdir().expect("tempdir"); + let mut config = sample_config(dir.path()); + config.relay.urls = vec!["ws://relay.test".to_owned()]; + let fixture = order_status_fixture(); + let resolution = request_resolution_for_fixture(&fixture); + let request = resolution.requests[0].clone(); + let status_view = order_status_from_receipt( + fixture.order_id.as_str(), + DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![fixture.request_event.clone()], + }, + ); + let existing_order_id = "ord_AAAAAAAAAAAAAAAAAAAAAw"; + let existing_request_event = signed_order_request_event( + &fixture.buyer, + existing_order_id, + fixture.listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + fixture.seller_pubkey.as_str(), + fixture.listing_event_id.as_str(), + ); + let existing_request = ResolvedSellerOrderRequest { + request_event_id: existing_request_event.id.to_string(), + listing_event_id: Some(fixture.listing_event_id.clone()), + order_id: existing_order_id.to_owned(), + listing_addr: fixture.listing_addr.clone(), + buyer_pubkey: fixture.buyer_pubkey.clone(), + seller_pubkey: fixture.seller_pubkey.clone(), + items: vec![RadrootsTradeOrderItem { + bin_id: "bin-1".to_owned(), + bin_count: 2, + }], + }; + let existing_decision_payload = + accepted_order_decision_payload_from_request(&existing_request); + let existing_decision_payload = canonicalize_active_order_decision_for_signer( + existing_decision_payload, + fixture.seller_pubkey.as_str(), + ) + .expect("canonical existing decision"); + let existing_decision_event_id = "existing_decision".to_owned(); + let projection = reduce_listing_inventory_accounting( + fixture.listing_addr.as_str(), + fixture.listing_event_id.as_str(), + vec![RadrootsListingInventoryBinAvailability { + bin_id: "bin-1".to_owned(), + available_count: 2, + }], + vec![ + active_request_record_from_resolved(&existing_request), + active_request_record_from_resolved(&request), + ], + vec![ + RadrootsActiveOrderDecisionRecord { + event_id: existing_decision_event_id.clone(), + author_pubkey: fixture.seller_pubkey.clone(), + counterparty_pubkey: fixture.buyer_pubkey.clone(), + root_event_id: existing_request.request_event_id.clone(), + prev_event_id: existing_request.request_event_id.clone(), + payload: existing_decision_payload, + }, + proposed_accept_decision_record(&request).expect("proposed accept decision"), + ], + vec![RadrootsActiveOrderFulfillmentRecord { + event_id: "existing_fulfillment".to_owned(), + author_pubkey: fixture.seller_pubkey.clone(), + counterparty_pubkey: fixture.buyer_pubkey.clone(), + root_event_id: existing_request.request_event_id.clone(), + prev_event_id: existing_decision_event_id, + payload: RadrootsTradeFulfillmentUpdated { + order_id: existing_request.order_id.clone(), + listing_addr: existing_request.listing_addr.clone(), + buyer_pubkey: existing_request.buyer_pubkey.clone(), + seller_pubkey: existing_request.seller_pubkey.clone(), + status: RadrootsActiveTradeFulfillmentState::SellerCancelled, + }, + }], + ); + let args = OrderDecisionArgs { + key: fixture.order_id.clone(), + decision: OrderDecisionArg::Accept, + reason: None, + idempotency_key: None, + }; + + let preflight = order_accept_inventory_preflight_view_from_projection( + &config, + &args, + &request, + &resolution, + &status_view, + projection, + ); + let inventory = preflight.inventory.expect("valid inventory preflight"); + + assert!(preflight.invalid_view.is_none()); + assert_eq!(inventory.state, "reserved"); + assert_eq!(inventory.commitment_valid, true); + assert_eq!(inventory.bins.len(), 1); + assert_eq!(inventory.bins[0].committed_count, 2); + assert_eq!(inventory.bins[0].remaining_count, Some(0)); + assert!(inventory.issues.is_empty()); + } + + #[test] fn order_status_from_receipt_reports_mismatched_commitment_inventory_invalid() { let fixture = order_status_fixture(); let decision_event = signed_order_decision_event(