cli

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

commit 5615bfada01189636aa00b54fcc88e3442d0c76a
parent e7d7a9be4a46135e77b7f385dfa6a236595c1a53
Author: triesap <tyson@radroots.org>
Date:   Wed, 29 Apr 2026 19:15:50 +0000

order: reject wrong counterparty decisions

Diffstat:
Msrc/runtime/order.rs | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 83 insertions(+), 1 deletion(-)

diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -1351,6 +1351,7 @@ fn order_status_record_from_event( RadrootsActiveOrderDecisionRecord { event_id: event.id, author_pubkey: event.author, + counterparty_pubkey: context.counterparty_pubkey, root_event_id: context.root_event_id.unwrap_or_default(), prev_event_id: context.prev_event_id.unwrap_or_default(), payload: envelope.payload, @@ -1402,6 +1403,7 @@ fn order_status_inventory_view( issue.code.as_str(), "missing_decision_inventory_commitments" | "decision_inventory_commitment_mismatch" + | "decision_counterparty_mismatch" | "unknown_inventory_bin" | "listing_inventory_over_reserved" | "invalid_inventory_order" @@ -1541,6 +1543,14 @@ fn active_order_reducer_issue_view(issue_value: RadrootsActiveOrderReducerIssue) "active order reducer reported decision author mismatch", vec![event_id], ), + RadrootsActiveOrderReducerIssue::DecisionCounterpartyMismatch { event_id } => { + issue_with_events( + "decision_counterparty_mismatch", + "buyer_pubkey", + "active order reducer reported decision counterparty mismatch", + vec![event_id], + ) + } RadrootsActiveOrderReducerIssue::DecisionBuyerMismatch { event_id } => issue_with_events( "decision_buyer_mismatch", "buyer_pubkey", @@ -2327,6 +2337,7 @@ fn proposed_accept_decision_record( Ok(RadrootsActiveOrderDecisionRecord { event_id: format!("pending_accept:{}", request.order_id), author_pubkey: request.seller_pubkey.clone(), + counterparty_pubkey: request.buyer_pubkey.clone(), root_event_id: request.request_event_id.clone(), prev_event_id: request.request_event_id.clone(), payload, @@ -5072,6 +5083,48 @@ mod tests { } #[test] + fn order_status_from_receipt_rejects_wrong_decision_counterparty() { + let fixture = order_status_fixture(); + let wrong_buyer = RadrootsIdentity::generate(); + let decision_event = signed_order_decision_event_with_counterparty( + &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(), + wrong_buyer.public_key_hex().as_str(), + RadrootsTradeOrderDecision::Accepted { + inventory_commitments: vec![RadrootsTradeInventoryCommitment { + bin_id: "bin-1".to_owned(), + bin_count: 2, + }], + }, + ); + let decision_event_id = decision_event.id.to_string(); + let receipt = 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(), decision_event], + }; + + let view = order_status_from_receipt(fixture.order_id.as_str(), receipt); + + assert_eq!(view.state, "invalid"); + let issue = view + .reducer_issues + .iter() + .find(|issue| issue.code == "decision_counterparty_mismatch") + .expect("decision counterparty mismatch issue"); + assert_eq!(issue.field, "buyer_pubkey"); + assert_eq!(issue.event_ids, vec![decision_event_id]); + let inventory = view.inventory.as_ref().expect("inventory view"); + assert_eq!(inventory.state, "invalid"); + assert_eq!(inventory.issues[0].code, "decision_counterparty_mismatch"); + } + + #[test] fn order_decision_preflight_rejects_existing_decision() { let dir = tempdir().expect("tempdir"); let mut config = sample_config(dir.path()); @@ -5198,6 +5251,7 @@ mod tests { RadrootsActiveOrderDecisionRecord { event_id: "existing_decision".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_request.request_event_id.clone(), payload: existing_decision_payload, @@ -5823,6 +5877,28 @@ mod tests { seller_pubkey: &str, decision: RadrootsTradeOrderDecision, ) -> radroots_nostr::prelude::RadrootsNostrEvent { + signed_order_decision_event_with_counterparty( + seller, + request_event, + order_id, + listing_addr, + buyer_pubkey, + seller_pubkey, + buyer_pubkey, + decision, + ) + } + + fn signed_order_decision_event_with_counterparty( + seller: &RadrootsIdentity, + request_event: &radroots_nostr::prelude::RadrootsNostrEvent, + order_id: &str, + listing_addr: &str, + buyer_pubkey: &str, + seller_pubkey: &str, + counterparty_pubkey: &str, + decision: RadrootsTradeOrderDecision, + ) -> radroots_nostr::prelude::RadrootsNostrEvent { let payload = RadrootsTradeOrderDecisionEvent { order_id: order_id.to_owned(), listing_addr: listing_addr.to_owned(), @@ -5839,7 +5915,13 @@ mod tests { &payload, ) .expect("order decision parts"); - radroots_nostr_build_event(parts.kind, parts.content, parts.tags) + let mut tags = parts.tags; + for tag in tags.iter_mut() { + if tag.first().map(String::as_str) == Some("p") && tag.len() > 1 { + tag[1] = counterparty_pubkey.to_owned(); + } + } + radroots_nostr_build_event(parts.kind, parts.content, tags) .expect("nostr event builder") .sign_with_keys(seller.keys()) .expect("signed order decision")