Skip to main content

bulk_client/msgs/
account.rs

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