Skip to main content

hypercall_signer/
action.rs

1use alloy::sol_types::SolValue;
2use hypercall_types::directives::{
3    ActionKey, CancelOrderByCloid, CancelOrderByOid, CreditOption, CreditToken, LimitOrder,
4    SendAsset, StartLiquidation, StopLiquidation, WithdrawToken,
5};
6use serde_json::{json, Value};
7
8use crate::RsmSignerError;
9
10#[derive(Debug)]
11pub struct ParsedRsmAction {
12    pub action_key: ActionKey,
13    pub action_json: Value,
14}
15
16pub fn decode_rsm_action(action: &[u8]) -> Result<ParsedRsmAction, RsmSignerError> {
17    if action.len() < 4 {
18        return Err(RsmSignerError::InvalidAction(format!(
19            "action bytes too short: expected at least 4 bytes, got {}",
20            action.len()
21        )));
22    }
23
24    let version = action[0];
25    let id = u32::from_be_bytes([0, action[1], action[2], action[3]]);
26    let data = &action[4..];
27    let action_key = ActionKey::from_rsm_action_parts(version, id).ok_or_else(|| {
28        RsmSignerError::UnsupportedAction(format!(
29            "unsupported RSM action version={} id={}",
30            version, id
31        ))
32    })?;
33
34    let action_json = match action_key {
35        ActionKey::RsmHlLimitOrder => {
36            let inner = LimitOrder::abi_decode(data).map_err(|error| {
37                RsmSignerError::InvalidAction(format!(
38                    "failed to decode LimitOrder payload: {}",
39                    error
40                ))
41            })?;
42            json!({
43                "asset": inner.asset.to_string(),
44                "isBuy": inner.isBuy,
45                "limitPx": inner.limitPx.to_string(),
46                "sz": inner.sz.to_string(),
47                "reduceOnly": inner.reduceOnly,
48                "encodedTif": inner.encodedTif.to_string(),
49                "cloid": inner.cloid.to_string(),
50            })
51        }
52        ActionKey::RsmHlCancelByOid => {
53            let inner = CancelOrderByOid::abi_decode(data).map_err(|error| {
54                RsmSignerError::InvalidAction(format!(
55                    "failed to decode CancelOrderByOid payload: {}",
56                    error
57                ))
58            })?;
59            json!({
60                "asset": inner.asset.to_string(),
61                "oid": inner.oid.to_string(),
62            })
63        }
64        ActionKey::RsmHlCancelByCloid => {
65            let inner = CancelOrderByCloid::abi_decode(data).map_err(|error| {
66                RsmSignerError::InvalidAction(format!(
67                    "failed to decode CancelOrderByCloid payload: {}",
68                    error
69                ))
70            })?;
71            json!({
72                "asset": inner.asset.to_string(),
73                "cloid": inner.cloid.to_string(),
74            })
75        }
76        ActionKey::RsmHlSendAsset => {
77            let inner = SendAsset::abi_decode(data).map_err(|error| {
78                RsmSignerError::InvalidAction(format!(
79                    "failed to decode SendAsset payload: {}",
80                    error
81                ))
82            })?;
83            json!({
84                "destination": inner.destination.to_string(),
85                "subAccount": inner.subAccount.to_string(),
86                "srcDex": inner.srcDex.to_string(),
87                "dstDex": inner.dstDex.to_string(),
88                "token": inner.token.to_string(),
89                "amountWei": inner.amountWei.to_string(),
90            })
91        }
92        ActionKey::SystemCreditToken => {
93            let inner = CreditToken::abi_decode(data).map_err(|error| {
94                RsmSignerError::InvalidAction(format!(
95                    "failed to decode CreditToken payload: {}",
96                    error
97                ))
98            })?;
99            json!({
100                "srcDex": inner.srcDex.to_string(),
101                "dstDex": inner.dstDex.to_string(),
102                "token": inner.token.to_string(),
103                "amountWei": inner.amountWei.to_string(),
104            })
105        }
106        ActionKey::SystemCreditOption => {
107            let inner = CreditOption::abi_decode(data).map_err(|error| {
108                RsmSignerError::InvalidAction(format!(
109                    "failed to decode CreditOption payload: {}",
110                    error
111                ))
112            })?;
113            json!({
114                "underlying": format!("0x{}", hex::encode(inner.underlying.as_slice())),
115                "expiry": inner.expiry.to_string(),
116                "strike": inner.strike.to_string(),
117                "isCall": inner.isCall,
118                "amountWei": inner.amountWei.to_string(),
119            })
120        }
121        ActionKey::SystemStartLiquidation => {
122            let inner = StartLiquidation::abi_decode(data).map_err(|error| {
123                RsmSignerError::InvalidAction(format!(
124                    "failed to decode StartLiquidation payload: {}",
125                    error
126                ))
127            })?;
128            json!({
129                "equity": inner.equity.to_string(),
130                "marginNeeded": inner.marginNeeded.to_string(),
131            })
132        }
133        ActionKey::SystemStopLiquidation => {
134            let inner = StopLiquidation::abi_decode(data).map_err(|error| {
135                RsmSignerError::InvalidAction(format!(
136                    "failed to decode StopLiquidation payload: {}",
137                    error
138                ))
139            })?;
140            json!({
141                "startTime": inner.startTime.to_string(),
142            })
143        }
144        ActionKey::SystemWithdrawToken => {
145            let inner = WithdrawToken::abi_decode(data).map_err(|error| {
146                RsmSignerError::InvalidAction(format!(
147                    "failed to decode WithdrawToken payload: {}",
148                    error
149                ))
150            })?;
151            json!({
152                "destination": inner.destination.to_string(),
153                "subAccount": inner.subAccount.to_string(),
154                "srcDex": inner.srcDex.to_string(),
155                "dstDex": inner.dstDex.to_string(),
156                "token": inner.token.to_string(),
157                "amountWei": inner.amountWei.to_string(),
158            })
159        }
160        other => {
161            return Err(RsmSignerError::UnsupportedAction(format!(
162                "unsupported RSM action key {}",
163                other.as_str()
164            )))
165        }
166    };
167
168    Ok(ParsedRsmAction {
169        action_key,
170        action_json,
171    })
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::encode_action_bytes;
178    use hypercall_types::directives::{
179        HL_ACTION_ID_LIMIT_ORDER, HL_ACTION_VERSION, SYSTEM_ACTION_ID_CREDIT_TOKEN,
180        SYSTEM_ACTION_VERSION,
181    };
182
183    #[test]
184    fn rejects_short_action_bytes() {
185        let err = decode_rsm_action(&[1, 2, 3]).unwrap_err().to_string();
186        assert!(err.contains("action bytes too short"));
187    }
188
189    #[test]
190    fn rejects_unknown_action_key() {
191        let err = decode_rsm_action(&[0xff, 0xff, 0xff, 0xff])
192            .unwrap_err()
193            .to_string();
194        assert!(err.contains("unsupported RSM action"));
195    }
196
197    #[test]
198    fn decodes_hl_limit_order_action() {
199        let action = encode_action_bytes(
200            HL_ACTION_VERSION,
201            HL_ACTION_ID_LIMIT_ORDER,
202            &LimitOrder {
203                asset: 7,
204                isBuy: true,
205                limitPx: 42_000,
206                sz: 10,
207                reduceOnly: false,
208                encodedTif: 1,
209                cloid: 99,
210            }
211            .abi_encode(),
212        )
213        .unwrap();
214
215        let parsed = decode_rsm_action(&action).unwrap();
216
217        assert_eq!(parsed.action_key, ActionKey::RsmHlLimitOrder);
218        assert_eq!(parsed.action_json["asset"], "7");
219        assert_eq!(parsed.action_json["isBuy"], true);
220        assert_eq!(parsed.action_json["cloid"], "99");
221    }
222
223    #[test]
224    fn decodes_system_credit_token_action() {
225        let action = encode_action_bytes(
226            SYSTEM_ACTION_VERSION,
227            SYSTEM_ACTION_ID_CREDIT_TOKEN,
228            &CreditToken {
229                srcDex: 1,
230                dstDex: 2,
231                token: 3,
232                amountWei: 4,
233            }
234            .abi_encode(),
235        )
236        .unwrap();
237
238        let parsed = decode_rsm_action(&action).unwrap();
239
240        assert_eq!(parsed.action_key, ActionKey::SystemCreditToken);
241        assert_eq!(parsed.action_json["srcDex"], "1");
242        assert_eq!(parsed.action_json["dstDex"], "2");
243        assert_eq!(parsed.action_json["amountWei"], "4");
244    }
245}