use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use reqwest::{Client, Url};
use serde::Deserialize;
use serde_json::{json, Value};
use solana_pubkey::Pubkey;
use solana_hash::Hash;
use crate::api::parts::{make_nonce, HttpConfig};
use crate::common::side::Side;
use crate::common::tif::TimeInForce;
use crate::msgs::*;
use crate::transaction::{Action, ActionMeta, Transaction, TransactionSigner};
#[derive(Clone)]
#[allow(unused)]
pub struct BulkHttpClient {
config: HttpConfig,
client: Client,
is_localhost: bool,
}
#[allow(unused)]
impl BulkHttpClient {
pub fn new (config: &HttpConfig) -> eyre::Result<Self> {
let client = Client::builder()
.timeout(config.default_timeout)
.build()?;
let is_localhost = Self::is_localhost(&config.base_url);
Ok(Self {
config: config.clone(),
client,
is_localhost,
})
}
pub fn with_url (base_url: &str, private_key: Option<&str>) -> eyre::Result<Self> {
if let Some(private_key) = private_key {
let signer = TransactionSigner::from_private_key(private_key)?;
let config = HttpConfig {
base_url: base_url.to_string(),
signer: Some(signer),
default_timeout: Duration::from_secs(10)
};
Self::new(&config)
} else {
let config = HttpConfig {
base_url: base_url.to_string(),
signer: None,
default_timeout: Duration::from_secs(10)
};
Self::new(&config)
}
}
pub fn config(&self) -> &HttpConfig {
&self.config
}
pub fn public_key(&self) -> Option<Pubkey> {
self.config.signer.as_ref()
.map(|x| x.public_key())
}
pub async fn get_exchange_info(&self) -> eyre::Result<Vec<MarketInfo>> {
let resp = self
.client
.get(format!("{}/exchangeInfo", self.config.base_url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_ticker(&self, symbol: &str) -> eyre::Result<Ticker> {
let resp = self
.client
.get(format!("{}/ticker/{}", self.config.base_url, symbol))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_klines(
&self,
symbol: &str,
interval: &str,
start_time: Option<u64>,
end_time: Option<u64>,
limit: Option<u32>,
) -> eyre::Result<Vec<Candle>> {
let mut params = vec![
("symbol".to_string(), symbol.to_string()),
("interval".to_string(), interval.to_string()),
("limit".to_string(), limit.unwrap_or(500).to_string()),
];
if let Some(st) = start_time {
params.push(("startTime".to_string(), st.to_string()));
}
if let Some(et) = end_time {
params.push(("endTime".to_string(), et.to_string()));
}
let resp = self
.client
.get(format!("{}/klines", self.config.base_url))
.query(¶ms)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_orderbook(
&self,
symbol: &str,
nlevels: Option<u32>,
aggregation: Option<f64>,
) -> eyre::Result<L2Snapshot> {
let mut params = vec![
("type".to_string(), "l2Book".to_string()),
("coin".to_string(), symbol.to_string()),
("nlevels".to_string(), nlevels.unwrap_or(20).to_string()),
];
if let Some(agg) = aggregation {
params.push(("aggregation".to_string(), agg.to_string()));
}
let resp = self
.client
.get(format!("{}/l2book", self.config.base_url))
.query(¶ms)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_account(&self, user: Pubkey) -> eyre::Result<AccountData> {
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FullAccountResponse {
pub full_account: AccountData,
}
let user: String = user.to_string();
let resp = self
.client
.post(format!("{}/account", self.config.base_url))
.json(&json!({ "type": "fullAccount", "user": user }))
.send()
.await?
.error_for_status()?;
let arr: Vec<FullAccountResponse> = resp.json().await?;
arr.into_iter()
.next()
.map(|r| r.full_account)
.ok_or_else(|| eyre::eyre!("empty fullAccount response"))
}
pub async fn get_open_orders(&self, user: &str) -> eyre::Result<Vec<OrderState>> {
let resp = self
.client
.post(format!("{}/account", self.config.base_url))
.json(&json!({ "type": "openOrders", "user": user }))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_fills(&self, user: &str) -> eyre::Result<Vec<Fill>> {
let resp = self
.client
.post(format!("{}/account", self.config.base_url))
.json(&json!({ "type": "fills", "user": user }))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_position_history(&self, user: &str) -> eyre::Result<Vec<PositionInfo>> {
let resp = self
.client
.post(format!("{}/account", self.config.base_url))
.json(&json!({ "type": "positions", "user": user }))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn place_tx(
&self,
actions: Vec<Action>,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Vec<Response>> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let pk = signer.public_key();
let mut tx = Transaction {
actions,
nonce,
account: account,
signer: signer.public_key(),
signature: Default::default(),
};
tx.sign(signer)?;
let body = serde_json::to_string(&tx)?;
let resp = self
.client
.post(format!("{}/order", self.config.base_url))
.header("content-type", "application/json")
.body(body)
.send()
.await?
.error_for_status()?;
let data: Value = resp.json().await?;
Ok(Response::parse_responses(&data))
}
pub async fn place_limit_order(
&self,
symbol: &str,
side: Side,
price: f64,
size: f64,
tif: TimeInForce,
reduce_only: bool,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let order = LimitOrder {
symbol: Arc::from(symbol),
is_buy: side == Side::Buy,
price,
size,
tif,
reduce_only,
iso: false,
meta: ActionMeta {
account,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![order.into()], None, None).await?;
Ok(results[0].clone())
}
pub async fn place_market_order(
&self,
symbol: &str,
side: Side,
size: f64,
reduce_only: bool,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let order = MarketOrder {
symbol: Arc::from(symbol),
is_buy: side == Side::Buy,
size,
reduce_only,
iso: false,
meta: ActionMeta {
account,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![order.into()], None, None).await?;
Ok(results[0].clone())
}
pub async fn cancel_order(
&self,
symbol: &str,
order_id: &str,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let cancel = CancelOrder {
symbol: symbol.to_string(),
oid: Hash::from_str(&order_id)?,
meta: ActionMeta {
account,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![cancel.into()], None, None).await?;
Ok(results[0].clone())
}
pub async fn cancel_all(
&self,
symbols: Vec<String>,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let cancel = CancelAll {
symbols,
meta: ActionMeta {
account,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![cancel.into()], None, None).await?;
Ok(results[0].clone())
}
pub async fn update_leverage(
&self,
settings: HashMap<String, f64>,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let settings = UpdateUserSettings {
max_leverage: settings,
meta: ActionMeta {
account,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![settings.into()], None, None).await?;
Ok(results[0].clone())
}
pub async fn manage_agent_wallet(
&self,
agent_pubkey: Pubkey,
delete: bool,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let settings = AgentWalletCreation {
agent: agent_pubkey,
delete,
meta: ActionMeta {
account,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![Action::AgentWalletCreation(settings)], None, None).await?;
Ok(results[0].clone())
}
pub async fn whitelist_faucet(
&self,
target_account: Pubkey,
whitelist: bool,
account: Option<Pubkey>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let account = if let Some(account) = account {
account
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let settings = WhitelistFaucet {
target: target_account,
whitelist,
meta: ActionMeta {
account,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![Action::WhitelistFaucet(settings)], None, None).await?;
Ok(results[0].clone())
}
pub async fn request_faucet(
&self,
user: Option<Pubkey>,
amount: Option<f64>,
nonce: Option<u64>,
) -> eyre::Result<Response> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
let user = if let Some(user) = user {
user
} else {
signer.public_key()
};
let nonce = nonce.unwrap_or_else(make_nonce);
let req = Faucet {
user,
amount,
meta: ActionMeta {
account: user,
nonce,
seqno: 0,
hash: None,
}
};
let results = self.place_tx(vec![Action::Faucet(req)], None, None).await?;
Ok(results[0].clone())
}
fn is_localhost(url_str: &str) -> bool {
let Ok(url) = Url::parse(url_str) else { return false };
match url.host_str() {
Some("localhost" | "127.0.0.1" | "::1") => true,
_ => false,
}
}
fn sign_generic_transaction(&self, mut body: Value) -> eyre::Result<Value> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required"))?;
let pk_b58 = signer.public_key_b58();
body["account"] = json!(pk_b58);
body["signer"] = json!(pk_b58);
let sig = self.sign_action_payload(&body["action"])?;
body["signature"] = json!(sig);
Ok(body)
}
fn sign_action_payload(&self, action: &Value) -> eyre::Result<String> {
let signer = self
.config
.signer
.as_ref()
.ok_or_else(|| eyre::eyre!("Private key required"))?;
let message = serde_json::to_string(action)?;
let sig = signer.sign_bytes(message.as_bytes());
Ok(bs58::encode(sig).into_string())
}
}