Skip to main content

bulk_client/msgs/
account.rs

1use std::collections::HashMap;
2use serde::{Deserialize, Serialize};
3use solana_pubkey::Pubkey;
4use crate::common::order_status::OrderStatus;
5use crate::common::order_type::OrderType;
6use crate::common::side::Side;
7use crate::common::tif::TimeInForce;
8use crate::transaction::ActionMeta;
9
10// ─────────────────────────────────────────────────────────────────────────────
11// Faucet Request
12// ─────────────────────────────────────────────────────────────────────────────
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct Faucet {
16    #[serde(with = "crate::msgs::serde_pubkey", rename = "u")]
17    pub user: Pubkey,
18    pub amount: Option<f64>,
19
20    #[serde(skip)]
21    pub meta: ActionMeta,
22}
23
24
25// ─────────────────────────────────────────────────────────────────────────────
26// Faucet issuer whitelist
27// ─────────────────────────────────────────────────────────────────────────────
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
30pub struct WhitelistFaucet {
31    #[serde(with = "crate::msgs::serde_pubkey")]
32    pub target: Pubkey,
33    pub whitelist: bool,
34
35    #[serde(skip)]
36    pub meta: ActionMeta,
37}
38
39// ─────────────────────────────────────────────────────────────────────────────
40// Agent Wallet
41// ─────────────────────────────────────────────────────────────────────────────
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct AgentWalletCreation {
45    #[serde(with = "crate::msgs::serde_pubkey", rename = "a")]
46    pub agent: Pubkey,
47    #[serde(rename = "d")]
48    pub delete: bool,
49
50    #[serde(skip)]
51    pub meta: ActionMeta,
52}
53
54// ─────────────────────────────────────────────────────────────────────────────
55// User Leverage Settings
56// ─────────────────────────────────────────────────────────────────────────────
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct UpdateUserSettings {
60    #[serde(rename = "m")]
61    pub max_leverage: HashMap<String, f64>,
62
63    #[serde(skip)]
64    pub meta: ActionMeta,
65}
66
67
68// ─────────────────────────────────────────────────────────────────────────────
69// Margin
70// ─────────────────────────────────────────────────────────────────────────────
71
72/// Account margin information (from WebSocket `marginUpdate` / `accountSnapshot`).
73#[derive(Debug, Clone, Default, Deserialize)]
74#[allow(unused)]
75pub struct Margin {
76    #[serde(rename = "totalBalance")]
77    pub total_balance: f64,
78    #[serde(rename = "availableBalance")]
79    pub available_balance: f64,
80    #[serde(rename = "marginUsed")]
81    pub margin_used: f64,
82    pub notional: f64,
83    #[serde(rename = "realizedPnl")]
84    pub realized_pnl: f64,
85    #[serde(rename = "unrealizedPnl")]
86    pub unrealized_pnl: f64,
87    pub fees: f64,
88    pub funding: f64,
89}
90
91// ─────────────────────────────────────────────────────────────────────────────
92// Positions
93// ─────────────────────────────────────────────────────────────────────────────
94
95/// Position information.
96///
97/// Deserializes from both WebSocket and HTTP payloads:
98/// - WS uses `"symbol"`, HTTP uses `"coin"` → `#[serde(alias)]`
99/// - Fields only present on WS default to `0.0` when absent (HTTP).
100#[derive(Debug, Clone, Deserialize)]
101#[allow(unused)]
102pub struct PositionInfo {
103    #[serde(alias = "coin")]
104    pub symbol: String,
105    pub size: f64,
106    pub price: f64,
107    #[serde(rename = "fairPrice", default)]
108    pub fair_price: f64,
109    #[serde(default)]
110    pub notional: f64,
111    #[serde(rename = "realizedPnl", default)]
112    pub realized_pnl: f64,
113    #[serde(rename = "unrealizedPnl", default)]
114    pub unrealized_pnl: f64,
115    #[serde(default)]
116    pub leverage: f64,
117    #[serde(rename = "liquidationPrice", default)]
118    pub liquidation_price: f64,
119    #[serde(rename = "maintenanceMargin", default)]
120    pub maintenance_margin: f64,
121}
122
123// ─────────────────────────────────────────────────────────────────────────────
124// Order State
125// ─────────────────────────────────────────────────────────────────────────────
126
127#[derive(Debug, Serialize, Deserialize, Clone)]
128#[serde(rename_all = "camelCase")]
129pub struct TriggerSpec {
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub is_above: Option<bool>,
132    pub px: f64,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub lim: Option<f64>,
135    pub oco: Option<String>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub px_hi: Option<f64>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub lim_hi: Option<f64>,
140    #[serde(rename = "trb", skip_serializing_if = "Option::is_none")]
141    pub trail_bps: Option<u32>,
142    #[serde(rename = "stb", skip_serializing_if = "Option::is_none")]
143    pub step_bps: Option<u32>,
144}
145
146/// Resting or historical order state.
147///
148/// Deserializes from both WebSocket and HTTP payloads:
149/// - `vwap`, `reduce_only`, `tif` are HTTP-only (default when absent).
150/// - `error` / `reason` is WS-only (default when absent).
151#[derive(Debug, Clone, Deserialize)]
152#[allow(unused)]
153pub struct OrderState {
154    #[serde(rename = "ot")]
155    pub order_type: OrderType,
156    pub status: OrderStatus,
157    #[serde(rename = "sym")]
158    pub symbol: String,
159    #[serde(rename = "oid")]
160    pub order_id: String,
161    #[serde(rename = "px")]
162    pub price: f64,
163    #[serde(rename = "origSz")]
164    pub original_size: f64,
165    #[serde(rename = "sz")]
166    pub signed_size: f64,
167    #[serde(rename = "fillSz")]
168    pub filled_size: f64,
169    pub vwap: f64,
170    pub tif: TimeInForce,
171    #[serde(rename = "r")]
172    pub reduce_only: bool,
173    #[serde(rename = "mk")]
174    pub maker: bool,
175    #[serde(default)]
176    pub trigger: Option<TriggerSpec>,
177    #[serde(rename = "ts")]
178    pub timestamp: u64,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub reason: Option<String>,
181}
182
183impl OrderState {
184    /// Provide side of order
185    pub fn side(&self) -> Side {
186        if self.signed_size < 0.0 {
187            Side::Sell
188        } else {
189            Side::Buy
190        }
191    }
192
193    /// Magnitude of size
194    pub fn amount(&self) -> f64 {
195        self.signed_size.abs()
196    }
197}
198
199// ─────────────────────────────────────────────────────────────────────────────
200// Fills
201// ─────────────────────────────────────────────────────────────────────────────
202
203#[derive(Debug, Clone, Deserialize)]
204#[allow(unused)]
205pub struct Fill {
206    pub timestamp: u64,
207    #[serde(alias = "coin")]
208    pub symbol: String,
209    #[serde(rename = "orderId")]
210    pub order_id: String,
211    pub price: f64,
212    pub size: f64,
213    #[serde(rename = "isBuy")]
214    pub side: Side,
215    #[serde(rename = "maker", default)]
216    pub is_maker: bool,
217    #[serde(rename = "counterpartyHint", default)]
218    pub cpty: String,
219    pub reason: Option<String>,
220}
221
222// ─────────────────────────────────────────────────────────────────────────────
223// Leverage Setting
224// ─────────────────────────────────────────────────────────────────────────────
225
226#[derive(Debug, Clone, Deserialize)]
227#[allow(unused)]
228pub struct LeverageSetting {
229    #[serde(alias = "coin")]
230    pub symbol: String,
231    pub leverage: f64,
232}
233
234// ─────────────────────────────────────────────────────────────────────────────
235// Full Account (HTTP response)
236// ─────────────────────────────────────────────────────────────────────────────
237
238/// The inner payload of a `fullAccount` HTTP response.
239///
240#[derive(Debug, Clone, Deserialize)]
241#[serde(rename_all = "camelCase")]
242#[allow(unused)]
243pub struct AccountData {
244    pub positions: Vec<PositionInfo>,
245    pub open_orders: Vec<OrderState>,
246    pub margin: Margin,
247    pub leverage_settings: Vec<LeverageSetting>,
248}
249
250
251//
252// Unit tests
253//
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_order_state_rejected_risk_limit() {
261        let json = r#"{
262            "ts": 1770918312787284000,
263            "ot": "limit",
264            "status": "rejectedRiskLimit",
265            "sym": "BTC-USD",
266            "oid": "EF2bxQ5pp3CDFAwRi44ExXb32sRmByByYxjwLYBfvRKQ",
267            "px": 100001.37,
268            "origSz": -0.02474,
269            "sz": -0.02474,
270            "fillSz": 0.0,
271            "vwap": 0.0,
272            "mk": true,
273            "r": false,
274            "tif": "gtc",
275            "reason": "no oracle / fair price reference yet for: BTC-USD"
276        }"#;
277
278        let order: OrderState = serde_json::from_str(json).unwrap();
279
280        assert_eq!(order.symbol, "BTC-USD");
281        assert_eq!(order.order_id, "EF2bxQ5pp3CDFAwRi44ExXb32sRmByByYxjwLYBfvRKQ");
282        assert_eq!(order.status, OrderStatus::RejectedRiskLimit);
283        assert!(order.signed_size < 0.0);
284        assert!((order.price - 100001.37).abs() < 1e-6);
285        assert!((order.original_size.abs() - 0.02474).abs() < 1e-8);
286        assert!((order.signed_size.abs() - 0.02474).abs() < 1e-8);
287        assert_eq!(order.filled_size, 0.0);
288        assert!(order.maker);
289        assert_eq!(order.timestamp, 1770918312787284000);
290        assert_eq!(
291            order.reason.as_deref(),
292            Some("no oracle / fair price reference yet for: BTC-USD")
293        );
294
295        // status helpers
296        assert!(order.status.is_terminal());
297        assert!(order.status.is_rejected());
298    }
299
300}