sui_deepbook_indexer/
server.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    // TODO: distinguish client error.
111    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
133/// Get all pools stored in database
134async 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    // Fetch all pools to map names to IDs
152    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    // Map provided pool names to pool IDs
160    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    // Parse start_time and end_time from query parameters (in seconds) and convert to milliseconds
172    let end_time = params
173        .get("end_time")
174        .and_then(|v| v.parse::<i64>().ok())
175        .map(|t| t * 1000) // Convert to milliseconds
176        .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) // Convert to milliseconds
187        .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
188
189    // Determine whether to query volume in base or quote
190    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    // Query the database for the historical volume
201    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    // Aggregate volume by pool ID and map back to pool names
210    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
224/// Get all historical volume for all pools
225async 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    // Parse start_time and end_time
267    let end_time = params
268        .get("end_time")
269        .and_then(|v| v.parse::<i64>().ok())
270        .map(|t| t * 1000) // Convert to milliseconds
271        .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) // Convert to milliseconds
282        .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    // Parse interval
359    let interval = params
360        .get("interval")
361        .and_then(|v| v.parse::<i64>().ok())
362        .unwrap_or(3600); // Default interval: 1 hour
363
364    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    // Parse start_time and end_time
373    let end_time = params
374        .get("end_time")
375        .and_then(|v| v.parse::<i64>().ok())
376        .map(|t| t * 1000) // Convert to milliseconds
377        .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) // Convert to milliseconds
388        .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    // Fetch base and quote historical volumes
460    let base_volumes = fetch_historical_volume(&params, true, &state).await?;
461    let quote_volumes = fetch_historical_volume(&params, false, &state).await?;
462
463    // Fetch pools data for metadata
464    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    // Calculate the start time for 24 hours ago
477    let start_time = end_time - (24 * 60 * 60 * 1000);
478
479    // Fetch last prices for all pools in a single query. Only trades in the last 24 hours will count.
480    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        // Conversion factors based on decimals
503        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)), // Fixed to 0 because all pools in pools table are active
528            ]),
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    // Fetch pools metadata first since it's required for other functions
553    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    // Prepare pool decimals for scaling
569    let pool_decimals: HashMap<String, (i16, i16)> = pool_metadata
570        .iter()
571        .map(|(_, (pool_id, decimals))| (pool_id.clone(), *decimals))
572        .collect();
573
574    // Parallelize fetching ticker, price changes, and high/low prices
575    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    // Prepare futures for orderbook queries
586    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    // Run all orderbook queries concurrently
599    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            // Extract data from the ticker function response
606            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            // Fetch the 24-hour price change percent
622            let price_change_percent = price_change_map.get(pool_name).copied().unwrap_or(0.0);
623
624            // Fetch the highest and lowest prices in the last 24 hours
625            let (highest_price, lowest_price) =
626                high_low_map.get(pool_id).copied().unwrap_or((0.0, 0.0));
627
628            // Process the parallel orderbook result
629            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    // Get the current timestamp in milliseconds
686    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    // Calculate the start time for 24 hours ago
692    let start_time = end_time - (24 * 60 * 60 * 1000);
693
694    let connection = &mut state.pool.get().await?;
695
696    // Query for trades within the last 24 hours for all pools
697    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    // Aggregate the highest and lowest prices for each pool
709    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    // Calculate the timestamp for 24 hours ago
732    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); // 24 hours in milliseconds
738    let timestamp_48h_ago = now - (48 * 60 * 60 * 1000); // 24 hours in milliseconds
739
740    let mut response = HashMap::new();
741
742    for (pool_name, (pool_id, (base_decimals, quote_decimals))) in pool_metadata.iter() {
743        // Get the latest price <= 24 hours ago. Only trades until 48 hours ago will count.
744        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        // Get the most recent price. Only trades until 24 hours ago will count.
756        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            // Scale the prices
769            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            // Calculate price change percentage
773            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            // If there's no price data for 24 hours or recent trades, insert 0.0 as price change
779            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    // Fetch pool data with proper error handling
794    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) // Convert to milliseconds
812        .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) // Convert to milliseconds
823        .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    // Fetch all pools to map names to IDs and decimals
920    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    // Parse start_time and end_time
933    let end_time = params
934        .get("end_time")
935        .and_then(|v| v.parse::<i64>().ok())
936        .map(|t| t * 1000) // Convert to milliseconds
937        .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) // Convert to milliseconds
948        .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000);
949
950    // Parse limit (default to 1 if not provided)
951    let limit = params
952        .get("limit")
953        .and_then(|v| v.parse::<i64>().ok())
954        .unwrap_or(1);
955
956    // Parse optional filters for balance managers
957    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    // Build the query dynamically
965    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    // Apply optional filters if parameters are provided
971    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    // Fetch latest trades (sorted by timestamp in descending order) within the time range, applying the limit
979    let trades = query
980        .order_by(schema::order_fills::checkpoint_timestamp_ms.desc()) // Ensures latest trades come first
981        .limit(limit) // Apply limit to get the most recent trades
982        .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    // Conversion factors for decimals
1003    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    // Map trades to JSON format
1008    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    // Parse start_time and end_time
1064    let end_time = params
1065        .get("end_time")
1066        .and_then(|v| v.parse::<i64>().ok())
1067        .map(|t| t * 1000) // Convert to milliseconds
1068        .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) // Convert to milliseconds
1079        .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    // Parse maker_id and taker_id as u128
1093    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    // Ignore the most significant bit for both IDs
1101    let maker_id = maker_id & !(1 << 127);
1102    let taker_id = taker_id & !(1 << 127);
1103
1104    // Return the sum of the modified IDs as the trade_id
1105    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
1155/// Level2 data for all pools
1156async 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, // Depth + Level 1 → Best bid and ask
1197        (Some(depth), Some(2)) | (Some(depth), None) => depth / 2, // Depth + Level 2 → Use depth
1198        (None, Some(1)) => 1u64,    // Only Level 1 → Best bid and ask
1199        (None, Some(2)) | (None, None) => 100u64, // Level 2 or default → 100 ticks
1200        _ => 100u64,                // Fallback to default
1201    };
1202
1203    // Fetch the pool data from the `pools` table
1204    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(&quote_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
1395/// DEEP total supply
1396async 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(&timestamp);
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}