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}