lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit dae8880dd03a6d2be057b4e8083f881d926fba7c
parent 42ee62c68a81efabeefac9b08ceaac0ced52fa56
Author: triesap <tyson@radroots.org>
Date:   Thu, 30 Apr 2026 07:07:35 +0000

events: add order revision proposals

Diffstat:
Mcrates/events/src/trade.rs | 33+++++++++++++++++++++++++++++++++
Mcrates/events_codec/src/trade/decode.rs | 139++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/events_codec/src/trade/encode.rs | 26++++++++++++++++++++++++++
Mcrates/events_codec/src/trade/mod.rs | 7++++---
4 files changed, 197 insertions(+), 8 deletions(-)

diff --git a/crates/events/src/trade.rs b/crates/events/src/trade.rs @@ -443,6 +443,39 @@ impl RadrootsTradeOrderRequested { #[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsTradeOrderRevisionProposed { + pub revision_id: String, + pub order_id: String, + pub listing_addr: String, + pub buyer_pubkey: String, + pub seller_pubkey: String, + pub root_event_id: String, + pub prev_event_id: String, + pub items: Vec<RadrootsTradeOrderItem>, + pub economics: RadrootsTradeOrderEconomics, + pub reason: String, +} + +impl RadrootsTradeOrderRevisionProposed { + pub fn validate(&self) -> Result<(), RadrootsActiveTradePayloadError> { + validate_required_field(&self.revision_id, "revision_id")?; + validate_required_field(&self.order_id, "order_id")?; + validate_required_field(&self.listing_addr, "listing_addr")?; + validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; + validate_required_field(&self.seller_pubkey, "seller_pubkey")?; + validate_required_field(&self.root_event_id, "root_event_id")?; + validate_required_field(&self.prev_event_id, "prev_event_id")?; + validate_required_field(&self.reason, "reason")?; + validate_order_items(&self.items)?; + self.economics.validate()?; + validate_order_economics_binding(&self.items, &self.economics) + } +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTradeInventoryCommitment { pub bin_id: String, pub bin_count: u32, diff --git a/crates/events_codec/src/trade/decode.rs b/crates/events_codec/src/trade/decode.rs @@ -11,7 +11,7 @@ use radroots_events::{ RadrootsActiveTradeMessageType, RadrootsActiveTradePayloadError, RadrootsTradeBuyerReceipt, RadrootsTradeEnvelope, RadrootsTradeEnvelopeError, RadrootsTradeFulfillmentUpdated, RadrootsTradeMessageType, RadrootsTradeOrderCancelled, RadrootsTradeOrderDecisionEvent, - RadrootsTradeOrderRequested, + RadrootsTradeOrderRequested, RadrootsTradeOrderRevisionProposed, }, }; #[cfg(feature = "serde_json")] @@ -381,6 +381,44 @@ pub fn active_trade_order_decision_from_event( } #[cfg(feature = "serde_json")] +pub fn active_trade_order_revision_proposal_from_event( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionProposed>, + RadrootsActiveTradeEnvelopeParseError, +> { + let envelope = active_trade_envelope_from_event::<RadrootsTradeOrderRevisionProposed>(event)?; + if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderRevisionProposed { + return Err( + RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }, + ); + } + envelope + .payload + .validate() + .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidPayload)?; + validate_active_order_binding( + event, + &envelope, + &envelope.payload.order_id, + &envelope.payload.listing_addr, + &envelope.payload.seller_pubkey, + &envelope.payload.buyer_pubkey, + )?; + let context = active_trade_event_context_from_tags(envelope.message_type, &event.tags)?; + if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { + return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("root_event_id")); + } + if context.prev_event_id.as_deref() != Some(envelope.payload.prev_event_id.as_str()) { + return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("prev_event_id")); + } + Ok(envelope) +} + +#[cfg(feature = "serde_json")] pub fn active_trade_fulfillment_update_from_event( event: &RadrootsNostrEvent, ) -> Result< @@ -643,13 +681,14 @@ mod tests { RadrootsTradeListingAddress, active_trade_buyer_receipt_from_event, active_trade_envelope_from_event, active_trade_fulfillment_update_from_event, active_trade_order_cancel_from_event, active_trade_order_decision_from_event, - active_trade_order_request_from_event, trade_envelope_from_event, - trade_event_context_from_tags, + active_trade_order_request_from_event, active_trade_order_revision_proposal_from_event, + trade_envelope_from_event, trade_event_context_from_tags, }; use crate::trade::encode::{ active_trade_buyer_receipt_event_build, active_trade_fulfillment_update_event_build, active_trade_order_cancel_event_build, active_trade_order_decision_event_build, - active_trade_order_request_event_build, trade_envelope_event_build, + active_trade_order_request_event_build, active_trade_order_revision_proposal_event_build, + trade_envelope_event_build, }; use crate::trade::tags::TAG_LISTING_EVENT; use radroots_core::{ @@ -672,7 +711,7 @@ mod tests { RadrootsTradeOrderDecision, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomicLine, RadrootsTradeOrderEconomics, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, - RadrootsTradePricingBasis, + RadrootsTradeOrderRevisionProposed, RadrootsTradePricingBasis, }, }; @@ -751,6 +790,32 @@ mod tests { } } + fn active_order_revision_proposal() -> RadrootsTradeOrderRevisionProposed { + let mut economics = request_economics(); + economics.quote_id = "revision-quote-1".into(); + economics.quote_version = 2; + economics.items[0].bin_count = 4; + economics.items[0].line_subtotal = usd("20"); + economics.subtotal = usd("20"); + economics.total = usd("20"); + economics.canonicalize(); + RadrootsTradeOrderRevisionProposed { + revision_id: "rev-1".into(), + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + root_event_id: "root-event".into(), + prev_event_id: "decision-event".into(), + items: vec![RadrootsTradeOrderItem { + bin_id: "lb".into(), + bin_count: 4, + }], + economics, + reason: "update count".into(), + } + } + fn active_fulfillment_update() -> RadrootsTradeFulfillmentUpdated { RadrootsTradeFulfillmentUpdated { order_id: "order-1".into(), @@ -906,6 +971,44 @@ mod tests { } #[test] + fn active_order_revision_proposal_builder_emits_canonical_chain_shape() { + let payload = active_order_revision_proposal(); + let built = active_trade_order_revision_proposal_event_build( + payload.root_event_id.as_str(), + payload.prev_event_id.as_str(), + &payload, + ) + .unwrap(); + let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderRevisionProposed> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_TRADE_ORDER_REVISION); + assert_eq!( + envelope.message_type, + RadrootsActiveTradeMessageType::TradeOrderRevisionProposed + ); + assert_eq!(built.tags[0], vec!["p".to_string(), "buyer".to_string()]); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert_eq!(envelope.payload.revision_id, "rev-1"); + assert_eq!(envelope.payload.economics.quote_version, 2); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_ROOT.to_string(), "root-event".to_string()]) + ); + assert!( + built + .tags + .iter() + .any(|tag| tag == &vec![TAG_E_PREV.to_string(), "decision-event".to_string()]) + ); + } + + #[test] fn active_fulfillment_update_builder_emits_canonical_chain_shape() { let payload = active_fulfillment_update(); let built = @@ -1153,6 +1256,32 @@ mod tests { } #[test] + fn active_order_revision_proposal_parse_validates_actor_counterparty_and_chain_payload() { + let payload = active_order_revision_proposal(); + let built = active_trade_order_revision_proposal_event_build( + payload.root_event_id.as_str(), + payload.prev_event_id.as_str(), + &payload, + ) + .unwrap(); + let mut event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = active_trade_order_revision_proposal_from_event(&event).unwrap(); + assert_eq!(envelope.payload, payload); + + event.author = "buyer".into(); + let err = active_trade_order_revision_proposal_from_event(&event).unwrap_err(); + assert_eq!(err, RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); + } + + #[test] fn active_revision_kinds_parse_with_chain_tags() { for (kind, message_type) in [ ( diff --git a/crates/events_codec/src/trade/encode.rs b/crates/events_codec/src/trade/encode.rs @@ -10,6 +10,7 @@ use radroots_events::{ RadrootsTradeEnvelope, RadrootsTradeEnvelopeError, RadrootsTradeFulfillmentUpdated, RadrootsTradeMessagePayload, RadrootsTradeMessageType, RadrootsTradeOrderCancelled, RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderRequested, + RadrootsTradeOrderRevisionProposed, }, }; @@ -232,6 +233,31 @@ pub fn active_trade_order_decision_event_build( } #[cfg(feature = "serde_json")] +pub fn active_trade_order_revision_proposal_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsTradeOrderRevisionProposed, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_active_payload_error)?; + if payload.root_event_id != root_event_id { + return Err(EventEncodeError::InvalidField("root_event_id")); + } + if payload.prev_event_id != prev_event_id { + return Err(EventEncodeError::InvalidField("prev_event_id")); + } + active_trade_envelope_event_build( + &payload.buyer_pubkey, + RadrootsActiveTradeMessageType::TradeOrderRevisionProposed, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} + +#[cfg(feature = "serde_json")] pub fn active_trade_fulfillment_update_event_build( root_event_id: &str, prev_event_id: &str, diff --git a/crates/events_codec/src/trade/mod.rs b/crates/events_codec/src/trade/mod.rs @@ -9,14 +9,15 @@ pub use decode::{ active_trade_buyer_receipt_from_event, active_trade_envelope_from_event, active_trade_event_context_from_tags, active_trade_fulfillment_update_from_event, active_trade_order_cancel_from_event, active_trade_order_decision_from_event, - active_trade_order_request_from_event, trade_envelope_from_event, - trade_event_context_from_tags, + active_trade_order_request_from_event, active_trade_order_revision_proposal_from_event, + trade_envelope_from_event, trade_event_context_from_tags, }; #[cfg(feature = "serde_json")] pub use encode::{ active_trade_buyer_receipt_event_build, active_trade_fulfillment_update_event_build, active_trade_order_cancel_event_build, active_trade_order_decision_event_build, - active_trade_order_request_event_build, trade_envelope_event_build, + active_trade_order_request_event_build, active_trade_order_revision_proposal_event_build, + trade_envelope_event_build, }; pub use tags::{ TAG_LISTING_EVENT, parse_trade_counterparty_tag, parse_trade_listing_event_tag,