Skip to main content

bulk_client/msgs/
order.rs

1use crate::common::tif::TimeInForce;
2use crate::transaction::ActionMeta;
3use serde::ser::{SerializeStruct, SerializeTuple};
4use serde::{Deserialize, Serialize};
5use sha2::Digest;
6use solana_hash::Hash;
7use solana_pubkey::Pubkey;
8use std::sync::Arc;
9
10struct FixedF64(f64);
11
12impl Serialize for FixedF64 {
13    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
14        crate::msgs::fixed_point::serialize(&self.0, serializer)
15    }
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct BuilderCode {
21    #[serde(with = "crate::msgs::serde_pubkey")]
22    pub to: Pubkey,
23    pub fee: u8,
24}
25
26fn deserialize_builder_code<'de, D>(deserializer: D) -> Result<Option<BuilderCode>, D::Error>
27where
28    D: serde::Deserializer<'de>,
29{
30    if !deserializer.is_human_readable() {
31        return <Option<BuilderCode> as Deserialize>::deserialize(deserializer);
32    }
33
34    struct BuilderCodeVisitor;
35
36    impl<'de> serde::de::Visitor<'de> for BuilderCodeVisitor {
37        type Value = Option<BuilderCode>;
38
39        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40            formatter.write_str("an omitted builderCode field or a builderCode object")
41        }
42
43        fn visit_none<E>(self) -> Result<Self::Value, E>
44        where
45            E: serde::de::Error,
46        {
47            Err(E::custom("builderCode must be omitted or an object"))
48        }
49
50        fn visit_unit<E>(self) -> Result<Self::Value, E>
51        where
52            E: serde::de::Error,
53        {
54            Err(E::custom("builderCode must be omitted or an object"))
55        }
56
57        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
58        where
59            D: serde::Deserializer<'de>,
60        {
61            <BuilderCode as Deserialize>::deserialize(deserializer).map(Some)
62        }
63    }
64
65    deserializer.deserialize_option(BuilderCodeVisitor)
66}
67
68struct OrderHashSafeF64(f64);
69
70impl Serialize for OrderHashSafeF64 {
71    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
72        crate::msgs::fixed_point::serialize(&self.0, serializer)
73    }
74}
75
76struct OrderHashMarketOrder<'a>(&'a MarketOrder);
77
78impl Serialize for OrderHashMarketOrder<'_> {
79    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
80        let mut tuple = serializer.serialize_tuple(5)?;
81        tuple.serialize_element(&self.0.symbol)?;
82        tuple.serialize_element(&self.0.is_buy)?;
83        tuple.serialize_element(&OrderHashSafeF64(self.0.size))?;
84        tuple.serialize_element(&self.0.reduce_only)?;
85        tuple.serialize_element(&self.0.iso)?;
86        tuple.end()
87    }
88}
89
90struct OrderHashLimitOrder<'a>(&'a LimitOrder);
91
92impl Serialize for OrderHashLimitOrder<'_> {
93    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
94        let mut tuple = serializer.serialize_tuple(7)?;
95        tuple.serialize_element(&self.0.symbol)?;
96        tuple.serialize_element(&self.0.is_buy)?;
97        tuple.serialize_element(&OrderHashSafeF64(self.0.price))?;
98        tuple.serialize_element(&OrderHashSafeF64(self.0.size))?;
99        tuple.serialize_element(&self.0.tif)?;
100        tuple.serialize_element(&self.0.reduce_only)?;
101        tuple.serialize_element(&self.0.iso)?;
102        tuple.end()
103    }
104}
105
106struct OrderHashMarketAction<'a>(&'a MarketOrder);
107
108impl Serialize for OrderHashMarketAction<'_> {
109    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
110        serializer.serialize_newtype_variant(
111            "Action",
112            0,
113            "MarketOrder",
114            &OrderHashMarketOrder(self.0),
115        )
116    }
117}
118
119struct OrderHashLimitAction<'a>(&'a LimitOrder);
120
121impl Serialize for OrderHashLimitAction<'_> {
122    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
123        serializer.serialize_newtype_variant(
124            "Action",
125            1,
126            "LimitOrder",
127            &OrderHashLimitOrder(self.0),
128        )
129    }
130}
131
132// ─────────────────────────────────────────────────────────────────────────────
133// Market Order
134// ─────────────────────────────────────────────────────────────────────────────
135
136#[derive(Clone, Debug, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct MarketOrder {
139    #[serde(rename = "c")]
140    pub symbol: Arc<str>,
141
142    #[serde(rename = "b")]
143    pub is_buy: bool,
144
145    #[serde(rename = "sz", with = "crate::msgs::fixed_point")]
146    pub size: f64,
147
148    #[serde(rename = "r")]
149    pub reduce_only: bool,
150
151    #[serde(rename = "i", default)]
152    pub iso: bool,
153
154    #[serde(
155        rename = "builderCode",
156        default,
157        deserialize_with = "deserialize_builder_code"
158    )]
159    pub builder_code: Option<BuilderCode>,
160
161    #[serde(skip)]
162    pub meta: ActionMeta,
163}
164
165impl Serialize for MarketOrder {
166    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
167        if serializer.is_human_readable() {
168            let mut state = serializer
169                .serialize_struct("MarketOrder", 5 + usize::from(self.builder_code.is_some()))?;
170            state.serialize_field("c", &self.symbol)?;
171            state.serialize_field("b", &self.is_buy)?;
172            state.serialize_field("sz", &FixedF64(self.size))?;
173            state.serialize_field("r", &self.reduce_only)?;
174            state.serialize_field("i", &self.iso)?;
175            if let Some(commission) = &self.builder_code {
176                state.serialize_field("builderCode", commission)?;
177            }
178            state.end()
179        } else {
180            let mut tuple = serializer.serialize_tuple(6)?;
181            tuple.serialize_element(&self.symbol)?;
182            tuple.serialize_element(&self.is_buy)?;
183            tuple.serialize_element(&FixedF64(self.size))?;
184            tuple.serialize_element(&self.reduce_only)?;
185            tuple.serialize_element(&self.iso)?;
186            tuple.serialize_element(&self.builder_code)?;
187            tuple.end()
188        }
189    }
190}
191
192impl MarketOrder {
193    /// Compute order ID
194    ///
195    /// # Arguments
196    /// - `account`: account associated with order
197    /// - `nonce`: nonce associated with tx
198    /// - `seqno`: action sequence number
199    pub fn order_id(&self, account: Pubkey, nonce: u64, seqno: u32) -> Hash {
200        let mut bin = Vec::<u8>::new();
201        bin.extend(seqno.to_le_bytes());
202        bin.extend(bincode::serialize(&OrderHashMarketAction(self)).unwrap());
203        bin.extend_from_slice(account.as_ref());
204        bin.extend_from_slice(&nonce.to_le_bytes());
205
206        let mut hasher = sha2::Sha256::new();
207        hasher.update(&bin);
208        let hash: [u8; 32] = hasher.finalize().into();
209        Hash::from(hash)
210    }
211}
212
213// ─────────────────────────────────────────────────────────────────────────────
214// Limit Order
215// ─────────────────────────────────────────────────────────────────────────────
216
217#[derive(Clone, Debug, Deserialize)]
218#[serde(deny_unknown_fields)]
219pub struct LimitOrder {
220    #[serde(rename = "c")]
221    pub symbol: Arc<str>,
222
223    #[serde(rename = "b")]
224    pub is_buy: bool,
225
226    #[serde(rename = "px", with = "crate::msgs::fixed_point")]
227    pub price: f64,
228
229    #[serde(rename = "sz", with = "crate::msgs::fixed_point")]
230    pub size: f64,
231
232    #[serde(rename = "tif")]
233    pub tif: TimeInForce,
234
235    #[serde(rename = "r")]
236    pub reduce_only: bool,
237
238    #[serde(rename = "i", default)]
239    pub iso: bool,
240
241    #[serde(
242        rename = "builderCode",
243        default,
244        deserialize_with = "deserialize_builder_code"
245    )]
246    pub builder_code: Option<BuilderCode>,
247
248    #[serde(skip)]
249    pub meta: ActionMeta,
250}
251
252impl Serialize for LimitOrder {
253    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
254        if serializer.is_human_readable() {
255            let mut state = serializer
256                .serialize_struct("LimitOrder", 7 + usize::from(self.builder_code.is_some()))?;
257            state.serialize_field("c", &self.symbol)?;
258            state.serialize_field("b", &self.is_buy)?;
259            state.serialize_field("px", &FixedF64(self.price))?;
260            state.serialize_field("sz", &FixedF64(self.size))?;
261            state.serialize_field("tif", &self.tif)?;
262            state.serialize_field("r", &self.reduce_only)?;
263            state.serialize_field("i", &self.iso)?;
264            if let Some(commission) = &self.builder_code {
265                state.serialize_field("builderCode", commission)?;
266            }
267            state.end()
268        } else {
269            let mut tuple = serializer.serialize_tuple(8)?;
270            tuple.serialize_element(&self.symbol)?;
271            tuple.serialize_element(&self.is_buy)?;
272            tuple.serialize_element(&FixedF64(self.price))?;
273            tuple.serialize_element(&FixedF64(self.size))?;
274            tuple.serialize_element(&self.tif)?;
275            tuple.serialize_element(&self.reduce_only)?;
276            tuple.serialize_element(&self.iso)?;
277            tuple.serialize_element(&self.builder_code)?;
278            tuple.end()
279        }
280    }
281}
282
283impl LimitOrder {
284    /// Compute order ID
285    ///
286    /// # Arguments
287    /// - `account`: account associated with order
288    /// - `nonce`: nonce associated with tx
289    /// - `seqno`: action sequence number
290    pub fn order_id(&self, account: Pubkey, nonce: u64, seqno: u32) -> Hash {
291        let mut bin = Vec::<u8>::new();
292        bin.extend(seqno.to_le_bytes());
293        bin.extend(bincode::serialize(&OrderHashLimitAction(self)).unwrap());
294        bin.extend_from_slice(account.as_ref());
295        bin.extend_from_slice(&nonce.to_le_bytes());
296
297        let mut hasher = sha2::Sha256::new();
298        hasher.update(&bin);
299        let hash: [u8; 32] = hasher.finalize().into();
300        Hash::from(hash)
301    }
302}
303
304// ─────────────────────────────────────────────────────────────────────────────
305// Modify Order
306// ─────────────────────────────────────────────────────────────────────────────
307
308/// Update order: changing order size
309#[derive(Clone, Debug, Serialize, Deserialize)]
310pub struct ModifyOrder {
311    #[serde(with = "crate::msgs::serde_hash", rename = "oid")]
312    pub order_id: Hash,
313    #[serde(rename = "c")]
314    pub symbol: String,
315    #[serde(rename = "sz")]
316    pub amount: f64,
317
318    #[serde(skip)]
319    pub meta: ActionMeta,
320}
321
322// ─────────────────────────────────────────────────────────────────────────────
323// Cancel Order
324// ─────────────────────────────────────────────────────────────────────────────
325
326#[derive(Clone, Debug, Serialize, Deserialize)]
327pub struct CancelOrder {
328    #[serde(rename = "c")]
329    pub symbol: String,
330    #[serde(with = "crate::msgs::serde_hash", rename = "oid")]
331    pub oid: Hash,
332
333    #[serde(skip)]
334    pub meta: ActionMeta,
335}
336
337// ─────────────────────────────────────────────────────────────────────────────
338// Cancel All Orders
339// ─────────────────────────────────────────────────────────────────────────────
340
341#[derive(Clone, Debug, Serialize, Deserialize)]
342pub struct CancelAll {
343    #[serde(rename = "c")]
344    pub symbols: Vec<String>,
345
346    #[serde(skip)]
347    pub meta: ActionMeta,
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn limit_order_without_builder_code_omits_json_field() {
356        assert!(!serde_json::to_value(LimitOrder {
357            symbol: Arc::from("BTC-USD"),
358            is_buy: true,
359            price: 100.0,
360            size: 1.0,
361            tif: TimeInForce::GTC,
362            reduce_only: false,
363            iso: false,
364            builder_code: None,
365            meta: ActionMeta::default(),
366        })
367        .expect("limit order should serialize")
368        .as_object()
369        .expect("limit order json should be an object")
370        .contains_key("builderCode"));
371    }
372
373    #[test]
374    fn limit_order_with_builder_code_includes_json_field() {
375        assert_eq!(
376            serde_json::to_value(LimitOrder {
377                symbol: Arc::from("BTC-USD"),
378                is_buy: true,
379                price: 100.0,
380                size: 1.0,
381                tif: TimeInForce::GTC,
382                reduce_only: false,
383                iso: false,
384                builder_code: Some(BuilderCode {
385                    to: Pubkey::new_unique(),
386                    fee: 5,
387                }),
388                meta: ActionMeta::default(),
389            })
390            .expect("limit order should serialize")["builderCode"]["fee"],
391            5
392        );
393    }
394
395    #[test]
396    fn market_order_without_builder_code_omits_json_field() {
397        assert!(!serde_json::to_value(MarketOrder {
398            symbol: Arc::from("BTC-USD"),
399            is_buy: false,
400            size: 1.0,
401            reduce_only: false,
402            iso: false,
403            builder_code: None,
404            meta: ActionMeta::default(),
405        })
406        .expect("market order should serialize")
407        .as_object()
408        .expect("market order json should be an object")
409        .contains_key("builderCode"));
410    }
411
412    #[test]
413    fn order_json_rejects_null_builder_code() {
414        assert!(serde_json::from_value::<LimitOrder>(serde_json::json!({
415            "c": "BTC-USD",
416            "b": true,
417            "px": 100.0,
418            "sz": 1.0,
419            "tif": "GTC",
420            "r": false,
421            "i": false,
422            "builderCode": null
423        }))
424        .is_err());
425
426        assert!(serde_json::from_value::<MarketOrder>(serde_json::json!({
427            "c": "BTC-USD",
428            "b": true,
429            "sz": 1.0,
430            "r": false,
431            "i": false,
432            "builderCode": null
433        }))
434        .is_err());
435    }
436
437    #[test]
438    fn order_id_ignores_commission() {
439        let account = Pubkey::new_unique();
440        let to = Pubkey::new_unique();
441
442        let without = LimitOrder {
443            symbol: Arc::from("BTC-USD"),
444            is_buy: true,
445            price: 100.0,
446            size: 1.0,
447            tif: TimeInForce::GTC,
448            reduce_only: false,
449            iso: false,
450            builder_code: None,
451            meta: ActionMeta::default(),
452        };
453        let with = LimitOrder {
454            builder_code: Some(BuilderCode { to, fee: 5 }),
455            ..without.clone()
456        };
457
458        assert_eq!(
459            without.order_id(account, 7, 3),
460            with.order_id(account, 7, 3)
461        );
462
463        let market_without = MarketOrder {
464            symbol: Arc::from("BTC-USD"),
465            is_buy: false,
466            size: 2.0,
467            reduce_only: false,
468            iso: true,
469            builder_code: None,
470            meta: ActionMeta::default(),
471        };
472        let market_with = MarketOrder {
473            builder_code: Some(BuilderCode { to, fee: 15 }),
474            ..market_without.clone()
475        };
476
477        assert_eq!(
478            market_without.order_id(account, 7, 3),
479            market_with.order_id(account, 7, 3)
480        );
481    }
482}