1use crate::msgs::conditional::{OnFill, Range, StopOrTP, Trailing, Trigger};
2use crate::msgs::liquidator::LiqConfig;
3use crate::msgs::multisig::{
4 CreateMultisig, MultisigApprove, MultisigCancel, MultisigExecute, MultisigPropose,
5 MultisigReject, UpdateMultisigPolicy,
6};
7use crate::msgs::risk::RiskConfigChange;
8use crate::msgs::subaccounts::{CreateSubAccount, RemoveSubAccount, RenameSubAccount, Transfer};
9use crate::msgs::{
10 AddMarket, AgentWalletCreation, ApproveCommissionFee, Beacon, CancelAll, CancelOrder, Faucet,
11 Join, LimitOrder, MarketOrder, Matrix, ModifyOrder, OpaqueAction, Price, PythOracle,
12 RevokeCommissionFee, UpdateUserSettings, UpdateValidatorSet, WhitelistFaucet,
13};
14use serde::ser::{SerializeTuple, Serializer};
15use serde::{Deserialize, Serialize};
16use solana_hash::Hash;
17use solana_keypair::Pubkey;
18
19#[derive(Clone, Copy, Debug, Default)]
21pub struct ActionMeta {
22 pub account: Pubkey,
23 pub nonce: u64,
24 pub seqno: u32,
25 pub hash: Option<Hash>,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub enum Action {
31 #[serde(rename = "m")]
33 MarketOrder(MarketOrder),
34 #[serde(rename = "l")]
36 LimitOrder(LimitOrder),
37 #[serde(rename = "mod")]
39 ModifyOrder(ModifyOrder),
40 #[serde(rename = "cx")]
42 Cancel(CancelOrder),
43 #[serde(rename = "cxa")]
45 CancelAll(CancelAll),
46 #[serde(rename = "st")]
48 Stop(StopOrTP),
49 #[serde(rename = "tp")]
51 TakeProfit(StopOrTP),
52 #[serde(rename = "rng")]
54 Range(Range),
55 #[serde(rename = "trig")]
57 Trigger(Trigger),
58 #[serde(rename = "trl")]
60 Trailing(Trailing),
61 #[serde(rename = "of")]
63 OnFill(OnFill),
64
65 #[serde(rename = "px")]
67 Price(Price),
68 #[serde(rename = "corrs")]
70 Corrs(Matrix),
71 #[serde(rename = "o")]
73 PythOracle(PythOracle),
74 #[serde(rename = "beacon")]
76 Beacon(Beacon),
77 #[serde(rename = "join")]
79 Join(Join),
80
81 Faucet(Faucet),
83 AgentWalletCreation(AgentWalletCreation),
85 UpdateUserSettings(UpdateUserSettings),
87
88 WhitelistFaucet(WhitelistFaucet),
90
91 AddMarket(AddMarket),
93 ConfigFairPrice(OpaqueAction),
95 ConfigVolatility(OpaqueAction),
97 ConfigSecurity(OpaqueAction),
99 ConfigRegime(OpaqueAction),
101 #[serde(alias = "configRiskMatrix")]
103 ConfigRisk(OpaqueAction),
104 #[serde(rename = "cfgf")]
106 ConfigFeePolicy(OpaqueAction),
107
108 CreateSubAccount(CreateSubAccount),
110 RemoveSubAccount(RemoveSubAccount),
112 Transfer(Transfer),
114 CreateMultisig(CreateMultisig),
116
117 #[serde(rename = "msp")]
119 MultisigPropose(MultisigPropose),
120 #[serde(rename = "msa")]
122 MultisigApprove(MultisigApprove),
123 #[serde(rename = "msr")]
125 MultisigReject(MultisigReject),
126 #[serde(rename = "msc")]
128 MultisigCancel(MultisigCancel),
129 #[serde(rename = "mse")]
131 MultisigExecute(MultisigExecute),
132 #[serde(rename = "msu")]
134 UpdateMultisigPolicy(UpdateMultisigPolicy),
135
136 #[serde(rename = "rsa", alias = "renameSubAccount")]
138 RenameSubAccount(RenameSubAccount),
139 #[serde(rename = "uvs")]
141 UpdateValidatorSet(UpdateValidatorSet),
142
143 #[serde(rename = "risk")]
145 UpdateRiskConfig(RiskConfigChange),
146 #[serde(rename = "abc", alias = "approveBuilderCode")]
148 ApproveCommissionFee(ApproveCommissionFee),
149 #[serde(rename = "rbc", alias = "revokeBuilderCode")]
151 RevokeCommissionFee(RevokeCommissionFee),
152
153 #[serde(rename = "liq")]
155 UpdateLiquidatorConfig(LiqConfig),
156}
157
158macro_rules! dispatch {
159 ($self:expr, $x:ident => $body:expr) => {
160 match $self {
161 Action::MarketOrder($x) => $body,
162 Action::LimitOrder($x) => $body,
163 Action::ModifyOrder($x) => $body,
164 Action::Cancel($x) => $body,
165 Action::CancelAll($x) => $body,
166 Action::Stop($x) => $body,
167 Action::TakeProfit($x) => $body,
168 Action::Range($x) => $body,
169 Action::Trigger($x) => $body,
170 Action::Trailing($x) => $body,
171 Action::OnFill($x) => $body,
172 Action::Price($x) => $body,
173 Action::Corrs($x) => $body,
174 Action::PythOracle($x) => $body,
175 Action::Beacon($x) => $body,
176 Action::Join($x) => $body,
177
178 Action::Faucet($x) => $body,
179 Action::AgentWalletCreation($x) => $body,
180 Action::UpdateUserSettings($x) => $body,
181 Action::WhitelistFaucet($x) => $body,
182
183 Action::AddMarket($x) => $body,
184 Action::ConfigFairPrice($x) => $body,
185 Action::ConfigVolatility($x) => $body,
186 Action::ConfigSecurity($x) => $body,
187 Action::ConfigRegime($x) => $body,
188 Action::ConfigRisk($x) => $body,
189 Action::ConfigFeePolicy($x) => $body,
190
191 Action::CreateSubAccount($x) => $body,
192 Action::RemoveSubAccount($x) => $body,
193 Action::Transfer($x) => $body,
194
195 Action::CreateMultisig($x) => $body,
196 Action::MultisigPropose($x) => $body,
197 Action::MultisigApprove($x) => $body,
198 Action::MultisigReject($x) => $body,
199 Action::MultisigCancel($x) => $body,
200 Action::MultisigExecute($x) => $body,
201 Action::UpdateMultisigPolicy($x) => $body,
202
203 Action::RenameSubAccount($x) => $body,
204 Action::UpdateValidatorSet($x) => $body,
205
206 Action::UpdateRiskConfig($x) => $body,
207 Action::ApproveCommissionFee($x) => $body,
208 Action::RevokeCommissionFee($x) => $body,
209 Action::UpdateLiquidatorConfig($x) => $body,
210 }
211 };
212}
213
214impl Action {
215 pub fn account(&self) -> &Pubkey {
217 dispatch!(self, x => &x.meta.account)
218 }
219
220 pub fn nonce(&self) -> u64 {
222 dispatch!(self, x => x.meta.nonce)
223 }
224
225 pub fn seqno(&self) -> u32 {
227 dispatch!(self, x => x.meta.seqno)
228 }
229
230 pub fn hash(&mut self) -> Hash {
232 use sha2::Digest;
233
234 let meta_ptr: *mut ActionMeta = dispatch!(self, x => {
236 if let Some(h) = x.meta.hash {
237 return h;
238 }
239 &mut x.meta as *mut ActionMeta
240 });
241
242 let (seqno, account, nonce) = unsafe {
244 let m = &*meta_ptr;
245 (m.seqno, m.account, m.nonce)
246 };
247
248 let mut hasher = sha2::Sha256::new();
249 hasher.update(&seqno.to_le_bytes());
250 bincode::serialize_into(&mut hasher, &OrderHashAction(&*self))
251 .expect("serialization failed");
252 hasher.update(account.as_ref());
254 hasher.update(&nonce.to_le_bytes());
255
256 let hash = Hash::from(Into::<[u8; 32]>::into(hasher.finalize()));
257
258 unsafe {
260 (*meta_ptr).hash = Some(hash);
261 }
262 hash
263 }
264
265 pub fn link(&mut self, meta: ActionMeta) {
267 dispatch!(self, x => {
268 x.meta = meta;
269 })
270 }
271}
272
273impl From<MarketOrder> for Action {
274 fn from(o: MarketOrder) -> Self {
275 Action::MarketOrder(o)
276 }
277}
278
279impl From<LimitOrder> for Action {
280 fn from(o: LimitOrder) -> Self {
281 Action::LimitOrder(o)
282 }
283}
284
285struct OrderHashSafeF64(f64);
286
287impl Serialize for OrderHashSafeF64 {
288 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
289 crate::msgs::fixed_point::serialize(&self.0, serializer)
290 }
291}
292
293struct OrderHashMarketOrder<'a>(&'a MarketOrder);
294
295impl Serialize for OrderHashMarketOrder<'_> {
296 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
297 let mut tuple = serializer.serialize_tuple(5)?;
298 tuple.serialize_element(&self.0.symbol)?;
299 tuple.serialize_element(&self.0.is_buy)?;
300 tuple.serialize_element(&OrderHashSafeF64(self.0.size))?;
301 tuple.serialize_element(&self.0.reduce_only)?;
302 tuple.serialize_element(&self.0.iso)?;
303 tuple.end()
304 }
305}
306
307struct OrderHashLimitOrder<'a>(&'a LimitOrder);
308
309impl Serialize for OrderHashLimitOrder<'_> {
310 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
311 let mut tuple = serializer.serialize_tuple(7)?;
312 tuple.serialize_element(&self.0.symbol)?;
313 tuple.serialize_element(&self.0.is_buy)?;
314 tuple.serialize_element(&OrderHashSafeF64(self.0.price))?;
315 tuple.serialize_element(&OrderHashSafeF64(self.0.size))?;
316 tuple.serialize_element(&self.0.tif)?;
317 tuple.serialize_element(&self.0.reduce_only)?;
318 tuple.serialize_element(&self.0.iso)?;
319 tuple.end()
320 }
321}
322
323struct OrderHashAction<'a>(&'a Action);
324
325impl Serialize for OrderHashAction<'_> {
326 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
327 match self.0 {
328 Action::MarketOrder(order) => serializer.serialize_newtype_variant(
329 "Action",
330 0,
331 "MarketOrder",
332 &OrderHashMarketOrder(order),
333 ),
334 Action::LimitOrder(order) => serializer.serialize_newtype_variant(
335 "Action",
336 1,
337 "LimitOrder",
338 &OrderHashLimitOrder(order),
339 ),
340 action => action.serialize(serializer),
341 }
342 }
343}
344
345impl From<ModifyOrder> for Action {
346 fn from(o: ModifyOrder) -> Self {
347 Action::ModifyOrder(o)
348 }
349}
350
351impl From<CancelAll> for Action {
352 fn from(o: CancelAll) -> Self {
353 Action::CancelAll(o)
354 }
355}
356
357impl From<CancelOrder> for Action {
358 fn from(o: CancelOrder) -> Self {
359 Action::Cancel(o)
360 }
361}
362
363impl From<Price> for Action {
364 fn from(o: Price) -> Self {
365 Action::Price(o)
366 }
367}
368
369impl From<PythOracle> for Action {
370 fn from(o: PythOracle) -> Self {
371 Action::PythOracle(o)
372 }
373}
374
375impl From<Faucet> for Action {
376 fn from(o: Faucet) -> Self {
377 Action::Faucet(o)
378 }
379}
380
381impl From<AgentWalletCreation> for Action {
382 fn from(o: AgentWalletCreation) -> Self {
383 Action::AgentWalletCreation(o)
384 }
385}
386
387impl From<UpdateUserSettings> for Action {
388 fn from(o: UpdateUserSettings) -> Self {
389 Action::UpdateUserSettings(o)
390 }
391}
392
393impl From<ApproveCommissionFee> for Action {
394 fn from(o: ApproveCommissionFee) -> Self {
395 Action::ApproveCommissionFee(o)
396 }
397}
398
399impl From<RevokeCommissionFee> for Action {
400 fn from(o: RevokeCommissionFee) -> Self {
401 Action::RevokeCommissionFee(o)
402 }
403}
404
405impl From<WhitelistFaucet> for Action {
406 fn from(o: WhitelistFaucet) -> Self {
407 Action::WhitelistFaucet(o)
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::common::tif::TimeInForce;
415 use std::sync::Arc;
416
417 #[test]
418 fn test_limit_hash() {
419 let limit = LimitOrder {
420 symbol: Arc::from("BTC-USD"),
421 is_buy: true,
422 price: 100000.0,
423 size: 1.0,
424 tif: TimeInForce::ALO,
425 reduce_only: false,
426 iso: false,
427 builder_code: None,
428 meta: ActionMeta {
429 account: Default::default(),
430 nonce: 1_776_128_000_000_000_000,
431 seqno: 0,
432 hash: None,
433 },
434 };
435
436 let mut action = Action::LimitOrder(limit);
437 let hash = action.hash();
438
439 assert_eq!(
440 hash.to_string(),
441 "9BreqftLa7ZAsYLkvJDRBRxiSukzGoTfbQNMBWWkUAUJ"
442 );
443 }
444
445 #[test]
446 fn order_hash_ignores_commission() {
447 let meta = ActionMeta {
448 account: Pubkey::new_unique(),
449 nonce: 7,
450 seqno: 3,
451 hash: None,
452 };
453 let mut without = Action::LimitOrder(LimitOrder {
454 symbol: Arc::from("BTC-USD"),
455 is_buy: true,
456 price: 100000.0,
457 size: 1.0,
458 tif: TimeInForce::GTC,
459 reduce_only: false,
460 iso: false,
461 builder_code: None,
462 meta,
463 });
464 let mut with = Action::LimitOrder(LimitOrder {
465 symbol: Arc::from("BTC-USD"),
466 is_buy: true,
467 price: 100000.0,
468 size: 1.0,
469 tif: TimeInForce::GTC,
470 reduce_only: false,
471 iso: false,
472 builder_code: Some(crate::msgs::BuilderCode {
473 to: Pubkey::new_unique(),
474 fee: 5,
475 }),
476 meta,
477 });
478
479 assert_eq!(without.hash(), with.hash());
480 }
481
482 #[test]
483 fn config_risk_accepts_risk_matrix_alias() {
484 let action: Action = serde_json::from_str(r#"{"configRiskMatrix":{"payload":[1,2,3]}}"#)
485 .expect("configRiskMatrix alias should deserialize");
486
487 match action {
488 Action::ConfigRisk(action) => assert_eq!(action.payload, vec![1, 2, 3]),
489 action => panic!("expected ConfigRisk, got {action:?}"),
490 }
491 }
492}