Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coinmarketcap Deepbook Endpoints #20453

Merged
merged 34 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c33bd2b
all historic volume
leecchh Nov 26, 2024
94123b0
normalize pool addresses
leecchh Nov 27, 2024
9988900
example bid-ask endpoint
leecchh Nov 27, 2024
fd3099e
level2 endpoint
leecchh Nov 27, 2024
b5b0424
mainnet packages
leecchh Nov 27, 2024
6a37fd9
get all mainnet pools without scaling
leecchh Nov 27, 2024
3b8e2a4
basic without division factor
leecchh Nov 27, 2024
440598e
cargo fmt
leecchh Nov 27, 2024
e05b806
accurate data
leecchh Dec 2, 2024
e42ec71
timestamp added
leecchh Dec 2, 2024
4c4d857
bid q
leecchh Dec 2, 2024
b6f2a15
sorted
leecchh Dec 2, 2024
abbc610
using pool name
leecchh Dec 2, 2024
2886422
move constant
leecchh Dec 2, 2024
55fc4ec
level2 function
leecchh Dec 2, 2024
d004f12
default 100
leecchh Dec 2, 2024
b682109
prep level
leecchh Dec 3, 2024
7ee13ca
level2 endpoint complete
leecchh Dec 3, 2024
53b63fd
cargo fmt
leecchh Dec 3, 2024
b8fa5c9
pool constants
leecchh Dec 3, 2024
2f48f7a
clippy errors
leecchh Dec 3, 2024
8e67b74
dereferencing error
leecchh Dec 3, 2024
0d8d9d0
binding changes
leecchh Dec 3, 2024
fb1692f
first mut changes
leecchh Dec 3, 2024
4b13680
use pool table
leecchh Dec 4, 2024
9cff5e9
all historical volume endpoint
leecchh Dec 4, 2024
7e66e73
historical volume
leecchh Dec 4, 2024
34ce848
cleanup
leecchh Dec 4, 2024
898dcb7
endpoints updated
leecchh Dec 4, 2024
47e282b
cleanup
leecchh Dec 4, 2024
a31b617
cleanup
leecchh Dec 4, 2024
71827b1
remove anyhow errors
leecchh Dec 4, 2024
973ab9e
clippy errors
leecchh Dec 4, 2024
bbeb61b
remove unwrap and constants
leecchh Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/sui-deepbook-indexer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ sui-indexer-builder.workspace = true
tempfile.workspace = true
axum.workspace = true
bigdecimal = { version = "0.4.5" }
serde_json = { version = "1.0", features = ["preserve_order"] }

