Skip to main content

hypercall_api/directives/
models.rs

1use crate::directives::json_types::{Bytes32Hex, JsonU128, JsonU32, JsonU64, JsonU8};
2use crate::error::ApiError;
3use alloy::sol_types::SolValue;
4use hypercall_types::directives::{
5    ActionKey, CancelOrderByCloid, CancelOrderByOid, LimitOrder, UpdateApiWallet,
6};
7use hypercall_types::WalletAddress;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct TypedDataRequest {
13    pub account: WalletAddress,
14    pub nonce: JsonU64,
15    pub action: sonic_rs::Value,
16}
17
18#[derive(Debug, Clone, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct SubmitRequest {
21    pub account: WalletAddress,
22    pub nonce: JsonU64,
23    pub action: sonic_rs::Value,
24    pub signature: String,
25}
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
28#[serde(deny_unknown_fields, rename_all = "camelCase")]
29pub struct HlLimitOrderAction {
30    pub asset: JsonU32,
31    pub is_buy: bool,
32    pub limit_px: JsonU64,
33    pub sz: JsonU64,
34    pub reduce_only: bool,
35    pub encoded_tif: JsonU8,
36    pub cloid: JsonU128,
37}
38
39#[derive(Debug, Clone, Deserialize, Serialize)]
40#[serde(deny_unknown_fields, rename_all = "camelCase")]
41pub struct HlCancelByOidAction {
42    pub asset: JsonU32,
43    pub oid: JsonU64,
44}
45
46#[derive(Debug, Clone, Deserialize, Serialize)]
47#[serde(deny_unknown_fields, rename_all = "camelCase")]
48pub struct HlCancelByCloidAction {
49    pub asset: JsonU32,
50    pub cloid: JsonU128,
51}
52
53#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(deny_unknown_fields, rename_all = "camelCase")]
55pub struct HcUpdateApiWalletAction {
56    pub name: Bytes32Hex,
57    pub addr: WalletAddress,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum EncodedTif {
62    Alo,
63    Gtc,
64    Ioc,
65}
66
67impl EncodedTif {
68    pub fn parse(value: u8) -> Result<Self, ApiError> {
69        match value {
70            1 => Ok(Self::Alo),
71            2 => Ok(Self::Gtc),
72            3 => Ok(Self::Ioc),
73            _ => Err(ApiError::invalid_field(
74                "action.encodedTif must be one of 1, 2, or 3",
75            )),
76        }
77    }
78}
79
80impl HlLimitOrderAction {
81    pub fn encoded_tif_kind(&self) -> Result<EncodedTif, ApiError> {
82        EncodedTif::parse(self.encoded_tif.into_inner())
83    }
84
85    pub fn validate(&self) -> Result<(), ApiError> {
86        self.encoded_tif_kind().map(|_| ())
87    }
88}
89
90impl From<&HlLimitOrderAction> for LimitOrder {
91    fn from(value: &HlLimitOrderAction) -> Self {
92        Self {
93            asset: value.asset.into_inner(),
94            isBuy: value.is_buy,
95            limitPx: value.limit_px.into_inner(),
96            sz: value.sz.into_inner(),
97            reduceOnly: value.reduce_only,
98            encodedTif: value.encoded_tif.into_inner(),
99            cloid: value.cloid.into_inner(),
100        }
101    }
102}
103
104impl From<&HlCancelByOidAction> for CancelOrderByOid {
105    fn from(value: &HlCancelByOidAction) -> Self {
106        Self {
107            asset: value.asset.into_inner(),
108            oid: value.oid.into_inner(),
109        }
110    }
111}
112
113impl From<&HlCancelByCloidAction> for CancelOrderByCloid {
114    fn from(value: &HlCancelByCloidAction) -> Self {
115        Self {
116            asset: value.asset.into_inner(),
117            cloid: value.cloid.into_inner(),
118        }
119    }
120}
121
122impl From<&HcUpdateApiWalletAction> for UpdateApiWallet {
123    fn from(value: &HcUpdateApiWalletAction) -> Self {
124        Self {
125            name: value.name.into_inner().into(),
126            addr: value.addr.inner(),
127        }
128    }
129}
130
131#[derive(Debug, Clone)]
132pub enum ParsedAction {
133    HlLimitOrder(HlLimitOrderAction),
134    HlCancelByOid(HlCancelByOidAction),
135    HlCancelByCloid(HlCancelByCloidAction),
136    HcUpdateApiWallet(HcUpdateApiWalletAction),
137}
138
139impl ParsedAction {
140    pub fn parse(action_key: ActionKey, value: sonic_rs::Value) -> Result<Self, ApiError> {
141        match action_key {
142            ActionKey::HlLimitOrder => {
143                let action: HlLimitOrderAction = sonic_rs::from_value(&value)
144                    .map_err(|e| ApiError::invalid_field(e.to_string()))?;
145                action.validate()?;
146                Ok(Self::HlLimitOrder(action))
147            }
148            ActionKey::HlCancelByOid => {
149                let action: HlCancelByOidAction = sonic_rs::from_value(&value)
150                    .map_err(|e| ApiError::invalid_field(e.to_string()))?;
151                Ok(Self::HlCancelByOid(action))
152            }
153            ActionKey::HlCancelByCloid => {
154                let action: HlCancelByCloidAction = sonic_rs::from_value(&value)
155                    .map_err(|e| ApiError::invalid_field(e.to_string()))?;
156                Ok(Self::HlCancelByCloid(action))
157            }
158            ActionKey::HcUpdateApiWallet => {
159                let action: HcUpdateApiWalletAction = sonic_rs::from_value(&value)
160                    .map_err(|e| ApiError::invalid_field(e.to_string()))?;
161                Ok(Self::HcUpdateApiWallet(action))
162            }
163            _ => Err(ApiError::unsupported_action(format!(
164                "Action '{}' is not supported in MVP",
165                action_key.as_str()
166            ))),
167        }
168    }
169
170    pub fn to_json_value(&self) -> sonic_rs::Value {
171        let value = match self {
172            Self::HlLimitOrder(action) => sonic_rs::to_value(action),
173            Self::HlCancelByOid(action) => sonic_rs::to_value(action),
174            Self::HlCancelByCloid(action) => sonic_rs::to_value(action),
175            Self::HcUpdateApiWallet(action) => sonic_rs::to_value(action),
176        };
177        value.expect("action serialization should always succeed")
178    }
179
180    pub fn encode_inner_abi(&self) -> Vec<u8> {
181        match self {
182            Self::HlLimitOrder(action) => LimitOrder::from(action).abi_encode(),
183            Self::HlCancelByOid(action) => CancelOrderByOid::from(action).abi_encode(),
184            Self::HlCancelByCloid(action) => CancelOrderByCloid::from(action).abi_encode(),
185            Self::HcUpdateApiWallet(action) => UpdateApiWallet::from(action).abi_encode(),
186        }
187    }
188}
189
190/// How far the directive got in the pipeline.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
192#[serde(rename_all = "snake_case")]
193pub enum DirectiveStage {
194    /// Parsed and signature recovered, but rejected by policy/auth.
195    Rejected,
196    /// Passed engine check, enqueued for async submission (no sync relayer).
197    Enqueued,
198    /// Submitted synchronously, tx_hash available.
199    Submitted,
200}
201
202/// A fill from HyperLiquid (best-effort, queried after submission).
203#[derive(Debug, Clone, Serialize)]
204#[serde(rename_all = "camelCase")]
205pub struct DirectiveFill {
206    pub coin: String,
207    pub side: String,
208    pub size: String,
209    pub price: String,
210    pub time: u64,
211}
212
213/// Single honest response for directive submission.
214/// Every field is populated with whatever we know. Nulls mean we don't know.
215#[derive(Debug, Clone, Serialize)]
216#[serde(rename_all = "camelCase")]
217pub struct SubmitResponse {
218    /// How far the directive got.
219    pub stage: DirectiveStage,
220    /// Unique directive identifier.
221    pub directive_id: String,
222    /// Which action was submitted.
223    pub action_key: String,
224    /// Account contract address.
225    pub account: WalletAddress,
226    /// Nonce used for the directive.
227    pub nonce: JsonU64,
228    /// Recovered signer address (null if signature recovery failed before this point).
229    pub recovered_signer: Option<WalletAddress>,
230    /// On-chain transaction hash (null if not submitted yet or async path used).
231    pub tx_hash: Option<String>,
232    /// Rejection reason (null if not rejected).
233    pub rejection: Option<DirectiveRejection>,
234    /// Best-effort fills from HyperLiquid. Null means we don't know, not "no fills."
235    pub fills: Option<Vec<DirectiveFill>>,
236}
237
238#[derive(Debug, Clone, Serialize)]
239#[serde(rename_all = "camelCase")]
240pub struct DirectiveRejection {
241    pub code: String,
242    pub message: String,
243}