diff --git a/Cargo.lock b/Cargo.lock index e4ccab9513b9b..2fc013cf64efc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1542,6 +1542,7 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core 0.4.3", + "axum-macros", "base64 0.21.7", "bytes", "futures-util", @@ -1632,6 +1633,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.87", + "quote 1.0.35", + "syn 2.0.79", +] + [[package]] name = "axum-server" version = "0.6.1" @@ -13259,6 +13272,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "axum 0.7.5", "backoff", "bcs", "bigdecimal", @@ -13272,18 +13286,15 @@ dependencies = [ "prometheus", "serde", "serde_yaml 0.8.26", - "sui-bridge", "sui-config", "sui-data-ingestion-core", "sui-indexer-builder", "sui-json-rpc-types", "sui-sdk", - "sui-test-transaction-builder", "sui-types", "tap", "telemetry-subscribers", "tempfile", - "test-cluster", "tokio", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 6921ede3d998d..9a642f285f463 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -265,6 +265,7 @@ aws-sdk-s3 = "0.29.0" aws-smithy-http = "0.56" aws-smithy-runtime-api = "0.56" axum = { version = "0.7", default-features = false, features = [ + "macros", "tokio", "http1", "http2", diff --git a/crates/sui-deepbook-indexer/Cargo.toml b/crates/sui-deepbook-indexer/Cargo.toml index b32a1769c6c9d..a416198b6057d 100644 --- a/crates/sui-deepbook-indexer/Cargo.toml +++ b/crates/sui-deepbook-indexer/Cargo.toml @@ -21,7 +21,6 @@ clap.workspace = true mysten-metrics.workspace = true prometheus.workspace = true serde_yaml.workspace = true -sui-bridge.workspace = true sui-sdk.workspace = true sui-json-rpc-types.workspace = true sui-data-ingestion-core.workspace = true @@ -32,12 +31,11 @@ backoff.workspace = true sui-config.workspace = true sui-indexer-builder.workspace = true tempfile.workspace = true -bigdecimal = "0.4.0" +axum.workspace = true +bigdecimal = { version = "0.4.5" } [dev-dependencies] sui-types = { workspace = true, features = ["test-utils"] } -sui-test-transaction-builder.workspace = true -test-cluster.workspace = true hex-literal = "0.3.4" [[bin]] diff --git a/crates/sui-deepbook-indexer/src/config.rs b/crates/sui-deepbook-indexer/src/config.rs index 750145b2aee09..3e854d87f3d42 100644 --- a/crates/sui-deepbook-indexer/src/config.rs +++ b/crates/sui-deepbook-indexer/src/config.rs @@ -17,6 +17,7 @@ pub struct IndexerConfig { pub deepbook_genesis_checkpoint: u64, pub concurrency: u64, pub metric_port: u16, + pub service_port: u16, } impl sui_config::Config for IndexerConfig {} diff --git a/crates/sui-deepbook-indexer/src/error.rs b/crates/sui-deepbook-indexer/src/error.rs new file mode 100644 index 0000000000000..525cb8d0b4d11 --- /dev/null +++ b/crates/sui-deepbook-indexer/src/error.rs @@ -0,0 +1,7 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, Clone)] +pub enum DeepBookError { + InternalError(String), +} diff --git a/crates/sui-deepbook-indexer/src/lib.rs b/crates/sui-deepbook-indexer/src/lib.rs index 3c461811dc991..80a90aebfbeeb 100644 --- a/crates/sui-deepbook-indexer/src/lib.rs +++ b/crates/sui-deepbook-indexer/src/lib.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 pub mod config; +pub mod error; pub mod events; pub mod metrics; pub mod models; pub mod postgres_manager; pub mod schema; +pub mod server; pub mod types; pub mod sui_datasource; diff --git a/crates/sui-deepbook-indexer/src/main.rs b/crates/sui-deepbook-indexer/src/main.rs index 3ef803c77a5b4..4e47ffaba2b03 100644 --- a/crates/sui-deepbook-indexer/src/main.rs +++ b/crates/sui-deepbook-indexer/src/main.rs @@ -14,6 +14,7 @@ use sui_data_ingestion_core::DataIngestionMetrics; use sui_deepbook_indexer::config::IndexerConfig; use sui_deepbook_indexer::metrics::DeepBookIndexerMetrics; use sui_deepbook_indexer::postgres_manager::get_connection_pool; +use sui_deepbook_indexer::server::run_server; use sui_deepbook_indexer::sui_datasource::SuiCheckpointDatasource; use sui_deepbook_indexer::sui_deepbook_indexer::PgDeepbookPersistent; use sui_deepbook_indexer::sui_deepbook_indexer::SuiDeepBookDataMapper; @@ -84,6 +85,11 @@ async fn main() -> Result<()> { ingestion_metrics.clone(), indexer_meterics.clone(), ); + + let service_address = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), config.service_port); + run_server(service_address, datastore.clone()); + let indexer = IndexerBuilder::new( "SuiDeepBookIndexer", sui_checkpoint_datasource, diff --git a/crates/sui-deepbook-indexer/src/models.rs b/crates/sui-deepbook-indexer/src/models.rs index f6cb9e26dc1bf..d0d57671e3bce 100644 --- a/crates/sui-deepbook-indexer/src/models.rs +++ b/crates/sui-deepbook-indexer/src/models.rs @@ -4,11 +4,12 @@ use diesel::data_types::PgTimestamp; use diesel::{Identifiable, Insertable, Queryable, Selectable}; +use serde::Serialize; use sui_indexer_builder::{Task, LIVE_TASK_TARGET_CHECKPOINT}; use crate::schema::{ - balances, flashloans, order_fills, order_updates, pool_prices, progress_store, proposals, - rebates, stakes, sui_error_transactions, trade_params_update, votes, + balances, flashloans, order_fills, order_updates, pool_prices, pools, progress_store, + proposals, rebates, stakes, sui_error_transactions, trade_params_update, votes, }; #[derive(Queryable, Selectable, Insertable, Identifiable, Debug)] @@ -57,6 +58,13 @@ pub struct OrderFill { pub onchain_timestamp: i64, } +#[derive(Queryable)] +pub struct OrderFillSummary { + pub maker_balance_manager_id: String, + pub taker_balance_manager_id: String, + pub base_quantity: i64, +} + #[derive(Queryable, Selectable, Insertable, Identifiable, Debug)] #[diesel(table_name = flashloans, primary_key(digest))] pub struct Flashloan { @@ -164,6 +172,24 @@ pub struct Votes { pub stake: i64, } +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, Serialize)] +#[diesel(table_name = pools, primary_key(pool_id))] +pub struct Pools { + pub pool_id: String, + pub pool_name: String, + pub base_asset_id: String, + pub base_asset_decimals: i16, + pub base_asset_symbol: String, + pub base_asset_name: String, + pub quote_asset_id: String, + pub quote_asset_decimals: i16, + pub quote_asset_symbol: String, + pub quote_asset_name: String, + pub min_size: i32, + pub lot_size: i32, + pub tick_size: i32, +} + #[derive(Queryable, Selectable, Insertable, Identifiable, Debug)] #[diesel(table_name = sui_error_transactions, primary_key(txn_digest))] pub struct SuiErrorTransactions { diff --git a/crates/sui-deepbook-indexer/src/server.rs b/crates/sui-deepbook-indexer/src/server.rs new file mode 100644 index 0000000000000..1f2a5a9985be6 --- /dev/null +++ b/crates/sui-deepbook-indexer/src/server.rs @@ -0,0 +1,144 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + error::DeepBookError, + models::{OrderFillSummary, Pools}, + schema, + sui_deepbook_indexer::PgDeepbookPersistent, +}; +use axum::{ + debug_handler, + extract::{Path, State}, + http::StatusCode, + routing::get, + Json, Router, +}; +use diesel::BoolExpressionMethods; +use diesel::QueryDsl; +use diesel::{ExpressionMethods, SelectableHelper}; +use diesel_async::RunQueryDsl; +use std::net::SocketAddr; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::{net::TcpListener, task::JoinHandle}; + +pub const GET_POOLS_PATH: &str = "/get_pools"; +pub const GET_24HR_VOLUME_PATH: &str = "/get_24hr_volume/:pool_id"; +pub const GET_24HR_VOLUME_BY_BALANCE_MANAGER_ID: &str = + "/get_24hr_volume_by_balance_manager_id/:pool_id/:balance_manager_id"; + +pub fn run_server(socket_address: SocketAddr, state: PgDeepbookPersistent) -> JoinHandle<()> { + tokio::spawn(async move { + let listener = TcpListener::bind(socket_address).await.unwrap(); + axum::serve(listener, make_router(state)).await.unwrap(); + }) +} + +pub(crate) fn make_router(state: PgDeepbookPersistent) -> Router { + Router::new() + .route("/", get(health_check)) + .route(GET_POOLS_PATH, get(get_pools)) + .route(GET_24HR_VOLUME_PATH, get(get_24hr_volume)) + .route( + GET_24HR_VOLUME_BY_BALANCE_MANAGER_ID, + get(get_24hr_volume_by_balance_manager_id), + ) + .with_state(state) +} + +impl axum::response::IntoResponse for DeepBookError { + // TODO: distinguish client error. + fn into_response(self) -> axum::response::Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {:?}", self), + ) + .into_response() + } +} + +impl From for DeepBookError +where + E: Into, +{ + fn from(err: E) -> Self { + Self::InternalError(err.into().to_string()) + } +} + +async fn health_check() -> StatusCode { + StatusCode::OK +} + +/// Get all pools stored in database +#[debug_handler] +async fn get_pools( + State(state): State, +) -> Result>, DeepBookError> { + let connection = &mut state.pool.get().await?; + let results = schema::pools::table + .select(Pools::as_select()) + .load(connection) + .await?; + + Ok(Json(results)) +} + +async fn get_24hr_volume( + Path(pool_id): Path, + State(state): State, +) -> Result, DeepBookError> { + let connection = &mut state.pool.get().await?; + let unix_ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let day_ago = unix_ts - 24 * 60 * 60 * 1000; + let vols: Vec = schema::order_fills::table + .select(schema::order_fills::base_quantity) + .filter(schema::order_fills::pool_id.eq(pool_id)) + .filter(schema::order_fills::onchain_timestamp.gt(day_ago)) + .load(connection) + .await?; + Ok(Json(vols.into_iter().map(|v| v as u64).sum())) +} + +async fn get_24hr_volume_by_balance_manager_id( + Path((pool_id, balance_manager_id)): Path<(String, String)>, + State(state): State, +) -> Result>, DeepBookError> { + let connection = &mut state.pool.get().await?; + let unix_ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let day_ago = unix_ts - 24 * 60 * 60 * 1000; + let results: Vec = schema::order_fills::table + .select(( + schema::order_fills::maker_balance_manager_id, + schema::order_fills::taker_balance_manager_id, + schema::order_fills::base_quantity, + )) + .filter(schema::order_fills::pool_id.eq(pool_id)) + .filter(schema::order_fills::onchain_timestamp.gt(day_ago)) + .filter( + schema::order_fills::maker_balance_manager_id + .eq(&balance_manager_id) + .or(schema::order_fills::taker_balance_manager_id.eq(&balance_manager_id)), + ) + .load(connection) + .await?; + + let mut maker_vol = 0; + let mut taker_vol = 0; + for order_fill in results { + if order_fill.maker_balance_manager_id == balance_manager_id { + maker_vol += order_fill.base_quantity; + }; + if order_fill.taker_balance_manager_id == balance_manager_id { + taker_vol += order_fill.base_quantity; + }; + } + + Ok(Json(vec![maker_vol, taker_vol])) +} diff --git a/crates/sui-deepbook-indexer/src/sui_deepbook_indexer.rs b/crates/sui-deepbook-indexer/src/sui_deepbook_indexer.rs index 99f709ece2b07..92eac3b0ad449 100644 --- a/crates/sui-deepbook-indexer/src/sui_deepbook_indexer.rs +++ b/crates/sui-deepbook-indexer/src/sui_deepbook_indexer.rs @@ -43,7 +43,7 @@ use crate::{models, schema}; /// Persistent layer impl #[derive(Clone)] pub struct PgDeepbookPersistent { - pool: PgPool, + pub pool: PgPool, save_progress_policy: ProgressSavingPolicy, } diff --git a/crates/sui-json-rpc-api/src/deepbook.rs b/crates/sui-json-rpc-api/src/deepbook.rs new file mode 100644 index 0000000000000..4f2f098918a3e --- /dev/null +++ b/crates/sui-json-rpc-api/src/deepbook.rs @@ -0,0 +1,14 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; + +use sui_open_rpc_macros::open_rpc; + +#[open_rpc(namespace = "suix", tag = "DeepBook Read API")] +#[rpc(server, client, namespace = "suix")] +pub trait DeepBookApi { + #[method(name = "ping")] + async fn ping(&self) -> RpcResult; +}