Skip to main content

bulk_keychain/
types.rs

1//! Type definitions for BULK transactions
2//!
3//! These types match the BULK exchange API specification exactly.
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7pub const MAX_COMMISSION_FEE_BPS: u8 = 15;
8pub const MAX_BUILDER_CODE_FEE_BPS: u8 = MAX_COMMISSION_FEE_BPS;
9
10/// BULK network identity committed into every transaction signature.
11#[repr(u8)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum SignatureDomain {
15    Mainnet = 1,
16    Testnet = 2,
17    Devnet = 3,
18}
19
20impl SignatureDomain {
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::Mainnet => "mainnet",
24            Self::Testnet => "testnet",
25            Self::Devnet => "devnet",
26        }
27    }
28}
29
30impl std::fmt::Display for SignatureDomain {
31    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        formatter.write_str(self.as_str())
33    }
34}
35
36impl std::str::FromStr for SignatureDomain {
37    type Err = crate::Error;
38
39    fn from_str(value: &str) -> crate::Result<Self> {
40        match value {
41            "mainnet" => Ok(Self::Mainnet),
42            "testnet" => Ok(Self::Testnet),
43            "devnet" => Ok(Self::Devnet),
44            _ => Err(crate::Error::InvalidSignatureDomain(value.to_string())),
45        }
46    }
47}
48
49/// 32-byte public key (Ed25519)
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Pubkey(pub [u8; 32]);
52
53impl Pubkey {
54    /// Create from raw bytes
55    pub fn from_bytes(bytes: [u8; 32]) -> Self {
56        Self(bytes)
57    }
58
59    /// Decode from base58 string
60    pub fn from_base58(s: &str) -> crate::Result<Self> {
61        let bytes = bs58::decode(s)
62            .into_vec()
63            .map_err(|e| crate::Error::InvalidBase58(e.to_string()))?;
64        if bytes.len() != 32 {
65            return Err(crate::Error::InvalidKeyLength {
66                expected: 32,
67                got: bytes.len(),
68            });
69        }
70        let mut arr = [0u8; 32];
71        arr.copy_from_slice(&bytes);
72        Ok(Self(arr))
73    }
74
75    /// Encode to base58 string
76    pub fn to_base58(&self) -> String {
77        bs58::encode(&self.0).into_string()
78    }
79
80    /// Get raw bytes
81    pub fn as_bytes(&self) -> &[u8; 32] {
82        &self.0
83    }
84}
85
86impl std::fmt::Display for Pubkey {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        write!(f, "{}", self.to_base58())
89    }
90}
91
92impl Serialize for Pubkey {
93    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94    where
95        S: Serializer,
96    {
97        serializer.serialize_str(&self.to_base58())
98    }
99}
100
101impl<'de> Deserialize<'de> for Pubkey {
102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103    where
104        D: Deserializer<'de>,
105    {
106        let s = String::deserialize(deserializer)?;
107        Pubkey::from_base58(&s).map_err(serde::de::Error::custom)
108    }
109}
110
111/// 32-byte hash (used for order IDs, client IDs)
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct Hash(pub [u8; 32]);
114
115impl Hash {
116    /// Create from raw bytes
117    pub fn from_bytes(bytes: [u8; 32]) -> Self {
118        Self(bytes)
119    }
120
121    /// Decode from base58 string
122    pub fn from_base58(s: &str) -> crate::Result<Self> {
123        let bytes = bs58::decode(s)
124            .into_vec()
125            .map_err(|e| crate::Error::InvalidBase58(e.to_string()))?;
126        if bytes.len() != 32 {
127            return Err(crate::Error::InvalidHashLength(bytes.len()));
128        }
129        let mut arr = [0u8; 32];
130        arr.copy_from_slice(&bytes);
131        Ok(Self(arr))
132    }
133
134    /// Encode to base58 string
135    pub fn to_base58(&self) -> String {
136        bs58::encode(&self.0).into_string()
137    }
138
139    /// Get raw bytes
140    pub fn as_bytes(&self) -> &[u8; 32] {
141        &self.0
142    }
143
144    /// Generate a random hash (useful for client order IDs)
145    pub fn random() -> Self {
146        use rand::Rng;
147        let mut bytes = [0u8; 32];
148        rand::thread_rng().fill(&mut bytes);
149        Self(bytes)
150    }
151
152    /// Compute SHA256 hash from raw bytes.
153    #[inline]
154    pub fn from_wincode_bytes(wincode_bytes: &[u8]) -> Self {
155        use sha2::{Digest, Sha256};
156        let hash: [u8; 32] = Sha256::digest(wincode_bytes).into();
157        Self(hash)
158    }
159}
160
161impl std::fmt::Display for Hash {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        write!(f, "{}", self.to_base58())
164    }
165}
166
167impl Serialize for Hash {
168    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
169    where
170        S: Serializer,
171    {
172        serializer.serialize_str(&self.to_base58())
173    }
174}
175
176impl<'de> Deserialize<'de> for Hash {
177    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
178    where
179        D: Deserializer<'de>,
180    {
181        let s = String::deserialize(deserializer)?;
182        Hash::from_base58(&s).map_err(serde::de::Error::custom)
183    }
184}
185
186// ============================================================================
187// Time In Force
188// ============================================================================
189
190/// Order time in force
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "UPPERCASE")]
193pub enum TimeInForce {
194    /// Good Till Cancel - rests on book until filled or cancelled
195    Gtc,
196    /// Immediate or Cancel - fill immediately or cancel
197    Ioc,
198    /// Add Liquidity Only - post-only, maker order
199    Alo,
200}
201
202impl TimeInForce {
203    /// Get the discriminant for wincode serialization
204    pub const fn discriminant(&self) -> u32 {
205        match self {
206            Self::Gtc => 0,
207            Self::Ioc => 1,
208            Self::Alo => 2,
209        }
210    }
211}
212
213// ============================================================================
214// Order Types
215// ============================================================================
216
217/// Order type (limit or trigger/market)
218#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub enum OrderType {
221    /// Limit order with time-in-force
222    Limit { tif: TimeInForce },
223    /// Trigger/Market order
224    Trigger {
225        #[serde(rename = "isMarket")]
226        is_market: bool,
227        #[serde(rename = "triggerPx")]
228        trigger_px: f64,
229    },
230}
231
232impl OrderType {
233    /// Create a limit order type
234    pub const fn limit(tif: TimeInForce) -> Self {
235        Self::Limit { tif }
236    }
237
238    /// Create a market order type (executes immediately at best price)
239    pub const fn market() -> Self {
240        Self::Trigger {
241            is_market: true,
242            trigger_px: 0.0,
243        }
244    }
245
246    /// Get the discriminant for wincode serialization
247    pub const fn discriminant(&self) -> u32 {
248        match self {
249            Self::Limit { .. } => 0,
250            Self::Trigger { .. } => 1,
251        }
252    }
253}
254
255// ============================================================================
256// Order
257// ============================================================================
258
259/// A trading order
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub struct Order {
262    /// Market symbol (e.g., "BTC-USD")
263    #[serde(rename = "c")]
264    pub symbol: String,
265    /// Buy (true) or Sell (false)
266    #[serde(rename = "b")]
267    pub is_buy: bool,
268    /// Price (0.0 for market orders)
269    #[serde(rename = "px")]
270    pub price: f64,
271    /// Size/Quantity
272    #[serde(rename = "sz")]
273    pub size: f64,
274    /// Reduce-only flag
275    #[serde(rename = "r")]
276    pub reduce_only: bool,
277    /// Isolated-margin lane flag
278    #[serde(rename = "i", default)]
279    pub iso: bool,
280    /// Order type
281    #[serde(rename = "t")]
282    pub order_type: OrderType,
283    /// Client order ID (optional)
284    #[serde(rename = "cloid", skip_serializing_if = "Option::is_none")]
285    pub client_id: Option<Hash>,
286    /// Optional builder-code fee paid by this order.
287    ///
288    /// Builder codes are encoded as commission fees on the wire.
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub commission: Option<Commission>,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub struct Commission {
295    pub to: Pubkey,
296    pub fee: u8,
297}
298
299pub type BuilderCode = Commission;
300
301impl Commission {
302    pub fn new(to: Pubkey, fee: u8) -> crate::Result<Self> {
303        if fee == 0 || fee > MAX_COMMISSION_FEE_BPS {
304            return Err(crate::Error::InvalidOrder(
305                "builder-code fee must be 1..=15 bps".to_string(),
306            ));
307        }
308        Ok(Self { to, fee })
309    }
310}
311
312impl Order {
313    /// Create a new limit order
314    pub fn limit(
315        symbol: impl Into<String>,
316        is_buy: bool,
317        price: f64,
318        size: f64,
319        tif: TimeInForce,
320    ) -> Self {
321        Self {
322            symbol: symbol.into(),
323            is_buy,
324            price,
325            size,
326            reduce_only: false,
327            iso: false,
328            order_type: OrderType::limit(tif),
329            client_id: None,
330            commission: None,
331        }
332    }
333
334    /// Create a market order
335    pub fn market(symbol: impl Into<String>, is_buy: bool, size: f64) -> Self {
336        Self {
337            symbol: symbol.into(),
338            is_buy,
339            price: 0.0,
340            size,
341            reduce_only: false,
342            iso: false,
343            order_type: OrderType::market(),
344            client_id: None,
345            commission: None,
346        }
347    }
348
349    /// Set reduce-only flag
350    pub fn reduce_only(mut self) -> Self {
351        self.reduce_only = true;
352        self
353    }
354
355    /// Set isolated-margin flag
356    pub fn isolated(mut self) -> Self {
357        self.iso = true;
358        self
359    }
360
361    /// Set client order ID
362    pub fn with_client_id(mut self, client_id: Hash) -> Self {
363        self.client_id = Some(client_id);
364        self
365    }
366
367    pub fn iso(mut self) -> Self {
368        self.iso = true;
369        self
370    }
371
372    pub fn with_commission(mut self, to: Pubkey, fee: u8) -> crate::Result<Self> {
373        self.commission = Some(Commission::new(to, fee)?);
374        Ok(self)
375    }
376
377    pub fn with_builder_code(self, to: Pubkey, fee: u8) -> crate::Result<Self> {
378        self.with_commission(to, fee)
379    }
380
381    /// Generate and set a random client order ID
382    pub fn with_random_client_id(mut self) -> Self {
383        self.client_id = Some(Hash::random());
384        self
385    }
386}
387
388// ============================================================================
389// Cancel
390// ============================================================================
391
392/// Cancel a specific order by ID
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub struct Cancel {
395    /// Market symbol
396    #[serde(rename = "c")]
397    pub symbol: String,
398    /// Order ID to cancel
399    #[serde(rename = "oid")]
400    pub order_id: Hash,
401}
402
403impl Cancel {
404    /// Create a new cancel request
405    pub fn new(symbol: impl Into<String>, order_id: Hash) -> Self {
406        Self {
407            symbol: symbol.into(),
408            order_id,
409        }
410    }
411}
412
413/// Modify an existing order
414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
415pub struct Modify {
416    /// Order ID to modify
417    #[serde(rename = "oid")]
418    pub order_id: Hash,
419    /// Market symbol
420    pub symbol: String,
421    /// New amount/size
422    pub amount: f64,
423}
424
425impl Modify {
426    /// Create a new modify request
427    pub fn new(order_id: Hash, symbol: impl Into<String>, amount: f64) -> Self {
428        Self {
429            order_id,
430            symbol: symbol.into(),
431            amount,
432        }
433    }
434}
435
436// ============================================================================
437// Cancel All
438// ============================================================================
439
440/// Cancel all orders (optionally filtered by symbols)
441#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
442pub struct CancelAll {
443    /// Symbols to cancel orders for (empty = all symbols)
444    #[serde(rename = "c")]
445    pub symbols: Vec<String>,
446}
447
448impl CancelAll {
449    /// Cancel all orders across all symbols
450    pub fn all() -> Self {
451        Self { symbols: vec![] }
452    }
453
454    /// Cancel all orders for specific symbols
455    pub fn for_symbols(symbols: Vec<String>) -> Self {
456        Self { symbols }
457    }
458}
459
460// ============================================================================
461// Conditional order types
462// ============================================================================
463
464/// Stop-loss order: triggers when price crosses threshold
465#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
466pub struct Stop {
467    pub symbol: String,
468    /// true = buy/long side, false = sell/short side
469    pub is_buy: bool,
470    pub size: f64,
471    pub trigger_price: f64,
472    /// Limit price; NaN means market-style fill
473    pub limit_price: f64,
474    /// Isolated-margin flag
475    #[serde(default)]
476    pub iso: bool,
477}
478
479/// Take-profit order: triggers when price crosses threshold
480#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
481pub struct TakeProfit {
482    pub symbol: String,
483    /// true = buy/long side, false = sell/short side
484    pub is_buy: bool,
485    pub size: f64,
486    pub trigger_price: f64,
487    /// Limit price; NaN means market-style fill
488    pub limit_price: f64,
489    /// Isolated-margin flag
490    #[serde(default)]
491    pub iso: bool,
492}
493
494/// Range / OCO order: collar around a position
495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496pub struct RangeOco {
497    pub symbol: String,
498    /// true = buy/long collar, false = sell/short collar
499    pub is_buy: bool,
500    pub size: f64,
501    pub collar_min: f64,
502    pub collar_max: f64,
503    /// Limit price for min side; NaN means market-style fill
504    pub limit_min: f64,
505    /// Limit price for max side; NaN means market-style fill
506    pub limit_max: f64,
507    /// Isolated-margin flag
508    #[serde(default)]
509    pub iso: bool,
510}
511
512/// Trigger basket: fires a set of actions when price crosses threshold.
513/// Nested actions may be: m, l, mod, cx, cxa, st, tp, rng.
514#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
515pub struct TriggerBasket {
516    pub symbol: String,
517    /// true = buy/long side, false = sell/short side
518    pub is_buy: bool,
519    pub trigger_price: f64,
520    pub actions: Vec<OrderItem>,
521}
522
523/// Trailing stop: protective stop that follows price by a fixed distance in bps,
524/// resetting forward on favorable moves in increments of `step_bps`.
525#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
526pub struct TrailingStop {
527    pub symbol: String,
528    /// Protected position direction (true = long, false = short)
529    pub is_buy: bool,
530    pub size: f64,
531    /// Trailing distance in basis points
532    pub trail_bps: u32,
533    /// Favorable reset step in basis points
534    pub step_bps: u32,
535    /// Optional triggered limit price; None means market-style trigger
536    pub limit_price: Option<f64>,
537    /// Isolated-margin flag
538    #[serde(default)]
539    pub iso: bool,
540}
541
542/// On-fill consequent: one-shot follow-up actions executed on first fill of a trigger action.
543/// Allowed consequent types: m, l, mod, cx, cxa, st, tp, rng, trig, trl.
544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
545pub struct OnFill {
546    /// Trigger action serialized inline with the on-fill registration.
547    pub trigger: Box<OrderItem>,
548    /// One-shot consequent actions executed on first fill of the trigger.
549    pub actions: Vec<OrderItem>,
550}
551
552// ============================================================================
553// Order Item (union type)
554// ============================================================================
555
556/// An item in the orders array
557#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
558#[serde(rename_all = "camelCase")]
559pub enum OrderItem {
560    /// Place a new order
561    Order(Order),
562    /// Modify an existing order size
563    Modify(Modify),
564    /// Cancel a specific order
565    Cancel(Cancel),
566    /// Cancel all orders
567    CancelAll(CancelAll),
568    /// Stop-loss conditional order
569    Stop(Stop),
570    /// Take-profit conditional order
571    TakeProfit(TakeProfit),
572    /// Range / OCO collar order
573    RangeOco(RangeOco),
574    /// Trigger basket: fires nested actions when price crosses threshold
575    TriggerBasket(TriggerBasket),
576    /// On-fill consequent: inline trigger plus one-shot follow-up actions
577    OnFill(OnFill),
578    /// Trailing stop: protective stop that follows price by a fixed bps distance
579    TrailingStop(TrailingStop),
580}
581
582impl OrderItem {
583    /// Get the discriminant for wincode serialization
584    pub const fn discriminant(&self) -> u32 {
585        match self {
586            Self::Order(order) => match order.order_type {
587                OrderType::Limit { .. } => 1,   // l
588                OrderType::Trigger { .. } => 0, // m
589            },
590            Self::Modify(_) => 2,        // mod
591            Self::Cancel(_) => 3,        // cx
592            Self::CancelAll(_) => 4,     // cxa
593            Self::Stop(_) => 5,          // st
594            Self::TakeProfit(_) => 6,    // tp
595            Self::RangeOco(_) => 7,      // rng
596            Self::TriggerBasket(_) => 8, // trig
597            Self::TrailingStop(_) => 9,  // trl
598            Self::OnFill(_) => 10,       // of
599        }
600    }
601}
602
603impl From<Order> for OrderItem {
604    fn from(order: Order) -> Self {
605        Self::Order(order)
606    }
607}
608
609impl From<Cancel> for OrderItem {
610    fn from(cancel: Cancel) -> Self {
611        Self::Cancel(cancel)
612    }
613}
614
615impl From<Modify> for OrderItem {
616    fn from(modify: Modify) -> Self {
617        Self::Modify(modify)
618    }
619}
620
621impl From<CancelAll> for OrderItem {
622    fn from(cancel_all: CancelAll) -> Self {
623        Self::CancelAll(cancel_all)
624    }
625}
626
627impl From<Stop> for OrderItem {
628    fn from(stop: Stop) -> Self {
629        Self::Stop(stop)
630    }
631}
632
633impl From<TakeProfit> for OrderItem {
634    fn from(tp: TakeProfit) -> Self {
635        Self::TakeProfit(tp)
636    }
637}
638
639impl From<RangeOco> for OrderItem {
640    fn from(rng: RangeOco) -> Self {
641        Self::RangeOco(rng)
642    }
643}
644
645impl From<TriggerBasket> for OrderItem {
646    fn from(trig: TriggerBasket) -> Self {
647        Self::TriggerBasket(trig)
648    }
649}
650
651impl From<OnFill> for OrderItem {
652    fn from(of: OnFill) -> Self {
653        Self::OnFill(of)
654    }
655}
656
657impl From<TrailingStop> for OrderItem {
658    fn from(trl: TrailingStop) -> Self {
659        Self::TrailingStop(trl)
660    }
661}
662
663// ============================================================================
664// Faucet
665// ============================================================================
666
667/// Request testnet funds
668#[derive(Debug, Clone, PartialEq)]
669pub struct Faucet {
670    /// User to receive funds
671    pub user: Pubkey,
672    /// Amount (optional, defaults to 10,000)
673    pub amount: Option<f64>,
674}
675
676impl Faucet {
677    /// Create a new faucet request
678    pub fn new(user: Pubkey) -> Self {
679        Self { user, amount: None }
680    }
681
682    /// Create a faucet request with specific amount
683    pub fn with_amount(user: Pubkey, amount: f64) -> Self {
684        Self {
685            user,
686            amount: Some(amount),
687        }
688    }
689}
690
691// ============================================================================
692// Agent Wallet
693// ============================================================================
694
695/// Register or remove an agent wallet
696#[derive(Debug, Clone, PartialEq)]
697pub struct AgentWallet {
698    /// Agent public key
699    pub agent: Pubkey,
700    /// Delete flag (true = remove, false = add)
701    pub delete: bool,
702}
703
704impl AgentWallet {
705    /// Add an agent wallet
706    pub fn add(agent: Pubkey) -> Self {
707        Self {
708            agent,
709            delete: false,
710        }
711    }
712
713    /// Remove an agent wallet
714    pub fn remove(agent: Pubkey) -> Self {
715        Self {
716            agent,
717            delete: true,
718        }
719    }
720}
721
722#[derive(Debug, Clone, PartialEq)]
723pub struct ApproveCommissionFee {
724    pub to: Pubkey,
725    pub max_fee: u8,
726}
727
728pub type ApproveBuilderCode = ApproveCommissionFee;
729
730#[derive(Debug, Clone, PartialEq)]
731pub struct RevokeCommissionFee {
732    pub to: Pubkey,
733}
734
735pub type RevokeBuilderCode = RevokeCommissionFee;
736
737// ============================================================================
738// Liquidator Config
739// ============================================================================
740
741/// Per-instrument liquidator limits
742#[derive(Debug, Clone, PartialEq, Default)]
743pub struct LiquidatorInstrumentConfig {
744    pub symbol: String,
745    pub max_exposure: f64,
746    pub reserve: f64,
747    pub rfactor: f64,
748    pub volume_percent: f64,
749    pub volume_min: f64,
750    pub volume_rampup: u64,
751    pub max_sweep_bps: f64,
752    pub max_adl_notional: f64,
753    pub max_adl_percent: f64,
754}
755
756impl LiquidatorInstrumentConfig {
757    /// Create an instrument config with every limit zeroed
758    pub fn new(symbol: impl Into<String>) -> Self {
759        Self {
760            symbol: symbol.into(),
761            ..Self::default()
762        }
763    }
764}
765
766/// Update the signing account's liquidator configuration
767#[derive(Debug, Clone, PartialEq, Default)]
768pub struct LiquidatorConfig {
769    pub cross_exposure: f64,
770    pub scoring_skew: f64,
771    /// Downscales every instrument's max exposure. Range 0-100 (0 = off)
772    pub toxicity: f64,
773    pub urgency_size_fraction: f64,
774    pub sweep_sds: f64,
775    pub instruments: Vec<LiquidatorInstrumentConfig>,
776}
777
778impl LiquidatorConfig {
779    /// Create a config with no instruments
780    pub fn new(
781        cross_exposure: f64,
782        scoring_skew: f64,
783        toxicity: f64,
784        urgency_size_fraction: f64,
785        sweep_sds: f64,
786    ) -> Self {
787        Self {
788            cross_exposure,
789            scoring_skew,
790            toxicity,
791            urgency_size_fraction,
792            sweep_sds,
793            instruments: Vec::new(),
794        }
795    }
796
797    /// Add an instrument config, replacing any existing entry for its symbol
798    pub fn with_instrument(mut self, instrument: LiquidatorInstrumentConfig) -> Self {
799        self.instruments.retain(|i| i.symbol != instrument.symbol);
800        self.instruments.push(instrument);
801        self
802    }
803
804    /// Instruments in canonical (sorted-symbol) wire order
805    pub fn sorted_instruments(&self) -> Vec<&LiquidatorInstrumentConfig> {
806        let mut sorted: Vec<_> = self.instruments.iter().collect();
807        sorted.sort_by(|a, b| a.symbol.cmp(&b.symbol));
808        sorted
809    }
810}
811
812/// Build the `liq` action body, with instruments in signed-byte order
813pub(crate) fn liquidator_config_to_json(config: &LiquidatorConfig) -> serde_json::Value {
814    let instruments: Vec<_> = config
815        .sorted_instruments()
816        .into_iter()
817        .map(|i| {
818            serde_json::json!({
819                "symbol": i.symbol,
820                "max_exposure": i.max_exposure,
821                "reserve": i.reserve,
822                "rfactor": i.rfactor,
823                "volume_percent": i.volume_percent,
824                "volume_min": i.volume_min,
825                "volume_rampup": i.volume_rampup,
826                "max_sweep_bps": i.max_sweep_bps,
827                "max_adl_notional": i.max_adl_notional,
828                "max_adl_percent": i.max_adl_percent,
829            })
830        })
831        .collect();
832
833    serde_json::json!({
834        "cross_exposure": config.cross_exposure,
835        "scoring_skew": config.scoring_skew,
836        "toxicity": config.toxicity,
837        "urgency_size_fraction": config.urgency_size_fraction,
838        "sweep_sds": config.sweep_sds,
839        "instruments": instruments,
840    })
841}
842
843// ============================================================================
844// User Settings
845// ============================================================================
846
847/// Update user settings (leverage)
848#[derive(Debug, Clone, PartialEq)]
849pub struct UserSettings {
850    /// Max leverage per symbol: [(symbol, leverage), ...]
851    pub max_leverage: Vec<(String, f64)>,
852}
853
854impl UserSettings {
855    /// Create new user settings
856    pub fn new(max_leverage: Vec<(String, f64)>) -> Self {
857        Self { max_leverage }
858    }
859
860    /// Set leverage for a single symbol
861    pub fn set_leverage(symbol: impl Into<String>, leverage: f64) -> Self {
862        Self {
863            max_leverage: vec![(symbol.into(), leverage)],
864        }
865    }
866}
867
868// ============================================================================
869// Oracle
870// ============================================================================
871
872/// Oracle price update (permissioned)
873#[derive(Debug, Clone, PartialEq)]
874pub struct OraclePrice {
875    /// Timestamp
876    pub timestamp: u64,
877    /// Asset symbol (e.g., "BTC")
878    pub asset: String,
879    /// Price
880    pub price: f64,
881}
882
883/// Pyth oracle price entry (admin `o` action)
884#[derive(Debug, Clone, PartialEq)]
885pub struct PythOraclePrice {
886    /// Timestamp
887    pub timestamp: u64,
888    /// Feed index
889    pub feed_index: u64,
890    /// Raw price integer
891    pub price: u64,
892    /// Decimal exponent
893    pub exponent: i16,
894}
895
896/// Whitelist/un-whitelist an account for faucet access (admin)
897#[derive(Debug, Clone, PartialEq)]
898pub struct WhitelistFaucet {
899    /// Target account pubkey
900    pub target: Pubkey,
901    /// true = whitelist, false = un-whitelist
902    pub whitelist: bool,
903}
904
905// ============================================================================
906// Create Sub Account
907// ============================================================================
908
909/// Create a named sub-account under the signing master account, with an
910/// optional initial margin transfer.
911#[derive(Debug, Clone, PartialEq)]
912pub struct CreateSubAccount {
913    /// Sub-account display name
914    pub name: String,
915    /// Optional initial margin amount. Default 0.0
916    pub margin_amount: Option<f64>,
917}
918
919impl CreateSubAccount {
920    /// Create a sub-account with no initial margin transfer.
921    pub fn new(name: impl Into<String>) -> Self {
922        Self {
923            name: name.into(),
924            margin_amount: None,
925        }
926    }
927
928    /// Create a sub-account with an initial margin transfer.
929    pub fn with_margin(name: impl Into<String>, margin_amount: f64) -> Self {
930        Self {
931            name: name.into(),
932            margin_amount: Some(margin_amount),
933        }
934    }
935}
936
937// ============================================================================
938// Remove Sub Account
939// ============================================================================
940
941/// Remove a sub-account belonging to the signing master account.
942#[derive(Debug, Clone, PartialEq)]
943pub struct RemoveSubAccount {
944    pub to_remove: Pubkey,
945}
946
947impl RemoveSubAccount {
948    pub fn new(to_remove: Pubkey) -> Self {
949        Self { to_remove }
950    }
951}
952
953// ============================================================================
954// Rename Sub Account
955// ============================================================================
956
957/// Rename a sub-account belonging to the signing master account.
958#[derive(Debug, Clone, PartialEq)]
959pub struct RenameSubAccount {
960    pub account: Pubkey,
961    pub name: String,
962}
963
964impl RenameSubAccount {
965    pub fn new(account: Pubkey, name: impl Into<String>) -> Self {
966        Self {
967            account,
968            name: name.into(),
969        }
970    }
971}
972
973// ============================================================================
974// Transfer
975// ============================================================================
976
977/// Direction of a margin transfer.
978#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
979pub enum TransferKind {
980    /// Between two accounts inside BULK.
981    #[default]
982    Internal,
983    /// To/from an external destination.
984    External,
985}
986
987/// Transfer margin between accounts.
988#[derive(Debug, Clone, PartialEq)]
989pub struct Transfer {
990    pub kind: TransferKind,
991    pub from: Pubkey,
992    pub to: Pubkey,
993    pub margin_amount: f64,
994}
995
996impl Transfer {
997    /// Internal transfer between two BULK accounts.
998    pub fn internal(from: Pubkey, to: Pubkey, margin_amount: f64) -> Self {
999        Self {
1000            kind: TransferKind::Internal,
1001            from,
1002            to,
1003            margin_amount,
1004        }
1005    }
1006
1007    /// External transfer in/out of BULK.
1008    pub fn external(from: Pubkey, to: Pubkey, margin_amount: f64) -> Self {
1009        Self {
1010            kind: TransferKind::External,
1011            from,
1012            to,
1013            margin_amount,
1014        }
1015    }
1016}
1017
1018// ============================================================================
1019// Portfolio withdraw
1020// ============================================================================
1021
1022/// Portfolio withdraw from the deposit/withdraw vault.
1023#[derive(Debug, Clone, PartialEq)]
1024pub struct Withdraw {
1025    pub user: Pubkey,
1026    pub vault: Pubkey,
1027    pub recipient_token_account: Pubkey,
1028    pub amount: u64,
1029    pub blockhash: Hash,
1030}
1031
1032/// Recover a withdraw lock after a withdraw is rejected or interrupted.
1033#[derive(Debug, Clone, PartialEq)]
1034pub struct WithdrawLockRecover {
1035    pub user: Pubkey,
1036    pub hash: Hash,
1037}
1038
1039// ============================================================================
1040// Multisig
1041// ============================================================================
1042
1043#[derive(Debug, Clone, PartialEq)]
1044pub struct CreateMultisig {
1045    pub signers: Vec<Pubkey>,
1046    pub threshold: u32,
1047    pub time_lock_secs: u32,
1048    pub proposal_lifetime_secs: u32,
1049}
1050
1051impl CreateMultisig {
1052    pub fn new(signers: Vec<Pubkey>, threshold: u32) -> Self {
1053        Self {
1054            signers,
1055            threshold,
1056            time_lock_secs: 0,
1057            proposal_lifetime_secs: 7 * 24 * 3600,
1058        }
1059    }
1060}
1061
1062#[derive(Debug, Clone, PartialEq)]
1063pub struct MultisigPropose {
1064    pub multisig: Pubkey,
1065    pub actions: Vec<Action>,
1066    pub proposal_lifetime_secs: Option<u32>,
1067}
1068
1069impl MultisigPropose {
1070    pub fn new(multisig: Pubkey, actions: Vec<Action>) -> Self {
1071        Self {
1072            multisig,
1073            actions,
1074            proposal_lifetime_secs: None,
1075        }
1076    }
1077}
1078
1079#[derive(Debug, Clone, PartialEq)]
1080pub struct MultisigApprove {
1081    pub multisig: Pubkey,
1082    pub proposal_id: u64,
1083}
1084
1085impl MultisigApprove {
1086    pub fn new(multisig: Pubkey, proposal_id: u64) -> Self {
1087        Self {
1088            multisig,
1089            proposal_id,
1090        }
1091    }
1092}
1093
1094#[derive(Debug, Clone, PartialEq)]
1095pub struct MultisigReject {
1096    pub multisig: Pubkey,
1097    pub proposal_id: u64,
1098}
1099
1100impl MultisigReject {
1101    pub fn new(multisig: Pubkey, proposal_id: u64) -> Self {
1102        Self {
1103            multisig,
1104            proposal_id,
1105        }
1106    }
1107}
1108
1109#[derive(Debug, Clone, PartialEq)]
1110pub struct MultisigCancel {
1111    pub multisig: Pubkey,
1112    pub proposal_id: u64,
1113}
1114
1115impl MultisigCancel {
1116    pub fn new(multisig: Pubkey, proposal_id: u64) -> Self {
1117        Self {
1118            multisig,
1119            proposal_id,
1120        }
1121    }
1122}
1123
1124#[derive(Debug, Clone, PartialEq)]
1125pub struct MultisigExecute {
1126    pub multisig: Pubkey,
1127    pub proposal_id: u64,
1128}
1129
1130impl MultisigExecute {
1131    pub fn new(multisig: Pubkey, proposal_id: u64) -> Self {
1132        Self {
1133            multisig,
1134            proposal_id,
1135        }
1136    }
1137}
1138
1139#[derive(Debug, Clone, PartialEq)]
1140pub struct UpdateMultisigPolicy {
1141    pub multisig: Pubkey,
1142    pub signers: Vec<Pubkey>,
1143    pub threshold: u32,
1144    pub time_lock_secs: u32,
1145    pub proposal_lifetime_secs: u32,
1146}
1147
1148impl UpdateMultisigPolicy {
1149    pub fn new(multisig: Pubkey, signers: Vec<Pubkey>, threshold: u32) -> Self {
1150        Self {
1151            multisig,
1152            signers,
1153            threshold,
1154            time_lock_secs: 0,
1155            proposal_lifetime_secs: 7 * 24 * 3600,
1156        }
1157    }
1158}
1159
1160// ============================================================================
1161// Action (main enum)
1162// ============================================================================
1163
1164/// Transaction action type
1165#[derive(Debug, Clone, PartialEq)]
1166pub enum Action {
1167    /// Order operations (place, cancel, cancel all)
1168    Order { orders: Vec<OrderItem> },
1169    /// Oracle price updates (`px`)
1170    Oracle { oracles: Vec<OraclePrice> },
1171    /// Batch Pyth oracle updates (`o`)
1172    PythOracle { oracles: Vec<PythOraclePrice> },
1173    /// Request testnet funds
1174    Faucet(Faucet),
1175    /// Update user settings
1176    UpdateUserSettings(UserSettings),
1177    /// Agent wallet management
1178    AgentWalletCreation(AgentWallet),
1179    /// Whitelist faucet access for an account (admin)
1180    WhitelistFaucet(WhitelistFaucet),
1181    /// Create a named sub-account (optional initial margin transfer)
1182    CreateSubAccount(CreateSubAccount),
1183    /// Remove a sub-account
1184    RemoveSubAccount(RemoveSubAccount),
1185    /// Rename a sub-account
1186    RenameSubAccount(RenameSubAccount),
1187    /// Margin transfer between accounts
1188    Transfer(Transfer),
1189    /// Portfolio withdraw
1190    Withdraw(Withdraw),
1191    /// Recover a withdraw lock
1192    WithdrawLockRecover(WithdrawLockRecover),
1193    /// Create a multisig account
1194    CreateMultisig(CreateMultisig),
1195    /// Propose one or more actions for a multisig account
1196    MultisigPropose(MultisigPropose),
1197    /// Approve a multisig proposal
1198    MultisigApprove(MultisigApprove),
1199    /// Reject a multisig proposal
1200    MultisigReject(MultisigReject),
1201    /// Cancel a multisig proposal
1202    MultisigCancel(MultisigCancel),
1203    /// Execute a multisig proposal
1204    MultisigExecute(MultisigExecute),
1205    /// Update a multisig policy
1206    UpdateMultisigPolicy(UpdateMultisigPolicy),
1207    /// Approve a builder-code recipient
1208    ApproveCommissionFee(ApproveCommissionFee),
1209    /// Revoke a builder-code recipient
1210    RevokeCommissionFee(RevokeCommissionFee),
1211    /// Update the signing account's liquidator configuration
1212    UpdateLiquidatorConfig(LiquidatorConfig),
1213}
1214
1215impl Action {
1216    /// Get the discriminant for wincode serialization
1217    pub const fn discriminant(&self) -> u32 {
1218        match self {
1219            Self::Order { .. } => 0,  // container variant, not a wire discriminant
1220            Self::Oracle { .. } => 5, // px
1221            Self::PythOracle { .. } => 6,
1222            Self::Faucet(_) => 7,
1223            Self::UpdateUserSettings(_) => 9,
1224            Self::AgentWalletCreation(_) => 8,
1225            Self::WhitelistFaucet(_) => 10,
1226            Self::ApproveCommissionFee(_) => 40,
1227            Self::RevokeCommissionFee(_) => 41,
1228            Self::CreateSubAccount(_) => 27,
1229            Self::RemoveSubAccount(_) => 28,
1230            Self::Transfer(_) => 29,
1231            Self::CreateMultisig(_) => 30,
1232            Self::MultisigPropose(_) => 31,
1233            Self::MultisigApprove(_) => 32,
1234            Self::MultisigReject(_) => 33,
1235            Self::MultisigCancel(_) => 34,
1236            Self::MultisigExecute(_) => 35,
1237            Self::UpdateMultisigPolicy(_) => 36,
1238            Self::RenameSubAccount(_) => 37,
1239            Self::Withdraw(_) => 45,
1240            Self::WithdrawLockRecover(_) => 54,
1241            Self::UpdateLiquidatorConfig(_) => 43,
1242        }
1243    }
1244
1245    /// Get the action type string for JSON
1246    pub const fn type_str(&self) -> &'static str {
1247        match self {
1248            Self::Order { .. } => "order",
1249            Self::Oracle { .. } => "px",
1250            Self::PythOracle { .. } => "o",
1251            Self::Faucet(_) => "faucet",
1252            Self::UpdateUserSettings(_) => "updateUserSettings",
1253            Self::AgentWalletCreation(_) => "agentWalletCreation",
1254            Self::WhitelistFaucet(_) => "whitelistFaucet",
1255            Self::ApproveCommissionFee(_) => "abc",
1256            Self::RevokeCommissionFee(_) => "rbc",
1257            Self::CreateSubAccount(_) => "createSubAccount",
1258            Self::RemoveSubAccount(_) => "removeSubAccount",
1259            Self::Transfer(_) => "transfer",
1260            Self::CreateMultisig(_) => "createMultisig",
1261            Self::MultisigPropose(_) => "msp",
1262            Self::MultisigApprove(_) => "msa",
1263            Self::MultisigReject(_) => "msr",
1264            Self::MultisigCancel(_) => "msc",
1265            Self::MultisigExecute(_) => "mse",
1266            Self::UpdateMultisigPolicy(_) => "msu",
1267            Self::RenameSubAccount(_) => "renameSubAccount",
1268            Self::Withdraw(_) => "withdraw",
1269            Self::WithdrawLockRecover(_) => "withdrawLockRecover",
1270            Self::UpdateLiquidatorConfig(_) => "liq",
1271        }
1272    }
1273}
1274
1275impl From<LiquidatorConfig> for Action {
1276    fn from(config: LiquidatorConfig) -> Self {
1277        Self::UpdateLiquidatorConfig(config)
1278    }
1279}
1280
1281impl From<RenameSubAccount> for Action {
1282    fn from(action: RenameSubAccount) -> Self {
1283        Self::RenameSubAccount(action)
1284    }
1285}
1286
1287impl From<CreateMultisig> for Action {
1288    fn from(action: CreateMultisig) -> Self {
1289        Self::CreateMultisig(action)
1290    }
1291}
1292
1293impl From<MultisigPropose> for Action {
1294    fn from(action: MultisigPropose) -> Self {
1295        Self::MultisigPropose(action)
1296    }
1297}
1298
1299impl From<MultisigApprove> for Action {
1300    fn from(action: MultisigApprove) -> Self {
1301        Self::MultisigApprove(action)
1302    }
1303}
1304
1305impl From<MultisigReject> for Action {
1306    fn from(action: MultisigReject) -> Self {
1307        Self::MultisigReject(action)
1308    }
1309}
1310
1311impl From<MultisigCancel> for Action {
1312    fn from(action: MultisigCancel) -> Self {
1313        Self::MultisigCancel(action)
1314    }
1315}
1316
1317impl From<MultisigExecute> for Action {
1318    fn from(action: MultisigExecute) -> Self {
1319        Self::MultisigExecute(action)
1320    }
1321}
1322
1323impl From<UpdateMultisigPolicy> for Action {
1324    fn from(action: UpdateMultisigPolicy) -> Self {
1325        Self::UpdateMultisigPolicy(action)
1326    }
1327}
1328
1329// ============================================================================
1330// Signed Transaction
1331// ============================================================================
1332
1333/// A signed transaction ready to submit to the API
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335pub struct SignedTransaction {
1336    /// Actions to execute atomically (compact tagged format)
1337    pub actions: Vec<serde_json::Value>,
1338    /// Transaction nonce
1339    #[serde(with = "crate::nonce::serde_decimal")]
1340    pub nonce: u64,
1341    /// Account public key (base58)
1342    pub account: String,
1343    /// Signer public key (base58)
1344    pub signer: String,
1345    /// Signature (base58)
1346    pub signature: String,
1347    /// Optional pre-computed order ID for client-side optimistic tracking.
1348    /// This is not part of the API request payload.
1349    #[serde(skip_serializing, skip_deserializing, default)]
1350    pub order_id: Option<String>,
1351    /// Optional pre-computed order IDs for multi-order transactions.
1352    /// This is not part of the API request payload.
1353    #[serde(skip_serializing, skip_deserializing, default)]
1354    pub order_ids: Option<Vec<String>>,
1355}
1356
1357impl SignedTransaction {
1358    /// Serialize to JSON string
1359    pub fn to_json(&self) -> crate::Result<String> {
1360        serde_json::to_string(self).map_err(crate::Error::from)
1361    }
1362
1363    /// Serialize to JSON bytes
1364    pub fn to_json_bytes(&self) -> crate::Result<Vec<u8>> {
1365        serde_json::to_vec(self).map_err(crate::Error::from)
1366    }
1367}