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 config(&self) -> &HttpConfig {
113 &self.config
114 }
115
116 pub fn public_key(&self) -> Option<Pubkey> {
118 self.config.signer.as_ref()
119 .map(|x| x.public_key())
120 }
121
122 pub async fn get_exchange_info(&self) -> eyre::Result<Vec<MarketInfo>> {
128 let resp = self
129 .client
130 .get(format!("{}/exchangeInfo", self.config.base_url))
131 .send()
132 .await?
133 .error_for_status()?;
134 Ok(resp.json().await?)
135 }
136
137 pub async fn get_ticker(&self, symbol: &str) -> eyre::Result<Ticker> {
139 let resp = self
140 .client
141 .get(format!("{}/ticker/{}", self.config.base_url, symbol))
142 .send()
143 .await?
144 .error_for_status()?;
145 Ok(resp.json().await?)
146 }
147
148 pub async fn get_klines(
157 &self,
158 symbol: &str,
159 interval: &str,
160 start_time: Option<u64>,
161 end_time: Option<u64>,
162 limit: Option<u32>,
163 ) -> eyre::Result<Vec<Candle>> {
164 let mut params = vec![
165 ("symbol".to_string(), symbol.to_string()),
166 ("interval".to_string(), interval.to_string()),
167 ("limit".to_string(), limit.unwrap_or(500).to_string()),
168 ];
169 if let Some(st) = start_time {
170 params.push(("startTime".to_string(), st.to_string()));
171 }
172 if let Some(et) = end_time {
173 params.push(("endTime".to_string(), et.to_string()));
174 }
175
176 let resp = self
177 .client
178 .get(format!("{}/klines", self.config.base_url))
179 .query(¶ms)
180 .send()
181 .await?
182 .error_for_status()?;
183 Ok(resp.json().await?)
184 }
185
186 pub async fn get_orderbook(
193 &self,
194 symbol: &str,
195 nlevels: Option<u32>,
196 aggregation: Option<f64>,
197 ) -> eyre::Result<L2Snapshot> {
198 let mut params = vec![
199 ("type".to_string(), "l2Book".to_string()),
200 ("coin".to_string(), symbol.to_string()),
201 ("nlevels".to_string(), nlevels.unwrap_or(20).to_string()),
202 ];
203 if let Some(agg) = aggregation {
204 params.push(("aggregation".to_string(), agg.to_string()));
205 }
206
207 let resp = self
208 .client
209 .get(format!("{}/l2book", self.config.base_url))
210 .query(¶ms)
211 .send()
212 .await?
213 .error_for_status()?;
214 Ok(resp.json().await?)
215 }
216
217 pub async fn get_account(&self, user: Pubkey) -> eyre::Result<AccountData> {
226 #[derive(Debug, Clone, Deserialize)]
227 #[serde(rename_all = "camelCase")]
228 pub struct FullAccountResponse {
229 pub full_account: AccountData,
230 }
231
232 let user: String = user.to_string();
233
234 let resp = self
235 .client
236 .post(format!("{}/account", self.config.base_url))
237 .json(&json!({ "type": "fullAccount", "user": user }))
238 .send()
239 .await?
240 .error_for_status()?;
241
242 let arr: Vec<FullAccountResponse> = resp.json().await?;
243 arr.into_iter()
244 .next()
245 .map(|r| r.full_account)
246 .ok_or_else(|| eyre::eyre!("empty fullAccount response"))
247 }
248
249 pub async fn get_open_orders(&self, user: &str) -> eyre::Result<Vec<OrderState>> {
254 let resp = self
255 .client
256 .post(format!("{}/account", self.config.base_url))
257 .json(&json!({ "type": "openOrders", "user": user }))
258 .send()
259 .await?
260 .error_for_status()?;
261 Ok(resp.json().await?)
262 }
263
264 pub async fn get_fills(&self, user: &str) -> eyre::Result<Vec<Fill>> {
269 let resp = self
270 .client
271 .post(format!("{}/account", self.config.base_url))
272 .json(&json!({ "type": "fills", "user": user }))
273 .send()
274 .await?
275 .error_for_status()?;
276 Ok(resp.json().await?)
277 }
278
279 pub async fn get_position_history(&self, user: &str) -> eyre::Result<Vec<PositionInfo>> {
284 let resp = self
285 .client
286 .post(format!("{}/account", self.config.base_url))
287 .json(&json!({ "type": "positions", "user": user }))
288 .send()
289 .await?
290 .error_for_status()?;
291 Ok(resp.json().await?)
292 }
293
294 pub async fn place_tx(
310 &self,
311 actions: Vec<Action>,
312 account: Option<Pubkey>,
313 nonce: Option<u64>,
314 ) -> eyre::Result<Vec<Response>> {
315 let signer = self
316 .config
317 .signer
318 .as_ref()
319 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
320
321 let account = if let Some(account) = account {
322 account
323 } else {
324 signer.public_key()
325 };
326
327 let nonce = nonce.unwrap_or_else(make_nonce);
328 let pk = signer.public_key();
329
330 let mut tx = Transaction {
332 actions,
333 nonce,
334 account: account,
335 signer: signer.public_key(),
336 signature: Default::default(),
337 };
338 tx.sign(signer)?;
339
340 let body = serde_json::to_string(&tx)?;
342
343 let resp = self
344 .client
345 .post(format!("{}/order", self.config.base_url))
346 .header("content-type", "application/json")
347 .body(body)
348 .send()
349 .await?
350 .error_for_status()?;
351
352 let data: Value = resp.json().await?;
353 Ok(Response::parse_responses(&data))
354 }
355
356 pub async fn place_limit_order(
358 &self,
359 symbol: &str,
360 side: Side,
361 price: f64,
362 size: f64,
363 tif: TimeInForce,
364 reduce_only: bool,
365 account: Option<Pubkey>,
366 nonce: Option<u64>,
367 ) -> eyre::Result<Response> {
368 let signer = self
369 .config
370 .signer
371 .as_ref()
372 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
373
374 let account = if let Some(account) = account {
375 account
376 } else {
377 signer.public_key()
378 };
379
380 let nonce = nonce.unwrap_or_else(make_nonce);
381
382 let order = LimitOrder {
383 symbol: Arc::from(symbol),
384 is_buy: side == Side::Buy,
385 price,
386 size,
387 tif,
388 reduce_only,
389 iso: false,
390 meta: ActionMeta {
391 account,
392 nonce,
393 seqno: 0,
394 hash: None,
395 }
396 };
397
398 let results = self.place_tx(vec![order.into()], None, None).await?;
399 Ok(results[0].clone())
400 }
401
402 pub async fn place_market_order(
404 &self,
405 symbol: &str,
406 side: Side,
407 size: f64,
408 reduce_only: bool,
409 account: Option<Pubkey>,
410 nonce: Option<u64>,
411 ) -> eyre::Result<Response> {
412 let signer = self
413 .config
414 .signer
415 .as_ref()
416 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
417
418 let account = if let Some(account) = account {
419 account
420 } else {
421 signer.public_key()
422 };
423
424 let nonce = nonce.unwrap_or_else(make_nonce);
425
426 let order = MarketOrder {
427 symbol: Arc::from(symbol),
428 is_buy: side == Side::Buy,
429 size,
430 reduce_only,
431 iso: false,
432 meta: ActionMeta {
433 account,
434 nonce,
435 seqno: 0,
436 hash: None,
437 }
438 };
439
440 let results = self.place_tx(vec![order.into()], None, None).await?;
441 Ok(results[0].clone())
442 }
443
444 pub async fn cancel_order(
446 &self,
447 symbol: &str,
448 order_id: &str,
449 account: Option<Pubkey>,
450 nonce: Option<u64>,
451 ) -> eyre::Result<Response> {
452 let signer = self
453 .config
454 .signer
455 .as_ref()
456 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
457
458 let account = if let Some(account) = account {
459 account
460 } else {
461 signer.public_key()
462 };
463
464 let nonce = nonce.unwrap_or_else(make_nonce);
465 let cancel = CancelOrder {
466 symbol: symbol.to_string(),
467 oid: Hash::from_str(&order_id)?,
468 meta: ActionMeta {
469 account,
470 nonce,
471 seqno: 0,
472 hash: None,
473 }
474 };
475
476 let results = self.place_tx(vec![cancel.into()], None, None).await?;
477 Ok(results[0].clone())
478 }
479
480 pub async fn cancel_all(
482 &self,
483 symbols: Vec<String>,
484 account: Option<Pubkey>,
485 nonce: Option<u64>,
486 ) -> eyre::Result<Response> {
487 let signer = self
488 .config
489 .signer
490 .as_ref()
491 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
492
493 let account = if let Some(account) = account {
494 account
495 } else {
496 signer.public_key()
497 };
498
499 let nonce = nonce.unwrap_or_else(make_nonce);
500 let cancel = CancelAll {
501 symbols,
502 meta: ActionMeta {
503 account,
504 nonce,
505 seqno: 0,
506 hash: None,
507 }
508 };
509
510 let results = self.place_tx(vec![cancel.into()], None, None).await?;
511 Ok(results[0].clone())
512 }
513
514 pub async fn update_leverage(
523 &self,
524 settings: HashMap<String, f64>,
525 account: Option<Pubkey>,
526 nonce: Option<u64>,
527 ) -> eyre::Result<Response> {
528 let signer = self
529 .config
530 .signer
531 .as_ref()
532 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
533
534 let account = if let Some(account) = account {
535 account
536 } else {
537 signer.public_key()
538 };
539
540 let nonce = nonce.unwrap_or_else(make_nonce);
541 let settings = UpdateUserSettings {
542 max_leverage: settings,
543 meta: ActionMeta {
544 account,
545 nonce,
546 seqno: 0,
547 hash: None,
548 }
549 };
550
551 let results = self.place_tx(vec![settings.into()], None, None).await?;
552 Ok(results[0].clone())
553 }
554
555 pub async fn manage_agent_wallet(
561 &self,
562 agent_pubkey: Pubkey,
563 delete: bool,
564 account: Option<Pubkey>,
565 nonce: Option<u64>,
566 ) -> eyre::Result<Response> {
567 let signer = self
568 .config
569 .signer
570 .as_ref()
571 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
572
573 let account = if let Some(account) = account {
574 account
575 } else {
576 signer.public_key()
577 };
578
579 let nonce = nonce.unwrap_or_else(make_nonce);
580 let settings = AgentWalletCreation {
581 agent: agent_pubkey,
582 delete,
583 meta: ActionMeta {
584 account,
585 nonce,
586 seqno: 0,
587 hash: None,
588 }
589 };
590
591 let results = self.place_tx(vec![Action::AgentWalletCreation(settings)], None, None).await?;
592 Ok(results[0].clone())
593 }
594
595 pub async fn whitelist_faucet(
608 &self,
609 target_account: Pubkey,
610 whitelist: bool,
611 account: Option<Pubkey>,
612 nonce: Option<u64>,
613 ) -> eyre::Result<Response> {
614 let signer = self
615 .config
616 .signer
617 .as_ref()
618 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
619
620 let account = if let Some(account) = account {
621 account
622 } else {
623 signer.public_key()
624 };
625
626 let nonce = nonce.unwrap_or_else(make_nonce);
627 let settings = WhitelistFaucet {
628 target: target_account,
629 whitelist,
630 meta: ActionMeta {
631 account,
632 nonce,
633 seqno: 0,
634 hash: None,
635 }
636 };
637 let results = self.place_tx(vec![Action::WhitelistFaucet(settings)], None, None).await?;
638 Ok(results[0].clone())
639 }
640
641 pub async fn request_faucet(
650 &self,
651 user: Option<Pubkey>,
652 amount: Option<f64>,
653 nonce: Option<u64>,
654 ) -> eyre::Result<Response> {
655 let signer = self
656 .config
657 .signer
658 .as_ref()
659 .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
660
661 let user = if let Some(user) = user {
662 user
663 } else {
664 signer.public_key()
665 };
666 let nonce = nonce.unwrap_or_else(make_nonce);
667
668 let req = Faucet {
669 user,
670 amount,
671 meta: ActionMeta {
672 account: user,
673 nonce,
674 seqno: 0,
675 hash: None,
676 }
677 };
678
679 let results = self.place_tx(vec![Action::Faucet(req)], None, None).await?;
680 Ok(results[0].clone())
681 }
682
683
684 fn is_localhost(url_str: &str) -> bool {
690 let Ok(url) = Url::parse(url_str) else { return false };
691 match url.host_str() {
692 Some("localhost" | "127.0.0.1" | "::1") => true,
693 _ => false,
694 }
695 }
696
697 fn sign_generic_transaction(&self, mut body: Value) -> eyre::Result<Value> {
701 let signer = self
702 .config
703 .signer
704 .as_ref()
705 .ok_or_else(|| eyre::eyre!("Private key required"))?;
706
707 let pk_b58 = signer.public_key_b58();
708 body["account"] = json!(pk_b58);
709 body["signer"] = json!(pk_b58);
710
711 let sig = self.sign_action_payload(&body["action"])?;
712 body["signature"] = json!(sig);
713
714 Ok(body)
715 }
716
717 fn sign_action_payload(&self, action: &Value) -> eyre::Result<String> {
723 let signer = self
724 .config
725 .signer
726 .as_ref()
727 .ok_or_else(|| eyre::eyre!("Private key required"))?;
728
729 let message = serde_json::to_string(action)?;
734 let sig = signer.sign_bytes(message.as_bytes());
735 Ok(bs58::encode(sig).into_string())
736 }
737}