lib

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

encode.rs (16491B)


      1 #[cfg(all(not(feature = "std"), feature = "serde_json"))]
      2 use alloc::string::String;
      3 
      4 #[cfg(feature = "serde_json")]
      5 use radroots_events::{
      6     RadrootsNostrEventPtr,
      7     ids::RadrootsEventId,
      8     order::{
      9         RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderEnvelope,
     10         RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderPayloadError,
     11         RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal,
     12     },
     13 };
     14 
     15 #[cfg(feature = "serde_json")]
     16 use crate::{error::EventEncodeError, order::tags::order_envelope_tags, wire::WireEventParts};
     17 
     18 #[cfg(feature = "serde_json")]
     19 fn map_order_envelope_error(error: RadrootsOrderEnvelopeError) -> EventEncodeError {
     20     match error {
     21         RadrootsOrderEnvelopeError::MissingOrderId => {
     22             EventEncodeError::EmptyRequiredField("order_id")
     23         }
     24         RadrootsOrderEnvelopeError::MissingListingAddr => {
     25             EventEncodeError::EmptyRequiredField("listing_addr")
     26         }
     27         RadrootsOrderEnvelopeError::InvalidVersion { .. } => {
     28             EventEncodeError::InvalidField("version")
     29         }
     30     }
     31 }
     32 
     33 #[cfg(feature = "serde_json")]
     34 fn map_order_payload_error(error: RadrootsOrderPayloadError) -> EventEncodeError {
     35     match error {
     36         RadrootsOrderPayloadError::EmptyField(field) => EventEncodeError::EmptyRequiredField(field),
     37         RadrootsOrderPayloadError::MissingItems => EventEncodeError::EmptyRequiredField("items"),
     38         RadrootsOrderPayloadError::InvalidItemBinCount { .. } => {
     39             EventEncodeError::InvalidField("items.bin_count")
     40         }
     41         RadrootsOrderPayloadError::MissingEconomicItems => {
     42             EventEncodeError::EmptyRequiredField("economics.items")
     43         }
     44         RadrootsOrderPayloadError::InvalidEconomicItemBinCount { .. } => {
     45             EventEncodeError::InvalidField("economics.items.bin_count")
     46         }
     47         RadrootsOrderPayloadError::InvalidEconomicItemQuantity { .. } => {
     48             EventEncodeError::InvalidField("economics.items.quantity_amount")
     49         }
     50         RadrootsOrderPayloadError::InvalidEconomicItemPrice { .. } => {
     51             EventEncodeError::InvalidField("economics.items.unit_price_amount")
     52         }
     53         RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { .. } => {
     54             EventEncodeError::InvalidField("economics.items.line_subtotal")
     55         }
     56         RadrootsOrderPayloadError::InvalidEconomicLineAmount { field, .. }
     57         | RadrootsOrderPayloadError::InvalidEconomicLineKind { field, .. }
     58         | RadrootsOrderPayloadError::InvalidEconomicLineEffect { field, .. }
     59         | RadrootsOrderPayloadError::InvalidEconomicCurrency { field }
     60         | RadrootsOrderPayloadError::InvalidEconomicOrdering { field }
     61         | RadrootsOrderPayloadError::InvalidEconomicTotal { field }
     62         | RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { field } => {
     63             EventEncodeError::InvalidField(field)
     64         }
     65         RadrootsOrderPayloadError::InvalidQuoteVersion => {
     66             EventEncodeError::InvalidField("economics.quote_version")
     67         }
     68         RadrootsOrderPayloadError::MissingInventoryCommitments => {
     69             EventEncodeError::EmptyRequiredField("inventory_commitments")
     70         }
     71         RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { .. } => {
     72             EventEncodeError::InvalidField("inventory_commitments.bin_count")
     73         }
     74     }
     75 }
     76 
     77 #[cfg(feature = "serde_json")]
     78 struct OrderEnvelopeEventBuildParts<'a, T> {
     79     recipient_pubkey: &'a str,
     80     message_type: RadrootsOrderEventType,
     81     listing_addr: &'a str,
     82     order_id: &'a str,
     83     listing_event: Option<&'a RadrootsNostrEventPtr>,
     84     root_event_id: Option<&'a RadrootsEventId>,
     85     prev_event_id: Option<&'a RadrootsEventId>,
     86     payload: &'a T,
     87 }
     88 
     89 #[cfg(feature = "serde_json")]
     90 fn order_envelope_event_build<T: serde::Serialize>(
     91     parts: OrderEnvelopeEventBuildParts<'_, T>,
     92 ) -> Result<WireEventParts, EventEncodeError> {
     93     if parts.message_type.requires_listing_snapshot() && parts.listing_event.is_none() {
     94         return Err(EventEncodeError::EmptyRequiredField("listing_event.id"));
     95     }
     96     if parts.message_type.requires_order_chain() {
     97         if parts.root_event_id.is_none() {
     98             return Err(EventEncodeError::EmptyRequiredField("root_event_id"));
     99         }
    100         if parts.prev_event_id.is_none() {
    101             return Err(EventEncodeError::EmptyRequiredField("prev_event_id"));
    102         }
    103     }
    104 
    105     let envelope = RadrootsOrderEnvelope::new(
    106         parts.message_type,
    107         parts.listing_addr,
    108         parts.order_id,
    109         parts.payload,
    110     );
    111     envelope.validate().map_err(map_order_envelope_error)?;
    112     let content = serde_json::to_string(&envelope).map_err(|_| EventEncodeError::Json)?;
    113     let tags = order_envelope_tags(
    114         parts.recipient_pubkey,
    115         parts.listing_addr,
    116         Some(parts.order_id),
    117         parts.listing_event,
    118         parts.root_event_id.map(RadrootsEventId::as_str),
    119         parts.prev_event_id.map(RadrootsEventId::as_str),
    120     )?;
    121     Ok(WireEventParts {
    122         kind: parts.message_type.kind(),
    123         content,
    124         tags,
    125     })
    126 }
    127 
    128 #[cfg(feature = "serde_json")]
    129 pub fn order_request_event_build(
    130     listing_event: &RadrootsNostrEventPtr,
    131     payload: &RadrootsOrderRequest,
    132 ) -> Result<WireEventParts, EventEncodeError> {
    133     payload.validate().map_err(map_order_payload_error)?;
    134     order_envelope_event_build(OrderEnvelopeEventBuildParts {
    135         recipient_pubkey: &payload.seller_pubkey,
    136         message_type: RadrootsOrderEventType::OrderRequested,
    137         listing_addr: &payload.listing_addr,
    138         order_id: &payload.order_id,
    139         listing_event: Some(listing_event),
    140         root_event_id: None,
    141         prev_event_id: None,
    142         payload,
    143     })
    144 }
    145 
    146 #[cfg(feature = "serde_json")]
    147 pub fn order_decision_event_build(
    148     root_event_id: &RadrootsEventId,
    149     prev_event_id: &RadrootsEventId,
    150     payload: &RadrootsOrderDecision,
    151 ) -> Result<WireEventParts, EventEncodeError> {
    152     payload.validate().map_err(map_order_payload_error)?;
    153     order_envelope_event_build(OrderEnvelopeEventBuildParts {
    154         recipient_pubkey: &payload.buyer_pubkey,
    155         message_type: RadrootsOrderEventType::OrderDecision,
    156         listing_addr: &payload.listing_addr,
    157         order_id: &payload.order_id,
    158         listing_event: None,
    159         root_event_id: Some(root_event_id),
    160         prev_event_id: Some(prev_event_id),
    161         payload,
    162     })
    163 }
    164 
    165 #[cfg(feature = "serde_json")]
    166 pub fn order_revision_proposal_event_build(
    167     root_event_id: &RadrootsEventId,
    168     prev_event_id: &RadrootsEventId,
    169     payload: &RadrootsOrderRevisionProposal,
    170 ) -> Result<WireEventParts, EventEncodeError> {
    171     payload.validate().map_err(map_order_payload_error)?;
    172     if payload.root_event_id.as_str() != root_event_id.as_str() {
    173         return Err(EventEncodeError::InvalidField("root_event_id"));
    174     }
    175     if payload.prev_event_id.as_str() != prev_event_id.as_str() {
    176         return Err(EventEncodeError::InvalidField("prev_event_id"));
    177     }
    178     order_envelope_event_build(OrderEnvelopeEventBuildParts {
    179         recipient_pubkey: &payload.buyer_pubkey,
    180         message_type: RadrootsOrderEventType::OrderRevisionProposed,
    181         listing_addr: &payload.listing_addr,
    182         order_id: &payload.order_id,
    183         listing_event: None,
    184         root_event_id: Some(root_event_id),
    185         prev_event_id: Some(prev_event_id),
    186         payload,
    187     })
    188 }
    189 
    190 #[cfg(feature = "serde_json")]
    191 pub fn order_revision_decision_event_build(
    192     root_event_id: &RadrootsEventId,
    193     prev_event_id: &RadrootsEventId,
    194     payload: &RadrootsOrderRevisionDecision,
    195 ) -> Result<WireEventParts, EventEncodeError> {
    196     payload.validate().map_err(map_order_payload_error)?;
    197     if payload.root_event_id.as_str() != root_event_id.as_str() {
    198         return Err(EventEncodeError::InvalidField("root_event_id"));
    199     }
    200     if payload.prev_event_id.as_str() != prev_event_id.as_str() {
    201         return Err(EventEncodeError::InvalidField("prev_event_id"));
    202     }
    203     order_envelope_event_build(OrderEnvelopeEventBuildParts {
    204         recipient_pubkey: &payload.seller_pubkey,
    205         message_type: RadrootsOrderEventType::OrderRevisionDecision,
    206         listing_addr: &payload.listing_addr,
    207         order_id: &payload.order_id,
    208         listing_event: None,
    209         root_event_id: Some(root_event_id),
    210         prev_event_id: Some(prev_event_id),
    211         payload,
    212     })
    213 }
    214 
    215 #[cfg(feature = "serde_json")]
    216 pub fn order_cancellation_event_build(
    217     root_event_id: &RadrootsEventId,
    218     prev_event_id: &RadrootsEventId,
    219     payload: &RadrootsOrderCancellation,
    220 ) -> Result<WireEventParts, EventEncodeError> {
    221     payload.validate().map_err(map_order_payload_error)?;
    222     order_envelope_event_build(OrderEnvelopeEventBuildParts {
    223         recipient_pubkey: &payload.seller_pubkey,
    224         message_type: RadrootsOrderEventType::OrderCancelled,
    225         listing_addr: &payload.listing_addr,
    226         order_id: &payload.order_id,
    227         listing_event: None,
    228         root_event_id: Some(root_event_id),
    229         prev_event_id: Some(prev_event_id),
    230         payload,
    231     })
    232 }
    233 
    234 #[cfg(all(test, feature = "serde_json"))]
    235 mod tests {
    236     use super::{
    237         OrderEnvelopeEventBuildParts, map_order_envelope_error, map_order_payload_error,
    238         order_envelope_event_build,
    239     };
    240     use crate::error::EventEncodeError;
    241     use radroots_events::{
    242         RadrootsNostrEventPtr,
    243         ids::RadrootsEventId,
    244         order::{RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderPayloadError},
    245     };
    246 
    247     fn event_id(character: char) -> RadrootsEventId {
    248         core::iter::repeat_n(character, 64)
    249             .collect::<String>()
    250             .parse()
    251             .unwrap()
    252     }
    253 
    254     fn payload() -> serde_json::Value {
    255         serde_json::json!({})
    256     }
    257 
    258     #[test]
    259     fn order_encode_error_mappers_cover_envelope_and_payload_variants() {
    260         assert_empty_required(
    261             map_order_envelope_error(RadrootsOrderEnvelopeError::MissingOrderId),
    262             "order_id",
    263         );
    264         assert_empty_required(
    265             map_order_envelope_error(RadrootsOrderEnvelopeError::MissingListingAddr),
    266             "listing_addr",
    267         );
    268         assert_invalid_field(
    269             map_order_envelope_error(RadrootsOrderEnvelopeError::InvalidVersion {
    270                 expected: 1,
    271                 got: 2,
    272             }),
    273             "version",
    274         );
    275         assert_empty_required(
    276             map_order_payload_error(RadrootsOrderPayloadError::EmptyField("buyer_pubkey")),
    277             "buyer_pubkey",
    278         );
    279         assert_empty_required(
    280             map_order_payload_error(RadrootsOrderPayloadError::MissingItems),
    281             "items",
    282         );
    283         assert_invalid_field(
    284             map_order_payload_error(RadrootsOrderPayloadError::InvalidItemBinCount { index: 0 }),
    285             "items.bin_count",
    286         );
    287         assert_empty_required(
    288             map_order_payload_error(RadrootsOrderPayloadError::MissingEconomicItems),
    289             "economics.items",
    290         );
    291         assert_invalid_field(
    292             map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemBinCount {
    293                 index: 0,
    294             }),
    295             "economics.items.bin_count",
    296         );
    297         assert_invalid_field(
    298             map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemQuantity {
    299                 index: 0,
    300             }),
    301             "economics.items.quantity_amount",
    302         );
    303         assert_invalid_field(
    304             map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemPrice {
    305                 index: 0,
    306             }),
    307             "economics.items.unit_price_amount",
    308         );
    309         assert_invalid_field(
    310             map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemSubtotal {
    311                 index: 0,
    312             }),
    313             "economics.items.line_subtotal",
    314         );
    315         for error in [
    316             RadrootsOrderPayloadError::InvalidEconomicLineAmount {
    317                 field: "adjustments.amount",
    318                 index: 0,
    319             },
    320             RadrootsOrderPayloadError::InvalidEconomicLineKind {
    321                 field: "discounts.kind",
    322                 index: 0,
    323             },
    324             RadrootsOrderPayloadError::InvalidEconomicLineEffect {
    325                 field: "discounts.effect",
    326                 index: 0,
    327             },
    328             RadrootsOrderPayloadError::InvalidEconomicCurrency {
    329                 field: "subtotal.currency",
    330             },
    331             RadrootsOrderPayloadError::InvalidEconomicOrdering {
    332                 field: "adjustments",
    333             },
    334             RadrootsOrderPayloadError::InvalidEconomicTotal { field: "total" },
    335             RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { field: "items" },
    336         ] {
    337             assert!(matches!(
    338                 map_order_payload_error(error),
    339                 EventEncodeError::InvalidField(_)
    340             ));
    341         }
    342         assert_invalid_field(
    343             map_order_payload_error(RadrootsOrderPayloadError::InvalidQuoteVersion),
    344             "economics.quote_version",
    345         );
    346         assert_empty_required(
    347             map_order_payload_error(RadrootsOrderPayloadError::MissingInventoryCommitments),
    348             "inventory_commitments",
    349         );
    350         assert_invalid_field(
    351             map_order_payload_error(RadrootsOrderPayloadError::InvalidInventoryCommitmentCount {
    352                 index: 0,
    353             }),
    354             "inventory_commitments.bin_count",
    355         );
    356     }
    357 
    358     #[test]
    359     fn order_envelope_event_build_requires_context_tags_by_message_type() {
    360         let payload = payload();
    361         let root_event_id = event_id('1');
    362         let prev_event_id = event_id('2');
    363 
    364         let missing_listing_event = order_envelope_event_build(OrderEnvelopeEventBuildParts {
    365             recipient_pubkey: "recipient",
    366             message_type: RadrootsOrderEventType::OrderRequested,
    367             listing_addr: "listing-address",
    368             order_id: "order-1",
    369             listing_event: None,
    370             root_event_id: None,
    371             prev_event_id: None,
    372             payload: &payload,
    373         })
    374         .unwrap_err();
    375         assert_empty_required(missing_listing_event, "listing_event.id");
    376 
    377         let missing_root = order_envelope_event_build(OrderEnvelopeEventBuildParts {
    378             recipient_pubkey: "recipient",
    379             message_type: RadrootsOrderEventType::OrderDecision,
    380             listing_addr: "listing-address",
    381             order_id: "order-1",
    382             listing_event: None,
    383             root_event_id: None,
    384             prev_event_id: Some(&prev_event_id),
    385             payload: &payload,
    386         })
    387         .unwrap_err();
    388         assert_empty_required(missing_root, "root_event_id");
    389 
    390         let missing_prev = order_envelope_event_build(OrderEnvelopeEventBuildParts {
    391             recipient_pubkey: "recipient",
    392             message_type: RadrootsOrderEventType::OrderDecision,
    393             listing_addr: "listing-address",
    394             order_id: "order-1",
    395             listing_event: None,
    396             root_event_id: Some(&root_event_id),
    397             prev_event_id: None,
    398             payload: &payload,
    399         })
    400         .unwrap_err();
    401         assert_empty_required(missing_prev, "prev_event_id");
    402 
    403         let invalid_listing_event = order_envelope_event_build(OrderEnvelopeEventBuildParts {
    404             recipient_pubkey: "recipient",
    405             message_type: RadrootsOrderEventType::OrderRequested,
    406             listing_addr: "listing-address",
    407             order_id: "order-1",
    408             listing_event: Some(&RadrootsNostrEventPtr {
    409                 id: String::new(),
    410                 relays: None,
    411             }),
    412             root_event_id: None,
    413             prev_event_id: None,
    414             payload: &payload,
    415         })
    416         .unwrap_err();
    417         assert_empty_required(invalid_listing_event, "listing_event.id");
    418     }
    419 
    420     fn assert_empty_required(error: EventEncodeError, field: &'static str) {
    421         match error {
    422             EventEncodeError::EmptyRequiredField(found) => assert_eq!(found, field),
    423             other => panic!("unexpected error: {other:?}"),
    424         }
    425     }
    426 
    427     fn assert_invalid_field(error: EventEncodeError, field: &'static str) {
    428         match error {
    429             EventEncodeError::InvalidField(found) => assert_eq!(found, field),
    430             other => panic!("unexpected error: {other:?}"),
    431         }
    432     }
    433 }