[dev-dependencies]
hex-literal = "0.3.4"
Expand Down
281 changes: 269 additions & 12 deletions crates/sui-deepbook-indexer/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::{
sui_deepbook_indexer::PgDeepbookPersistent,
};
use axum::{
debug_handler,
extract::{Path, Query, State},
http::StatusCode,
routing::get,
Expand All @@ -19,16 +18,33 @@ use diesel::BoolExpressionMethods;
use diesel::QueryDsl;
use diesel::{ExpressionMethods, SelectableHelper};
use diesel_async::RunQueryDsl;
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{collections::HashMap, net::SocketAddr};
use tokio::{net::TcpListener, task::JoinHandle};

use std::str::FromStr;
use sui_json_rpc_types::{SuiObjectData, SuiObjectDataOptions, SuiObjectResponse};
use sui_sdk::SuiClientBuilder;
use sui_types::{
base_types::{ObjectID, ObjectRef, SuiAddress},
programmable_transaction_builder::ProgrammableTransactionBuilder,
transaction::{Argument, CallArg, Command, ObjectArg, ProgrammableMoveCall, TransactionKind},
type_input::TypeInput,
TypeTag,
};

pub const SUI_MAINNET_URL: &str = "https://fullnode.mainnet.sui.io:443";
pub const GET_POOLS_PATH: &str = "/get_pools";
pub const GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID_WITH_INTERVAL: &str =
"/get_historical_volume_by_balance_manager_id_with_interval/:pool_ids/:balance_manager_id";
pub const GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID: &str =
"/get_historical_volume_by_balance_manager_id/:pool_ids/:balance_manager_id";
pub const GET_HISTORICAL_VOLUME_PATH: &str = "/get_historical_volume/:pool_ids";
pub const HISTORICAL_VOLUME_PATH: &str = "/historical_volume/:pool_names";
pub const ALL_HISTORICAL_VOLUME_PATH: &str = "/all_historical_volume";
pub const LEVEL2_PATH: &str = "/orderbook/:pool_name";
pub const DEEPBOOK_PACKAGE_ID: &str =
"0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809";

pub fn run_server(socket_address: SocketAddr, state: PgDeepbookPersistent) -> JoinHandle<()> {
tokio::spawn(async move {
Expand All @@ -41,7 +57,8 @@ pub(crate) fn make_router(state: PgDeepbookPersistent) -> Router {
Router::new()
.route("/", get(health_check))
.route(GET_POOLS_PATH, get(get_pools))
.route(GET_HISTORICAL_VOLUME_PATH, get(get_historical_volume))
.route(HISTORICAL_VOLUME_PATH, get(historical_volume))
.route(ALL_HISTORICAL_VOLUME_PATH, get(all_historical_volume))
.route(
GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID_WITH_INTERVAL,
get(get_historical_volume_by_balance_manager_id_with_interval),
Expand All @@ -50,6 +67,7 @@ pub(crate) fn make_router(state: PgDeepbookPersistent) -> Router {
GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID,
get(get_historical_volume_by_balance_manager_id),
)
.route(LEVEL2_PATH, get(orderbook))
.with_state(state)
}

Expand Down Expand Up @@ -78,7 +96,6 @@ async fn health_check() -> StatusCode {
}

/// Get all pools stored in database
#[debug_handler]
async fn get_pools(
State(state): State<PgDeepbookPersistent>,
) -> Result<Json<Vec<Pools>>, DeepBookError> {
Expand All @@ -91,16 +108,32 @@ async fn get_pools(
Ok(Json(results))
}

async fn get_historical_volume(
Path(pool_ids): Path<String>,
async fn historical_volume(
Path(pool_names): Path<String>,
Query(params): Query<HashMap<String, String>>,
State(state): State<PgDeepbookPersistent>,
) -> Result<Json<HashMap<String, u64>>, DeepBookError> {
let connection = &mut state.pool.get().await?;

let pool_ids_list: Vec<String> = pool_ids.split(',').map(|s| s.to_string()).collect();
// Fetch all pools to map names to IDs
let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
let pool_name_to_id: HashMap<String, String> = pools
.0
.into_iter()
.map(|pool| (pool.pool_name, pool.pool_id))
.collect();

// Map provided pool names to pool IDs
let pool_ids_list: Vec<String> = pool_names
.split(',')
.filter_map(|name| pool_name_to_id.get(name).cloned())
.collect();

if pool_ids_list.is_empty() {
return Err(DeepBookError::InternalError(
"No valid pool names provided".to_string(),
));
}

// Get start_time and end_time from query parameters (in seconds)
// Parse start_time and end_time from query parameters (in seconds) and convert to milliseconds
let end_time = params
.get("end_time")
.and_then(|v| v.parse::<i64>().ok())
Expand Down Expand Up @@ -129,22 +162,47 @@ async fn get_historical_volume(
sql::<diesel::sql_types::BigInt>("quote_quantity")
};

// Query the database for the historical volume
let connection = &mut state.pool.get().await?;
let results: Vec<(String, i64)> = schema::order_fills::table
.select((schema::order_fills::pool_id, column_to_query))
.filter(schema::order_fills::pool_id.eq_any(pool_ids_list))
.filter(schema::order_fills::onchain_timestamp.between(start_time, end_time))
.load(connection)
.await?;

// Aggregate volume by pool
// Aggregate volume by pool ID and map back to pool names
let mut volume_by_pool = HashMap::new();
for (pool_id, volume) in results {
*volume_by_pool.entry(pool_id).or_insert(0) += volume as u64;
if let Some(pool_name) = pool_name_to_id
.iter()
.find(|(_, id)| **id == pool_id)
.map(|(name, _)| name)
{
*volume_by_pool.entry(pool_name.clone()).or_insert(0) += volume as u64;
}
}

Ok(Json(volume_by_pool))
}

/// Get all historical volume for all pools
async fn all_historical_volume(
Query(params): Query<HashMap<String, String>>,
State(state): State<PgDeepbookPersistent>,
) -> Result<Json<HashMap<String, u64>>, DeepBookError> {
let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;

let pool_names: String = pools
.0
.into_iter()
.map(|pool| pool.pool_name)
.collect::<Vec<String>>()
.join(",");

historical_volume(Path(pool_names), Query(params), State(state)).await
}

async fn get_historical_volume_by_balance_manager_id(
Path((pool_ids, balance_manager_id)): Path<(String, String)>,
Query(params): Query<HashMap<String, String>>,
Expand Down Expand Up @@ -310,3 +368,202 @@ async fn get_historical_volume_by_balance_manager_id_with_interval(

Ok(Json(metrics_by_interval))
}

/// Level2 data for all pools
async fn orderbook(
Path(pool_name): Path<String>,
Query(params): Query<HashMap<String, String>>,
State(state): State<PgDeepbookPersistent>,
) -> Result<Json<HashMap<String, Value>>, DeepBookError> {
let depth = params
.get("depth")
.map(|v| v.parse::<u64>())
.transpose()
.map_err(|_| {
DeepBookError::InternalError("Depth must be a non-negative integer".to_string())
})?
.map(|depth| if depth == 0 { 200 } else { depth });

if let Some(depth) = depth {
if depth == 1 {
return Err(DeepBookError::InternalError(
"Depth cannot be 1. Use a value greater than 1 or 0 for the entire orderbook"
.to_string(),
));
}
}

let level = params
.get("level")
.map(|v| v.parse::<u64>())
.transpose()
.map_err(|_| {
DeepBookError::InternalError("Level must be an integer between 1 and 2".to_string())
})?;

if let Some(level) = level {
if !(1..=2).contains(&level) {
return Err(DeepBookError::InternalError(
"Level must be 1 or 2".to_string(),
));
}
}

let ticks_from_mid = match (depth, level) {
(Some(_), Some(1)) => 1u64, // Depth + Level 1 → Best bid and ask
(Some(depth), Some(2)) | (Some(depth), None) => depth / 2, // Depth + Level 2 → Use depth
(None, Some(1)) => 1u64, // Only Level 1 → Best bid and ask
(None, Some(2)) | (None, None) => 100u64, // Level 2 or default → 100 ticks
_ => 100u64, // Fallback to default
};

// Fetch the pool data from the `pools` table
let connection = &mut state.pool.get().await?;
let pool_data = schema::pools::table
.filter(schema::pools::pool_name.eq(pool_name.clone()))
.select((
schema::pools::pool_id,
schema::pools::base_asset_id,
schema::pools::base_asset_decimals,
schema::pools::quote_asset_id,
schema::pools::quote_asset_decimals,
))
.first::<(String, String, i16, String, i16)>(connection)
.await?;

let (pool_id, base_asset_id, base_decimals, quote_asset_id, quote_decimals) = pool_data;
let base_decimals = base_decimals as u8;
let quote_decimals = quote_decimals as u8;

let pool_address = ObjectID::from_hex_literal(&pool_id)?;

let sui_client = SuiClientBuilder::default().build(SUI_MAINNET_URL).await?;
let mut ptb = ProgrammableTransactionBuilder::new();

let pool_object: SuiObjectResponse = sui_client
.read_api()
.get_object_with_options(pool_address, SuiObjectDataOptions::full_content())
.await?;
let pool_data: &SuiObjectData =
pool_object
.data
.as_ref()
.ok_or(DeepBookError::InternalError(format!(
"Missing data in pool object response for '{}'",
pool_name
)))?;
let pool_object_ref: ObjectRef = (pool_data.object_id, pool_data.version, pool_data.digest);

let pool_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(pool_object_ref));
ptb.input(pool_input)?;

let input_argument = CallArg::Pure(bcs::to_bytes(&ticks_from_mid).unwrap());
ptb.input(input_argument)?;

let sui_clock_object_id = ObjectID::from_hex_literal(
"0x0000000000000000000000000000000000000000000000000000000000000006",
)?;
let sui_clock_object: SuiObjectResponse = sui_client
.read_api()
.get_object_with_options(sui_clock_object_id, SuiObjectDataOptions::full_content())
.await?;
let clock_data: &SuiObjectData =
sui_clock_object
.data
.as_ref()
.ok_or(DeepBookError::InternalError(
"Missing data in clock object response".to_string(),
))?;

let sui_clock_object_ref: ObjectRef =
(clock_data.object_id, clock_data.version, clock_data.digest);

let clock_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(sui_clock_object_ref));
ptb.input(clock_input)?;

let base_coin_type = parse_type_input(&base_asset_id)?;
let quote_coin_type = parse_type_input(&quote_asset_id)?;

let package = ObjectID::from_hex_literal(&DEEPBOOK_PACKAGE_ID)
.map_err(|e| DeepBookError::InternalError(format!("Invalid pool ID: {}", e)))?;
let module = "pool".to_string();
let function = "get_level2_ticks_from_mid".to_string();
leecchh marked this conversation as resolved.
Show resolved Hide resolved

ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall {
package,
module,
function,
type_arguments: vec![base_coin_type, quote_coin_type],
arguments: vec![Argument::Input(0), Argument::Input(1), Argument::Input(2)],
})));

let builder = ptb.finish();
let tx = TransactionKind::ProgrammableTransaction(builder);

let result = sui_client
.read_api()
.dev_inspect_transaction_block(SuiAddress::default(), tx, None, None, None)
.await?;

let mut binding = result.results.unwrap();
let bid_prices = &binding
.first_mut()
.unwrap()
.return_values
.first_mut()
.unwrap()
.0;
let bid_parsed_prices: Vec<u64> = bcs::from_bytes(bid_prices).unwrap();
let bid_quantities = &binding.first_mut().unwrap().return_values.get(1).unwrap().0;
let bid_parsed_quantities: Vec<u64> = bcs::from_bytes(bid_quantities).unwrap();

let ask_prices = &binding.first_mut().unwrap().return_values.get(2).unwrap().0;
let ask_parsed_prices: Vec<u64> = bcs::from_bytes(ask_prices).unwrap();
let ask_quantities = &binding.first_mut().unwrap().return_values.get(3).unwrap().0;
let ask_parsed_quantities: Vec<u64> = bcs::from_bytes(ask_quantities).unwrap();
leecchh marked this conversation as resolved.
Show resolved Hide resolved

let mut result = HashMap::new();

let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
result.insert("timestamp".to_string(), Value::from(timestamp.to_string()));

let bids: Vec<Value> = bid_parsed_prices
.into_iter()
.zip(bid_parsed_quantities.into_iter())
.take(ticks_from_mid as usize)
.map(|(price, quantity)| {
let price_factor = 10u64.pow((9 - base_decimals + quote_decimals).try_into().unwrap());
let quantity_factor = 10u64.pow((base_decimals).try_into().unwrap());
Value::Array(vec![
Value::from((price as f64 / price_factor as f64).to_string()),
Value::from((quantity as f64 / quantity_factor as f64).to_string()),
])
})
.collect();
result.insert("bids".to_string(), Value::Array(bids));

let asks: Vec<Value> = ask_parsed_prices
.into_iter()
.zip(ask_parsed_quantities.into_iter())
.take(ticks_from_mid as usize)
.map(|(price, quantity)| {
let price_factor = 10u64.pow((9 - base_decimals + quote_decimals).try_into().unwrap());
let quantity_factor = 10u64.pow((base_decimals).try_into().unwrap());
Value::Array(vec![
Value::from((price as f64 / price_factor as f64).to_string()),
Value::from((quantity as f64 / quantity_factor as f64).to_string()),
])
})
.collect();
result.insert("asks".to_string(), Value::Array(asks));

Ok(Json(result))
}

fn parse_type_input(type_str: &str) -> Result<TypeInput, DeepBookError> {
let type_tag = TypeTag::from_str(type_str)?;
Ok(TypeInput::from(type_tag))
}
Loading