From 10c1b4af6233124f458c82180da2f5587ca224c0 Mon Sep 17 00:00:00 2001 From: ilya Date: Tue, 31 Dec 2024 12:27:48 +0000 Subject: [PATCH 1/6] get_token_first_trade_block --- crates/database/src/trades.rs | 52 +++++++++++++++++++ crates/orderbook/src/api.rs | 9 +++- .../src/api/get_token_first_trade_block.rs | 43 +++++++++++++++ crates/orderbook/src/database/orders.rs | 19 +++++++ database/sql/V077__orders_token_indexes.sql | 3 ++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 crates/orderbook/src/api/get_token_first_trade_block.rs create mode 100644 database/sql/V077__orders_token_indexes.sql diff --git a/crates/database/src/trades.rs b/crates/database/src/trades.rs index 36c4c07dfd..46e88a7e39 100644 --- a/crates/database/src/trades.rs +++ b/crates/database/src/trades.rs @@ -106,6 +106,31 @@ AND t.log_index BETWEEN (SELECT * from previous_settlement) AND $2 .await } +pub async fn token_first_trade_block( + ex: &mut PgConnection, + token: Address, +) -> Result, sqlx::Error> { + const QUERY: &str = r#" +SELECT MIN(sub.block_number) AS earliest_block +FROM ( + SELECT t.block_number + FROM trades t + JOIN orders o ON t.order_uid = o.uid + WHERE o.sell_token = $1 OR o.buy_token = $1 + + UNION ALL + + SELECT t.block_number + FROM trades t + JOIN jit_orders j ON t.order_uid = j.uid + WHERE j.sell_token = $1 OR j.buy_token = $1 +) AS sub +"#; + + let (block_number,) = sqlx::query_as(QUERY).bind(token).fetch_one(ex).await?; + Ok(block_number) +} + #[cfg(test)] mod tests { use { @@ -579,4 +604,31 @@ mod tests { }] ); } + + #[tokio::test] + #[ignore] + async fn postgres_token_first_trade_block() { + let mut db = PgConnection::connect("postgresql://").await.unwrap(); + let mut db = db.begin().await.unwrap(); + crate::clear_DANGER_(&mut db).await.unwrap(); + + let token = Default::default(); + assert_eq!(token_first_trade_block(&mut db, token).await.unwrap(), None); + + let (owners, order_ids) = generate_owners_and_order_ids(2, 2).await; + let event_index_a = EventIndex { + block_number: 123, + log_index: 0, + }; + let event_index_b = EventIndex { + block_number: 124, + log_index: 0, + }; + add_order_and_trade(&mut db, owners[0], order_ids[0], event_index_a, None, None).await; + add_order_and_trade(&mut db, owners[1], order_ids[1], event_index_b, None, None).await; + assert_eq!( + token_first_trade_block(&mut db, token).await.unwrap(), + Some(123) + ); + } } diff --git a/crates/orderbook/src/api.rs b/crates/orderbook/src/api.rs index a272950148..30fae45413 100644 --- a/crates/orderbook/src/api.rs +++ b/crates/orderbook/src/api.rs @@ -23,6 +23,7 @@ mod get_order_by_uid; mod get_order_status; mod get_orders_by_tx; mod get_solver_competition; +mod get_token_first_trade_block; mod get_total_surplus; mod get_trades; mod get_user_orders; @@ -105,7 +106,13 @@ pub fn handle_all_routes( ), ( "v1/get_total_surplus", - box_filter(get_total_surplus::get(database)), + box_filter(get_total_surplus::get(database.clone())), + ), + ( + "v1/get_token_first_trade_block", + box_filter(get_token_first_trade_block::get_token_first_trade_block( + database, + )), ), ]; diff --git a/crates/orderbook/src/api/get_token_first_trade_block.rs b/crates/orderbook/src/api/get_token_first_trade_block.rs new file mode 100644 index 0000000000..bfc0743a53 --- /dev/null +++ b/crates/orderbook/src/api/get_token_first_trade_block.rs @@ -0,0 +1,43 @@ +use { + crate::database::Postgres, + anyhow::Context, + hyper::StatusCode, + primitive_types::H160, + std::convert::Infallible, + warp::{ + reply::{json, with_status}, + Filter, + Rejection, + }, +}; + +fn get_native_prices_request() -> impl Filter + Clone { + warp::path!("v1" / "token" / H160 / "first_trade_block").and(warp::get()) +} + +pub fn get_token_first_trade_block( + db: Postgres, +) -> impl Filter + Clone { + get_native_prices_request().and_then(move |token: H160| { + let db = db.clone(); + async move { + Result::<_, Infallible>::Ok( + match db + .token_first_trade_block(&token) + .await + .context("get_token_first_trade_block error") + { + Ok(Some(block)) => with_status(json(&block), StatusCode::OK), + Ok(None) => with_status( + super::error("NotFound", "no trade for token exists"), + StatusCode::NOT_FOUND, + ), + Err(err) => { + tracing::error!(?err, ?token, "Failed to fetch token's first trade block"); + crate::api::internal_error_reply() + } + }, + ) + } + }) +} diff --git a/crates/orderbook/src/database/orders.rs b/crates/orderbook/src/database/orders.rs index 938389b210..46612c3d2c 100644 --- a/crates/orderbook/src/database/orders.rs +++ b/crates/orderbook/src/database/orders.rs @@ -492,6 +492,25 @@ impl Postgres { .map(full_order_into_model_order) .collect::>>() } + + pub async fn token_first_trade_block(&self, token: &H160) -> Result> { + let timer = super::Metrics::get() + .database_queries + .with_label_values(&["token_first_trade_block"]) + .start_timer(); + + let mut ex = self.pool.acquire().await?; + let block_number = database::trades::token_first_trade_block(&mut ex, ByteArray(token.0)) + .await + .map_err(anyhow::Error::from)? + .map(u32::try_from) + .transpose() + .map_err(anyhow::Error::from)?; + + timer.stop_and_record(); + + Ok(block_number) + } } #[async_trait] diff --git a/database/sql/V077__orders_token_indexes.sql b/database/sql/V077__orders_token_indexes.sql new file mode 100644 index 0000000000..d212427efd --- /dev/null +++ b/database/sql/V077__orders_token_indexes.sql @@ -0,0 +1,3 @@ +CREATE INDEX orders_sell_buy_tokens ON orders (sell_token, buy_token); + +CREATE INDEX jit_orders_sell_buy_tokens ON jit_orders (sell_token, buy_token); From 20a9984095f4a8c3bbff0c072dd08c5e759a2989 Mon Sep 17 00:00:00 2001 From: ilya Date: Tue, 31 Dec 2024 12:37:00 +0000 Subject: [PATCH 2/6] openapi --- crates/orderbook/openapi.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index aa4c2da3ac..d2933e23a9 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -308,6 +308,29 @@ paths: description: No liquidity was found. "500": description: Unexpected error. + "/api/v1/token/{token}/first_trade_block": + get: + summary: Get the block number when the token was traded first. + description: |- + Returns the block number when the token was traded for the first time. + parameters: + - name: token + in: path + required: true + schema: + $ref: "#/components/schemas/Address" + responses: + "200": + description: The block number when the token was traded first. + content: + application/json: + schema: + type: integer + description: The block number when the token was traded first. + "404": + description: No trades were found for this token. + "500": + description: Unexpected error. /api/v1/quote: post: summary: Quote a price and fee for the specified order parameters. @@ -407,7 +430,7 @@ paths: "200": description: Version content: - text/plain: { } + text/plain: {} "/api/v1/app_data/{app_data_hash}": get: summary: Get the full `appData` from contract `appDataHash`. From 9d380c96e5d9fcfe2a80f2d82d7a3055229943ec Mon Sep 17 00:00:00 2001 From: ilya Date: Tue, 31 Dec 2024 13:30:37 +0000 Subject: [PATCH 3/6] MIN(t.block_number) --- crates/database/src/trades.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/database/src/trades.rs b/crates/database/src/trades.rs index 46e88a7e39..38d2ca06d0 100644 --- a/crates/database/src/trades.rs +++ b/crates/database/src/trades.rs @@ -113,14 +113,14 @@ pub async fn token_first_trade_block( const QUERY: &str = r#" SELECT MIN(sub.block_number) AS earliest_block FROM ( - SELECT t.block_number + SELECT MIN(t.block_number) AS block_number FROM trades t JOIN orders o ON t.order_uid = o.uid WHERE o.sell_token = $1 OR o.buy_token = $1 UNION ALL - SELECT t.block_number + SELECT MIN(t.block_number) AS block_number FROM trades t JOIN jit_orders j ON t.order_uid = j.uid WHERE j.sell_token = $1 OR j.buy_token = $1 From bc5d866b39c6036b5fcaa09a78be7074c2faf5fd Mon Sep 17 00:00:00 2001 From: ilya Date: Tue, 31 Dec 2024 13:31:16 +0000 Subject: [PATCH 4/6] Revert "openapi" This reverts commit 20a9984095f4a8c3bbff0c072dd08c5e759a2989. --- crates/orderbook/openapi.yml | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index d2933e23a9..aa4c2da3ac 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -308,29 +308,6 @@ paths: description: No liquidity was found. "500": description: Unexpected error. - "/api/v1/token/{token}/first_trade_block": - get: - summary: Get the block number when the token was traded first. - description: |- - Returns the block number when the token was traded for the first time. - parameters: - - name: token - in: path - required: true - schema: - $ref: "#/components/schemas/Address" - responses: - "200": - description: The block number when the token was traded first. - content: - application/json: - schema: - type: integer - description: The block number when the token was traded first. - "404": - description: No trades were found for this token. - "500": - description: Unexpected error. /api/v1/quote: post: summary: Quote a price and fee for the specified order parameters. @@ -430,7 +407,7 @@ paths: "200": description: Version content: - text/plain: {} + text/plain: { } "/api/v1/app_data/{app_data_hash}": get: summary: Get the full `appData` from contract `appDataHash`. From ca9222f3a04e1daf10d3bc499901661173f18832 Mon Sep 17 00:00:00 2001 From: ilya Date: Thu, 2 Jan 2025 12:14:00 +0000 Subject: [PATCH 5/6] Generalize the endpoint --- crates/orderbook/src/api.rs | 8 +++----- ...t_trade_block.rs => get_token_metadata.rs} | 20 ++++++------------- crates/orderbook/src/database/orders.rs | 8 +++++--- crates/orderbook/src/dto/mod.rs | 8 ++++++++ 4 files changed, 22 insertions(+), 22 deletions(-) rename crates/orderbook/src/api/{get_token_first_trade_block.rs => get_token_metadata.rs} (57%) diff --git a/crates/orderbook/src/api.rs b/crates/orderbook/src/api.rs index 30fae45413..8eb7321f1f 100644 --- a/crates/orderbook/src/api.rs +++ b/crates/orderbook/src/api.rs @@ -23,7 +23,7 @@ mod get_order_by_uid; mod get_order_status; mod get_orders_by_tx; mod get_solver_competition; -mod get_token_first_trade_block; +mod get_token_metadata; mod get_total_surplus; mod get_trades; mod get_user_orders; @@ -109,10 +109,8 @@ pub fn handle_all_routes( box_filter(get_total_surplus::get(database.clone())), ), ( - "v1/get_token_first_trade_block", - box_filter(get_token_first_trade_block::get_token_first_trade_block( - database, - )), + "v1/get_token_metadata", + box_filter(get_token_metadata::get_token_metadata(database)), ), ]; diff --git a/crates/orderbook/src/api/get_token_first_trade_block.rs b/crates/orderbook/src/api/get_token_metadata.rs similarity index 57% rename from crates/orderbook/src/api/get_token_first_trade_block.rs rename to crates/orderbook/src/api/get_token_metadata.rs index bfc0743a53..75eaaf99e0 100644 --- a/crates/orderbook/src/api/get_token_first_trade_block.rs +++ b/crates/orderbook/src/api/get_token_metadata.rs @@ -4,18 +4,14 @@ use { hyper::StatusCode, primitive_types::H160, std::convert::Infallible, - warp::{ - reply::{json, with_status}, - Filter, - Rejection, - }, + warp::{reply, Filter, Rejection}, }; fn get_native_prices_request() -> impl Filter + Clone { - warp::path!("v1" / "token" / H160 / "first_trade_block").and(warp::get()) + warp::path!("v1" / "token" / H160 / "metadata").and(warp::get()) } -pub fn get_token_first_trade_block( +pub fn get_token_metadata( db: Postgres, ) -> impl Filter + Clone { get_native_prices_request().and_then(move |token: H160| { @@ -23,15 +19,11 @@ pub fn get_token_first_trade_block( async move { Result::<_, Infallible>::Ok( match db - .token_first_trade_block(&token) + .token_metadata(&token) .await - .context("get_token_first_trade_block error") + .context("get_token_metadata error") { - Ok(Some(block)) => with_status(json(&block), StatusCode::OK), - Ok(None) => with_status( - super::error("NotFound", "no trade for token exists"), - StatusCode::NOT_FOUND, - ), + Ok(metadata) => reply::with_status(reply::json(&metadata), StatusCode::OK), Err(err) => { tracing::error!(?err, ?token, "Failed to fetch token's first trade block"); crate::api::internal_error_reply() diff --git a/crates/orderbook/src/database/orders.rs b/crates/orderbook/src/database/orders.rs index 46612c3d2c..32c0640e48 100644 --- a/crates/orderbook/src/database/orders.rs +++ b/crates/orderbook/src/database/orders.rs @@ -1,6 +1,6 @@ use { super::Postgres, - crate::orderbook::AddOrderError, + crate::{dto::TokenMetadata, orderbook::AddOrderError}, anyhow::{Context as _, Result}, app_data::AppDataHash, async_trait::async_trait, @@ -493,7 +493,7 @@ impl Postgres { .collect::>>() } - pub async fn token_first_trade_block(&self, token: &H160) -> Result> { + pub async fn token_metadata(&self, token: &H160) -> Result { let timer = super::Metrics::get() .database_queries .with_label_values(&["token_first_trade_block"]) @@ -509,7 +509,9 @@ impl Postgres { timer.stop_and_record(); - Ok(block_number) + Ok(TokenMetadata { + first_trade_block: block_number, + }) } } diff --git a/crates/orderbook/src/dto/mod.rs b/crates/orderbook/src/dto/mod.rs index fb3365dd95..b706e79d4d 100644 --- a/crates/orderbook/src/dto/mod.rs +++ b/crates/orderbook/src/dto/mod.rs @@ -5,3 +5,11 @@ pub use { auction::{Auction, AuctionId, AuctionWithId}, order::Order, }; +use {serde::Serialize, serde_with::serde_as}; + +#[serde_as] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenMetadata { + pub first_trade_block: Option, +} From 96bf896d8e14b8e93ca317fa496873d97bdfa1e5 Mon Sep 17 00:00:00 2001 From: ilya Date: Mon, 6 Jan 2025 10:57:11 +0000 Subject: [PATCH 6/6] Nits --- .../orderbook/src/api/get_token_metadata.rs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/orderbook/src/api/get_token_metadata.rs b/crates/orderbook/src/api/get_token_metadata.rs index 75eaaf99e0..a08ec3a09d 100644 --- a/crates/orderbook/src/api/get_token_metadata.rs +++ b/crates/orderbook/src/api/get_token_metadata.rs @@ -1,6 +1,5 @@ use { crate::database::Postgres, - anyhow::Context, hyper::StatusCode, primitive_types::H160, std::convert::Infallible, @@ -17,19 +16,16 @@ pub fn get_token_metadata( get_native_prices_request().and_then(move |token: H160| { let db = db.clone(); async move { - Result::<_, Infallible>::Ok( - match db - .token_metadata(&token) - .await - .context("get_token_metadata error") - { - Ok(metadata) => reply::with_status(reply::json(&metadata), StatusCode::OK), - Err(err) => { - tracing::error!(?err, ?token, "Failed to fetch token's first trade block"); - crate::api::internal_error_reply() - } - }, - ) + let result = db.token_metadata(&token).await; + let response = match result { + Ok(metadata) => reply::with_status(reply::json(&metadata), StatusCode::OK), + Err(err) => { + tracing::error!(?err, ?token, "Failed to fetch token's first trade block"); + crate::api::internal_error_reply() + } + }; + + Result::<_, Infallible>::Ok(response) } }) }