lib

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

encode.rs (14123B)


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