1use crate::api::parts::{make_nonce, HttpConfig};
40use crate::common::side::Side;
41use crate::common::tif::TimeInForce;
42use crate::msgs::*;
43use crate::transaction::{Action, ActionMeta, Transaction, TransactionSigner};
44use reqwest::{Client, Url};
45use serde::Deserialize;
46use serde_json::{json, Value};
47use solana_hash::Hash;
48use solana_pubkey::Pubkey;
49use std::collections::HashMap;
50use std::str::FromStr;
51use std::sync::Arc;
52use std::time::Duration;
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().timeout(config.default_timeout).build()?;
75
76 let is_localhost = Self::is_localhost(&config.base_url);
77
78 Ok(Self {
79 config: config.clone(),
80 client,
81 is_localhost,
82 })
83 }
84
85 pub fn with_url(base_url: &str, private_key: Option<&str>) -> eyre::Result<Self> {
91 if let Some(private_key) = private_key {
92 let signer = TransactionSigner::from_private_key(private_key)?;
93 let config = HttpConfig {
94 base_url: base_url.to_string(),
95 signer: Some(signer),
96 default_timeout: Duration::from_secs(10),
97 };
98 Self::new(&config)
99 } else {
100 let config = HttpConfig {
101 base_url: base_url.to_string(),
102 signer: None,
103 default_timeout: Duration::from_secs(10),
104 };
105 Self::new(&config)
106 }
107 }
108
109 pub fn with_signer(base_url: &str, signer: TransactionSigner) -> eyre::Result<Self> {
118 let config = HttpConfig {
119 base_url: base_url.to_string(),
120 signer: Some(signer),
121 default_timeout: Duration::from_secs(10),
122 };
123 Self::new(&config)
124 }
125
126 pub fn config(&self) -> &HttpConfig {
128 &self.config
129 }
130
131 pub fn public_key(&self) -> Option<Pubkey> {
133 self.config.signer.as_ref().map(|x| x.public_key())
134 }
135
136 pub async fn get_exchange_info(&self) -> eyre::Result<Vec<MarketInfo>> {
142 let resp = self
143 .client
144 .get(format!("{}/exchangeInfo", self.config.base_url))
145 .send()
146 .await?
147 .error_for_status()?;
148 Ok(resp.json().await?)
149 }
150
151 pub async fn get_ticker(&self, symbol: &str) -> eyre::Result<Ticker> {
153 let resp = self
154 .client
155 .get(format!("{}/ticker/{}", self.config.base_url, symbol))
156 .send()
157 .await?
158 .error_for_status()?;
159 Ok(resp.json().await?)
160 }
161
162 pub async fn get_klines(
171 &self,
172 symbol: &str,
173 interval: &str,
174 start_time: Option<u64>,
175 end_time: Option<u64>,
176 limit: Option<u32>,
177 ) -> eyre::Result<Vec<Candle>> {
178 let mut params = vec![
179 ("symbol".to_string(), symbol.to_string()),
180 ("interval".to_string(), interval.to_string()),
181 ("limit".to_string(), limit.unwrap_or(500).to_string()),
182 ];
183 if let Some(st) = start_time {
184 params.push(("startTime".to_string(), st.to_string()));
185 }
186 if let Some(et) = end_time {
187 params.push(("endTime".to_string(), et.to_string()));
188 }
189
190 let resp = self
191 .client
192 .get(format!("{}/klines", self.config.base_url))
193 .query(¶ms)
194 .send()
195 .await?
196 .error_for_status()?;
197 Ok(resp.json().await?)
198 }
199
200 pub async fn get_orderbook(
207 &self,
208 symbol: &str,
209 nlevels: Option<u32>,
210 aggregation: Option<f64>,
211 ) -> eyre::Result<L2Snapshot> {
212 let mut params = vec![
213 ("type".to_string(), "l2Book".to_string()),
214 ("coin".to_string(), symbol.to_string()),
215 ("nlevels".to_string(), nlevels.unwrap_or(20).to_string()),
216 ];
217 if let Some(agg) = aggregation {
218 params.push(("aggregation".to_string(), agg.to_string()));
219 }
220
221 let resp = self
222 .client
223 .get(format!("{}/l2book", self.config.base_url))
224 .query(¶ms)
225 .send()
226 .await?
227 .error_for_status()?;
228 Ok(resp.json().await?)
229 }
230
231 pub async fn get_account(&self, user: Pubkey) -> eyre::Result<AccountData> {
240 #[derive(Debug, Clone, Deserialize)]
241 #[serde(rename_all = "camelCase")]
242 pub struct FullAccountResponse {
243 pub full_account: AccountData,
244 }
245
246 let user: String = user.to_string();
247
248 let resp = self
249 .client
250 .post(format!("{}/account", self.config.base_url))
251 .json(&json!({ "type": "fullAccount", "user": user }))
252 .send()
253 .await?
254 .error_for_status()?;
255
256 let arr: Vec<FullAccountResponse> = resp.json().await?;
257 arr.into_iter()
258 .next()
259 .map(|r| r.full_account)
260 .ok_or_else(|| eyre::eyre!("empty fullAccount response"))
261 }
262
263 pub async fn get_open_orders(&self, user: &str) -> eyre::Result<Vec<OrderState>> {
268 let resp = self
269 .client
270 .post(format!("{}/account", self.config.base_url))
271 .json(&json!({ "type": "openOrders", "user": user }))
272 .send()
273 .await?
274 .error_for_status()?;
275 Ok(resp.json().await?)
276 }
277
278 pub async fn get_fills(&self, user: &str) -> eyre::Result<Vec<Fill>> {
283 let resp = self
284 .client
285 .post(format!("{}/account", self.config.base_url))
286 .json(&json!({ "type": "fills", "user": user }))
287 .send()
288 .await?
289 .error_for_status()?;
290 Ok(resp.json().await?)
291 }
292
293 pub async fn get_position_history(&self, user: &str) -> eyre::Result<Vec<PositionInfo>> {
298 let resp = self
299 .client
300 .post(format!("{}/account", self.config.base_url))
301 .json(&json!({ "type": "positions", "user": user }))
302 .send()
303 .await?
304 .error_for_status()?;
305 Ok(resp.json().await?)
306 }
307
308 pub async fn place_tx(
324 &self,
325 actions: Vec<Action>,
326 account: Option<Pubkey>,
327 nonce: Option<u64>,
328 ) -> eyre::Result<Vec<Response>> {
329 let signer = self
330 .config
331 .signer
332 .as_ref()
333 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
334
335 let account = if let Some(account) = account {
336 account
337 } else {
338 signer.public_key()
339 };
340
341 let nonce = nonce.unwrap_or_else(make_nonce);
342 let mut tx = Transaction {
344 actions,
345 nonce,
346 account: account,
347 signer: signer.public_key(),
348 signature: Default::default(),
349 };
350 tx.sign(signer)?;
351
352 let body = serde_json::to_string(&tx)?;
354
355 let mut request = self
356 .client
357 .post(format!("{}/order", self.config.base_url))
358 .header("content-type", "application/json");
359 if let Some(mode) = signer.tx_signature_mode_hint_header_value() {
360 request = request.header("X-Bulk-Sig-Mode", mode);
361 }
362 let resp = request.body(body).send().await?;
363 let status = resp.status();
364 if !status.is_success() {
365 let text = resp.text().await.unwrap_or_default();
366 return Err(eyre::eyre!(
367 "HTTP {} from /order: {}",
368 status,
369 text.chars().take(600).collect::<String>()
370 ));
371 }
372
373 let data: Value = resp.json().await?;
374 Ok(Response::parse_responses(&data))
375 }
376
377 pub async fn place_limit_order(
379 &self,
380 symbol: &str,
381 side: Side,
382 price: f64,
383 size: f64,
384 tif: TimeInForce,
385 reduce_only: bool,
386 account: Option<Pubkey>,
387 nonce: Option<u64>,
388 ) -> eyre::Result<Response> {
389 let signer = self
390 .config
391 .signer
392 .as_ref()
393 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
394
395 let account = if let Some(account) = account {
396 account
397 } else {
398 signer.public_key()
399 };
400
401 let nonce = nonce.unwrap_or_else(make_nonce);
402
403 let order = LimitOrder {
404 symbol: Arc::from(symbol),
405 is_buy: side == Side::Buy,
406 price,
407 size,
408 tif,
409 reduce_only,
410 iso: false,
411 builder_code: None,
412 meta: ActionMeta {
413 account,
414 nonce,
415 seqno: 0,
416 hash: None,
417 },
418 };
419
420 let results = self.place_tx(vec![order.into()], None, None).await?;
421 Ok(results[0].clone())
422 }
423
424 pub async fn place_market_order(
426 &self,
427 symbol: &str,
428 side: Side,
429 size: f64,
430 reduce_only: bool,
431 account: Option<Pubkey>,
432 nonce: Option<u64>,
433 ) -> eyre::Result<Response> {
434 let signer = self
435 .config
436 .signer
437 .as_ref()
438 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
439
440 let account = if let Some(account) = account {
441 account
442 } else {
443 signer.public_key()
444 };
445
446 let nonce = nonce.unwrap_or_else(make_nonce);
447
448 let order = MarketOrder {
449 symbol: Arc::from(symbol),
450 is_buy: side == Side::Buy,
451 size,
452 reduce_only,
453 iso: false,
454 builder_code: None,
455 meta: ActionMeta {
456 account,
457 nonce,
458 seqno: 0,
459 hash: None,
460 },
461 };
462
463 let results = self.place_tx(vec![order.into()], None, None).await?;
464 Ok(results[0].clone())
465 }
466
467 pub async fn cancel_order(
469 &self,
470 symbol: &str,
471 order_id: &str,
472 account: Option<Pubkey>,
473 nonce: Option<u64>,
474 ) -> eyre::Result<Response> {
475 let signer = self
476 .config
477 .signer
478 .as_ref()
479 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
480
481 let account = if let Some(account) = account {
482 account
483 } else {
484 signer.public_key()
485 };
486
487 let nonce = nonce.unwrap_or_else(make_nonce);
488 let cancel = CancelOrder {
489 symbol: symbol.to_string(),
490 oid: Hash::from_str(&order_id)?,
491 meta: ActionMeta {
492 account,
493 nonce,
494 seqno: 0,
495 hash: None,
496 },
497 };
498
499 let results = self.place_tx(vec![cancel.into()], None, None).await?;
500 Ok(results[0].clone())
501 }
502
503 pub async fn cancel_all(
505 &self,
506 symbols: Vec<String>,
507 account: Option<Pubkey>,
508 nonce: Option<u64>,
509 ) -> eyre::Result<Response> {
510 let signer = self
511 .config
512 .signer
513 .as_ref()
514 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
515
516 let account = if let Some(account) = account {
517 account
518 } else {
519 signer.public_key()
520 };
521
522 let nonce = nonce.unwrap_or_else(make_nonce);
523 let cancel = CancelAll {
524 symbols,
525 meta: ActionMeta {
526 account,
527 nonce,
528 seqno: 0,
529 hash: None,
530 },
531 };
532
533 let results = self.place_tx(vec![cancel.into()], None, None).await?;
534 Ok(results[0].clone())
535 }
536
537 pub async fn update_leverage(
546 &self,
547 settings: HashMap<String, f64>,
548 account: Option<Pubkey>,
549 nonce: Option<u64>,
550 ) -> eyre::Result<Response> {
551 let signer = self
552 .config
553 .signer
554 .as_ref()
555 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
556
557 let account = if let Some(account) = account {
558 account
559 } else {
560 signer.public_key()
561 };
562
563 let nonce = nonce.unwrap_or_else(make_nonce);
564 let settings = UpdateUserSettings {
565 max_leverage: settings,
566 meta: ActionMeta {
567 account,
568 nonce,
569 seqno: 0,
570 hash: None,
571 },
572 };
573
574 let results = self.place_tx(vec![settings.into()], None, None).await?;
575 Ok(results[0].clone())
576 }
577
578 pub async fn manage_agent_wallet(
584 &self,
585 agent_pubkey: Pubkey,
586 delete: bool,
587 account: Option<Pubkey>,
588 nonce: Option<u64>,
589 ) -> eyre::Result<Response> {
590 let signer = self
591 .config
592 .signer
593 .as_ref()
594 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
595
596 let account = if let Some(account) = account {
597 account
598 } else {
599 signer.public_key()
600 };
601
602 let nonce = nonce.unwrap_or_else(make_nonce);
603 let settings = AgentWalletCreation {
604 agent: agent_pubkey,
605 delete,
606 meta: ActionMeta {
607 account,
608 nonce,
609 seqno: 0,
610 hash: None,
611 },
612 };
613
614 let results = self
615 .place_tx(vec![Action::AgentWalletCreation(settings)], None, None)
616 .await?;
617 Ok(results[0].clone())
618 }
619
620 pub async fn approve_builder_code(
624 &self,
625 to: Pubkey,
626 fee: u8,
627 account: Option<Pubkey>,
628 nonce: Option<u64>,
629 ) -> eyre::Result<Response> {
630 let signer = self
631 .config
632 .signer
633 .as_ref()
634 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
635
636 let account = if let Some(account) = account {
637 account
638 } else {
639 signer.public_key()
640 };
641
642 let nonce = nonce.unwrap_or_else(make_nonce);
643 let action = ApproveCommissionFee {
644 to,
645 max_fee: fee,
646 meta: ActionMeta {
647 account,
648 nonce,
649 seqno: 0,
650 hash: None,
651 },
652 };
653
654 let results = self.place_tx(vec![action.into()], None, None).await?;
655 Ok(results[0].clone())
656 }
657
658 pub async fn revoke_builder_code(
660 &self,
661 to: Pubkey,
662 account: Option<Pubkey>,
663 nonce: Option<u64>,
664 ) -> eyre::Result<Response> {
665 let signer = self
666 .config
667 .signer
668 .as_ref()
669 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
670
671 let account = if let Some(account) = account {
672 account
673 } else {
674 signer.public_key()
675 };
676
677 let nonce = nonce.unwrap_or_else(make_nonce);
678 let action = RevokeCommissionFee {
679 to,
680 meta: ActionMeta {
681 account,
682 nonce,
683 seqno: 0,
684 hash: None,
685 },
686 };
687
688 let results = self.place_tx(vec![action.into()], None, None).await?;
689 Ok(results[0].clone())
690 }
691
692 pub async fn whitelist_faucet(
705 &self,
706 target_account: Pubkey,
707 whitelist: bool,
708 account: Option<Pubkey>,
709 nonce: Option<u64>,
710 ) -> eyre::Result<Response> {
711 let signer = self
712 .config
713 .signer
714 .as_ref()
715 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
716
717 let account = if let Some(account) = account {
718 account
719 } else {
720 signer.public_key()
721 };
722
723 let nonce = nonce.unwrap_or_else(make_nonce);
724 let settings = WhitelistFaucet {
725 target: target_account,
726 whitelist,
727 meta: ActionMeta {
728 account,
729 nonce,
730 seqno: 0,
731 hash: None,
732 },
733 };
734 let results = self
735 .place_tx(vec![Action::WhitelistFaucet(settings)], None, None)
736 .await?;
737 Ok(results[0].clone())
738 }
739
740 pub async fn request_faucet(
749 &self,
750 user: Option<Pubkey>,
751 amount: Option<f64>,
752 nonce: Option<u64>,
753 ) -> eyre::Result<Response> {
754 let signer = self
755 .config
756 .signer
757 .as_ref()
758 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
759
760 let user = if let Some(user) = user {
761 user
762 } else {
763 signer.public_key()
764 };
765 let nonce = nonce.unwrap_or_else(make_nonce);
766
767 let req = Faucet {
768 user,
769 amount,
770 meta: ActionMeta {
771 account: user,
772 nonce,
773 seqno: 0,
774 hash: None,
775 },
776 };
777
778 let results = self.place_tx(vec![Action::Faucet(req)], None, None).await?;
779 Ok(results[0].clone())
780 }
781
782 fn is_localhost(url_str: &str) -> bool {
788 let Ok(url) = Url::parse(url_str) else {
789 return false;
790 };
791 match url.host_str() {
792 Some("localhost" | "127.0.0.1" | "::1") => true,
793 _ => false,
794 }
795 }
796
797 fn sign_generic_transaction(&self, mut body: Value) -> eyre::Result<Value> {
801 let signer = self
802 .config
803 .signer
804 .as_ref()
805 .ok_or_else(|| eyre::eyre!("Private key required"))?;
806
807 let pk_b58 = signer.public_key_b58();
808 body["account"] = json!(pk_b58);
809 body["signer"] = json!(pk_b58);
810
811 let sig = self.sign_action_payload(&body["action"])?;
812 body["signature"] = json!(sig);
813
814 Ok(body)
815 }
816
817 fn sign_action_payload(&self, action: &Value) -> eyre::Result<String> {
823 let signer = self
824 .config
825 .signer
826 .as_ref()
827 .ok_or_else(|| eyre::eyre!("Private key required"))?;
828
829 let message = serde_json::to_string(action)?;
834 let sig = signer.sign_bytes(message.as_bytes())?;
835 Ok(bs58::encode(sig).into_string())
836 }
837}