use std::ops::Deref;
use std::sync::Mutex;
use bon::bon;
use bullet_exchange_interface::message::UserActionDiscriminants;
use bullet_exchange_interface::transaction::{Amount, Gas, PriorityFeeBips};
use bullet_exchange_interface::types::MarketId;
use url::Url;
use crate::generated::Client as GeneratedClient;
use crate::metadata::{ExchangeMetadata, SymbolInfo};
use crate::{Keypair, SDKError, SDKResult};
pub struct Client {
rest_url: String,
ws_url: String,
generated_client: GeneratedClient,
pub(crate) ws_client: reqwest::Client,
chain_id: u64,
chain_hash: Mutex<[u8; 32]>,
user_actions: Option<Vec<UserActionDiscriminants>>,
keypair: Option<Keypair>,
metadata: ExchangeMetadata,
max_priority_fee_bips: PriorityFeeBips,
max_fee: Amount,
gas_limit: Option<Gas>,
}
#[derive(Debug, Clone)]
pub enum Network {
Mainnet,
Testnet,
Custom(String),
}
impl Network {
pub fn url(&self) -> &str {
match self {
Network::Mainnet => "https://tradingapi.bullet.xyz",
Network::Testnet => "https://tradingapi.testnet.bullet.xyz",
Network::Custom(url) => url,
}
}
}
impl From<&str> for Network {
fn from(s: &str) -> Self {
match s.to_lowercase().as_str() {
"mainnet" => Network::Mainnet,
"testnet" => Network::Testnet,
_ => Network::Custom(s.to_string()),
}
}
}
impl From<String> for Network {
fn from(s: String) -> Self {
Network::from(s.as_str())
}
}
pub struct ChainData {
pub chain_hash: [u8; 32],
pub chain_id: u64,
}
pub const MAX_FEE: &Amount = &Amount(10000000000_u128);
pub const MAX_PRIORITY_FEE_BIPS: &PriorityFeeBips = &PriorityFeeBips(0);
#[bon]
impl Client {
#[builder]
pub async fn new(
#[builder(into)] network: Network,
reqwest_client: Option<reqwest::Client>,
max_priority_fee_bips: Option<PriorityFeeBips>,
max_fee: Option<Amount>,
gas_limit: Option<Gas>,
keypair: Option<Keypair>,
user_actions: Option<Vec<UserActionDiscriminants>>,
) -> SDKResult<Self> {
let url = network.url();
let parsed = Url::parse(url).map_err(|_| SDKError::InvalidNetworkUrl)?;
let (rest_url, ws_url) = match parsed.scheme() {
"https" => (url.to_string(), format!("wss://{}/ws", parsed.authority())),
"http" => (url.to_string(), format!("ws://{}/ws", parsed.authority())),
_ => return Err(SDKError::InvalidNetworkUrl),
};
let generated_client = match reqwest_client {
Some(client) => GeneratedClient::new_with_client(&rest_url, client),
None => GeneratedClient::new(&rest_url),
};
#[cfg(not(target_arch = "wasm32"))]
let ws_client = reqwest::Client::builder().http1_only().build()?;
#[cfg(target_arch = "wasm32")]
let ws_client = reqwest::Client::new();
let chain_data = Self::fetch_schema(&generated_client, &user_actions).await?;
let max_priority_fee_bips = max_priority_fee_bips.unwrap_or(*MAX_PRIORITY_FEE_BIPS);
let max_fee = max_fee.unwrap_or(*MAX_FEE);
let exchange_info = generated_client.exchange_info().await?;
let metadata = ExchangeMetadata::from_symbols(&exchange_info.into_inner().symbols);
Ok(Self {
rest_url,
ws_url,
generated_client,
ws_client,
chain_id: chain_data.chain_id,
chain_hash: Mutex::new(chain_data.chain_hash),
user_actions,
gas_limit,
max_priority_fee_bips,
max_fee,
keypair,
metadata,
})
}
async fn fetch_schema(
generated_client: &GeneratedClient,
user_actions: &Option<Vec<UserActionDiscriminants>>,
) -> SDKResult<ChainData> {
use bullet_exchange_interface::schema::{Schema, SchemaFile, trim};
use bullet_exchange_interface::transaction::Transaction;
let schema_obj = generated_client.schema().await?;
let obj = schema_obj.into_inner();
let sobj = serde_json::to_string(&obj)
.map_err(|_| SDKError::InvalidSchemaResponse("failed to serialize schema"))?;
let schema_file = serde_json::from_str::<SchemaFile>(&sobj)
.map_err(|_| SDKError::InvalidSchemaResponse("failed to parse SchemaFile"))?;
let our_schema = Schema::of_single_type::<Transaction>()
.map_err(|_| SDKError::InvalidSchemaResponse("failed to derive local schema"))?;
let filter = |name: &str, variant: &str| {
Self::filter_variants(name, variant, user_actions.as_deref())
};
let left = trim(&our_schema, &filter);
let right = trim(&schema_file.schema, &filter);
if left != right {
return Err(SDKError::SchemaOutdated);
}
let chain_hash_bytes = hex::decode(schema_file.chain_hash.replace("0x", ""))
.map_err(|e| SDKError::InvalidChainHash(e.to_string()))?;
let chain_hash = chain_hash_bytes.try_into().map_err(|v: Vec<u8>| {
SDKError::InvalidChainHash(format!("expected 32 bytes, got {}", v.len()))
})?;
let chain_id = schema_file.schema.chain_data().chain_id;
Ok(ChainData { chain_hash, chain_id })
}
pub async fn update_schema(&self) -> SDKResult<()> {
let chain_data = Self::fetch_schema(self.client(), self.user_actions()).await?;
*self.chain_hash.lock().expect("Taking the chain-hash lock can never fail.") =
chain_data.chain_hash;
Ok(())
}
fn filter_variants(
name: &str,
variant: &str,
user_actions: Option<&[UserActionDiscriminants]>,
) -> bool {
match name {
"Transaction" => variant == "V0",
"RuntimeCall" => variant == "Exchange",
"CallMessage" => variant == "User",
"UserAction" => match user_actions {
Some(actions) => UserActionDiscriminants::try_from(variant)
.map(|v| actions.contains(&v))
.unwrap_or(false),
None => UserActionDiscriminants::try_from(variant).is_ok(),
},
"UniquenessData" => variant == "Generation",
_ => {
true
}
}
}
pub async fn mainnet() -> SDKResult<Self> {
Self::builder().network(Network::Mainnet).build().await
}
pub fn client(&self) -> &GeneratedClient {
&self.generated_client
}
pub fn chain_id(&self) -> u64 {
self.chain_id
}
pub fn chain_hash(&self) -> [u8; 32] {
*self.chain_hash.lock().expect("Taking the chain-hash lock can never fail.")
}
pub fn user_actions(&self) -> &Option<Vec<UserActionDiscriminants>> {
&self.user_actions
}
pub fn url(&self) -> &str {
&self.rest_url
}
pub fn ws_url(&self) -> &str {
&self.ws_url
}
pub fn keypair(&self) -> Option<&Keypair> {
self.keypair.as_ref()
}
pub fn max_fee(&self) -> Amount {
self.max_fee
}
pub fn max_priority_fee_bips(&self) -> PriorityFeeBips {
self.max_priority_fee_bips
}
pub fn gas_limit(&self) -> Option<Gas> {
self.gas_limit.clone()
}
pub fn market_id(&self, symbol: &str) -> Option<MarketId> {
self.metadata.market_id(symbol)
}
pub fn symbols(&self) -> &[SymbolInfo] {
self.metadata.symbols()
}
pub fn symbol_info(&self, market_id: MarketId) -> Option<&SymbolInfo> {
self.metadata.symbol_info_by_id(market_id)
}
pub fn symbol_info_by_name(&self, symbol: &str) -> Option<&SymbolInfo> {
self.metadata.symbol_info_by_name(symbol)
}
pub async fn refresh_metadata(&mut self) -> SDKResult<()> {
let info = self.generated_client.exchange_info().await?;
self.metadata = ExchangeMetadata::from_symbols(&info.into_inner().symbols);
Ok(())
}
}
impl Deref for Client {
type Target = GeneratedClient;
fn deref(&self) -> &Self::Target {
self.client()
}
}