1use crate::{
5 error::DeepBookError,
6 models::{BalancesSummary, OrderFillSummary, Pools},
7 schema::{self},
8 sui_deepbook_indexer::PgDeepbookPersistent,
9};
10use axum::http::Method;
11use axum::{
12 Json, Router,
13 extract::{Path, Query, State},
14 http::StatusCode,
15 routing::get,
16};
17use diesel::BoolExpressionMethods;
18use diesel::QueryDsl;
19use diesel::dsl::{count_star, sql};
20use diesel::dsl::{max, min};
21use diesel::{ExpressionMethods, SelectableHelper};
22use diesel_async::RunQueryDsl;
23use serde_json::Value;
24use std::time::{SystemTime, UNIX_EPOCH};
25use std::{collections::HashMap, net::SocketAddr};
26use tokio::{net::TcpListener, task::JoinHandle};
27use tower_http::cors::{AllowMethods, Any, CorsLayer};
28
29use futures::future::join_all;
30use std::str::FromStr;
31use sui_json_rpc_types::{SuiObjectData, SuiObjectDataOptions, SuiObjectResponse};
32use sui_sdk::SuiClientBuilder;
33use sui_types::{
34 TypeTag,
35 base_types::{ObjectID, ObjectRef, SuiAddress},
36 programmable_transaction_builder::ProgrammableTransactionBuilder,
37 transaction::{Argument, CallArg, Command, ObjectArg, ProgrammableMoveCall, TransactionKind},
38 type_input::TypeInput,
39};
40use tokio::join;
41
42pub const SUI_MAINNET_URL: &str = "https://fullnode.mainnet.sui.io:443";
43pub const GET_POOLS_PATH: &str = "/get_pools";
44pub const GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID_WITH_INTERVAL: &str =
45 "/historical_volume_by_balance_manager_id_with_interval/{pool_names}/{balance_manager_id}";
46pub const GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID: &str =
47 "/historical_volume_by_balance_manager_id/{pool_names}/{balance_manager_id}";
48pub const HISTORICAL_VOLUME_PATH: &str = "/historical_volume/{pool_names}";
49pub const ALL_HISTORICAL_VOLUME_PATH: &str = "/all_historical_volume";
50pub const GET_NET_DEPOSITS: &str = "/get_net_deposits/{asset_ids}/{timestamp}";
51pub const TICKER_PATH: &str = "/ticker";
52pub const TRADES_PATH: &str = "/trades/{pool_name}";
53pub const ORDER_UPDATES_PATH: &str = "/order_updates/{pool_name}";
54pub const TRADE_COUNT_PATH: &str = "/trade_count";
55pub const ASSETS_PATH: &str = "/assets";
56pub const SUMMARY_PATH: &str = "/summary";
57pub const LEVEL2_PATH: &str = "/orderbook/{pool_name}";
58pub const LEVEL2_MODULE: &str = "pool";
59pub const LEVEL2_FUNCTION: &str = "get_level2_ticks_from_mid";
60pub const DEEPBOOK_PACKAGE_ID: &str =
61 "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809";
62pub const DEEP_TOKEN_PACKAGE_ID: &str =
63 "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270";
64pub const DEEP_TREASURY_ID: &str =
65 "0x032abf8948dda67a271bcc18e776dbbcfb0d58c8d288a700ff0d5521e57a1ffe";
66pub const DEEP_SUPPLY_MODULE: &str = "deep";
67pub const DEEP_SUPPLY_FUNCTION: &str = "total_supply";
68pub const DEEP_SUPPLY_PATH: &str = "/deep_supply";
69
70pub fn run_server(socket_address: SocketAddr, state: PgDeepbookPersistent) -> JoinHandle<()> {
71 tokio::spawn(async move {
72 let listener = TcpListener::bind(socket_address).await.unwrap();
73 axum::serve(listener, make_router(state)).await.unwrap();
74 })
75}
76
77pub(crate) fn make_router(state: PgDeepbookPersistent) -> Router {
78 let cors = CorsLayer::new()
79 .allow_methods(AllowMethods::list(vec![Method::GET, Method::OPTIONS]))
80 .allow_headers(Any)
81 .allow_origin(Any);
82
83 Router::new()
84 .route("/", get(health_check))
85 .route(GET_POOLS_PATH, get(get_pools))
86 .route(HISTORICAL_VOLUME_PATH, get(historical_volume))
87 .route(ALL_HISTORICAL_VOLUME_PATH, get(all_historical_volume))
88 .route(
89 GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID_WITH_INTERVAL,
90 get(get_historical_volume_by_balance_manager_id_with_interval),
91 )
92 .route(
93 GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID,
94 get(get_historical_volume_by_balance_manager_id),
95 )
96 .route(LEVEL2_PATH, get(orderbook))
97 .route(GET_NET_DEPOSITS, get(get_net_deposits))
98 .route(TICKER_PATH, get(ticker))
99 .route(TRADES_PATH, get(trades))
100 .route(TRADE_COUNT_PATH, get(trade_count))
101 .route(ORDER_UPDATES_PATH, get(order_updates))
102 .route(ASSETS_PATH, get(assets))
103 .route(SUMMARY_PATH, get(summary))
104 .route(DEEP_SUPPLY_PATH, get(deep_supply))
105 .layer(cors)
106 .with_state(state)
107}
108
109impl axum::response::IntoResponse for DeepBookError {
110 fn into_response(self) -> axum::response::Response {
112 (
113 StatusCode::INTERNAL_SERVER_ERROR,
114 format!("Something went wrong: {:?}", self),
115 )
116 .into_response()
117 }
118}
119
120impl<E> From<E> for DeepBookError
121where
122 E: Into<anyhow::Error>,
123{
124 fn from(err: E) -> Self {
125 Self::InternalError(err.into().to_string())
126 }
127}
128
129async fn health_check() -> StatusCode {
130 StatusCode::OK
131}
132
133async fn get_pools(
135 State(state): State<PgDeepbookPersistent>,
136) -> Result<Json<Vec<Pools>>, DeepBookError> {
137 let connection = &mut state.pool.get().await?;
138 let results = schema::pools::table
139 .select(Pools::as_select())
140 .load(connection)
141 .await?;
142
143 Ok(Json(results))
144}
145
146async fn historical_volume(
147 Path(pool_names): Path<String>,
148 Query(params): Query<HashMap<String, String>>,
149 State(state): State<PgDeepbookPersistent>,
150) -> Result<Json<HashMap<String, u64>>, DeepBookError> {
151 let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
153 let pool_name_to_id: HashMap<String, String> = pools
154 .0
155 .into_iter()
156 .map(|pool| (pool.pool_name, pool.pool_id))
157 .collect();
158
159 let pool_ids_list: Vec<String> = pool_names
161 .split(',')
162 .filter_map(|name| pool_name_to_id.get(name).cloned())
163 .collect();
164
165 if pool_ids_list.is_empty() {
166 return Err(DeepBookError::InternalError(
167 "No valid pool names provided".to_string(),
168 ));
169 }
170
171 let end_time = params
173 .get("end_time")
174 .and_then(|v| v.parse::<i64>().ok())
175 .map(|t| t * 1000) .unwrap_or_else(|| {
177 SystemTime::now()
178 .duration_since(UNIX_EPOCH)
179 .unwrap()
180 .as_millis() as i64
181 });
182
183 let start_time = params
184 .get("start_time")
185 .and_then(|v| v.parse::<i64>().ok())
186 .map(|t| t * 1000) .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
188
189 let volume_in_base = params
191 .get("volume_in_base")
192 .map(|v| v == "true")
193 .unwrap_or(false);
194 let column_to_query = if volume_in_base {
195 sql::<diesel::sql_types::BigInt>("base_quantity")
196 } else {
197 sql::<diesel::sql_types::BigInt>("quote_quantity")
198 };
199
200 let connection = &mut state.pool.get().await?;
202 let results: Vec<(String, i64)> = schema::order_fills::table
203 .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time))
204 .filter(schema::order_fills::pool_id.eq_any(pool_ids_list))
205 .select((schema::order_fills::pool_id, column_to_query))
206 .load(connection)
207 .await?;
208
209 let mut volume_by_pool = HashMap::new();
211 for (pool_id, volume) in results {
212 if let Some(pool_name) = pool_name_to_id
213 .iter()
214 .find(|(_, id)| **id == pool_id)
215 .map(|(name, _)| name)
216 {
217 *volume_by_pool.entry(pool_name.clone()).or_insert(0) += volume as u64;
218 }
219 }
220
221 Ok(Json(volume_by_pool))
222}
223
224async fn all_historical_volume(
226 Query(params): Query<HashMap<String, String>>,
227 State(state): State<PgDeepbookPersistent>,
228) -> Result<Json<HashMap<String, u64>>, DeepBookError> {
229 let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
230
231 let pool_names: String = pools
232 .0
233 .into_iter()
234 .map(|pool| pool.pool_name)
235 .collect::<Vec<String>>()
236 .join(",");
237
238 historical_volume(Path(pool_names), Query(params), State(state)).await
239}
240
241async fn get_historical_volume_by_balance_manager_id(
242 Path((pool_names, balance_manager_id)): Path<(String, String)>,
243 Query(params): Query<HashMap<String, String>>,
244 State(state): State<PgDeepbookPersistent>,
245) -> Result<Json<HashMap<String, Vec<i64>>>, DeepBookError> {
246 let connection = &mut state.pool.get().await?;
247
248 let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
249 let pool_name_to_id: HashMap<String, String> = pools
250 .0
251 .into_iter()
252 .map(|pool| (pool.pool_name, pool.pool_id))
253 .collect();
254
255 let pool_ids_list: Vec<String> = pool_names
256 .split(',')
257 .filter_map(|name| pool_name_to_id.get(name).cloned())
258 .collect();
259
260 if pool_ids_list.is_empty() {
261 return Err(DeepBookError::InternalError(
262 "No valid pool names provided".to_string(),
263 ));
264 }
265
266 let end_time = params
268 .get("end_time")
269 .and_then(|v| v.parse::<i64>().ok())
270 .map(|t| t * 1000) .unwrap_or_else(|| {
272 SystemTime::now()
273 .duration_since(UNIX_EPOCH)
274 .unwrap()
275 .as_millis() as i64
276 });
277
278 let start_time = params
279 .get("start_time")
280 .and_then(|v| v.parse::<i64>().ok())
281 .map(|t| t * 1000) .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
283
284 let volume_in_base = params
285 .get("volume_in_base")
286 .map(|v| v == "true")
287 .unwrap_or(false);
288 let column_to_query = if volume_in_base {
289 sql::<diesel::sql_types::BigInt>("base_quantity")
290 } else {
291 sql::<diesel::sql_types::BigInt>("quote_quantity")
292 };
293
294 let results: Vec<OrderFillSummary> = schema::order_fills::table
295 .select((
296 schema::order_fills::pool_id,
297 schema::order_fills::maker_balance_manager_id,
298 schema::order_fills::taker_balance_manager_id,
299 column_to_query,
300 ))
301 .filter(schema::order_fills::pool_id.eq_any(&pool_ids_list))
302 .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time))
303 .filter(
304 schema::order_fills::maker_balance_manager_id
305 .eq(&balance_manager_id)
306 .or(schema::order_fills::taker_balance_manager_id.eq(&balance_manager_id)),
307 )
308 .load(connection)
309 .await?;
310
311 let mut volume_by_pool: HashMap<String, Vec<i64>> = HashMap::new();
312 for order_fill in results {
313 if let Some(pool_name) = pool_name_to_id
314 .iter()
315 .find(|(_, id)| **id == order_fill.pool_id)
316 .map(|(name, _)| name)
317 {
318 let entry = volume_by_pool
319 .entry(pool_name.clone())
320 .or_insert(vec![0, 0]);
321 if order_fill.maker_balance_manager_id == balance_manager_id {
322 entry[0] += order_fill.quantity;
323 }
324 if order_fill.taker_balance_manager_id == balance_manager_id {
325 entry[1] += order_fill.quantity;
326 }
327 }
328 }
329
330 Ok(Json(volume_by_pool))
331}
332
333async fn get_historical_volume_by_balance_manager_id_with_interval(
334 Path((pool_names, balance_manager_id)): Path<(String, String)>,
335 Query(params): Query<HashMap<String, String>>,
336 State(state): State<PgDeepbookPersistent>,
337) -> Result<Json<HashMap<String, HashMap<String, Vec<i64>>>>, DeepBookError> {
338 let connection = &mut state.pool.get().await?;
339
340 let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
341 let pool_name_to_id: HashMap<String, String> = pools
342 .0
343 .into_iter()
344 .map(|pool| (pool.pool_name, pool.pool_id))
345 .collect();
346
347 let pool_ids_list: Vec<String> = pool_names
348 .split(',')
349 .filter_map(|name| pool_name_to_id.get(name).cloned())
350 .collect();
351
352 if pool_ids_list.is_empty() {
353 return Err(DeepBookError::InternalError(
354 "No valid pool names provided".to_string(),
355 ));
356 }
357
358 let interval = params
360 .get("interval")
361 .and_then(|v| v.parse::<i64>().ok())
362 .unwrap_or(3600); if interval <= 0 {
365 return Err(DeepBookError::InternalError(
366 "Interval must be greater than 0".to_string(),
367 ));
368 }
369
370 let interval_ms = interval * 1000;
371
372 let end_time = params
374 .get("end_time")
375 .and_then(|v| v.parse::<i64>().ok())
376 .map(|t| t * 1000) .unwrap_or_else(|| {
378 SystemTime::now()
379 .duration_since(UNIX_EPOCH)
380 .unwrap()
381 .as_millis() as i64
382 });
383
384 let start_time = params
385 .get("start_time")
386 .and_then(|v| v.parse::<i64>().ok())
387 .map(|t| t * 1000) .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
389
390 let mut metrics_by_interval: HashMap<String, HashMap<String, Vec<i64>>> = HashMap::new();
391
392 let mut current_start = start_time;
393 while current_start + interval_ms <= end_time {
394 let current_end = current_start + interval_ms;
395
396 let volume_in_base = params
397 .get("volume_in_base")
398 .map(|v| v == "true")
399 .unwrap_or(false);
400 let column_to_query = if volume_in_base {
401 sql::<diesel::sql_types::BigInt>("base_quantity")
402 } else {
403 sql::<diesel::sql_types::BigInt>("quote_quantity")
404 };
405
406 let results: Vec<OrderFillSummary> = schema::order_fills::table
407 .select((
408 schema::order_fills::pool_id,
409 schema::order_fills::maker_balance_manager_id,
410 schema::order_fills::taker_balance_manager_id,
411 column_to_query,
412 ))
413 .filter(schema::order_fills::pool_id.eq_any(&pool_ids_list))
414 .filter(
415 schema::order_fills::checkpoint_timestamp_ms.between(current_start, current_end),
416 )
417 .filter(
418 schema::order_fills::maker_balance_manager_id
419 .eq(&balance_manager_id)
420 .or(schema::order_fills::taker_balance_manager_id.eq(&balance_manager_id)),
421 )
422 .load(connection)
423 .await?;
424
425 let mut volume_by_pool: HashMap<String, Vec<i64>> = HashMap::new();
426 for order_fill in results {
427 if let Some(pool_name) = pool_name_to_id
428 .iter()
429 .find(|(_, id)| **id == order_fill.pool_id)
430 .map(|(name, _)| name)
431 {
432 let entry = volume_by_pool
433 .entry(pool_name.clone())
434 .or_insert(vec![0, 0]);
435 if order_fill.maker_balance_manager_id == balance_manager_id {
436 entry[0] += order_fill.quantity;
437 }
438 if order_fill.taker_balance_manager_id == balance_manager_id {
439 entry[1] += order_fill.quantity;
440 }
441 }
442 }
443
444 metrics_by_interval.insert(
445 format!("[{}, {}]", current_start / 1000, current_end / 1000),
446 volume_by_pool,
447 );
448
449 current_start = current_end;
450 }
451
452 Ok(Json(metrics_by_interval))
453}
454
455async fn ticker(
456 Query(params): Query<HashMap<String, String>>,
457 State(state): State<PgDeepbookPersistent>,
458) -> Result<Json<HashMap<String, HashMap<String, Value>>>, DeepBookError> {
459 let base_volumes = fetch_historical_volume(¶ms, true, &state).await?;
461 let quote_volumes = fetch_historical_volume(¶ms, false, &state).await?;
462
463 let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
465 let pool_map: HashMap<String, &Pools> = pools
466 .0
467 .iter()
468 .map(|pool| (pool.pool_id.clone(), pool))
469 .collect();
470
471 let end_time = SystemTime::now()
472 .duration_since(UNIX_EPOCH)
473 .map_err(|_| DeepBookError::InternalError("System time error".to_string()))?
474 .as_millis() as i64;
475
476 let start_time = end_time - (24 * 60 * 60 * 1000);
478
479 let connection = &mut state.pool.get().await?;
481 let last_prices: Vec<(String, i64)> = schema::order_fills::table
482 .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time))
483 .select((schema::order_fills::pool_id, schema::order_fills::price))
484 .order_by((
485 schema::order_fills::pool_id.asc(),
486 schema::order_fills::checkpoint_timestamp_ms.desc(),
487 ))
488 .distinct_on(schema::order_fills::pool_id)
489 .load(connection)
490 .await?;
491
492 let last_price_map: HashMap<String, i64> = last_prices.into_iter().collect();
493
494 let mut response = HashMap::new();
495
496 for (pool_id, pool) in &pool_map {
497 let pool_name = &pool.pool_name;
498 let base_volume = base_volumes.get(pool_name).copied().unwrap_or(0);
499 let quote_volume = quote_volumes.get(pool_name).copied().unwrap_or(0);
500 let last_price = last_price_map.get(pool_id).copied();
501
502 let base_factor = 10u64.pow(pool.base_asset_decimals as u32);
504 let quote_factor = 10u64.pow(pool.quote_asset_decimals as u32);
505 let price_factor =
506 10u64.pow((9 - pool.base_asset_decimals + pool.quote_asset_decimals) as u32);
507
508 response.insert(
509 pool_name.clone(),
510 HashMap::from([
511 (
512 "last_price".to_string(),
513 Value::from(
514 last_price
515 .map(|price| price as f64 / price_factor as f64)
516 .unwrap_or(0.0),
517 ),
518 ),
519 (
520 "base_volume".to_string(),
521 Value::from(base_volume as f64 / base_factor as f64),
522 ),
523 (
524 "quote_volume".to_string(),
525 Value::from(quote_volume as f64 / quote_factor as f64),
526 ),
527 ("isFrozen".to_string(), Value::from(0)), ]),
529 );
530 }
531
532 Ok(Json(response))
533}
534
535async fn fetch_historical_volume(
536 params: &HashMap<String, String>,
537 volume_in_base: bool,
538 state: &PgDeepbookPersistent,
539) -> Result<HashMap<String, u64>, DeepBookError> {
540 let mut params_with_volume = params.clone();
541 params_with_volume.insert("volume_in_base".to_string(), volume_in_base.to_string());
542
543 all_historical_volume(Query(params_with_volume), State(state.clone()))
544 .await
545 .map(|Json(volumes)| volumes)
546}
547
548#[allow(clippy::get_first)]
549async fn summary(
550 State(state): State<PgDeepbookPersistent>,
551) -> Result<Json<Vec<HashMap<String, Value>>>, DeepBookError> {
552 let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
554 let pool_metadata: HashMap<String, (String, (i16, i16))> = pools
555 .0
556 .iter()
557 .map(|pool| {
558 (
559 pool.pool_name.clone(),
560 (
561 pool.pool_id.clone(),
562 (pool.base_asset_decimals, pool.quote_asset_decimals),
563 ),
564 )
565 })
566 .collect();
567
568 let pool_decimals: HashMap<String, (i16, i16)> = pool_metadata
570 .iter()
571 .map(|(_, (pool_id, decimals))| (pool_id.clone(), *decimals))
572 .collect();
573
574 let (ticker_result, price_change_result, high_low_result) = join!(
576 ticker(Query(HashMap::new()), State(state.clone())),
577 price_change_24h(&pool_metadata, State(state.clone())),
578 high_low_prices_24h(&pool_decimals, State(state.clone()))
579 );
580
581 let Json(ticker_map) = ticker_result?;
582 let price_change_map = price_change_result?;
583 let high_low_map = high_low_result?;
584
585 let orderbook_futures: Vec<_> = ticker_map
587 .keys()
588 .map(|pool_name| {
589 let pool_name_clone = pool_name.clone();
590 orderbook(
591 Path(pool_name_clone),
592 Query(HashMap::from([("level".to_string(), "1".to_string())])),
593 State(state.clone()),
594 )
595 })
596 .collect();
597
598 let orderbook_results = join_all(orderbook_futures).await;
600
601 let mut response = Vec::new();
602
603 for ((pool_name, ticker_info), orderbook_result) in ticker_map.iter().zip(orderbook_results) {
604 if let Some((pool_id, _)) = pool_metadata.get(pool_name) {
605 let last_price = ticker_info
607 .get("last_price")
608 .and_then(|price| price.as_f64())
609 .unwrap_or(0.0);
610
611 let base_volume = ticker_info
612 .get("base_volume")
613 .and_then(|volume| volume.as_f64())
614 .unwrap_or(0.0);
615
616 let quote_volume = ticker_info
617 .get("quote_volume")
618 .and_then(|volume| volume.as_f64())
619 .unwrap_or(0.0);
620
621 let price_change_percent = price_change_map.get(pool_name).copied().unwrap_or(0.0);
623
624 let (highest_price, lowest_price) =
626 high_low_map.get(pool_id).copied().unwrap_or((0.0, 0.0));
627
628 let orderbook_data = orderbook_result.ok().map(|Json(data)| data);
630
631 let highest_bid = orderbook_data
632 .as_ref()
633 .and_then(|data| data.get("bids"))
634 .and_then(|bids| bids.as_array())
635 .and_then(|bids| bids.get(0))
636 .and_then(|bid| bid.as_array())
637 .and_then(|bid| bid.get(0))
638 .and_then(|price| price.as_str()?.parse::<f64>().ok())
639 .unwrap_or(0.0);
640
641 let lowest_ask = orderbook_data
642 .as_ref()
643 .and_then(|data| data.get("asks"))
644 .and_then(|asks| asks.as_array())
645 .and_then(|asks| asks.get(0))
646 .and_then(|ask| ask.as_array())
647 .and_then(|ask| ask.get(0))
648 .and_then(|price| price.as_str()?.parse::<f64>().ok())
649 .unwrap_or(0.0);
650
651 let mut summary_data = HashMap::new();
652 summary_data.insert(
653 "trading_pairs".to_string(),
654 Value::String(pool_name.clone()),
655 );
656 let parts: Vec<&str> = pool_name.split('_').collect();
657 let base_currency = parts.get(0).unwrap_or(&"Unknown").to_string();
658 let quote_currency = parts.get(1).unwrap_or(&"Unknown").to_string();
659
660 summary_data.insert("base_currency".to_string(), Value::String(base_currency));
661 summary_data.insert("quote_currency".to_string(), Value::String(quote_currency));
662 summary_data.insert("last_price".to_string(), Value::from(last_price));
663 summary_data.insert("base_volume".to_string(), Value::from(base_volume));
664 summary_data.insert("quote_volume".to_string(), Value::from(quote_volume));
665 summary_data.insert(
666 "price_change_percent_24h".to_string(),
667 Value::from(price_change_percent),
668 );
669 summary_data.insert("highest_price_24h".to_string(), Value::from(highest_price));
670 summary_data.insert("lowest_price_24h".to_string(), Value::from(lowest_price));
671 summary_data.insert("highest_bid".to_string(), Value::from(highest_bid));
672 summary_data.insert("lowest_ask".to_string(), Value::from(lowest_ask));
673
674 response.push(summary_data);
675 }
676 }
677
678 Ok(Json(response))
679}
680
681async fn high_low_prices_24h(
682 pool_decimals: &HashMap<String, (i16, i16)>,
683 State(state): State<PgDeepbookPersistent>,
684) -> Result<HashMap<String, (f64, f64)>, DeepBookError> {
685 let end_time = SystemTime::now()
687 .duration_since(UNIX_EPOCH)
688 .map_err(|_| DeepBookError::InternalError("System time error".to_string()))?
689 .as_millis() as i64;
690
691 let start_time = end_time - (24 * 60 * 60 * 1000);
693
694 let connection = &mut state.pool.get().await?;
695
696 let results: Vec<(String, Option<i64>, Option<i64>)> = schema::order_fills::table
698 .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time))
699 .group_by(schema::order_fills::pool_id)
700 .select((
701 schema::order_fills::pool_id,
702 max(schema::order_fills::price),
703 min(schema::order_fills::price),
704 ))
705 .load(connection)
706 .await?;
707
708 let mut price_map: HashMap<String, (f64, f64)> = HashMap::new();
710
711 for (pool_id, max_price_opt, min_price_opt) in results {
712 if let Some((base_decimals, quote_decimals)) = pool_decimals.get(&pool_id) {
713 let scaling_factor = 10f64.powi((9 - base_decimals + quote_decimals) as i32);
714
715 let max_price_f64 = max_price_opt.unwrap_or(0) as f64 / scaling_factor;
716 let min_price_f64 = min_price_opt.unwrap_or(0) as f64 / scaling_factor;
717
718 price_map.insert(pool_id, (max_price_f64, min_price_f64));
719 }
720 }
721
722 Ok(price_map)
723}
724
725async fn price_change_24h(
726 pool_metadata: &HashMap<String, (String, (i16, i16))>,
727 State(state): State<PgDeepbookPersistent>,
728) -> Result<HashMap<String, f64>, DeepBookError> {
729 let connection = &mut state.pool.get().await?;
730
731 let now = SystemTime::now()
733 .duration_since(UNIX_EPOCH)
734 .map_err(|_| DeepBookError::InternalError("System time error".to_string()))?
735 .as_millis() as i64;
736
737 let timestamp_24h_ago = now - (24 * 60 * 60 * 1000); let timestamp_48h_ago = now - (48 * 60 * 60 * 1000); let mut response = HashMap::new();
741
742 for (pool_name, (pool_id, (base_decimals, quote_decimals))) in pool_metadata.iter() {
743 let earliest_trade_24h = schema::order_fills::table
745 .filter(
746 schema::order_fills::checkpoint_timestamp_ms
747 .between(timestamp_48h_ago, timestamp_24h_ago),
748 )
749 .filter(schema::order_fills::pool_id.eq(pool_id))
750 .order_by(schema::order_fills::checkpoint_timestamp_ms.desc())
751 .select(schema::order_fills::price)
752 .first::<i64>(connection)
753 .await;
754
755 let most_recent_trade = schema::order_fills::table
757 .filter(schema::order_fills::checkpoint_timestamp_ms.between(timestamp_24h_ago, now))
758 .filter(schema::order_fills::pool_id.eq(pool_id))
759 .order_by(schema::order_fills::checkpoint_timestamp_ms.desc())
760 .select(schema::order_fills::price)
761 .first::<i64>(connection)
762 .await;
763
764 if let (Ok(earliest_price), Ok(most_recent_price)) = (earliest_trade_24h, most_recent_trade)
765 {
766 let price_factor = 10u64.pow((9 - base_decimals + quote_decimals) as u32);
767
768 let earliest_price_scaled = earliest_price as f64 / price_factor as f64;
770 let most_recent_price_scaled = most_recent_price as f64 / price_factor as f64;
771
772 let price_change_percent =
774 ((most_recent_price_scaled / earliest_price_scaled) - 1.0) * 100.0;
775
776 response.insert(pool_name.clone(), price_change_percent);
777 } else {
778 response.insert(pool_name.clone(), 0.0);
780 }
781 }
782
783 Ok(response)
784}
785
786async fn order_updates(
787 Path(pool_name): Path<String>,
788 Query(params): Query<HashMap<String, String>>,
789 State(state): State<PgDeepbookPersistent>,
790) -> Result<Json<Vec<HashMap<String, Value>>>, DeepBookError> {
791 let connection = &mut state.pool.get().await?;
792
793 let (pool_id, base_decimals, quote_decimals) = schema::pools::table
795 .filter(schema::pools::pool_name.eq(pool_name.clone()))
796 .select((
797 schema::pools::pool_id,
798 schema::pools::base_asset_decimals,
799 schema::pools::quote_asset_decimals,
800 ))
801 .first::<(String, i16, i16)>(connection)
802 .await
803 .map_err(|_| DeepBookError::InternalError(format!("Pool '{}' not found", pool_name)))?;
804
805 let base_decimals = base_decimals as u8;
806 let quote_decimals = quote_decimals as u8;
807
808 let end_time = params
809 .get("end_time")
810 .and_then(|v| v.parse::<i64>().ok())
811 .map(|t| t * 1000) .unwrap_or_else(|| {
813 SystemTime::now()
814 .duration_since(UNIX_EPOCH)
815 .unwrap()
816 .as_millis() as i64
817 });
818
819 let start_time = params
820 .get("start_time")
821 .and_then(|v| v.parse::<i64>().ok())
822 .map(|t| t * 1000) .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
824
825 let limit = params
826 .get("limit")
827 .and_then(|v| v.parse::<i64>().ok())
828 .unwrap_or(1);
829
830 let mut query = schema::order_updates::table
831 .filter(schema::order_updates::checkpoint_timestamp_ms.between(start_time, end_time))
832 .filter(schema::order_updates::pool_id.eq(pool_id))
833 .order_by(schema::order_updates::checkpoint_timestamp_ms.desc())
834 .select((
835 schema::order_updates::order_id,
836 schema::order_updates::price,
837 schema::order_updates::original_quantity,
838 schema::order_updates::quantity,
839 schema::order_updates::filled_quantity,
840 schema::order_updates::checkpoint_timestamp_ms,
841 schema::order_updates::is_bid,
842 schema::order_updates::balance_manager_id,
843 schema::order_updates::status,
844 ))
845 .limit(limit)
846 .into_boxed();
847
848 let balance_manager_filter = params.get("balance_manager_id").cloned();
849 if let Some(manager_id) = balance_manager_filter {
850 query = query.filter(schema::order_updates::balance_manager_id.eq(manager_id));
851 }
852
853 let status_filter = params.get("status").cloned();
854 if let Some(status) = status_filter {
855 query = query.filter(schema::order_updates::status.eq(status));
856 }
857
858 let trades = query
859 .load::<(String, i64, i64, i64, i64, i64, bool, String, String)>(connection)
860 .await
861 .map_err(|_| DeepBookError::InternalError("Error fetching trade details".to_string()))?;
862
863 let base_factor = 10u64.pow(base_decimals as u32);
864 let price_factor = 10u64.pow((9 - base_decimals + quote_decimals) as u32);
865
866 let trade_data: Vec<HashMap<String, Value>> = trades
867 .into_iter()
868 .map(
869 |(
870 order_id,
871 price,
872 original_quantity,
873 quantity,
874 filled_quantity,
875 timestamp,
876 is_bid,
877 balance_manager_id,
878 status,
879 )| {
880 let trade_type = if is_bid { "buy" } else { "sell" };
881 HashMap::from([
882 ("order_id".to_string(), Value::from(order_id)),
883 (
884 "price".to_string(),
885 Value::from(price as f64 / price_factor as f64),
886 ),
887 (
888 "original_quantity".to_string(),
889 Value::from(original_quantity as f64 / base_factor as f64),
890 ),
891 (
892 "remaining_quantity".to_string(),
893 Value::from(quantity as f64 / base_factor as f64),
894 ),
895 (
896 "filled_quantity".to_string(),
897 Value::from(filled_quantity as f64 / base_factor as f64),
898 ),
899 ("timestamp".to_string(), Value::from(timestamp as u64)),
900 ("type".to_string(), Value::from(trade_type)),
901 (
902 "balance_manager_id".to_string(),
903 Value::from(balance_manager_id),
904 ),
905 ("status".to_string(), Value::from(status)),
906 ])
907 },
908 )
909 .collect();
910
911 Ok(Json(trade_data))
912}
913
914async fn trades(
915 Path(pool_name): Path<String>,
916 Query(params): Query<HashMap<String, String>>,
917 State(state): State<PgDeepbookPersistent>,
918) -> Result<Json<Vec<HashMap<String, Value>>>, DeepBookError> {
919 let connection = &mut state.pool.get().await?;
921 let pool_data = schema::pools::table
922 .filter(schema::pools::pool_name.eq(pool_name.clone()))
923 .select((
924 schema::pools::pool_id,
925 schema::pools::base_asset_decimals,
926 schema::pools::quote_asset_decimals,
927 ))
928 .first::<(String, i16, i16)>(connection)
929 .await
930 .map_err(|_| DeepBookError::InternalError(format!("Pool '{}' not found", pool_name)))?;
931
932 let end_time = params
934 .get("end_time")
935 .and_then(|v| v.parse::<i64>().ok())
936 .map(|t| t * 1000) .unwrap_or_else(|| {
938 SystemTime::now()
939 .duration_since(UNIX_EPOCH)
940 .unwrap()
941 .as_millis() as i64
942 });
943
944 let start_time = params
945 .get("start_time")
946 .and_then(|v| v.parse::<i64>().ok())
947 .map(|t| t * 1000) .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
949
950 let limit = params
952 .get("limit")
953 .and_then(|v| v.parse::<i64>().ok())
954 .unwrap_or(1);
955
956 let maker_balance_manager_filter = params.get("maker_balance_manager_id").cloned();
958 let taker_balance_manager_filter = params.get("taker_balance_manager_id").cloned();
959
960 let (pool_id, base_decimals, quote_decimals) = pool_data;
961 let base_decimals = base_decimals as u8;
962 let quote_decimals = quote_decimals as u8;
963
964 let mut query = schema::order_fills::table
966 .filter(schema::order_fills::pool_id.eq(pool_id))
967 .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time))
968 .into_boxed();
969
970 if let Some(maker_id) = maker_balance_manager_filter {
972 query = query.filter(schema::order_fills::maker_balance_manager_id.eq(maker_id));
973 }
974 if let Some(taker_id) = taker_balance_manager_filter {
975 query = query.filter(schema::order_fills::taker_balance_manager_id.eq(taker_id));
976 }
977
978 let trades = query
980 .order_by(schema::order_fills::checkpoint_timestamp_ms.desc()) .limit(limit) .select((
983 schema::order_fills::maker_order_id,
984 schema::order_fills::taker_order_id,
985 schema::order_fills::price,
986 schema::order_fills::base_quantity,
987 schema::order_fills::quote_quantity,
988 schema::order_fills::checkpoint_timestamp_ms,
989 schema::order_fills::taker_is_bid,
990 schema::order_fills::maker_balance_manager_id,
991 schema::order_fills::taker_balance_manager_id,
992 ))
993 .load::<(String, String, i64, i64, i64, i64, bool, String, String)>(connection)
994 .await
995 .map_err(|_| {
996 DeepBookError::InternalError(format!(
997 "No trades found for pool '{}' in the specified time range",
998 pool_name
999 ))
1000 })?;
1001
1002 let base_factor = 10u64.pow(base_decimals as u32);
1004 let quote_factor = 10u64.pow(quote_decimals as u32);
1005 let price_factor = 10u64.pow((9 - base_decimals + quote_decimals) as u32);
1006
1007 let trade_data: Vec<HashMap<String, Value>> = trades
1009 .into_iter()
1010 .map(
1011 |(
1012 maker_order_id,
1013 taker_order_id,
1014 price,
1015 base_quantity,
1016 quote_quantity,
1017 timestamp,
1018 taker_is_bid,
1019 maker_balance_manager_id,
1020 taker_balance_manager_id,
1021 )| {
1022 let trade_id = calculate_trade_id(&maker_order_id, &taker_order_id).unwrap_or(0);
1023 let trade_type = if taker_is_bid { "buy" } else { "sell" };
1024
1025 HashMap::from([
1026 ("trade_id".to_string(), Value::from(trade_id.to_string())),
1027 ("maker_order_id".to_string(), Value::from(maker_order_id)),
1028 ("taker_order_id".to_string(), Value::from(taker_order_id)),
1029 (
1030 "maker_balance_manager_id".to_string(),
1031 Value::from(maker_balance_manager_id),
1032 ),
1033 (
1034 "taker_balance_manager_id".to_string(),
1035 Value::from(taker_balance_manager_id),
1036 ),
1037 (
1038 "price".to_string(),
1039 Value::from(price as f64 / price_factor as f64),
1040 ),
1041 (
1042 "base_volume".to_string(),
1043 Value::from(base_quantity as f64 / base_factor as f64),
1044 ),
1045 (
1046 "quote_volume".to_string(),
1047 Value::from(quote_quantity as f64 / quote_factor as f64),
1048 ),
1049 ("timestamp".to_string(), Value::from(timestamp as u64)),
1050 ("type".to_string(), Value::from(trade_type)),
1051 ])
1052 },
1053 )
1054 .collect();
1055
1056 Ok(Json(trade_data))
1057}
1058
1059async fn trade_count(
1060 Query(params): Query<HashMap<String, String>>,
1061 State(state): State<PgDeepbookPersistent>,
1062) -> Result<Json<i64>, DeepBookError> {
1063 let end_time = params
1065 .get("end_time")
1066 .and_then(|v| v.parse::<i64>().ok())
1067 .map(|t| t * 1000) .unwrap_or_else(|| {
1069 SystemTime::now()
1070 .duration_since(UNIX_EPOCH)
1071 .unwrap()
1072 .as_millis() as i64
1073 });
1074
1075 let start_time = params
1076 .get("start_time")
1077 .and_then(|v| v.parse::<i64>().ok())
1078 .map(|t| t * 1000) .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
1080
1081 let connection = &mut state.pool.get().await?;
1082 let result: i64 = schema::order_fills::table
1083 .select(count_star())
1084 .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time))
1085 .first(connection)
1086 .await?;
1087
1088 Ok(Json(result))
1089}
1090
1091fn calculate_trade_id(maker_id: &str, taker_id: &str) -> Result<u128, DeepBookError> {
1092 let maker_id = maker_id
1094 .parse::<u128>()
1095 .map_err(|_| DeepBookError::InternalError("Invalid maker_id".to_string()))?;
1096 let taker_id = taker_id
1097 .parse::<u128>()
1098 .map_err(|_| DeepBookError::InternalError("Invalid taker_id".to_string()))?;
1099
1100 let maker_id = maker_id & !(1 << 127);
1102 let taker_id = taker_id & !(1 << 127);
1103
1104 Ok(maker_id + taker_id)
1106}
1107
1108pub async fn assets(
1109 State(state): State<PgDeepbookPersistent>,
1110) -> Result<Json<HashMap<String, HashMap<String, Value>>>, DeepBookError> {
1111 let connection = &mut state.pool.get().await?;
1112 let assets = schema::assets::table
1113 .select((
1114 schema::assets::symbol,
1115 schema::assets::name,
1116 schema::assets::ucid,
1117 schema::assets::package_address_url,
1118 schema::assets::package_id,
1119 ))
1120 .load::<(String, String, Option<i32>, Option<String>, Option<String>)>(connection)
1121 .await
1122 .map_err(|err| DeepBookError::InternalError(format!("Failed to query assets: {}", err)))?;
1123
1124 let mut response = HashMap::new();
1125
1126 for (symbol, name, ucid, package_address_url, package_id) in assets {
1127 let mut asset_info = HashMap::new();
1128 asset_info.insert("name".to_string(), Value::String(name));
1129 asset_info.insert(
1130 "can_withdraw".to_string(),
1131 Value::String("true".to_string()),
1132 );
1133 asset_info.insert("can_deposit".to_string(), Value::String("true".to_string()));
1134
1135 if let Some(ucid) = ucid {
1136 asset_info.insert(
1137 "unified_cryptoasset_id".to_string(),
1138 Value::String(ucid.to_string()),
1139 );
1140 }
1141 if let Some(addresses) = package_address_url {
1142 asset_info.insert("contractAddressUrl".to_string(), Value::String(addresses));
1143 }
1144
1145 if let Some(addresses) = package_id {
1146 asset_info.insert("contractAddress".to_string(), Value::String(addresses));
1147 }
1148
1149 response.insert(symbol, asset_info);
1150 }
1151
1152 Ok(Json(response))
1153}
1154
1155async fn orderbook(
1157 Path(pool_name): Path<String>,
1158 Query(params): Query<HashMap<String, String>>,
1159 State(state): State<PgDeepbookPersistent>,
1160) -> Result<Json<HashMap<String, Value>>, DeepBookError> {
1161 let depth = params
1162 .get("depth")
1163 .map(|v| v.parse::<u64>())
1164 .transpose()
1165 .map_err(|_| {
1166 DeepBookError::InternalError("Depth must be a non-negative integer".to_string())
1167 })?
1168 .map(|depth| if depth == 0 { 200 } else { depth });
1169
1170 if let Some(depth) = depth
1171 && depth == 1
1172 {
1173 return Err(DeepBookError::InternalError(
1174 "Depth cannot be 1. Use a value greater than 1 or 0 for the entire orderbook"
1175 .to_string(),
1176 ));
1177 }
1178
1179 let level = params
1180 .get("level")
1181 .map(|v| v.parse::<u64>())
1182 .transpose()
1183 .map_err(|_| {
1184 DeepBookError::InternalError("Level must be an integer between 1 and 2".to_string())
1185 })?;
1186
1187 if let Some(level) = level
1188 && !(1..=2).contains(&level)
1189 {
1190 return Err(DeepBookError::InternalError(
1191 "Level must be 1 or 2".to_string(),
1192 ));
1193 }
1194
1195 let ticks_from_mid = match (depth, level) {
1196 (Some(_), Some(1)) => 1u64, (Some(depth), Some(2)) | (Some(depth), None) => depth / 2, (None, Some(1)) => 1u64, (None, Some(2)) | (None, None) => 100u64, _ => 100u64, };
1202
1203 let connection = &mut state.pool.get().await?;
1205 let pool_data = schema::pools::table
1206 .filter(schema::pools::pool_name.eq(pool_name.clone()))
1207 .select((
1208 schema::pools::pool_id,
1209 schema::pools::base_asset_id,
1210 schema::pools::base_asset_decimals,
1211 schema::pools::quote_asset_id,
1212 schema::pools::quote_asset_decimals,
1213 ))
1214 .first::<(String, String, i16, String, i16)>(connection)
1215 .await?;
1216
1217 let (pool_id, base_asset_id, base_decimals, quote_asset_id, quote_decimals) = pool_data;
1218 let base_decimals = base_decimals as u8;
1219 let quote_decimals = quote_decimals as u8;
1220
1221 let pool_address = ObjectID::from_hex_literal(&pool_id)?;
1222
1223 let sui_client = SuiClientBuilder::default().build(SUI_MAINNET_URL).await?;
1224 let mut ptb = ProgrammableTransactionBuilder::new();
1225
1226 let pool_object: SuiObjectResponse = sui_client
1227 .read_api()
1228 .get_object_with_options(pool_address, SuiObjectDataOptions::full_content())
1229 .await?;
1230 let pool_data: &SuiObjectData =
1231 pool_object
1232 .data
1233 .as_ref()
1234 .ok_or(DeepBookError::InternalError(format!(
1235 "Missing data in pool object response for '{}'",
1236 pool_name
1237 )))?;
1238 let pool_object_ref: ObjectRef = (pool_data.object_id, pool_data.version, pool_data.digest);
1239
1240 let pool_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(pool_object_ref));
1241 ptb.input(pool_input)?;
1242
1243 let input_argument = CallArg::Pure(bcs::to_bytes(&ticks_from_mid).map_err(|_| {
1244 DeepBookError::InternalError("Failed to serialize ticks_from_mid".to_string())
1245 })?);
1246 ptb.input(input_argument)?;
1247
1248 let sui_clock_object_id = ObjectID::from_hex_literal(
1249 "0x0000000000000000000000000000000000000000000000000000000000000006",
1250 )?;
1251 let sui_clock_object: SuiObjectResponse = sui_client
1252 .read_api()
1253 .get_object_with_options(sui_clock_object_id, SuiObjectDataOptions::full_content())
1254 .await?;
1255 let clock_data: &SuiObjectData =
1256 sui_clock_object
1257 .data
1258 .as_ref()
1259 .ok_or(DeepBookError::InternalError(
1260 "Missing data in clock object response".to_string(),
1261 ))?;
1262
1263 let sui_clock_object_ref: ObjectRef =
1264 (clock_data.object_id, clock_data.version, clock_data.digest);
1265
1266 let clock_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(sui_clock_object_ref));
1267 ptb.input(clock_input)?;
1268
1269 let base_coin_type = parse_type_input(&base_asset_id)?;
1270 let quote_coin_type = parse_type_input("e_asset_id)?;
1271
1272 let package = ObjectID::from_hex_literal(DEEPBOOK_PACKAGE_ID)
1273 .map_err(|e| DeepBookError::InternalError(format!("Invalid pool ID: {}", e)))?;
1274 let module = LEVEL2_MODULE.to_string();
1275 let function = LEVEL2_FUNCTION.to_string();
1276
1277 ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall {
1278 package,
1279 module,
1280 function,
1281 type_arguments: vec![base_coin_type, quote_coin_type],
1282 arguments: vec![Argument::Input(0), Argument::Input(1), Argument::Input(2)],
1283 })));
1284
1285 let builder = ptb.finish();
1286 let tx = TransactionKind::ProgrammableTransaction(builder);
1287
1288 let result = sui_client
1289 .read_api()
1290 .dev_inspect_transaction_block(SuiAddress::default(), tx, None, None, None)
1291 .await?;
1292
1293 let mut binding = result.results.ok_or(DeepBookError::InternalError(
1294 "No results from dev_inspect_transaction_block".to_string(),
1295 ))?;
1296 let bid_prices = &binding
1297 .first_mut()
1298 .ok_or(DeepBookError::InternalError(
1299 "No return values for bid prices".to_string(),
1300 ))?
1301 .return_values
1302 .first_mut()
1303 .ok_or(DeepBookError::InternalError(
1304 "No bid price data found".to_string(),
1305 ))?
1306 .0;
1307 let bid_parsed_prices: Vec<u64> = bcs::from_bytes(bid_prices).map_err(|_| {
1308 DeepBookError::InternalError("Failed to deserialize bid prices".to_string())
1309 })?;
1310 let bid_quantities = &binding
1311 .first_mut()
1312 .ok_or(DeepBookError::InternalError(
1313 "No return values for bid quantities".to_string(),
1314 ))?
1315 .return_values
1316 .get(1)
1317 .ok_or(DeepBookError::InternalError(
1318 "No bid quantity data found".to_string(),
1319 ))?
1320 .0;
1321 let bid_parsed_quantities: Vec<u64> = bcs::from_bytes(bid_quantities).map_err(|_| {
1322 DeepBookError::InternalError("Failed to deserialize bid quantities".to_string())
1323 })?;
1324
1325 let ask_prices = &binding
1326 .first_mut()
1327 .ok_or(DeepBookError::InternalError(
1328 "No return values for ask prices".to_string(),
1329 ))?
1330 .return_values
1331 .get(2)
1332 .ok_or(DeepBookError::InternalError(
1333 "No ask price data found".to_string(),
1334 ))?
1335 .0;
1336 let ask_parsed_prices: Vec<u64> = bcs::from_bytes(ask_prices).map_err(|_| {
1337 DeepBookError::InternalError("Failed to deserialize ask prices".to_string())
1338 })?;
1339 let ask_quantities = &binding
1340 .first_mut()
1341 .ok_or(DeepBookError::InternalError(
1342 "No return values for ask quantities".to_string(),
1343 ))?
1344 .return_values
1345 .get(3)
1346 .ok_or(DeepBookError::InternalError(
1347 "No ask quantity data found".to_string(),
1348 ))?
1349 .0;
1350 let ask_parsed_quantities: Vec<u64> = bcs::from_bytes(ask_quantities).map_err(|_| {
1351 DeepBookError::InternalError("Failed to deserialize ask quantities".to_string())
1352 })?;
1353
1354 let mut result = HashMap::new();
1355
1356 let timestamp = SystemTime::now()
1357 .duration_since(UNIX_EPOCH)
1358 .map_err(|_| DeepBookError::InternalError("System time error".to_string()))?
1359 .as_millis() as i64;
1360 result.insert("timestamp".to_string(), Value::from(timestamp.to_string()));
1361
1362 let bids: Vec<Value> = bid_parsed_prices
1363 .into_iter()
1364 .zip(bid_parsed_quantities.into_iter())
1365 .take(ticks_from_mid as usize)
1366 .map(|(price, quantity)| {
1367 let price_factor = 10u64.pow((9 - base_decimals + quote_decimals).into());
1368 let quantity_factor = 10u64.pow((base_decimals).into());
1369 Value::Array(vec![
1370 Value::from((price as f64 / price_factor as f64).to_string()),
1371 Value::from((quantity as f64 / quantity_factor as f64).to_string()),
1372 ])
1373 })
1374 .collect();
1375 result.insert("bids".to_string(), Value::Array(bids));
1376
1377 let asks: Vec<Value> = ask_parsed_prices
1378 .into_iter()
1379 .zip(ask_parsed_quantities.into_iter())
1380 .take(ticks_from_mid as usize)
1381 .map(|(price, quantity)| {
1382 let price_factor = 10u64.pow((9 - base_decimals + quote_decimals).into());
1383 let quantity_factor = 10u64.pow((base_decimals).into());
1384 Value::Array(vec![
1385 Value::from((price as f64 / price_factor as f64).to_string()),
1386 Value::from((quantity as f64 / quantity_factor as f64).to_string()),
1387 ])
1388 })
1389 .collect();
1390 result.insert("asks".to_string(), Value::Array(asks));
1391
1392 Ok(Json(result))
1393}
1394
1395async fn deep_supply() -> Result<Json<u64>, DeepBookError> {
1397 let sui_client = SuiClientBuilder::default().build(SUI_MAINNET_URL).await?;
1398 let mut ptb = ProgrammableTransactionBuilder::new();
1399
1400 let deep_treasury_object_id = ObjectID::from_hex_literal(DEEP_TREASURY_ID)?;
1401 let deep_treasury_object: SuiObjectResponse = sui_client
1402 .read_api()
1403 .get_object_with_options(
1404 deep_treasury_object_id,
1405 SuiObjectDataOptions::full_content(),
1406 )
1407 .await?;
1408 let deep_treasury_data: &SuiObjectData =
1409 deep_treasury_object
1410 .data
1411 .as_ref()
1412 .ok_or(DeepBookError::InternalError(
1413 "Incorrect Treasury ID".to_string(),
1414 ))?;
1415
1416 let deep_treasury_ref: ObjectRef = (
1417 deep_treasury_data.object_id,
1418 deep_treasury_data.version,
1419 deep_treasury_data.digest,
1420 );
1421
1422 let deep_treasury_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(deep_treasury_ref));
1423 ptb.input(deep_treasury_input)?;
1424
1425 let package = ObjectID::from_hex_literal(DEEP_TOKEN_PACKAGE_ID).map_err(|e| {
1426 DeepBookError::InternalError(format!("Invalid deep token package ID: {}", e))
1427 })?;
1428 let module = DEEP_SUPPLY_MODULE.to_string();
1429 let function = DEEP_SUPPLY_FUNCTION.to_string();
1430
1431 ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall {
1432 package,
1433 module,
1434 function,
1435 type_arguments: vec![],
1436 arguments: vec![Argument::Input(0)],
1437 })));
1438
1439 let builder = ptb.finish();
1440 let tx = TransactionKind::ProgrammableTransaction(builder);
1441
1442 let result = sui_client
1443 .read_api()
1444 .dev_inspect_transaction_block(SuiAddress::default(), tx, None, None, None)
1445 .await?;
1446
1447 let mut binding = result.results.ok_or(DeepBookError::InternalError(
1448 "No results from dev_inspect_transaction_block".to_string(),
1449 ))?;
1450
1451 let total_supply = &binding
1452 .first_mut()
1453 .ok_or(DeepBookError::InternalError(
1454 "No return values for total supply".to_string(),
1455 ))?
1456 .return_values
1457 .first_mut()
1458 .ok_or(DeepBookError::InternalError(
1459 "No total supply data found".to_string(),
1460 ))?
1461 .0;
1462
1463 let total_supply_value: u64 = bcs::from_bytes(total_supply).map_err(|_| {
1464 DeepBookError::InternalError("Failed to deserialize total supply".to_string())
1465 })?;
1466
1467 Ok(Json(total_supply_value))
1468}
1469
1470async fn get_net_deposits(
1471 Path((asset_ids, timestamp)): Path<(String, String)>,
1472 State(state): State<PgDeepbookPersistent>,
1473) -> Result<Json<HashMap<String, i64>>, DeepBookError> {
1474 let connection = &mut state.pool.get().await?;
1475 let mut query =
1476 "SELECT asset, SUM(amount)::bigint AS amount, deposit FROM balances WHERE checkpoint_timestamp_ms < "
1477 .to_string();
1478 query.push_str(×tamp);
1479 query.push_str("000 AND asset in (");
1480 for asset in asset_ids.split(",") {
1481 if asset.starts_with("0x") {
1482 let len = asset.len();
1483 query.push_str(&format!("'{}',", &asset[2..len]));
1484 } else {
1485 query.push_str(&format!("'{}',", asset));
1486 }
1487 }
1488 query.pop();
1489 query.push_str(") GROUP BY asset, deposit");
1490
1491 let results: Vec<BalancesSummary> = diesel::sql_query(query).load(connection).await?;
1492 let mut net_deposits = HashMap::new();
1493 for result in results {
1494 let mut asset = result.asset;
1495 if !asset.starts_with("0x") {
1496 asset.insert_str(0, "0x");
1497 }
1498 let amount = result.amount;
1499 if result.deposit {
1500 *net_deposits.entry(asset).or_insert(0) += amount;
1501 } else {
1502 *net_deposits.entry(asset).or_insert(0) -= amount;
1503 }
1504 }
1505
1506 Ok(Json(net_deposits))
1507}
1508
1509fn parse_type_input(type_str: &str) -> Result<TypeInput, DeepBookError> {
1510 let type_tag = TypeTag::from_str(type_str)?;
1511 Ok(TypeInput::from(type_tag))
1512}