From 3f8ef40660600d75f1d1b0fdad1fc8b3937c5551 Mon Sep 17 00:00:00 2001 From: Leonid Tyurin Date: Wed, 25 Dec 2024 17:03:46 +0700 Subject: [PATCH] feat: add paginated search for tokens --- .../src/clients/token_info.rs | 8 ++- .../multichain-aggregator-logic/src/lib.rs | 3 +- .../src/repository/addresses.rs | 11 +++- .../src/types/addresses.rs | 1 + .../src/types/dapp.rs | 1 + .../src/types/hashes.rs | 1 + .../src/types/token_info.rs | 1 + .../proto/v1/api_config_http.yaml | 3 + .../proto/v1/multichain-aggregator.proto | 23 +++++++- .../v1/multichain-aggregator.swagger.yaml | 56 +++++++++++++++++++ .../src/services/multichain_aggregator.rs | 48 +++++++++++++--- 11 files changed, 141 insertions(+), 15 deletions(-) diff --git a/multichain-aggregator/multichain-aggregator-logic/src/clients/token_info.rs b/multichain-aggregator/multichain-aggregator-logic/src/clients/token_info.rs index 09d0811cf..397998b7c 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/clients/token_info.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/clients/token_info.rs @@ -20,7 +20,13 @@ pub struct TokenInfo { #[derive(Debug, Deserialize)] pub struct TokenInfoSearchResponse { pub token_infos: Vec, - pub next_page_params: Option, + pub next_page_params: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Pagination { + pub page_token: String, + pub page_size: u32, } impl TokenInfoClient { diff --git a/multichain-aggregator/multichain-aggregator-logic/src/lib.rs b/multichain-aggregator/multichain-aggregator-logic/src/lib.rs index 4d39099ed..2e1c1f683 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/lib.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/lib.rs @@ -9,5 +9,6 @@ mod types; pub use import::batch_import; pub use types::{ - api_keys::ApiKey, batch_import_request::BatchImportRequest, chains::Chain, ChainId, + api_keys::ApiKey, batch_import_request::BatchImportRequest, chains::Chain, token_info::Token, + ChainId, }; diff --git a/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs b/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs index 431f28f25..160725640 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs @@ -6,8 +6,8 @@ use alloy_primitives::Address as AddressAlloy; use entity::addresses::{ActiveModel, Column, Entity, Model}; use regex::Regex; use sea_orm::{ - prelude::Expr, sea_query::OnConflict, ActiveValue::NotSet, ConnectionTrait, DbErr, EntityTrait, - IntoSimpleExpr, Iterable, QueryFilter, QueryOrder, QuerySelect, + prelude::Expr, sea_query::OnConflict, ActiveValue::NotSet, ColumnTrait, ConnectionTrait, DbErr, + EntityTrait, IntoSimpleExpr, Iterable, QueryFilter, QueryOrder, QuerySelect, }; use std::sync::OnceLock; @@ -54,7 +54,7 @@ pub async fn search_by_query(db: &C, q: &str) -> Result, Service where C: ConnectionTrait, { - search_by_query_paginated(db, q, None, 100) + search_by_query_paginated(db, q, None, None, 100) .await .map(|(addresses, _)| addresses) } @@ -62,6 +62,7 @@ where pub async fn search_by_query_paginated( db: &C, q: &str, + chain_id: Option, page_token: Option<(AddressAlloy, ChainId)>, limit: u64, ) -> Result<(Vec
, Option<(AddressAlloy, ChainId)>), ServiceError> @@ -84,6 +85,10 @@ where .order_by_asc(Column::ChainId) .limit(limit + 1); + if let Some(chain_id) = chain_id { + query = query.filter(Column::ChainId.eq(chain_id)); + } + if hex_regex().is_match(q) { query = query.filter(Expr::cust_with_expr( "encode(hash, 'hex') LIKE $1", diff --git a/multichain-aggregator/multichain-aggregator-logic/src/types/addresses.rs b/multichain-aggregator/multichain-aggregator-logic/src/types/addresses.rs index 58e9ff785..625441d82 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/types/addresses.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/types/addresses.rs @@ -66,6 +66,7 @@ impl From
for proto::Address { is_contract: Some(v.is_contract), is_verified_contract: Some(v.is_verified_contract), is_token: Some(v.is_token), + chain_id: v.chain_id.to_string(), } } } diff --git a/multichain-aggregator/multichain-aggregator-logic/src/types/dapp.rs b/multichain-aggregator/multichain-aggregator-logic/src/types/dapp.rs index 287dd8c01..807dbd412 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/types/dapp.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/types/dapp.rs @@ -31,6 +31,7 @@ impl From for proto::MarketplaceDapp { title: v.title, logo: v.logo, short_description: v.short_description, + chain_id: v.chain_id.to_string(), } } } diff --git a/multichain-aggregator/multichain-aggregator-logic/src/types/hashes.rs b/multichain-aggregator/multichain-aggregator-logic/src/types/hashes.rs index caaf06adf..dc51f70b9 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/types/hashes.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/types/hashes.rs @@ -37,6 +37,7 @@ impl From for proto::Hash { Self { hash: v.hash.to_string(), hash_type: db_hash_type_to_proto_hash_type(v.hash_type).into(), + chain_id: v.chain_id.to_string(), } } } diff --git a/multichain-aggregator/multichain-aggregator-logic/src/types/token_info.rs b/multichain-aggregator/multichain-aggregator-logic/src/types/token_info.rs index 4396cfea9..2bf15609d 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/types/token_info.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/types/token_info.rs @@ -35,6 +35,7 @@ impl From for proto::Token { name: v.name, symbol: v.symbol, icon_url: v.icon_url, + chain_id: v.chain_id.to_string(), } } } diff --git a/multichain-aggregator/multichain-aggregator-proto/proto/v1/api_config_http.yaml b/multichain-aggregator/multichain-aggregator-proto/proto/v1/api_config_http.yaml index 6a8eafd19..24e370b51 100644 --- a/multichain-aggregator/multichain-aggregator-proto/proto/v1/api_config_http.yaml +++ b/multichain-aggregator/multichain-aggregator-proto/proto/v1/api_config_http.yaml @@ -14,6 +14,9 @@ http: - selector: blockscout.multichainAggregator.v1.MultichainAggregatorService.ListAddresses get: /api/v1/addresses + - selector: blockscout.multichainAggregator.v1.MultichainAggregatorService.ListTokens + get: /api/v1/tokens + #################### Health #################### - selector: blockscout.multichainAggregator.v1.Health.Check diff --git a/multichain-aggregator/multichain-aggregator-proto/proto/v1/multichain-aggregator.proto b/multichain-aggregator/multichain-aggregator-proto/proto/v1/multichain-aggregator.proto index d22826cc0..0bbceb87a 100644 --- a/multichain-aggregator/multichain-aggregator-proto/proto/v1/multichain-aggregator.proto +++ b/multichain-aggregator/multichain-aggregator-proto/proto/v1/multichain-aggregator.proto @@ -8,6 +8,7 @@ service MultichainAggregatorService { rpc BatchImport(BatchImportRequest) returns (BatchImportResponse) {} rpc QuickSearch(QuickSearchRequest) returns (QuickSearchResponse) {} rpc ListAddresses(ListAddressesRequest) returns (ListAddressesResponse) {} + rpc ListTokens(ListTokensRequest) returns (ListTokensResponse) {} } message Pagination { @@ -33,11 +34,13 @@ message Address { optional bool is_contract = 6; optional bool is_verified_contract = 7; optional bool is_token = 8; + string chain_id = 9; } message BlockRange { uint64 min_block_number = 1; uint64 max_block_number = 2; + string chain_id = 3; } message Hash { @@ -49,6 +52,7 @@ message Hash { } HashType hash_type = 2; + string chain_id = 3; } message MarketplaceDapp { @@ -56,6 +60,7 @@ message MarketplaceDapp { string title = 2; string logo = 3; string short_description = 4; + string chain_id = 5; } message Token { @@ -63,6 +68,7 @@ message Token { string icon_url = 2; string name = 3; string symbol = 4; + string chain_id = 5; } message BatchImportRequest { @@ -98,11 +104,24 @@ message QuickSearchResponse { message ListAddressesRequest { string q = 1; - optional uint32 page_size = 2; - optional string page_token = 3; + optional string chain_id = 2; + optional uint32 page_size = 3; + optional string page_token = 4; } message ListAddressesResponse { repeated Address addresses = 1; Pagination pagination = 2; } + +message ListTokensRequest { + string q = 1; + optional string chain_id = 2; + optional uint32 page_size = 3; + optional string page_token = 4; +} + +message ListTokensResponse { + repeated Token tokens = 1; + Pagination pagination = 2; +} diff --git a/multichain-aggregator/multichain-aggregator-proto/swagger/v1/multichain-aggregator.swagger.yaml b/multichain-aggregator/multichain-aggregator-proto/swagger/v1/multichain-aggregator.swagger.yaml index a811c9748..2b1a1a095 100644 --- a/multichain-aggregator/multichain-aggregator-proto/swagger/v1/multichain-aggregator.swagger.yaml +++ b/multichain-aggregator/multichain-aggregator-proto/swagger/v1/multichain-aggregator.swagger.yaml @@ -27,6 +27,10 @@ paths: in: query required: false type: string + - name: chain_id + in: query + required: false + type: string - name: page_size in: query required: false @@ -77,6 +81,38 @@ paths: type: string tags: - MultichainAggregatorService + /api/v1/tokens: + get: + operationId: MultichainAggregatorService_ListTokens + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListTokensResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/rpcStatus' + parameters: + - name: q + in: query + required: false + type: string + - name: chain_id + in: query + required: false + type: string + - name: page_size + in: query + required: false + type: integer + format: int64 + - name: page_token + in: query + required: false + type: string + tags: + - MultichainAggregatorService /health: get: summary: |- @@ -206,6 +242,8 @@ definitions: type: boolean is_token: type: boolean + chain_id: + type: string v1BatchImportRequest: type: object properties: @@ -242,6 +280,8 @@ definitions: max_block_number: type: string format: uint64 + chain_id: + type: string v1Hash: type: object properties: @@ -249,6 +289,8 @@ definitions: type: string hash_type: $ref: '#/definitions/HashHashType' + chain_id: + type: string v1HealthCheckResponse: type: object properties: @@ -264,6 +306,16 @@ definitions: $ref: '#/definitions/v1Address' pagination: $ref: '#/definitions/v1Pagination' + v1ListTokensResponse: + type: object + properties: + tokens: + type: array + items: + type: object + $ref: '#/definitions/v1Token' + pagination: + $ref: '#/definitions/v1Pagination' v1MarketplaceDapp: type: object properties: @@ -275,6 +327,8 @@ definitions: type: string short_description: type: string + chain_id: + type: string v1Pagination: type: object properties: @@ -301,3 +355,5 @@ definitions: type: string symbol: type: string + chain_id: + type: string diff --git a/multichain-aggregator/multichain-aggregator-server/src/services/multichain_aggregator.rs b/multichain-aggregator/multichain-aggregator-server/src/services/multichain_aggregator.rs index 230521cca..10b7b6fde 100644 --- a/multichain-aggregator/multichain-aggregator-server/src/services/multichain_aggregator.rs +++ b/multichain-aggregator/multichain-aggregator-server/src/services/multichain_aggregator.rs @@ -11,7 +11,10 @@ use multichain_aggregator_logic::{ api_key_manager::ApiKeyManager, clients::{dapp::DappClient, token_info::TokenInfoClient}, error::ServiceError, - Chain, + Chain, Token, +}; +use multichain_aggregator_proto::blockscout::multichain_aggregator::v1::{ + ListTokensRequest, ListTokensResponse, }; use sea_orm::DatabaseConnection; use std::str::FromStr; @@ -71,9 +74,8 @@ impl MultichainAggregatorService for MultichainAggregator { logic::batch_import(&self.db, import_request) .await - .map_err(|err| { + .inspect_err(|err| { tracing::error!(error = ?err, "failed to batch import"); - Status::internal("failed to batch import") })?; Ok(Response::new(BatchImportResponse { @@ -90,17 +92,17 @@ impl MultichainAggregatorService for MultichainAggregator { let page_token: Option<(alloy_primitives::Address, logic::ChainId)> = inner.page_token.map(parse_query_2).transpose()?; let page_size = self.normalize_page_size(inner.page_size); - + let chain_id = inner.chain_id.map(parse_query).transpose()?; let (addresses, next_page_token) = logic::repository::addresses::search_by_query_paginated( &self.db, &inner.q, + chain_id, page_token, page_size as u64, ) .await - .map_err(|err| { + .inspect_err(|err| { tracing::error!(error = ?err, "failed to list addresses"); - Status::internal("failed to list addresses") })?; Ok(Response::new(ListAddressesResponse { @@ -112,6 +114,37 @@ impl MultichainAggregatorService for MultichainAggregator { })) } + async fn list_tokens( + &self, + request: Request, + ) -> Result, Status> { + let inner = request.into_inner(); + + let chain_id = inner.chain_id.map(parse_query).transpose()?; + let res = self + .token_info_client + .search_tokens(&inner.q, chain_id, inner.page_size, inner.page_token) + .await + .inspect_err(|err| { + tracing::error!(error = ?err, "failed to list tokens"); + })?; + + let tokens = res + .token_infos + .into_iter() + .map(|t| Token::try_from(t).map(|t| t.into())) + .collect::, _>>() + .map_err(ServiceError::from)?; + + Ok(Response::new(ListTokensResponse { + tokens, + pagination: res.next_page_params.map(|p| Pagination { + page_token: p.page_token, + page_size: p.page_size, + }), + })) + } + async fn quick_search( &self, request: Request, @@ -126,9 +159,8 @@ impl MultichainAggregatorService for MultichainAggregator { &self.chains, ) .await - .map_err(|err| { + .inspect_err(|err| { tracing::error!(error = ?err, "failed to quick search"); - Status::internal("failed to quick search") })?; Ok(Response::new(results.into()))