Skip to main content

hypercall_db_diesel/
engine_enums.rs

1//! Postgres enum types for the engine journal schema.
2//!
3//! Provides Diesel 2.x `ToSql`/`FromSql` impls for:
4//! - `CommandType` ↔ `engine_command_type` enum
5//! - `EventType`   ↔ `engine_event_type` enum
6
7use diesel::deserialize::{self, FromSql};
8use diesel::pg::{Pg, PgValue};
9use diesel::serialize::{self, IsNull, Output, ToSql};
10use diesel::sql_types::Uuid as DieselUuid;
11use diesel::{AsExpression, FromSqlRow, QueryId, SqlType};
12use serde::{Deserialize, Serialize};
13use std::io::Write;
14
15// ---------------------------------------------------------------------------
16// SQL type markers for Postgres enums
17// ---------------------------------------------------------------------------
18// TODO: Replace these hand-rolled ToSql/FromSql impls with a Postgres enum
19// migration + Diesel's built-in enum support. The current approach duplicates
20// every variant as a string match; a proper `CREATE TYPE ... AS ENUM` column
21// would let Diesel derive the mapping and catch variant drift at compile time.
22// ---------------------------------------------------------------------------
23
24/// SQL type marker for the `engine_command_type` Postgres enum.
25#[derive(SqlType, QueryId, Debug)]
26#[diesel(postgres_type(name = "engine_command_type"))]
27pub struct EngineCommandTypeSql;
28
29/// SQL type marker for the `engine_event_type` Postgres enum.
30#[derive(SqlType, QueryId, Debug)]
31#[diesel(postgres_type(name = "engine_event_type"))]
32pub struct EngineEventTypeSql;
33
34// ---------------------------------------------------------------------------
35// CommandType
36// ---------------------------------------------------------------------------
37
38/// Rust-side mirror of the `engine_command_type` Postgres enum.
39#[derive(
40    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, AsExpression, FromSqlRow,
41)]
42#[diesel(sql_type = EngineCommandTypeSql)]
43pub enum CommandType {
44    CreateOrder,
45    CancelOrder,
46    ReplaceOrder,
47    CreateMarket,
48    DeleteMarket,
49    ExpireMarket,
50    LiquidationState,
51    TickExpiry,
52    TickSnapshot,
53    RfqExecute,
54    PriceUpdate,
55    IvUpdate,
56    TierUpdate,
57    HypercorePositionUpdate,
58    MmpConfigUpdate,
59    TradingModeUpdate,
60    DepositUpdate,
61    LiquidationBonusUpdate,
62    ApproveAgent,
63    RevokeAgent,
64    NonceAdvance,
65    HypercoreEquityUpdate,
66    OptionDepositUpdate,
67    OptionWithdrawalUpdate,
68    CashWithdrawalUpdate,
69    SetPmSettlementPoolConfig,
70    RecordPmVaultDeposit,
71    RequestPmVaultWithdrawal,
72    AccruePmSettlementInterest,
73    ApplyPmSettlementRepayment,
74    JournalPmRecoveryPlan,
75    MarkPmRecoveryActionSubmitted,
76    ResolvePmRecoveryAction,
77}
78
79impl CommandType {
80    /// Derive from a command_type string (as stored in the Text column).
81    pub fn from_str(s: &str) -> Option<Self> {
82        match s {
83            "CreateOrder" => Some(CommandType::CreateOrder),
84            "CancelOrder" => Some(CommandType::CancelOrder),
85            "ReplaceOrder" => Some(CommandType::ReplaceOrder),
86            "CreateMarket" => Some(CommandType::CreateMarket),
87            "DeleteMarket" => Some(CommandType::DeleteMarket),
88            "ExpireMarket" => Some(CommandType::ExpireMarket),
89            "LiquidationState" => Some(CommandType::LiquidationState),
90            "TickExpiry" => Some(CommandType::TickExpiry),
91            "TickSnapshot" => Some(CommandType::TickSnapshot),
92            "RfqExecute" => Some(CommandType::RfqExecute),
93            "PriceUpdate" => Some(CommandType::PriceUpdate),
94            "IvUpdate" => Some(CommandType::IvUpdate),
95            "TierUpdate" => Some(CommandType::TierUpdate),
96            "HypercorePositionUpdate" => Some(CommandType::HypercorePositionUpdate),
97            "MmpConfigUpdate" => Some(CommandType::MmpConfigUpdate),
98            "TradingModeUpdate" => Some(CommandType::TradingModeUpdate),
99            "DepositUpdate" => Some(CommandType::DepositUpdate),
100            "LiquidationBonusUpdate" => Some(CommandType::LiquidationBonusUpdate),
101            "ApproveAgent" => Some(CommandType::ApproveAgent),
102            "RevokeAgent" => Some(CommandType::RevokeAgent),
103            "NonceAdvance" => Some(CommandType::NonceAdvance),
104            "HypercoreEquityUpdate" => Some(CommandType::HypercoreEquityUpdate),
105            "OptionDepositUpdate" => Some(CommandType::OptionDepositUpdate),
106            "OptionWithdrawalUpdate" => Some(CommandType::OptionWithdrawalUpdate),
107            "CashWithdrawalUpdate" => Some(CommandType::CashWithdrawalUpdate),
108            "SetPmSettlementPoolConfig" => Some(CommandType::SetPmSettlementPoolConfig),
109            "RecordPmVaultDeposit" => Some(CommandType::RecordPmVaultDeposit),
110            "RequestPmVaultWithdrawal" => Some(CommandType::RequestPmVaultWithdrawal),
111            "AccruePmSettlementInterest" => Some(CommandType::AccruePmSettlementInterest),
112            "ApplyPmSettlementRepayment" => Some(CommandType::ApplyPmSettlementRepayment),
113            "JournalPmRecoveryPlan" => Some(CommandType::JournalPmRecoveryPlan),
114            "MarkPmRecoveryActionSubmitted" => Some(CommandType::MarkPmRecoveryActionSubmitted),
115            "ResolvePmRecoveryAction" => Some(CommandType::ResolvePmRecoveryAction),
116            _ => None,
117        }
118    }
119
120    /// Return the string representation stored in Postgres.
121    pub fn as_str(&self) -> &'static str {
122        match self {
123            CommandType::CreateOrder => "CreateOrder",
124            CommandType::CancelOrder => "CancelOrder",
125            CommandType::ReplaceOrder => "ReplaceOrder",
126            CommandType::CreateMarket => "CreateMarket",
127            CommandType::DeleteMarket => "DeleteMarket",
128            CommandType::ExpireMarket => "ExpireMarket",
129            CommandType::LiquidationState => "LiquidationState",
130            CommandType::TickExpiry => "TickExpiry",
131            CommandType::TickSnapshot => "TickSnapshot",
132            CommandType::RfqExecute => "RfqExecute",
133            CommandType::PriceUpdate => "PriceUpdate",
134            CommandType::IvUpdate => "IvUpdate",
135            CommandType::TierUpdate => "TierUpdate",
136            CommandType::HypercorePositionUpdate => "HypercorePositionUpdate",
137            CommandType::MmpConfigUpdate => "MmpConfigUpdate",
138            CommandType::TradingModeUpdate => "TradingModeUpdate",
139            CommandType::DepositUpdate => "DepositUpdate",
140            CommandType::LiquidationBonusUpdate => "LiquidationBonusUpdate",
141            CommandType::ApproveAgent => "ApproveAgent",
142            CommandType::RevokeAgent => "RevokeAgent",
143            CommandType::NonceAdvance => "NonceAdvance",
144            CommandType::HypercoreEquityUpdate => "HypercoreEquityUpdate",
145            CommandType::OptionDepositUpdate => "OptionDepositUpdate",
146            CommandType::OptionWithdrawalUpdate => "OptionWithdrawalUpdate",
147            CommandType::CashWithdrawalUpdate => "CashWithdrawalUpdate",
148            CommandType::SetPmSettlementPoolConfig => "SetPmSettlementPoolConfig",
149            CommandType::RecordPmVaultDeposit => "RecordPmVaultDeposit",
150            CommandType::RequestPmVaultWithdrawal => "RequestPmVaultWithdrawal",
151            CommandType::AccruePmSettlementInterest => "AccruePmSettlementInterest",
152            CommandType::ApplyPmSettlementRepayment => "ApplyPmSettlementRepayment",
153            CommandType::JournalPmRecoveryPlan => "JournalPmRecoveryPlan",
154            CommandType::MarkPmRecoveryActionSubmitted => "MarkPmRecoveryActionSubmitted",
155            CommandType::ResolvePmRecoveryAction => "ResolvePmRecoveryAction",
156        }
157    }
158}
159
160impl std::fmt::Display for CommandType {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        f.write_str(self.as_str())
163    }
164}
165
166impl ToSql<EngineCommandTypeSql, Pg> for CommandType {
167    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
168        out.write_all(self.as_str().as_bytes())?;
169        Ok(IsNull::No)
170    }
171}
172
173impl FromSql<EngineCommandTypeSql, Pg> for CommandType {
174    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
175        let s = std::str::from_utf8(bytes.as_bytes())
176            .expect("engine_command_type column contains invalid UTF-8");
177        Ok(CommandType::from_str(s).unwrap_or_else(|| {
178            panic!(
179                "Persisted engine_command_type '{}' is not a known variant. \
180                 This indicates data corruption or a missing enum variant.",
181                s
182            )
183        }))
184    }
185}
186
187// ---------------------------------------------------------------------------
188// EventType
189// ---------------------------------------------------------------------------
190
191/// Rust-side mirror of the `engine_event_type` Postgres enum.
192#[derive(
193    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, AsExpression, FromSqlRow,
194)]
195#[diesel(sql_type = EngineEventTypeSql)]
196pub enum EventType {
197    OrderAction,
198    OrderUpdate,
199    OrderInfo,
200    MarketAction,
201    MarketUpdate,
202    OrderFilled,
203    OrderbookUpdated,
204    L2Update,
205    Trade,
206    TransactionRequest,
207    TransactionUpdate,
208    MmpTriggered,
209    PositionExpired,
210    TierUpdate,
211    HypercorePositionUpdate,
212    LiquidationStateChange,
213    RfqFilled,
214}
215
216impl EventType {
217    /// Derive from an event_type string (as stored in the Text column).
218    pub fn from_str(s: &str) -> Option<Self> {
219        match s {
220            "OrderAction" => Some(EventType::OrderAction),
221            "OrderUpdate" => Some(EventType::OrderUpdate),
222            "OrderInfo" => Some(EventType::OrderInfo),
223            "MarketAction" => Some(EventType::MarketAction),
224            "MarketUpdate" => Some(EventType::MarketUpdate),
225            "OrderFilled" => Some(EventType::OrderFilled),
226            "OrderbookUpdated" => Some(EventType::OrderbookUpdated),
227            "L2Update" => Some(EventType::L2Update),
228            "Trade" => Some(EventType::Trade),
229            "TransactionRequest" => Some(EventType::TransactionRequest),
230            "TransactionUpdate" => Some(EventType::TransactionUpdate),
231            "MmpTriggered" => Some(EventType::MmpTriggered),
232            "PositionExpired" => Some(EventType::PositionExpired),
233            "TierUpdate" => Some(EventType::TierUpdate),
234            "HypercorePositionUpdate" => Some(EventType::HypercorePositionUpdate),
235            "LiquidationStateChange" => Some(EventType::LiquidationStateChange),
236            "RfqFilled" => Some(EventType::RfqFilled),
237            _ => None,
238        }
239    }
240
241    /// Return the string representation stored in Postgres.
242    pub fn as_str(&self) -> &'static str {
243        match self {
244            EventType::OrderAction => "OrderAction",
245            EventType::OrderUpdate => "OrderUpdate",
246            EventType::OrderInfo => "OrderInfo",
247            EventType::MarketAction => "MarketAction",
248            EventType::MarketUpdate => "MarketUpdate",
249            EventType::OrderFilled => "OrderFilled",
250            EventType::OrderbookUpdated => "OrderbookUpdated",
251            EventType::L2Update => "L2Update",
252            EventType::Trade => "Trade",
253            EventType::TransactionRequest => "TransactionRequest",
254            EventType::TransactionUpdate => "TransactionUpdate",
255            EventType::MmpTriggered => "MmpTriggered",
256            EventType::PositionExpired => "PositionExpired",
257            EventType::TierUpdate => "TierUpdate",
258            EventType::HypercorePositionUpdate => "HypercorePositionUpdate",
259            EventType::LiquidationStateChange => "LiquidationStateChange",
260            EventType::RfqFilled => "RfqFilled",
261        }
262    }
263}
264
265impl std::fmt::Display for EventType {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        f.write_str(self.as_str())
268    }
269}
270
271impl From<EventType> for hypercall_db::EventType {
272    fn from(e: EventType) -> Self {
273        e.as_str().parse().expect("EventType variant mismatch")
274    }
275}
276
277impl ToSql<EngineEventTypeSql, Pg> for EventType {
278    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
279        out.write_all(self.as_str().as_bytes())?;
280        Ok(IsNull::No)
281    }
282}
283
284impl FromSql<EngineEventTypeSql, Pg> for EventType {
285    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
286        let s = std::str::from_utf8(bytes.as_bytes())
287            .expect("engine_event_type column contains invalid UTF-8");
288        Ok(EventType::from_str(s).unwrap_or_else(|| {
289            panic!(
290                "Persisted engine_event_type '{}' is not a known variant. \
291                 This indicates data corruption or a missing enum variant.",
292                s
293            )
294        }))
295    }
296}
297
298// ---------------------------------------------------------------------------
299// DbUuid — newtype wrapper for uuid::Uuid with Diesel traits
300// ---------------------------------------------------------------------------
301// Diesel 2.2's `uuid` feature provides ToSql/FromSql for bare uuid::Uuid,
302// but we need a newtype for AsExpression/FromSqlRow derives (orphan rule +
303// Nullable<Uuid> column support). Manual ToSql/FromSql bridge the newtype.
304
305/// Newtype wrapper for `uuid::Uuid` with Diesel `ToSql`/`FromSql` impls.
306/// Needed because orphan rules prevent deriving `AsExpression`/`FromSqlRow`
307/// on the bare `uuid::Uuid` type.
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, AsExpression, FromSqlRow)]
309#[diesel(sql_type = DieselUuid)]
310pub struct DbUuid(pub uuid::Uuid);
311
312impl From<uuid::Uuid> for DbUuid {
313    fn from(u: uuid::Uuid) -> Self {
314        DbUuid(u)
315    }
316}
317
318impl From<DbUuid> for uuid::Uuid {
319    fn from(u: DbUuid) -> Self {
320        u.0
321    }
322}
323
324impl std::fmt::Display for DbUuid {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        self.0.fmt(f)
327    }
328}
329
330impl Serialize for DbUuid {
331    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
332        self.0.serialize(serializer)
333    }
334}
335
336impl<'de> Deserialize<'de> for DbUuid {
337    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
338        uuid::Uuid::deserialize(deserializer).map(DbUuid)
339    }
340}
341
342impl ToSql<DieselUuid, Pg> for DbUuid {
343    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
344        out.write_all(self.0.as_bytes())?;
345        Ok(IsNull::No)
346    }
347}
348
349impl FromSql<DieselUuid, Pg> for DbUuid {
350    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
351        let raw = bytes.as_bytes();
352        assert!(
353            raw.len() == 16,
354            "Persisted UUID has {} bytes, expected 16. Data corruption suspected.",
355            raw.len()
356        );
357        Ok(DbUuid(uuid::Uuid::from_slice(raw).unwrap_or_else(|e| {
358            panic!("Persisted UUID bytes are invalid: {}", e)
359        })))
360    }
361}
362
363// ---------------------------------------------------------------------------
364// DirectiveActionKey
365// ---------------------------------------------------------------------------
366
367/// SQL type marker for the `directive_action_key` Postgres enum.
368#[derive(SqlType, QueryId, Debug)]
369#[diesel(postgres_type(name = "directive_action_key"))]
370pub struct DirectiveActionKeySql;
371
372/// Rust-side mirror of the `directive_action_key` Postgres enum.
373///
374/// This is a thin Diesel adapter around `hypercall_types::directives::ActionKey`.
375/// Use `From` conversions to go between the two.
376#[derive(
377    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, AsExpression, FromSqlRow,
378)]
379#[diesel(sql_type = DirectiveActionKeySql)]
380pub enum DirectiveActionKeyDb {
381    HlLimitOrder,
382    HlCancelByOid,
383    HlCancelByCloid,
384    HcUpdateApiWallet,
385    HlSendAsset,
386    HcTransferOption,
387    RsmHlLimitOrder,
388    RsmHlCancelByOid,
389    RsmHlCancelByCloid,
390    RsmHlSendAsset,
391    SystemCreditToken,
392    SystemCreditOption,
393    SystemStartLiquidation,
394    SystemStopLiquidation,
395    SystemWithdrawToken,
396}
397
398impl DirectiveActionKeyDb {
399    /// Return the string representation stored in Postgres.
400    pub fn as_str(&self) -> &'static str {
401        hypercall_types::directives::ActionKey::from(*self).as_str()
402    }
403
404    /// Parse from the string stored in Postgres.
405    pub fn from_str(s: &str) -> Option<Self> {
406        hypercall_types::directives::ActionKey::parse(s)
407            .ok()
408            .map(Self::from)
409    }
410}
411
412impl std::fmt::Display for DirectiveActionKeyDb {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        f.write_str(self.as_str())
415    }
416}
417
418impl From<hypercall_types::directives::ActionKey> for DirectiveActionKeyDb {
419    fn from(key: hypercall_types::directives::ActionKey) -> Self {
420        use hypercall_types::directives::ActionKey as AK;
421        match key {
422            AK::HlLimitOrder => Self::HlLimitOrder,
423            AK::HlCancelByOid => Self::HlCancelByOid,
424            AK::HlCancelByCloid => Self::HlCancelByCloid,
425            AK::HcUpdateApiWallet => Self::HcUpdateApiWallet,
426            AK::HlSendAsset => Self::HlSendAsset,
427            AK::HcTransferOption => Self::HcTransferOption,
428            AK::RsmHlLimitOrder => Self::RsmHlLimitOrder,
429            AK::RsmHlCancelByOid => Self::RsmHlCancelByOid,
430            AK::RsmHlCancelByCloid => Self::RsmHlCancelByCloid,
431            AK::RsmHlSendAsset => Self::RsmHlSendAsset,
432            AK::SystemCreditToken => Self::SystemCreditToken,
433            AK::SystemCreditOption => Self::SystemCreditOption,
434            AK::SystemStartLiquidation => Self::SystemStartLiquidation,
435            AK::SystemStopLiquidation => Self::SystemStopLiquidation,
436            AK::SystemWithdrawToken => Self::SystemWithdrawToken,
437            // non_exhaustive: future variants are mapped via as_str() round-trip
438            _ => {
439                panic!(
440                    "Unknown ActionKey variant: {}. \
441                     Add it to DirectiveActionKeyDb.",
442                    key.as_str()
443                )
444            }
445        }
446    }
447}
448
449impl From<DirectiveActionKeyDb> for hypercall_types::directives::ActionKey {
450    fn from(key: DirectiveActionKeyDb) -> Self {
451        use DirectiveActionKeyDb as DB;
452        match key {
453            DB::HlLimitOrder => Self::HlLimitOrder,
454            DB::HlCancelByOid => Self::HlCancelByOid,
455            DB::HlCancelByCloid => Self::HlCancelByCloid,
456            DB::HcUpdateApiWallet => Self::HcUpdateApiWallet,
457            DB::HlSendAsset => Self::HlSendAsset,
458            DB::HcTransferOption => Self::HcTransferOption,
459            DB::RsmHlLimitOrder => Self::RsmHlLimitOrder,
460            DB::RsmHlCancelByOid => Self::RsmHlCancelByOid,
461            DB::RsmHlCancelByCloid => Self::RsmHlCancelByCloid,
462            DB::RsmHlSendAsset => Self::RsmHlSendAsset,
463            DB::SystemCreditToken => Self::SystemCreditToken,
464            DB::SystemCreditOption => Self::SystemCreditOption,
465            DB::SystemStartLiquidation => Self::SystemStartLiquidation,
466            DB::SystemStopLiquidation => Self::SystemStopLiquidation,
467            DB::SystemWithdrawToken => Self::SystemWithdrawToken,
468        }
469    }
470}
471
472impl ToSql<DirectiveActionKeySql, Pg> for DirectiveActionKeyDb {
473    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
474        out.write_all(self.as_str().as_bytes())?;
475        Ok(IsNull::No)
476    }
477}
478
479impl FromSql<DirectiveActionKeySql, Pg> for DirectiveActionKeyDb {
480    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
481        let s = std::str::from_utf8(bytes.as_bytes())
482            .expect("directive_action_key column contains invalid UTF-8");
483        Ok(DirectiveActionKeyDb::from_str(s).unwrap_or_else(|| {
484            panic!(
485                "Persisted directive_action_key '{}' is not a known variant. \
486                 This indicates data corruption or a missing enum variant.",
487                s
488            )
489        }))
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn test_command_type_roundtrip() {
499        for ct in &[
500            CommandType::CreateOrder,
501            CommandType::CancelOrder,
502            CommandType::ReplaceOrder,
503            CommandType::CreateMarket,
504            CommandType::DeleteMarket,
505            CommandType::ExpireMarket,
506            CommandType::LiquidationState,
507            CommandType::TickExpiry,
508            CommandType::TickSnapshot,
509            CommandType::RfqExecute,
510            CommandType::PriceUpdate,
511            CommandType::IvUpdate,
512            CommandType::TierUpdate,
513            CommandType::HypercorePositionUpdate,
514            CommandType::MmpConfigUpdate,
515            CommandType::TradingModeUpdate,
516            CommandType::DepositUpdate,
517            CommandType::LiquidationBonusUpdate,
518            CommandType::ApproveAgent,
519            CommandType::RevokeAgent,
520            CommandType::NonceAdvance,
521            CommandType::HypercoreEquityUpdate,
522            CommandType::OptionDepositUpdate,
523            CommandType::OptionWithdrawalUpdate,
524            CommandType::CashWithdrawalUpdate,
525            CommandType::SetPmSettlementPoolConfig,
526            CommandType::RecordPmVaultDeposit,
527            CommandType::RequestPmVaultWithdrawal,
528            CommandType::AccruePmSettlementInterest,
529            CommandType::ApplyPmSettlementRepayment,
530            CommandType::JournalPmRecoveryPlan,
531            CommandType::MarkPmRecoveryActionSubmitted,
532            CommandType::ResolvePmRecoveryAction,
533        ] {
534            let s = ct.as_str();
535            let parsed = CommandType::from_str(s).unwrap();
536            assert_eq!(*ct, parsed, "roundtrip failed for {}", s);
537        }
538    }
539
540    #[test]
541    fn test_event_type_roundtrip() {
542        for et in &[
543            EventType::OrderAction,
544            EventType::OrderUpdate,
545            EventType::OrderInfo,
546            EventType::MarketAction,
547            EventType::MarketUpdate,
548            EventType::OrderFilled,
549            EventType::OrderbookUpdated,
550            EventType::L2Update,
551            EventType::Trade,
552            EventType::TransactionRequest,
553            EventType::TransactionUpdate,
554            EventType::MmpTriggered,
555            EventType::PositionExpired,
556            EventType::TierUpdate,
557            EventType::HypercorePositionUpdate,
558            EventType::LiquidationStateChange,
559            EventType::RfqFilled,
560        ] {
561            let s = et.as_str();
562            let parsed = EventType::from_str(s).unwrap();
563            assert_eq!(*et, parsed, "roundtrip failed for {}", s);
564        }
565    }
566
567    #[test]
568    fn test_command_type_from_str_unknown() {
569        assert_eq!(CommandType::from_str("Unknown"), None);
570    }
571
572    #[test]
573    fn test_event_type_from_str_unknown() {
574        assert_eq!(EventType::from_str("Unknown"), None);
575    }
576
577    #[test]
578    fn test_directive_action_key_db_roundtrip() {
579        use hypercall_types::directives::ActionKey;
580
581        let all_variants = [
582            DirectiveActionKeyDb::HlLimitOrder,
583            DirectiveActionKeyDb::HlCancelByOid,
584            DirectiveActionKeyDb::HlCancelByCloid,
585            DirectiveActionKeyDb::HcUpdateApiWallet,
586            DirectiveActionKeyDb::HlSendAsset,
587            DirectiveActionKeyDb::HcTransferOption,
588            DirectiveActionKeyDb::RsmHlLimitOrder,
589            DirectiveActionKeyDb::RsmHlCancelByOid,
590            DirectiveActionKeyDb::RsmHlCancelByCloid,
591            DirectiveActionKeyDb::RsmHlSendAsset,
592            DirectiveActionKeyDb::SystemCreditToken,
593            DirectiveActionKeyDb::SystemCreditOption,
594            DirectiveActionKeyDb::SystemStartLiquidation,
595            DirectiveActionKeyDb::SystemStopLiquidation,
596            DirectiveActionKeyDb::SystemWithdrawToken,
597        ];
598
599        for db_key in all_variants {
600            let s = db_key.as_str();
601            let parsed = DirectiveActionKeyDb::from_str(s).unwrap();
602            assert_eq!(db_key, parsed, "roundtrip failed for {}", s);
603
604            // Verify ActionKey <-> DirectiveActionKeyDb conversion
605            let domain_key: ActionKey = db_key.into();
606            let back: DirectiveActionKeyDb = domain_key.into();
607            assert_eq!(db_key, back, "domain roundtrip failed for {}", s);
608        }
609    }
610
611    #[test]
612    fn test_directive_action_key_db_from_str_unknown() {
613        assert_eq!(DirectiveActionKeyDb::from_str("not_real"), None);
614    }
615}