Skip to main content

bulk_client/transaction/
actions.rs

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/// Meta data for an action
20#[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    // Market = ordinal(0)
32    #[serde(rename = "m")]
33    MarketOrder(MarketOrder),
34    // Limit = ordinal(1)
35    #[serde(rename = "l")]
36    LimitOrder(LimitOrder),
37    // Modify = ordinal(2)
38    #[serde(rename = "mod")]
39    ModifyOrder(ModifyOrder),
40    // Cancel = ordinal(3)
41    #[serde(rename = "cx")]
42    Cancel(CancelOrder),
43    // CancelAll = ordinal(4)
44    #[serde(rename = "cxa")]
45    CancelAll(CancelAll),
46    // Stop = ordinal(5)
47    #[serde(rename = "st")]
48    Stop(StopOrTP),
49    // TakeProfit = ordinal(6)
50    #[serde(rename = "tp")]
51    TakeProfit(StopOrTP),
52    // Range = ordinal(7)
53    #[serde(rename = "rng")]
54    Range(Range),
55    // Trigger = ordinal(8)
56    #[serde(rename = "trig")]
57    Trigger(Trigger),
58    // Trailing = ordinal(9)
59    #[serde(rename = "trl")]
60    Trailing(Trailing),
61    // OnFill = ordinal(10)
62    #[serde(rename = "of")]
63    OnFill(OnFill),
64
65    // Price = ordinal(11)
66    #[serde(rename = "px")]
67    Price(Price),
68    // Corrs = ordinal(12)
69    #[serde(rename = "corrs")]
70    Corrs(Matrix),
71    // PythOracle = ordinal(13)
72    #[serde(rename = "o")]
73    PythOracle(PythOracle),
74    // Beacon = ordinal(14)
75    #[serde(rename = "beacon")]
76    Beacon(Beacon),
77    // Join = ordinal(15)
78    #[serde(rename = "join")]
79    Join(Join),
80
81    // Faucet = ordinal(16)
82    Faucet(Faucet),
83    // AgentWallet = ordinal(17)
84    AgentWalletCreation(AgentWalletCreation),
85    // UpdateUserSettings = ordinal(18)
86    UpdateUserSettings(UpdateUserSettings),
87
88    // WhitelistFaucet = ordinal(19)
89    WhitelistFaucet(WhitelistFaucet),
90
91    // AddMarket = ordinal(20)
92    AddMarket(AddMarket),
93    // ConfigFairPrice = ordinal(21)
94    ConfigFairPrice(OpaqueAction),
95    // ConfigVolatility = ordinal(22)
96    ConfigVolatility(OpaqueAction),
97    // ConfigSecurity = ordinal(23)
98    ConfigSecurity(OpaqueAction),
99    // ConfigRegime = ordinal(24)
100    ConfigRegime(OpaqueAction),
101    // ConfigRisk = ordinal(25)
102    #[serde(alias = "configRiskMatrix")]
103    ConfigRisk(OpaqueAction),
104    // ConfigFeePolicy = ordinal(26)
105    #[serde(rename = "cfgf")]
106    ConfigFeePolicy(OpaqueAction),
107
108    // CreateSubAccount = ordinal(27)
109    CreateSubAccount(CreateSubAccount),
110    // RemoveSubAccount = ordinal(28)
111    RemoveSubAccount(RemoveSubAccount),
112    // Transfer = ordinal(29)
113    Transfer(Transfer),
114    // CreateMultisig = ordinal(30)
115    CreateMultisig(CreateMultisig),
116
117    // MultisigPropose = ordinal(31)
118    #[serde(rename = "msp")]
119    MultisigPropose(MultisigPropose),
120    // MultisigApprove = ordinal(32)
121    #[serde(rename = "msa")]
122    MultisigApprove(MultisigApprove),
123    // MultisigReject = ordinal(33)
124    #[serde(rename = "msr")]
125    MultisigReject(MultisigReject),
126    // MultisigCancel = ordinal(34)
127    #[serde(rename = "msc")]
128    MultisigCancel(MultisigCancel),
129    // MultisigExecute = ordinal(35)
130    #[serde(rename = "mse")]
131    MultisigExecute(MultisigExecute),
132    // UpdateMultisigPolicy = ordinal(36)
133    #[serde(rename = "msu")]
134    UpdateMultisigPolicy(UpdateMultisigPolicy),
135
136    // RenameSubAccount = ordinal(37)
137    #[serde(rename = "rsa", alias = "renameSubAccount")]
138    RenameSubAccount(RenameSubAccount),
139    // UpdateValidatorSet = ordinal(38)
140    #[serde(rename = "uvs")]
141    UpdateValidatorSet(UpdateValidatorSet),
142
143    // UpdateRiskConfig = ordinal(39)
144    #[serde(rename = "risk")]
145    UpdateRiskConfig(RiskConfigChange),
146    // ApproveCommissionFee = ordinal(40)
147    #[serde(rename = "abc", alias = "approveBuilderCode")]
148    ApproveCommissionFee(ApproveCommissionFee),
149    // RevokeCommissionFee = ordinal(41)
150    #[serde(rename = "rbc", alias = "revokeBuilderCode")]
151    RevokeCommissionFee(RevokeCommissionFee),
152
153    // UpdateLiquidatorConfig = ordinal(42)
154    #[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    /// Get account associated with action
216    pub fn account(&self) -> &Pubkey {
217        dispatch!(self, x => &x.meta.account)
218    }
219
220    /// Get nonce associated with action
221    pub fn nonce(&self) -> u64 {
222        dispatch!(self, x => x.meta.nonce)
223    }
224
225    /// Get nonce associated with action
226    pub fn seqno(&self) -> u32 {
227        dispatch!(self, x => x.meta.seqno)
228    }
229
230    /// Get or compute hash of action
231    pub fn hash(&mut self) -> Hash {
232        use sha2::Digest;
233
234        // Single dispatch: cache check + extract raw pointer to meta
235        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        // Read meta fields through pointer — no live borrows of self
243        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        // Immutable borrow of self released here
253        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        // Write result — no active borrows of self
259        unsafe {
260            (*meta_ptr).hash = Some(hash);
261        }
262        hash
263    }
264
265    /// Link tx and action meta information in each action
266    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}