diff --git a/multichain-aggregator/Cargo.lock b/multichain-aggregator/Cargo.lock index eb7c9da44..239a32601 100644 --- a/multichain-aggregator/Cargo.lock +++ b/multichain-aggregator/Cargo.lock @@ -934,7 +934,7 @@ dependencies = [ [[package]] name = "blockscout-chains" version = "0.1.0" -source = "git+https://github.com/blockscout/blockscout-rs#3892914d5d334699a92cd90f571988e86c7c9b2b" +source = "git+https://github.com/blockscout/blockscout-rs#e03d4016016f687a7b2229156fe6425931c53dbc" dependencies = [ "reqwest 0.12.9", "reqwest-middleware", @@ -1613,7 +1613,7 @@ dependencies = [ [[package]] name = "env-collector" version = "0.1.1" -source = "git+https://github.com/blockscout/blockscout-rs#3892914d5d334699a92cd90f571988e86c7c9b2b" +source = "git+https://github.com/blockscout/blockscout-rs#e03d4016016f687a7b2229156fe6425931c53dbc" dependencies = [ "anyhow", "config 0.14.1", @@ -2810,11 +2810,15 @@ dependencies = [ "multichain-aggregator-proto", "pretty_assertions", "regex", + "reqwest 0.12.9", "sea-orm", + "serde", + "serde_json", "thiserror", "tokio", "tonic", "tracing", + "url", ] [[package]] @@ -2864,6 +2868,7 @@ dependencies = [ "tokio", "tonic", "tracing", + "url", ] [[package]] @@ -5776,6 +5781,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/multichain-aggregator/Cargo.toml b/multichain-aggregator/Cargo.toml index 51c459de1..c88458954 100644 --- a/multichain-aggregator/Cargo.toml +++ b/multichain-aggregator/Cargo.toml @@ -62,3 +62,4 @@ pretty_assertions = "1.3" regex = "1.10" reqwest = "0.12" thiserror = "1.0" +url = { version = "2.4" } diff --git a/multichain-aggregator/multichain-aggregator-logic/Cargo.toml b/multichain-aggregator/multichain-aggregator-logic/Cargo.toml index c6eeb7c2e..e13a09755 100644 --- a/multichain-aggregator/multichain-aggregator-logic/Cargo.toml +++ b/multichain-aggregator/multichain-aggregator-logic/Cargo.toml @@ -12,9 +12,13 @@ tracing = { workspace = true } sea-orm = { workspace = true } alloy-primitives = { workspace = true } regex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } thiserror = { workspace = true } tonic = { workspace = true } tokio = { workspace = true } +reqwest = { workspace = true } +url = { workspace = true } [dev-dependencies] blockscout-service-launcher = { workspace = true } diff --git a/multichain-aggregator/multichain-aggregator-logic/src/dapp_client.rs b/multichain-aggregator/multichain-aggregator-logic/src/dapp_client.rs new file mode 100644 index 000000000..204948b40 --- /dev/null +++ b/multichain-aggregator/multichain-aggregator-logic/src/dapp_client.rs @@ -0,0 +1,46 @@ +use crate::error::ServiceError; +use serde::Deserialize; +use url::Url; + +pub struct DappClient { + http: reqwest::Client, + url: Url, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Dapp { + pub id: String, + pub title: String, + pub logo: String, + pub short_description: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DappWithChainId { + pub dapp: Dapp, + pub chain_id: String, +} + +impl DappClient { + pub fn new(url: Url) -> Self { + let http = reqwest::Client::new(); + Self { http, url } + } + + pub async fn search_dapps(&self, query: &str) -> Result, ServiceError> { + let mut url = self.url.clone(); + url.set_path("/api/v1/marketplace/dapps:search"); + url.query_pairs_mut().append_pair("query", query); + + self.http + .get(url) + .send() + .await + .map_err(|e| ServiceError::Internal(e.into()))? + .json::>() + .await + .map_err(|e| ServiceError::Internal(e.into())) + } +} diff --git a/multichain-aggregator/multichain-aggregator-logic/src/lib.rs b/multichain-aggregator/multichain-aggregator-logic/src/lib.rs index 7286abe3e..77e545e4d 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/lib.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/lib.rs @@ -1,11 +1,11 @@ pub mod api_key_manager; +pub mod dapp_client; pub mod error; mod import; mod proto; pub mod repository; pub mod search; mod types; -mod dapp_client; pub use import::batch_import; pub use types::{ diff --git a/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs b/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs index 517d1c934..60e0bd0f2 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/repository/addresses.rs @@ -6,11 +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, PostgresQueryBuilder}, - ActiveValue::NotSet, - ConnectionTrait, DbErr, EntityTrait, IntoSimpleExpr, Iterable, QueryFilter, QueryOrder, - QuerySelect, QueryTrait, + prelude::Expr, sea_query::OnConflict, ActiveValue::NotSet, ConnectionTrait, DbErr, EntityTrait, + IntoSimpleExpr, Iterable, QueryFilter, QueryOrder, QuerySelect, }; use std::sync::OnceLock; @@ -88,8 +85,10 @@ where .limit(limit + 1); if hex_regex().is_match(q) { - let prefix = format!("\\x{}", q.strip_prefix("0x").unwrap_or(q)); - query = query.filter(Expr::cust_with_expr("hash LIKE $1", format!("{}%", prefix))); + query = query.filter(Expr::cust_with_expr( + "encode(hash, 'hex') LIKE $1", + format!("{}%", q.to_lowercase().strip_prefix("0x").unwrap_or(q)), + )); } else { let ts_query = prepare_ts_query(q); query = query.filter(Expr::cust_with_expr( @@ -100,8 +99,6 @@ where )); } - println!("{}", query.as_query().to_string(PostgresQueryBuilder)); - let addresses = query .all(db) .await? diff --git a/multichain-aggregator/multichain-aggregator-logic/src/search.rs b/multichain-aggregator/multichain-aggregator-logic/src/search.rs index 2d07f6d9b..0e2cf2951 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/search.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/search.rs @@ -1,15 +1,17 @@ use crate::{ + dapp_client::DappClient, error::ServiceError, repository::{addresses, block_ranges, hashes}, types::{ chains::Chain, + dapp::MarketplaceDapp, search_results::{ChainSearchResult, SearchResults}, ChainId, }, }; use sea_orm::DatabaseConnection; use std::collections::BTreeMap; -use tokio::try_join; +use tokio::join; macro_rules! populate_search_results { ($target:expr, $explorers:expr, $from:expr, $field:ident) => { @@ -28,16 +30,18 @@ macro_rules! populate_search_results { pub async fn quick_search( db: &DatabaseConnection, + dapp_client: &DappClient, query: String, chains: &[Chain], ) -> Result { let raw_query = query.trim(); - let ((blocks, transactions), block_numbers, addresses) = try_join!( + let (hashes, block_numbers, addresses, dapps) = join!( hashes::search_by_query(db, raw_query), block_ranges::search_by_query(db, raw_query), - addresses::search_by_query(db, raw_query) - )?; + addresses::search_by_query(db, raw_query), + dapp_client.search_dapps(raw_query), + ); let explorers: BTreeMap = chains .iter() @@ -45,10 +49,27 @@ pub async fn quick_search( .collect(); let mut results = SearchResults::default(); - populate_search_results!(results, explorers, addresses, addresses); - populate_search_results!(results, explorers, blocks, blocks); - populate_search_results!(results, explorers, transactions, transactions); - populate_search_results!(results, explorers, block_numbers, block_numbers); + + if let Ok((blocks, transactions)) = hashes { + populate_search_results!(results, explorers, blocks, blocks); + populate_search_results!(results, explorers, transactions, transactions); + } + + if let Ok(block_numbers) = block_numbers { + populate_search_results!(results, explorers, block_numbers, block_numbers); + } + + if let Ok(addresses) = addresses { + populate_search_results!(results, explorers, addresses, addresses); + } + + if let Ok(dapps) = dapps { + let dapps: Vec = dapps + .into_iter() + .filter_map(|d| d.try_into().ok()) + .collect(); + populate_search_results!(results, explorers, dapps, dapps); + } Ok(results) } diff --git a/multichain-aggregator/multichain-aggregator-logic/src/types/dapp.rs b/multichain-aggregator/multichain-aggregator-logic/src/types/dapp.rs new file mode 100644 index 000000000..402168809 --- /dev/null +++ b/multichain-aggregator/multichain-aggregator-logic/src/types/dapp.rs @@ -0,0 +1,36 @@ +use super::ChainId; +use crate::{dapp_client::DappWithChainId, error::ParseError, proto}; + +#[derive(Debug)] +pub struct MarketplaceDapp { + pub id: String, + pub title: String, + pub logo: String, + pub short_description: String, + pub chain_id: ChainId, +} + +impl TryFrom for MarketplaceDapp { + type Error = ParseError; + + fn try_from(v: DappWithChainId) -> Result { + Ok(Self { + id: v.dapp.id, + title: v.dapp.title, + logo: v.dapp.logo, + short_description: v.dapp.short_description, + chain_id: v.chain_id.parse()?, + }) + } +} + +impl From for proto::MarketplaceDapp { + fn from(v: MarketplaceDapp) -> Self { + Self { + id: v.id, + title: v.title, + logo: v.logo, + short_description: v.short_description, + } + } +} diff --git a/multichain-aggregator/multichain-aggregator-logic/src/types/mod.rs b/multichain-aggregator/multichain-aggregator-logic/src/types/mod.rs index a1a3fcf72..af1989286 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/types/mod.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/types/mod.rs @@ -3,6 +3,7 @@ pub mod api_keys; pub mod batch_import_request; pub mod block_ranges; pub mod chains; +pub mod dapp; pub mod hashes; pub mod search_results; pub type ChainId = i64; diff --git a/multichain-aggregator/multichain-aggregator-logic/src/types/search_results.rs b/multichain-aggregator/multichain-aggregator-logic/src/types/search_results.rs index 57752bdd2..28e603002 100644 --- a/multichain-aggregator/multichain-aggregator-logic/src/types/search_results.rs +++ b/multichain-aggregator/multichain-aggregator-logic/src/types/search_results.rs @@ -1,7 +1,7 @@ use super::{block_ranges::ChainBlockNumber, ChainId}; use crate::{ proto, - types::{addresses::Address, hashes::Hash}, + types::{addresses::Address, dapp::MarketplaceDapp, hashes::Hash}, }; use std::collections::BTreeMap; @@ -12,6 +12,7 @@ pub struct ChainSearchResult { pub blocks: Vec, pub transactions: Vec, pub block_numbers: Vec, + pub dapps: Vec, } impl From for proto::quick_search_response::ChainSearchResult { @@ -22,6 +23,7 @@ impl From for proto::quick_search_response::ChainSearchResult blocks: v.blocks.into_iter().map(|b| b.into()).collect(), transactions: v.transactions.into_iter().map(|t| t.into()).collect(), block_numbers: v.block_numbers.into_iter().map(|b| b.into()).collect(), + dapps: v.dapps.into_iter().map(|d| d.into()).collect(), } } } 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 c3f52ec37..cef20c9d1 100644 --- a/multichain-aggregator/multichain-aggregator-proto/proto/v1/multichain-aggregator.proto +++ b/multichain-aggregator/multichain-aggregator-proto/proto/v1/multichain-aggregator.proto @@ -51,6 +51,13 @@ message Hash { HashType hash_type = 2; } +message MarketplaceDapp { + string id = 1; + string title = 2; + string logo = 4; + string short_description = 5; +} + message BatchImportRequest { string chain_id = 1; repeated Address addresses = 2; @@ -75,6 +82,7 @@ message QuickSearchResponse { repeated Hash blocks = 3; repeated Hash transactions = 4; repeated ChainBlockNumber block_numbers = 5; + repeated MarketplaceDapp dapps = 6; } map items = 1; 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 d4ca46c17..5eb1eb3d7 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 @@ -158,6 +158,11 @@ definitions: items: type: object $ref: '#/definitions/QuickSearchResponseChainBlockNumber' + dapps: + type: array + items: + type: object + $ref: '#/definitions/v1MarketplaceDapp' protobufAny: type: object properties: @@ -254,6 +259,17 @@ definitions: $ref: '#/definitions/v1Address' pagination: $ref: '#/definitions/v1Pagination' + v1MarketplaceDapp: + type: object + properties: + id: + type: string + title: + type: string + logo: + type: string + short_description: + type: string v1Pagination: type: object properties: diff --git a/multichain-aggregator/multichain-aggregator-server/Cargo.toml b/multichain-aggregator/multichain-aggregator-server/Cargo.toml index b32aca7ad..b1c2d8300 100644 --- a/multichain-aggregator/multichain-aggregator-server/Cargo.toml +++ b/multichain-aggregator/multichain-aggregator-server/Cargo.toml @@ -22,6 +22,7 @@ tokio = { workspace = true } tonic = { workspace = true } tracing = { workspace = true } env-collector = { workspace = true } +url = { workspace = true, features = ["serde"] } [dev-dependencies] blockscout-service-launcher = { workspace = true, features = [ "test-server","test-database"] } diff --git a/multichain-aggregator/multichain-aggregator-server/src/server.rs b/multichain-aggregator/multichain-aggregator-server/src/server.rs index 50c6b37ef..4822c5a73 100644 --- a/multichain-aggregator/multichain-aggregator-server/src/server.rs +++ b/multichain-aggregator/multichain-aggregator-server/src/server.rs @@ -10,7 +10,7 @@ use crate::{ use blockscout_chains::BlockscoutChainsClient; use blockscout_service_launcher::{database, launcher, launcher::LaunchSettings}; use migration::Migrator; -use multichain_aggregator_logic::repository; +use multichain_aggregator_logic::{dapp_client::DappClient, repository}; use std::sync::Arc; const SERVICE_NAME: &str = "multichain_aggregator"; @@ -69,10 +69,13 @@ pub async fn run(settings: Settings) -> Result<(), anyhow::Error> { .collect::>(); repository::chains::upsert_many(&db, blockscout_chains.clone()).await?; + let dapp_client = DappClient::new(settings.service.dapp_client.url); + let multichain_aggregator = Arc::new(MultichainAggregator::new( db, blockscout_chains, - settings.service, + dapp_client, + settings.service.max_page_size, )); let router = Router { 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 eaca38a26..cbbdb7fd9 100644 --- a/multichain-aggregator/multichain-aggregator-server/src/services/multichain_aggregator.rs +++ b/multichain-aggregator/multichain-aggregator-server/src/services/multichain_aggregator.rs @@ -1,13 +1,11 @@ -use crate::{ - proto::{ - multichain_aggregator_service_server::MultichainAggregatorService, BatchImportRequest, - BatchImportResponse, ListAddressesRequest, ListAddressesResponse, Pagination, - QuickSearchRequest, QuickSearchResponse, - }, - settings::ServiceSettings, +use crate::proto::{ + multichain_aggregator_service_server::MultichainAggregatorService, BatchImportRequest, + BatchImportResponse, ListAddressesRequest, ListAddressesResponse, Pagination, + QuickSearchRequest, QuickSearchResponse, }; use multichain_aggregator_logic::{ - self as logic, api_key_manager::ApiKeyManager, error::ServiceError, Chain, + self as logic, api_key_manager::ApiKeyManager, dapp_client::DappClient, error::ServiceError, + Chain, }; use sea_orm::DatabaseConnection; use std::str::FromStr; @@ -20,23 +18,29 @@ pub struct MultichainAggregator { api_key_manager: ApiKeyManager, // Cached chains chains: Vec, - - settings: ServiceSettings, + dapp_client: DappClient, + max_page_size: u32, } impl MultichainAggregator { - pub fn new(db: DatabaseConnection, chains: Vec, settings: ServiceSettings) -> Self { + pub fn new( + db: DatabaseConnection, + chains: Vec, + dapp_client: DappClient, + max_page_size: u32, + ) -> Self { Self { db: db.clone(), api_key_manager: ApiKeyManager::new(db), chains, - settings, + dapp_client, + max_page_size, } } fn normalize_page_size(&self, size: Option) -> u32 { size.unwrap_or(DEFAULT_PAGE_SIZE) - .clamp(1, self.settings.max_page_size) + .clamp(1, self.max_page_size) } } @@ -107,12 +111,13 @@ impl MultichainAggregatorService for MultichainAggregator { ) -> Result, Status> { let inner = request.into_inner(); - let results = logic::search::quick_search(&self.db, inner.q, &self.chains) - .await - .map_err(|err| { - tracing::error!(error = ?err, "failed to quick search"); - Status::internal("failed to quick search") - })?; + let results = + logic::search::quick_search(&self.db, &self.dapp_client, inner.q, &self.chains) + .await + .map_err(|err| { + tracing::error!(error = ?err, "failed to quick search"); + Status::internal("failed to quick search") + })?; Ok(Response::new(results.into())) } diff --git a/multichain-aggregator/multichain-aggregator-server/src/settings.rs b/multichain-aggregator/multichain-aggregator-server/src/settings.rs index 5fdfedad0..cbba72219 100644 --- a/multichain-aggregator/multichain-aggregator-server/src/settings.rs +++ b/multichain-aggregator/multichain-aggregator-server/src/settings.rs @@ -4,6 +4,7 @@ use blockscout_service_launcher::{ tracing::{JaegerSettings, TracingSettings}, }; use serde::{Deserialize, Serialize}; +use url::Url; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -24,10 +25,17 @@ pub struct Settings { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct ServiceSettings { + pub dapp_client: DappClientSettings, #[serde(default = "default_max_page_size")] pub max_page_size: u32, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DappClientSettings { + pub url: Url, +} + impl ConfigSettings for Settings { const SERVICE_NAME: &'static str = "MULTICHAIN_AGGREGATOR"; } @@ -44,7 +52,12 @@ impl Settings { create_database: Default::default(), run_migrations: Default::default(), }, - service: ServiceSettings { max_page_size: 100 }, + service: ServiceSettings { + dapp_client: DappClientSettings { + url: Url::parse("http://localhost:8050").unwrap(), + }, + max_page_size: 100, + }, } } }