lib

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

commit 6a96de34907d3322e932b5fe5d52184d31da840b
parent de11b3829f43a0e0ab710b329631e973060d8260
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Apr 2026 21:50:35 +0000

events_codec: add active trade event codec

Diffstat:
Mcrates/events_codec/src/trade/decode.rs | 524+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events_codec/src/trade/encode.rs | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events_codec/src/trade/mod.rs | 12+++++++++---
3 files changed, 644 insertions(+), 12 deletions(-)

diff --git a/crates/events_codec/src/trade/decode.rs b/crates/events_codec/src/trade/decode.rs @@ -4,9 +4,14 @@ use alloc::{borrow::ToOwned, format, string::String, vec::Vec}; #[cfg(feature = "serde_json")] use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, - kinds::{KIND_PROFILE, is_trade_kind}, + kinds::{KIND_PROFILE, is_active_trade_public_kind, is_trade_kind}, tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, - trade::{RadrootsTradeEnvelope, RadrootsTradeEnvelopeError, RadrootsTradeMessageType}, + trade::{ + RadrootsActiveTradeEnvelope, RadrootsActiveTradeEnvelopeError, + RadrootsActiveTradeMessageType, RadrootsActiveTradePayloadError, RadrootsTradeEnvelope, + RadrootsTradeEnvelopeError, RadrootsTradeMessageType, RadrootsTradeOrderDecisionEvent, + RadrootsTradeOrderRequested, + }, }; #[cfg(feature = "serde_json")] use serde::de::DeserializeOwned; @@ -37,6 +42,27 @@ pub enum RadrootsTradeEnvelopeParseError { } #[cfg(feature = "serde_json")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsActiveTradeEnvelopeParseError { + InvalidKind(u32), + InvalidJson, + InvalidEnvelope(RadrootsActiveTradeEnvelopeError), + InvalidPayload(RadrootsActiveTradePayloadError), + MessageTypeKindMismatch { + event_kind: u32, + message_type: RadrootsActiveTradeMessageType, + }, + MissingTag(&'static str), + InvalidTag(&'static str), + ListingAddrTagMismatch, + OrderIdTagMismatch, + PayloadBindingMismatch(&'static str), + AuthorMismatch, + CounterpartyTagMismatch, + InvalidListingAddr(RadrootsTradeListingAddressError), +} + +#[cfg(feature = "serde_json")] impl core::fmt::Display for RadrootsTradeEnvelopeParseError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -61,6 +87,44 @@ impl core::fmt::Display for RadrootsTradeEnvelopeParseError { } } +#[cfg(feature = "serde_json")] +impl core::fmt::Display for RadrootsActiveTradeEnvelopeParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InvalidKind(kind) => write!(f, "invalid active trade event kind: {kind}"), + Self::InvalidJson => write!(f, "invalid active trade envelope json"), + Self::InvalidEnvelope(error) => write!(f, "{error}"), + Self::InvalidPayload(error) => write!(f, "{error}"), + Self::MessageTypeKindMismatch { + event_kind, + message_type, + } => write!( + f, + "active trade envelope type {message_type:?} does not match event kind {event_kind}" + ), + Self::MissingTag(tag) => write!(f, "missing required active trade tag: {tag}"), + Self::InvalidTag(tag) => write!(f, "invalid active trade tag: {tag}"), + Self::ListingAddrTagMismatch => { + write!( + f, + "active trade listing address tag does not match envelope" + ) + } + Self::OrderIdTagMismatch => { + write!(f, "active trade order id tag does not match envelope") + } + Self::PayloadBindingMismatch(field) => { + write!(f, "active trade payload {field} does not match envelope") + } + Self::AuthorMismatch => write!(f, "active trade event author does not match payload"), + Self::CounterpartyTagMismatch => { + write!(f, "active trade counterparty tag does not match payload") + } + Self::InvalidListingAddr(error) => write!(f, "{error}"), + } + } +} + #[cfg(all(feature = "std", feature = "serde_json"))] impl std::error::Error for RadrootsTradeEnvelopeParseError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { @@ -72,6 +136,18 @@ impl std::error::Error for RadrootsTradeEnvelopeParseError { } } +#[cfg(all(feature = "std", feature = "serde_json"))] +impl std::error::Error for RadrootsActiveTradeEnvelopeParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidEnvelope(error) => Some(error), + Self::InvalidPayload(error) => Some(error), + Self::InvalidListingAddr(error) => Some(error), + _ => None, + } + } +} + #[cfg(feature = "serde_json")] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTradeEventContext { @@ -203,6 +279,107 @@ pub fn trade_envelope_from_event<T: DeserializeOwned>( } #[cfg(feature = "serde_json")] +pub fn active_trade_envelope_from_event<T: DeserializeOwned>( + event: &RadrootsNostrEvent, +) -> Result<RadrootsActiveTradeEnvelope<T>, RadrootsActiveTradeEnvelopeParseError> { + if !is_active_trade_public_kind(event.kind) { + return Err(RadrootsActiveTradeEnvelopeParseError::InvalidKind( + event.kind, + )); + } + let envelope = serde_json::from_str::<RadrootsActiveTradeEnvelope<T>>(&event.content) + .map_err(|_| RadrootsActiveTradeEnvelopeParseError::InvalidJson)?; + envelope + .validate() + .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidEnvelope)?; + if envelope.message_type.kind() != event.kind { + return Err( + RadrootsActiveTradeEnvelopeParseError::MessageTypeKindMismatch { + event_kind: event.kind, + message_type: envelope.message_type, + }, + ); + } + + let listing_addr = required_active_tag_value(&event.tags, "a")?; + if envelope.listing_addr != listing_addr { + return Err(RadrootsActiveTradeEnvelopeParseError::ListingAddrTagMismatch); + } + RadrootsTradeListingAddress::parse(&envelope.listing_addr) + .map_err(RadrootsActiveTradeEnvelopeParseError::InvalidListingAddr)?; + + let tag_order_id = required_active_tag_value(&event.tags, TAG_D)?; + if tag_order_id != envelope.order_id { + return Err(RadrootsActiveTradeEnvelopeParseError::OrderIdTagMismatch); + } + + active_trade_event_context_from_tags(envelope.message_type, &event.tags)?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn active_trade_order_request_from_event( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeOrderRequested>, + RadrootsActiveTradeEnvelopeParseError, +> { + let envelope = active_trade_envelope_from_event::<RadrootsTradeOrderRequested>(event)?; + if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderRequested { + 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.buyer_pubkey, + &envelope.payload.seller_pubkey, + )?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] +pub fn active_trade_order_decision_from_event( + event: &RadrootsNostrEvent, +) -> Result< + RadrootsActiveTradeEnvelope<RadrootsTradeOrderDecisionEvent>, + RadrootsActiveTradeEnvelopeParseError, +> { + let envelope = active_trade_envelope_from_event::<RadrootsTradeOrderDecisionEvent>(event)?; + if envelope.message_type != RadrootsActiveTradeMessageType::TradeOrderDecision { + 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, + )?; + Ok(envelope) +} + +#[cfg(feature = "serde_json")] pub fn trade_event_context_from_tags( message_type: RadrootsTradeMessageType, tags: &[Vec<String>], @@ -239,6 +416,46 @@ pub fn trade_event_context_from_tags( } #[cfg(feature = "serde_json")] +pub fn active_trade_event_context_from_tags( + message_type: RadrootsActiveTradeMessageType, + tags: &[Vec<String>], +) -> Result<RadrootsTradeEventContext, RadrootsActiveTradeEnvelopeParseError> { + let counterparty_pubkey = parse_trade_counterparty_tag(tags) + .map_err(map_tag_parse_error_for_active_trade_envelope)?; + let listing_event = parse_trade_listing_event_tag(tags) + .map_err(map_tag_parse_error_for_active_trade_envelope)?; + let root_event_id = + parse_trade_root_tag(tags).map_err(map_tag_parse_error_for_active_trade_envelope)?; + let prev_event_id = + parse_trade_prev_tag(tags).map_err(map_tag_parse_error_for_active_trade_envelope)?; + + if message_type.requires_listing_snapshot() && listing_event.is_none() { + return Err(RadrootsActiveTradeEnvelopeParseError::MissingTag( + TAG_LISTING_EVENT, + )); + } + if message_type.requires_trade_chain() { + if root_event_id.is_none() { + return Err(RadrootsActiveTradeEnvelopeParseError::MissingTag( + TAG_E_ROOT, + )); + } + if prev_event_id.is_none() { + return Err(RadrootsActiveTradeEnvelopeParseError::MissingTag( + TAG_E_PREV, + )); + } + } + + Ok(RadrootsTradeEventContext { + counterparty_pubkey, + listing_event, + root_event_id, + prev_event_id, + }) +} + +#[cfg(feature = "serde_json")] fn map_tag_parse_error_for_trade_envelope( error: crate::error::EventParseError, ) -> RadrootsTradeEnvelopeParseError { @@ -259,20 +476,94 @@ fn map_tag_parse_error_for_trade_envelope( } } +#[cfg(feature = "serde_json")] +fn required_active_tag_value<'a>( + tags: &'a [Vec<String>], + key: &'static str, +) -> Result<&'a str, RadrootsActiveTradeEnvelopeParseError> { + let tag = tags + .iter() + .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) + .ok_or(RadrootsActiveTradeEnvelopeParseError::MissingTag(key))?; + let value = tag + .get(1) + .map(|value| value.as_str()) + .ok_or(RadrootsActiveTradeEnvelopeParseError::InvalidTag(key))?; + if value.trim().is_empty() { + return Err(RadrootsActiveTradeEnvelopeParseError::InvalidTag(key)); + } + Ok(value) +} + +#[cfg(feature = "serde_json")] +fn map_tag_parse_error_for_active_trade_envelope( + error: crate::error::EventParseError, +) -> RadrootsActiveTradeEnvelopeParseError { + match error { + crate::error::EventParseError::MissingTag(tag) => { + RadrootsActiveTradeEnvelopeParseError::MissingTag(tag) + } + crate::error::EventParseError::InvalidTag(tag) => { + RadrootsActiveTradeEnvelopeParseError::InvalidTag(tag) + } + crate::error::EventParseError::InvalidKind { expected: _, got } => { + RadrootsActiveTradeEnvelopeParseError::InvalidKind(got) + } + crate::error::EventParseError::InvalidNumber(tag, _) + | crate::error::EventParseError::InvalidJson(tag) => { + RadrootsActiveTradeEnvelopeParseError::InvalidTag(tag) + } + } +} + +#[cfg(feature = "serde_json")] +fn validate_active_order_binding<T>( + event: &RadrootsNostrEvent, + envelope: &RadrootsActiveTradeEnvelope<T>, + payload_order_id: &str, + payload_listing_addr: &str, + expected_author: &str, + expected_counterparty: &str, +) -> Result<(), RadrootsActiveTradeEnvelopeParseError> { + if envelope.order_id != payload_order_id { + return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("order_id")); + } + if envelope.listing_addr != payload_listing_addr { + return Err(RadrootsActiveTradeEnvelopeParseError::PayloadBindingMismatch("listing_addr")); + } + if event.author != expected_author { + return Err(RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); + } + let counterparty = parse_trade_counterparty_tag(&event.tags) + .map_err(map_tag_parse_error_for_active_trade_envelope)?; + if counterparty != expected_counterparty { + return Err(RadrootsActiveTradeEnvelopeParseError::CounterpartyTagMismatch); + } + Ok(()) +} + #[cfg(all(test, feature = "serde_json"))] mod tests { use super::{ - RadrootsTradeEnvelopeParseError, RadrootsTradeListingAddress, trade_envelope_from_event, - trade_event_context_from_tags, + RadrootsActiveTradeEnvelopeParseError, RadrootsTradeEnvelopeParseError, + RadrootsTradeListingAddress, active_trade_envelope_from_event, + active_trade_order_decision_from_event, active_trade_order_request_from_event, + trade_envelope_from_event, trade_event_context_from_tags, + }; + use crate::trade::encode::{ + active_trade_order_decision_event_build, active_trade_order_request_event_build, + trade_envelope_event_build, }; - use crate::trade::encode::trade_envelope_event_build; use crate::trade::tags::TAG_LISTING_EVENT; use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, + kinds::{KIND_TRADE_ORDER_DECISION, KIND_TRADE_ORDER_REQUEST}, tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, trade::{ - RadrootsTradeEnvelope, RadrootsTradeMessagePayload, RadrootsTradeMessageType, - RadrootsTradeOrder, RadrootsTradeOrderItem, + RadrootsActiveTradeEnvelope, RadrootsActiveTradeMessageType, RadrootsTradeEnvelope, + RadrootsTradeInventoryCommitment, RadrootsTradeMessagePayload, + RadrootsTradeMessageType, RadrootsTradeOrder, RadrootsTradeOrderDecision, + RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderItem, RadrootsTradeOrderRequested, }, }; @@ -290,6 +581,41 @@ mod tests { } } + fn active_order_request() -> RadrootsTradeOrderRequested { + RadrootsTradeOrderRequested { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + items: vec![RadrootsTradeOrderItem { + bin_id: "lb".into(), + bin_count: 3, + }], + } + } + + fn active_order_decision() -> RadrootsTradeOrderDecisionEvent { + RadrootsTradeOrderDecisionEvent { + order_id: "order-1".into(), + listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".into(), + buyer_pubkey: "buyer".into(), + seller_pubkey: "seller".into(), + decision: RadrootsTradeOrderDecision::Accepted { + inventory_commitments: vec![RadrootsTradeInventoryCommitment { + bin_id: "lb".into(), + bin_count: 3, + }], + }, + } + } + + fn listing_event_ptr() -> RadrootsNostrEventPtr { + RadrootsNostrEventPtr { + id: "listing-snapshot".into(), + relays: Some("wss://relay.example.com".into()), + } + } + #[test] fn listing_address_roundtrips() { let addr = RadrootsTradeListingAddress::parse("30402:seller:AAAAAAAAAAAAAAAAAAAAAg") @@ -333,6 +659,190 @@ mod tests { } #[test] + fn active_order_request_builder_emits_canonical_shape() { + let payload = active_order_request(); + let built = active_trade_order_request_event_build(&listing_event_ptr(), &payload).unwrap(); + let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderRequested> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_TRADE_ORDER_REQUEST); + assert_eq!( + envelope.message_type, + RadrootsActiveTradeMessageType::TradeOrderRequested + ); + assert_eq!(envelope.order_id, "order-1"); + assert_eq!(built.tags[0], vec!["p".to_string(), "seller".to_string()]); + assert_eq!( + built.tags[1], + vec![ + "a".to_string(), + "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".to_string() + ] + ); + assert_eq!( + built.tags[2], + vec![TAG_D.to_string(), "order-1".to_string()] + ); + assert!( + built + .tags + .iter() + .any(|tag| tag.first().map(String::as_str) == Some(TAG_LISTING_EVENT)) + ); + assert!( + !built + .tags + .iter() + .any(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) + ); + } + + #[test] + fn active_order_decision_builder_emits_canonical_chain_shape() { + let payload = active_order_decision(); + let built = + active_trade_order_decision_event_build("root-event", "prev-event", &payload).unwrap(); + let envelope: RadrootsActiveTradeEnvelope<RadrootsTradeOrderDecisionEvent> = + serde_json::from_str(&built.content).unwrap(); + + assert_eq!(built.kind, KIND_TRADE_ORDER_DECISION); + assert_eq!( + envelope.message_type, + RadrootsActiveTradeMessageType::TradeOrderDecision + ); + 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!( + 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(), "prev-event".to_string()]) + ); + } + + #[test] + fn active_order_request_parse_roundtrips_and_validates_tags() { + let payload = active_order_request(); + let built = active_trade_order_request_event_build(&listing_event_ptr(), &payload).unwrap(); + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "buyer".into(), + created_at: 1, + kind: built.kind, + tags: built.tags, + content: built.content, + sig: "sig".into(), + }; + let envelope = active_trade_order_request_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!( + envelope.message_type, + RadrootsActiveTradeMessageType::TradeOrderRequested + ); + } + + #[test] + fn active_order_decision_parse_roundtrips_and_validates_chain_tags() { + let payload = active_order_decision(); + let built = + active_trade_order_decision_event_build("root-event", "prev-event", &payload).unwrap(); + let 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_decision_from_event(&event).unwrap(); + + assert_eq!(envelope.payload, payload); + assert_eq!( + envelope.message_type, + RadrootsActiveTradeMessageType::TradeOrderDecision + ); + } + + #[test] + fn active_parse_rejects_forbidden_kind() { + let event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: 3431, + tags: Vec::new(), + content: "{}".into(), + sig: "sig".into(), + }; + let err = active_trade_envelope_from_event::<serde_json::Value>(&event).unwrap_err(); + assert_eq!( + err, + RadrootsActiveTradeEnvelopeParseError::InvalidKind(3431) + ); + } + + #[test] + fn active_parse_rejects_missing_required_refs() { + let payload = active_order_decision(); + let built = + active_trade_order_decision_event_build("root-event", "prev-event", &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(), + }; + event + .tags + .retain(|tag| tag.first().map(String::as_str) != Some(TAG_E_PREV)); + + let err = active_trade_order_decision_from_event(&event).unwrap_err(); + assert_eq!( + err, + RadrootsActiveTradeEnvelopeParseError::MissingTag(TAG_E_PREV) + ); + } + + #[test] + fn active_parse_rejects_author_and_counterparty_mismatch() { + let payload = active_order_request(); + let built = active_trade_order_request_event_build(&listing_event_ptr(), &payload).unwrap(); + let mut event = RadrootsNostrEvent { + id: "event-id".into(), + author: "seller".into(), + created_at: 1, + kind: built.kind, + tags: built.tags.clone(), + content: built.content.clone(), + sig: "sig".into(), + }; + let err = active_trade_order_request_from_event(&event).unwrap_err(); + assert_eq!(err, RadrootsActiveTradeEnvelopeParseError::AuthorMismatch); + + event.author = "buyer".into(); + event.tags[0] = vec!["p".into(), "other-seller".into()]; + let err = active_trade_order_request_from_event(&event).unwrap_err(); + assert_eq!( + err, + RadrootsActiveTradeEnvelopeParseError::CounterpartyTagMismatch + ); + } + + #[test] fn parse_rejects_listing_addr_mismatch() { let payload = RadrootsTradeMessagePayload::OrderRequest(base_order()); let built = trade_envelope_event_build( diff --git a/crates/events_codec/src/trade/encode.rs b/crates/events_codec/src/trade/encode.rs @@ -5,8 +5,10 @@ use alloc::string::String; use radroots_events::{ RadrootsNostrEventPtr, trade::{ - RadrootsTradeEnvelope, RadrootsTradeEnvelopeError, RadrootsTradeMessagePayload, - RadrootsTradeMessageType, + RadrootsActiveTradeEnvelope, RadrootsActiveTradeEnvelopeError, + RadrootsActiveTradeMessageType, RadrootsActiveTradePayloadError, RadrootsTradeEnvelope, + RadrootsTradeEnvelopeError, RadrootsTradeMessagePayload, RadrootsTradeMessageType, + RadrootsTradeOrderDecisionEvent, RadrootsTradeOrderRequested, }, }; @@ -29,6 +31,42 @@ fn map_envelope_error(error: RadrootsTradeEnvelopeError) -> EventEncodeError { } #[cfg(feature = "serde_json")] +fn map_active_envelope_error(error: RadrootsActiveTradeEnvelopeError) -> EventEncodeError { + match error { + RadrootsActiveTradeEnvelopeError::MissingOrderId => { + EventEncodeError::EmptyRequiredField("order_id") + } + RadrootsActiveTradeEnvelopeError::MissingListingAddr => { + EventEncodeError::EmptyRequiredField("listing_addr") + } + RadrootsActiveTradeEnvelopeError::InvalidVersion { .. } => { + EventEncodeError::InvalidField("version") + } + } +} + +#[cfg(feature = "serde_json")] +fn map_active_payload_error(error: RadrootsActiveTradePayloadError) -> EventEncodeError { + match error { + RadrootsActiveTradePayloadError::EmptyField(field) => { + EventEncodeError::EmptyRequiredField(field) + } + RadrootsActiveTradePayloadError::MissingItems => { + EventEncodeError::EmptyRequiredField("items") + } + RadrootsActiveTradePayloadError::InvalidItemBinCount { .. } => { + EventEncodeError::InvalidField("items.bin_count") + } + RadrootsActiveTradePayloadError::MissingInventoryCommitments => { + EventEncodeError::EmptyRequiredField("inventory_commitments") + } + RadrootsActiveTradePayloadError::InvalidInventoryCommitmentCount { .. } => { + EventEncodeError::InvalidField("inventory_commitments.bin_count") + } + } +} + +#[cfg(feature = "serde_json")] pub fn trade_envelope_event_build( recipient_pubkey: impl Into<String>, message_type: RadrootsTradeMessageType, @@ -77,3 +115,81 @@ pub fn trade_envelope_event_build( tags, }) } + +#[cfg(feature = "serde_json")] +fn active_trade_envelope_event_build<T: serde::Serialize>( + recipient_pubkey: &str, + message_type: RadrootsActiveTradeMessageType, + listing_addr: &str, + order_id: &str, + listing_event: Option<&RadrootsNostrEventPtr>, + root_event_id: Option<&str>, + prev_event_id: Option<&str>, + payload: &T, +) -> Result<WireEventParts, EventEncodeError> { + if message_type.requires_listing_snapshot() && listing_event.is_none() { + return Err(EventEncodeError::EmptyRequiredField("listing_event.id")); + } + if message_type.requires_trade_chain() { + if root_event_id.is_none() { + return Err(EventEncodeError::EmptyRequiredField("root_event_id")); + } + if prev_event_id.is_none() { + return Err(EventEncodeError::EmptyRequiredField("prev_event_id")); + } + } + + let envelope = RadrootsActiveTradeEnvelope::new(message_type, listing_addr, order_id, payload); + envelope.validate().map_err(map_active_envelope_error)?; + let content = serde_json::to_string(&envelope).map_err(|_| EventEncodeError::Json)?; + let tags = trade_envelope_tags( + recipient_pubkey, + listing_addr, + Some(order_id), + listing_event, + root_event_id, + prev_event_id, + )?; + Ok(WireEventParts { + kind: message_type.kind(), + content, + tags, + }) +} + +#[cfg(feature = "serde_json")] +pub fn active_trade_order_request_event_build( + listing_event: &RadrootsNostrEventPtr, + payload: &RadrootsTradeOrderRequested, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_active_payload_error)?; + active_trade_envelope_event_build( + &payload.seller_pubkey, + RadrootsActiveTradeMessageType::TradeOrderRequested, + &payload.listing_addr, + &payload.order_id, + Some(listing_event), + None, + None, + payload, + ) +} + +#[cfg(feature = "serde_json")] +pub fn active_trade_order_decision_event_build( + root_event_id: &str, + prev_event_id: &str, + payload: &RadrootsTradeOrderDecisionEvent, +) -> Result<WireEventParts, EventEncodeError> { + payload.validate().map_err(map_active_payload_error)?; + active_trade_envelope_event_build( + &payload.buyer_pubkey, + RadrootsActiveTradeMessageType::TradeOrderDecision, + &payload.listing_addr, + &payload.order_id, + None, + Some(root_event_id), + Some(prev_event_id), + payload, + ) +} diff --git a/crates/events_codec/src/trade/mod.rs b/crates/events_codec/src/trade/mod.rs @@ -4,11 +4,17 @@ pub mod tags; #[cfg(feature = "serde_json")] pub use decode::{ - RadrootsTradeEnvelopeParseError, RadrootsTradeEventContext, RadrootsTradeListingAddress, - RadrootsTradeListingAddressError, trade_envelope_from_event, trade_event_context_from_tags, + RadrootsActiveTradeEnvelopeParseError, RadrootsTradeEnvelopeParseError, + RadrootsTradeEventContext, RadrootsTradeListingAddress, RadrootsTradeListingAddressError, + active_trade_envelope_from_event, active_trade_event_context_from_tags, + active_trade_order_decision_from_event, active_trade_order_request_from_event, + trade_envelope_from_event, trade_event_context_from_tags, }; #[cfg(feature = "serde_json")] -pub use encode::trade_envelope_event_build; +pub use encode::{ + active_trade_order_decision_event_build, active_trade_order_request_event_build, + trade_envelope_event_build, +}; pub use tags::{ TAG_LISTING_EVENT, parse_trade_counterparty_tag, parse_trade_listing_event_tag, parse_trade_prev_tag, parse_trade_root_tag, push_trade_chain_tags, trade_envelope_tags,