1use std::collections::HashMap;
40use std::str::FromStr;
41use std::sync::Arc;
42use std::time::Duration;
43use reqwest::{Client, Url};
44use serde::Deserialize;
45use serde_json::{json, Value};
46use solana_pubkey::Pubkey;
47use solana_hash::Hash;
48use crate::api::parts::{make_nonce, HttpConfig};
49use crate::common::side::Side;
50use crate::common::tif::TimeInForce;
51use crate::msgs::*;
52use crate::transaction::{Action, ActionMeta, Transaction, TransactionSigner};
53
54#[derive(Clone)]
60#[allow(unused)]
61pub struct BulkHttpClient {
62 config: HttpConfig,
63 client: Client,
64 is_localhost: bool,
65}
66
67#[allow(unused)]
68impl BulkHttpClient {
69 pub fn new (config: &HttpConfig) -> eyre::Result<Self> {
74 let client = Client::builder()
75 .timeout(config.default_timeout)
76 .build()?;
77
78 let is_localhost = Self::is_localhost(&config.base_url);
79
80 Ok(Self {
81 config: config.clone(),
82 client,
83 is_localhost,
84 })
85 }
86
87 pub fn with_url (base_url: &str, private_key: Option<&str>) -> eyre::Result<Self> {
93 if let Some(private_key) = private_key {
94 let signer = TransactionSigner::from_private_key(private_key)?;
95 let config = HttpConfig {
96 base_url: base_url.to_string(),
97 signer: Some(signer),
98 default_timeout: Duration::from_secs(10)
99 };
100 Self::new(&config)
101 } else {
102 let config = HttpConfig {
103 base_url: base_url.to_string(),
104 signer: None,
105 default_timeout: Duration::from_secs(10)
106 };
107 Self::new(&config)
108 }
109 }
110
111 pub fn with_signer(base_url: &str, signer: TransactionSigner) -> eyre::Result<Self> {
120 let config = HttpConfig {
121 base_url: base_url.to_string(),
122 signer: Some(signer),
123 default_timeout: Duration::from_secs(10),
124 };
125 Self::new(&config)
126 }
127
128 pub fn config(&self) -> &HttpConfig {
130 &self.config
131 }
132
133 pub fn public_key(&self) -> Option<Pubkey> {
135 self.config.signer.as_ref()
136 .map(|x| x.public_key())
137 }
138
139 pub async fn get_exchange_info(&self) -> eyre::Result<Vec<MarketInfo>> {
145 let resp = self
146 .client
147 .get(format!("{}/exchangeInfo", self.config.base_url))
148 .send()
149 .await?
150 .error_for_status()?;
151 Ok(resp.json().await?)
152 }
153
154 pub async fn get_ticker(&self, symbol: &str) -> eyre::Result<Ticker> {
156 let resp = self
157 .client
158 .get(format!("{}/ticker/{}", self.config.base_url, symbol))
159 .send()
160 .await?
161 .error_for_status()?;
162 Ok(resp.json().await?)
163 }
164
165 pub async fn get_klines(
174 &self,
175 symbol: &str,
176 interval: &str,
177 start_time: Option<u64>,
178 end_time: Option<u64>,
179 limit: Option<u32>,
180 ) -> eyre::Result<Vec<Candle>> {
181 let mut params = vec![
182 ("symbol".to_string(), symbol.to_string()),
183 ("interval".to_string(), interval.to_string()),
184 ("limit".to_string(), limit.unwrap_or(500).to_string()),
185 ];
186 if let Some(st) = start_time {
187 params.push(("startTime".to_string(), st.to_string()));
188 }
189 if let Some(et) = end_time {
190 params.push(("endTime".to_string(), et.to_string()));
191 }
192
193 let resp = self
194 .client
195 .get(format!("{}/klines", self.config.base_url))
196 .query(¶ms)
197 .send()
198 .await?
199 .error_for_status()?;
200 Ok(resp.json().await?)
201 }
202
203 pub async fn get_orderbook(
210 &self,
211 symbol: &str,
212 nlevels: Option<u32>,
213 aggregation: Option<f64>,
214 ) -> eyre::Result<L2Snapshot> {
215 let mut params = vec![
216 ("type".to_string(), "l2Book".to_string()),
217 ("coin".to_string(), symbol.to_string()),
218 ("nlevels".to_string(), nlevels.unwrap_or(20).to_string()),
219 ];
220 if let Some(agg) = aggregation {
221 params.push(("aggregation".to_string(), agg.to_string()));
222 }
223
224 let resp = self
225 .client
226 .get(format!("{}/l2book", self.config.base_url))
227 .query(¶ms)
228 .send()
229 .await?
230 .error_for_status()?;
231 Ok(resp.json().await?)
232 }
233
234 pub async fn get_account(&self, user: Pubkey) -> eyre::Result<AccountData> {
243 #[derive(Debug, Clone, Deserialize)]
244 #[serde(rename_all = "camelCase")]
245 pub struct FullAccountResponse {
246 pub full_account: AccountData,
247 }
248
249 let user: String = user.to_string();
250
251 let resp = self
252 .client
253 .post(format!("{}/account", self.config.base_url))
254 .json(&json!({ "type": "fullAccount", "user": user }))
255 .send()
256 .await?
257 .error_for_status()?;
258
259 let arr: Vec<FullAccountResponse> = resp.json().await?;
260 arr.into_iter()
261 .next()
262 .map(|r| r.full_account)
263 .ok_or_else(|| eyre::eyre!("empty fullAccount response"))
264 }
265
266 pub async fn get_open_orders(&self, user: &str) -> eyre::Result<Vec<OrderState>> {
271 let resp = self
272 .client
273 .post(format!("{}/account", self.config.base_url))
274 .json(&json!({ "type": "openOrders", "user": user }))
275 .send()
276 .await?
277 .error_for_status()?;
278 Ok(resp.json().await?)
279 }
280
281 pub async fn get_fills(&self, user: &str) -> eyre::Result<Vec<Fill>> {
286 let resp = self
287 .client
288 .post(format!("{}/account", self.config.base_url))
289 .json(&json!({ "type": "fills", "user": user }))
290 .send()
291 .await?
292 .error_for_status()?;
293 Ok(resp.json().await?)
294 }
295
296 pub async fn get_position_history(&self, user: &str) -> eyre::Result<Vec<PositionInfo>> {
301 let resp = self
302 .client
303 .post(format!("{}/account", self.config.base_url))
304 .json(&json!({ "type": "positions", "user": user }))
305 .send()
306 .await?
307 .error_for_status()?;
308 Ok(resp.json().await?)
309 }
310
311 pub async fn place_tx(
327 &self,
328 actions: Vec<Action>,
329 account: Option<Pubkey>,
330 nonce: Option<u64>,
331 ) -> eyre::Result<Vec<Response>> {
332 let signer = self
333 .config
334 .signer
335 .as_ref()
336 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
337
338 let account = if let Some(account) = account {
339 account
340 } else {
341 signer.public_key()
342 };
343
344 let nonce = nonce.unwrap_or_else(make_nonce);
345 let pk = signer.public_key();
346
347 let mut tx = Transaction {
349 actions,
350 nonce,
351 account: account,
352 signer: signer.public_key(),
353 signature: Default::default(),
354 };
355 tx.sign(signer)?;
356
357 let body = serde_json::to_string(&tx)?;
359
360 let mut request = self
361 .client
362 .post(format!("{}/order", self.config.base_url))
363 .header("content-type", "application/json");
364 if let Some(mode) = signer.tx_signature_mode_hint_header_value() {
365 request = request.header("X-Bulk-Sig-Mode", mode);
366 }
367 let resp = request.body(body).send().await?.error_for_status()?;
368
369 let data: Value = resp.json().await?;
370 Ok(Response::parse_responses(&data))
371 }
372
373 pub async fn place_limit_order(
375 &self,
376 symbol: &str,
377 side: Side,
378 price: f64,
379 size: f64,
380 tif: TimeInForce,
381 reduce_only: bool,
382 account: Option<Pubkey>,
383 nonce: Option<u64>,
384 ) -> eyre::Result<Response> {
385 let signer = self
386 .config
387 .signer
388 .as_ref()
389 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
390
391 let account = if let Some(account) = account {
392 account
393 } else {
394 signer.public_key()
395 };
396
397 let nonce = nonce.unwrap_or_else(make_nonce);
398
399 let order = LimitOrder {
400 symbol: Arc::from(symbol),
401 is_buy: side == Side::Buy,
402 price,
403 size,
404 tif,
405 reduce_only,
406 iso: false,
407 meta: ActionMeta {
408 account,
409 nonce,
410 seqno: 0,
411 hash: None,
412 }
413 };
414
415 let results = self.place_tx(vec![order.into()], None, None).await?;
416 Ok(results[0].clone())
417 }
418
419 pub async fn place_market_order(
421 &self,
422 symbol: &str,
423 side: Side,
424 size: f64,
425 reduce_only: bool,
426 account: Option<Pubkey>,
427 nonce: Option<u64>,
428 ) -> eyre::Result<Response> {
429 let signer = self
430 .config
431 .signer
432 .as_ref()
433 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
434
435 let account = if let Some(account) = account {
436 account
437 } else {
438 signer.public_key()
439 };
440
441 let nonce = nonce.unwrap_or_else(make_nonce);
442
443 let order = MarketOrder {
444 symbol: Arc::from(symbol),
445 is_buy: side == Side::Buy,
446 size,
447 reduce_only,
448 iso: false,
449 meta: ActionMeta {
450 account,
451 nonce,
452 seqno: 0,
453 hash: None,
454 }
455 };
456
457 let results = self.place_tx(vec![order.into()], None, None).await?;
458 Ok(results[0].clone())
459 }
460
461 pub async fn cancel_order(
463 &self,
464 symbol: &str,
465 order_id: &str,
466 account: Option<Pubkey>,
467 nonce: Option<u64>,
468 ) -> eyre::Result<Response> {
469 let signer = self
470 .config
471 .signer
472 .as_ref()
473 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
474
475 let account = if let Some(account) = account {
476 account
477 } else {
478 signer.public_key()
479 };
480
481 let nonce = nonce.unwrap_or_else(make_nonce);
482 let cancel = CancelOrder {
483 symbol: symbol.to_string(),
484 oid: Hash::from_str(&order_id)?,
485 meta: ActionMeta {
486 account,
487 nonce,
488 seqno: 0,
489 hash: None,
490 }
491 };
492
493 let results = self.place_tx(vec![cancel.into()], None, None).await?;
494 Ok(results[0].clone())
495 }
496
497 pub async fn cancel_all(
499 &self,
500 symbols: Vec<String>,
501 account: Option<Pubkey>,
502 nonce: Option<u64>,
503 ) -> eyre::Result<Response> {
504 let signer = self
505 .config
506 .signer
507 .as_ref()
508 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
509
510 let account = if let Some(account) = account {
511 account
512 } else {
513 signer.public_key()
514 };
515
516 let nonce = nonce.unwrap_or_else(make_nonce);
517 let cancel = CancelAll {
518 symbols,
519 meta: ActionMeta {
520 account,
521 nonce,
522 seqno: 0,
523 hash: None,
524 }
525 };
526
527 let results = self.place_tx(vec![cancel.into()], None, None).await?;
528 Ok(results[0].clone())
529 }
530
531 pub async fn update_leverage(
540 &self,
541 settings: HashMap<String, f64>,
542 account: Option<Pubkey>,
543 nonce: Option<u64>,
544 ) -> eyre::Result<Response> {
545 let signer = self
546 .config
547 .signer
548 .as_ref()
549 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
550
551 let account = if let Some(account) = account {
552 account
553 } else {
554 signer.public_key()
555 };
556
557 let nonce = nonce.unwrap_or_else(make_nonce);
558 let settings = UpdateUserSettings {
559 max_leverage: settings,
560 meta: ActionMeta {
561 account,
562 nonce,
563 seqno: 0,
564 hash: None,
565 }
566 };
567
568 let results = self.place_tx(vec![settings.into()], None, None).await?;
569 Ok(results[0].clone())
570 }
571
572 pub async fn manage_agent_wallet(
578 &self,
579 agent_pubkey: Pubkey,
580 delete: bool,
581 account: Option<Pubkey>,
582 nonce: Option<u64>,
583 ) -> eyre::Result<Response> {
584 let signer = self
585 .config
586 .signer
587 .as_ref()
588 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
589
590 let account = if let Some(account) = account {
591 account
592 } else {
593 signer.public_key()
594 };
595
596 let nonce = nonce.unwrap_or_else(make_nonce);
597 let settings = AgentWalletCreation {
598 agent: agent_pubkey,
599 delete,
600 meta: ActionMeta {
601 account,
602 nonce,
603 seqno: 0,
604 hash: None,
605 }
606 };
607
608 let results = self.place_tx(vec![Action::AgentWalletCreation(settings)], None, None).await?;
609 Ok(results[0].clone())
610 }
611
612 pub async fn whitelist_faucet(
625 &self,
626 target_account: Pubkey,
627 whitelist: bool,
628 account: Option<Pubkey>,
629 nonce: Option<u64>,
630 ) -> eyre::Result<Response> {
631 let signer = self
632 .config
633 .signer
634 .as_ref()
635 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
636
637 let account = if let Some(account) = account {
638 account
639 } else {
640 signer.public_key()
641 };
642
643 let nonce = nonce.unwrap_or_else(make_nonce);
644 let settings = WhitelistFaucet {
645 target: target_account,
646 whitelist,
647 meta: ActionMeta {
648 account,
649 nonce,
650 seqno: 0,
651 hash: None,
652 }
653 };
654 let results = self.place_tx(vec![Action::WhitelistFaucet(settings)], None, None).await?;
655 Ok(results[0].clone())
656 }
657
658 pub async fn request_faucet(
667 &self,
668 user: Option<Pubkey>,
669 amount: Option<f64>,
670 nonce: Option<u64>,
671 ) -> eyre::Result<Response> {
672 let signer = self
673 .config
674 .signer
675 .as_ref()
676 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
677
678 let user = if let Some(user) = user {
679 user
680 } else {
681 signer.public_key()
682 };
683 let nonce = nonce.unwrap_or_else(make_nonce);
684
685 let req = Faucet {
686 user,
687 amount,
688 meta: ActionMeta {
689 account: user,
690 nonce,
691 seqno: 0,
692 hash: None,
693 }
694 };
695
696 let results = self.place_tx(vec![Action::Faucet(req)], None, None).await?;
697 Ok(results[0].clone())
698 }
699
700
701 fn is_localhost(url_str: &str) -> bool {
707 let Ok(url) = Url::parse(url_str) else { return false };
708 match url.host_str() {
709 Some("localhost" | "127.0.0.1" | "::1") => true,
710 _ => false,
711 }
712 }
713
714 fn sign_generic_transaction(&self, mut body: Value) -> eyre::Result<Value> {
718 let signer = self
719 .config
720 .signer
721 .as_ref()
722 .ok_or_else(|| eyre::eyre!("Private key required"))?;
723
724 let pk_b58 = signer.public_key_b58();
725 body["account"] = json!(pk_b58);
726 body["signer"] = json!(pk_b58);
727
728 let sig = self.sign_action_payload(&body["action"])?;
729 body["signature"] = json!(sig);
730
731 Ok(body)
732 }
733
734 fn sign_action_payload(&self, action: &Value) -> eyre::Result<String> {
740 let signer = self
741 .config
742 .signer
743 .as_ref()
744 .ok_or_else(|| eyre::eyre!("Private key required"))?;
745
746 let message = serde_json::to_string(action)?;
751 let sig = signer.sign_bytes(message.as_bytes())?;
752 Ok(bs58::encode(sig).into_string())
753 }
754}