1use std::ops::Deref;
2use std::sync::Mutex;
3
4use bon::bon;
5use bullet_exchange_interface::message::UserActionDiscriminants;
6use bullet_exchange_interface::transaction::{Amount, Gas, PriorityFeeBips};
7use bullet_exchange_interface::types::MarketId;
8use url::Url;
9
10use crate::generated::Client as GeneratedClient;
11use crate::metadata::{ExchangeMetadata, SymbolInfo};
12use crate::{Keypair, SDKError, SDKResult};
13
14pub struct Client {
36 rest_url: String,
37 ws_url: String,
38 generated_client: GeneratedClient,
39 pub(crate) ws_client: reqwest::Client,
40 chain_id: u64,
41 chain_hash: Mutex<[u8; 32]>,
42 user_actions: Option<Vec<UserActionDiscriminants>>,
43
44 keypair: Option<Keypair>,
45
46 metadata: ExchangeMetadata,
48
49 max_priority_fee_bips: PriorityFeeBips,
51 max_fee: Amount,
53 gas_limit: Option<Gas>,
55}
56
57#[derive(Debug, Clone)]
74pub enum Network {
75 Mainnet,
76 Testnet,
77 Custom(String),
78}
79
80impl Network {
81 pub fn url(&self) -> &str {
83 match self {
84 Network::Mainnet => "https://tradingapi.bullet.xyz",
85 Network::Testnet => "https://tradingapi.testnet.bullet.xyz",
86 Network::Custom(url) => url,
87 }
88 }
89}
90
91impl From<&str> for Network {
92 fn from(s: &str) -> Self {
93 match s.to_lowercase().as_str() {
94 "mainnet" => Network::Mainnet,
95 "testnet" => Network::Testnet,
96 _ => Network::Custom(s.to_string()),
97 }
98 }
99}
100
101impl From<String> for Network {
102 fn from(s: String) -> Self {
103 Network::from(s.as_str())
104 }
105}
106
107pub struct ChainData {
108 pub chain_hash: [u8; 32],
109 pub chain_id: u64,
110}
111
112pub const MAX_FEE: &Amount = &Amount(10000000000_u128);
113pub const MAX_PRIORITY_FEE_BIPS: &PriorityFeeBips = &PriorityFeeBips(0);
114
115#[bon]
116impl Client {
117 #[builder]
119 pub async fn new(
120 #[builder(into)] network: Network,
121 reqwest_client: Option<reqwest::Client>,
127 max_priority_fee_bips: Option<PriorityFeeBips>,
128 max_fee: Option<Amount>,
129 gas_limit: Option<Gas>,
130 keypair: Option<Keypair>,
131 user_actions: Option<Vec<UserActionDiscriminants>>,
142 ) -> SDKResult<Self> {
143 let url = network.url();
144 let parsed = Url::parse(url).map_err(|_| SDKError::InvalidNetworkUrl)?;
145
146 let (rest_url, ws_url) = match parsed.scheme() {
147 "https" => (url.to_string(), format!("wss://{}/ws", parsed.authority())),
148 "http" => (url.to_string(), format!("ws://{}/ws", parsed.authority())),
149 _ => return Err(SDKError::InvalidNetworkUrl),
150 };
151 let generated_client = match reqwest_client {
152 Some(client) => GeneratedClient::new_with_client(&rest_url, client),
153 None => GeneratedClient::new(&rest_url),
154 };
155
156 #[cfg(not(target_arch = "wasm32"))]
160 let ws_client = reqwest::Client::builder().http1_only().build()?;
161 #[cfg(target_arch = "wasm32")]
162 let ws_client = reqwest::Client::new();
163
164 let chain_data = Self::fetch_schema(&generated_client, &user_actions).await?;
166
167 let max_priority_fee_bips = max_priority_fee_bips.unwrap_or(*MAX_PRIORITY_FEE_BIPS);
168 let max_fee = max_fee.unwrap_or(*MAX_FEE);
169
170 let exchange_info = generated_client.exchange_info().await?;
171 let metadata = ExchangeMetadata::from_symbols(&exchange_info.into_inner().symbols);
172
173 Ok(Self {
174 rest_url,
175 ws_url,
176 generated_client,
177 ws_client,
178 chain_id: chain_data.chain_id,
179 chain_hash: Mutex::new(chain_data.chain_hash),
180 user_actions,
181 gas_limit,
182 max_priority_fee_bips,
183 max_fee,
184 keypair,
185 metadata,
186 })
187 }
188
189 async fn fetch_schema(
190 generated_client: &GeneratedClient,
191 user_actions: &Option<Vec<UserActionDiscriminants>>,
192 ) -> SDKResult<ChainData> {
193 use bullet_exchange_interface::schema::{Schema, SchemaFile, trim};
194 use bullet_exchange_interface::transaction::Transaction;
195
196 let schema_obj = generated_client.schema().await?;
197
198 let obj = schema_obj.into_inner();
200 let sobj = serde_json::to_string(&obj)
201 .map_err(|_| SDKError::InvalidSchemaResponse("failed to serialize schema"))?;
202 let schema_file = serde_json::from_str::<SchemaFile>(&sobj)
203 .map_err(|_| SDKError::InvalidSchemaResponse("failed to parse SchemaFile"))?;
204 let our_schema = Schema::of_single_type::<Transaction>()
205 .map_err(|_| SDKError::InvalidSchemaResponse("failed to derive local schema"))?;
206 let filter = |name: &str, variant: &str| {
207 Self::filter_variants(name, variant, user_actions.as_deref())
208 };
209 let left = trim(&our_schema, &filter);
210 let right = trim(&schema_file.schema, &filter);
211 if left != right {
212 return Err(SDKError::SchemaOutdated);
213 }
214
215 let chain_hash_bytes = hex::decode(schema_file.chain_hash.replace("0x", ""))
217 .map_err(|e| SDKError::InvalidChainHash(e.to_string()))?;
218
219 let chain_hash = chain_hash_bytes.try_into().map_err(|v: Vec<u8>| {
220 SDKError::InvalidChainHash(format!("expected 32 bytes, got {}", v.len()))
221 })?;
222 let chain_id = schema_file.schema.chain_data().chain_id;
223 Ok(ChainData { chain_hash, chain_id })
224 }
225
226 pub async fn update_schema(&self) -> SDKResult<()> {
227 let chain_data = Self::fetch_schema(self.client(), self.user_actions()).await?;
228
229 *self.chain_hash.lock().expect("Taking the chain-hash lock can never fail.") =
232 chain_data.chain_hash;
233 Ok(())
234 }
235
236 fn filter_variants(
255 name: &str,
256 variant: &str,
257 user_actions: Option<&[UserActionDiscriminants]>,
258 ) -> bool {
259 match name {
260 "Transaction" => variant == "V0",
261 "RuntimeCall" => variant == "Exchange",
262 "CallMessage" => variant == "User",
263 "UserAction" => match user_actions {
264 Some(actions) => UserActionDiscriminants::try_from(variant)
265 .map(|v| actions.contains(&v))
266 .unwrap_or(false),
267 None => UserActionDiscriminants::try_from(variant).is_ok(),
268 },
269 "UniquenessData" => variant == "Generation",
270 _ => {
271 true
273 }
274 }
275 }
276
277 pub async fn mainnet() -> SDKResult<Self> {
291 Self::builder().network(Network::Mainnet).build().await
292 }
293
294 pub fn client(&self) -> &GeneratedClient {
299 &self.generated_client
300 }
301
302 pub fn chain_id(&self) -> u64 {
304 self.chain_id
305 }
306
307 pub fn chain_hash(&self) -> [u8; 32] {
309 *self.chain_hash.lock().expect("Taking the chain-hash lock can never fail.")
312 }
313
314 pub fn user_actions(&self) -> &Option<Vec<UserActionDiscriminants>> {
315 &self.user_actions
316 }
317
318 pub fn url(&self) -> &str {
320 &self.rest_url
321 }
322 pub fn ws_url(&self) -> &str {
324 &self.ws_url
325 }
326
327 pub fn keypair(&self) -> Option<&Keypair> {
329 self.keypair.as_ref()
330 }
331
332 pub fn max_fee(&self) -> Amount {
334 self.max_fee
335 }
336
337 pub fn max_priority_fee_bips(&self) -> PriorityFeeBips {
339 self.max_priority_fee_bips
340 }
341
342 pub fn gas_limit(&self) -> Option<Gas> {
344 self.gas_limit.clone()
345 }
346
347 pub fn market_id(&self, symbol: &str) -> Option<MarketId> {
360 self.metadata.market_id(symbol)
361 }
362
363 pub fn symbols(&self) -> &[SymbolInfo] {
365 self.metadata.symbols()
366 }
367
368 pub fn symbol_info(&self, market_id: MarketId) -> Option<&SymbolInfo> {
370 self.metadata.symbol_info_by_id(market_id)
371 }
372
373 pub fn symbol_info_by_name(&self, symbol: &str) -> Option<&SymbolInfo> {
375 self.metadata.symbol_info_by_name(symbol)
376 }
377
378 pub async fn refresh_metadata(&mut self) -> SDKResult<()> {
382 let info = self.generated_client.exchange_info().await?;
383 self.metadata = ExchangeMetadata::from_symbols(&info.into_inner().symbols);
384 Ok(())
385 }
386}
387
388impl Deref for Client {
397 type Target = GeneratedClient;
398
399 fn deref(&self) -> &Self::Target {
400 self.client()
401 }
402}