Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
24 changes: 23 additions & 1 deletion multichain-aggregator/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 multichain-aggregator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ futures = "0.3"
lazy_static = { version = "1.4" }
prometheus = { version = "0.13" }
recache = { git = "https://github.com/blockscout/blockscout-rs", rev = "096c4c1" }
strum = { version = "0.27", default-features = false, features = ["derive"] }

# tests
pretty_assertions = "1.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true, features = ["std"] }
thiserror = { workspace = true }
tonic = { workspace = true }
tokio = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ use crate::{
self, MIN_QUERY_LENGTH,
coin_price::{CoinPriceCache, try_fetch_coin_price},
dapp_search,
filecoin::try_filecoin_address_to_evm_address,
macros::{maybe_cache_lookup, preload_domain_info},
quick_search::{self, SearchContext},
quick_search::{self, SearchContext, SearchTerm},
},
types::{
ChainId,
Expand All @@ -27,7 +26,7 @@ use crate::{
domains::{Domain, DomainInfo, ProtocolInfo},
hashes::{Hash, HashType},
interop_messages::{ExtendedInteropMessage, MessageDirection},
search_results::QuickSearchResult,
search_results::{QuickSearchResult, Redirect},
tokens::{AggregatedToken, TokenType},
},
};
Expand Down Expand Up @@ -117,6 +116,15 @@ impl Cluster {
Ok(())
}

pub fn search_context(&self, is_aggregated: bool) -> SearchContext<'_> {
SearchContext {
cluster: self,
db: Arc::new(self.db.clone()),
domain_primary_chain_id: self.domain_primary_chain_id,
is_aggregated,
}
}

/// If `chain_ids` is empty, then cluster will include all active chains.
pub async fn active_chain_ids(&self) -> Result<Vec<ChainId>, ServiceError> {
let chain_ids = if self.chain_ids.is_empty() {
Expand Down Expand Up @@ -577,10 +585,7 @@ impl Cluster {
// 3. Otherwise, fallback to a contract name search
// TODO: support joint paginated search for domain names without TLD and contract names;
// we need to first handle all pages for domains and then switch to contract names
if let Some(address) = alloy_primitives::Address::from_str(&query)
.ok()
.or_else(|| try_filecoin_address_to_evm_address(&query))
{
if let Some(address) = SearchTerm::try_parse_address(&query) {
(vec![address], None)
} else if domain_name_with_tld_regex().is_match(&query) {
let domains = self
Expand Down Expand Up @@ -853,12 +858,7 @@ impl Cluster {
is_aggregated: bool,
unlimited_per_chain: bool,
) -> Result<QuickSearchResult, ServiceError> {
let context = SearchContext {
cluster: self,
db: Arc::new(self.db.clone()),
domain_primary_chain_id: self.domain_primary_chain_id,
is_aggregated,
};
let context = self.search_context(is_aggregated);
let result = quick_search::quick_search(
query,
&self.quick_search_chains,
Expand All @@ -868,6 +868,12 @@ impl Cluster {
.await?;
Ok(result)
}

pub async fn check_redirect(&self, query: &str) -> Result<Option<Redirect>, ServiceError> {
let context = self.search_context(false);
let result = quick_search::check_redirect(query, &context).await?;
Ok(result)
}
}

async fn get_domain_info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ use crate::{
MIN_QUERY_LENGTH, cluster::Cluster, filecoin::try_filecoin_address_to_evm_address,
macros::preload_domain_info,
},
types::{ChainId, domains::Domain, hashes::HashType, search_results::QuickSearchResult},
types::{
ChainId,
domains::Domain,
hashes::HashType,
search_results::{QuickSearchResult, Redirect, RedirectType},
},
};
use sea_orm::DatabaseConnection;
use std::sync::Arc;
Expand Down Expand Up @@ -60,6 +65,18 @@ pub async fn quick_search(
Ok(results)
}

pub async fn check_redirect(
query: &str,
search_context: &SearchContext<'_>,
) -> Result<Option<Redirect>, ServiceError> {
let raw_query = query.trim();

match parse_redirect_search_term(raw_query) {
Some(term) => term.try_to_redirect(search_context).await,
None => Ok(None),
}
}

pub struct SearchContext<'a> {
pub cluster: &'a Cluster,
pub db: Arc<DatabaseConnection>,
Expand Down Expand Up @@ -95,7 +112,13 @@ impl SearchTerm {
SearchTerm::Hash(hash) => {
let (hashes, _) = search_context
.cluster
.search_hashes(hash.to_string(), None, vec![], num_active_chains, None)
.search_hashes(
hash.to_string(),
None,
active_chain_ids,
num_active_chains,
None,
)
.await?;

let (blocks, transactions): (Vec<_>, Vec<_>) = hashes
Expand Down Expand Up @@ -203,28 +226,102 @@ impl SearchTerm {

Ok(results)
}

async fn try_to_redirect(
self,
search_context: &SearchContext<'_>,
) -> Result<Option<Redirect>, ServiceError> {
let active_chain_ids = search_context.cluster.active_chain_ids().await?;

let redirect = match self {
SearchTerm::AddressHash(address) => {
Some(Redirect::new(RedirectType::Address, address, None))
}
SearchTerm::Hash(hash) => search_context
.cluster
.search_hashes(hash.to_string(), None, active_chain_ids, 1, None)
.await?
.0
.into_iter()
.next()
.map(|h| {
let redirect_type = match h.hash_type {
HashType::Block => RedirectType::Block,
HashType::Transaction => RedirectType::Transaction,
};
Redirect::new(redirect_type, h.hash, Some(h.chain_id))
}),
SearchTerm::Domain(query) => search_context
.cluster
.search_domains_cached(
query,
vec![search_context.domain_primary_chain_id],
QUICK_SEARCH_NUM_ITEMS,
None,
)
.await?
.0
.into_iter()
.next()
.and_then(|d| d.address)
.map(|a| Redirect::new(RedirectType::Address, a, None)),
_ => None,
};

Ok(redirect)
}

pub fn try_parse_hash(q: &str) -> Option<alloy_primitives::B256> {
q.parse::<alloy_primitives::B256>().ok()
}

pub fn try_parse_address(q: &str) -> Option<alloy_primitives::Address> {
q.parse::<alloy_primitives::Address>()
.ok()
.or_else(|| try_filecoin_address_to_evm_address(q))
}

pub fn try_parse_block_number(q: &str) -> Option<alloy_primitives::BlockNumber> {
q.parse::<alloy_primitives::BlockNumber>().ok()
}
}

/// Try to parse a query as a redirect search term.
/// Anything other than a hash or address is considered a domain name.
pub fn parse_redirect_search_term(query: &str) -> Option<SearchTerm> {
let query = query.trim();

if let Some(hash) = SearchTerm::try_parse_hash(query) {
return Some(SearchTerm::Hash(hash));
}

if let Some(address) = SearchTerm::try_parse_address(query) {
return Some(SearchTerm::AddressHash(address));
}

if query.len() >= MIN_QUERY_LENGTH {
return Some(SearchTerm::Domain(query.to_string()));
}

None
}

pub fn parse_search_terms(query: &str) -> Vec<SearchTerm> {
let query = query.trim();
let mut terms = vec![];

// If a term is an address or a hash, we can ignore other search types
if let Ok(hash) = query.parse::<alloy_primitives::B256>() {
if let Some(hash) = SearchTerm::try_parse_hash(query) {
terms.push(SearchTerm::Hash(hash));
return terms;
}
if let Some(address) = query
.parse::<alloy_primitives::Address>()
.ok()
.or_else(|| try_filecoin_address_to_evm_address(query))
{
if let Some(address) = SearchTerm::try_parse_address(query) {
terms.push(SearchTerm::TokenInfo(address.to_string()));
terms.push(SearchTerm::AddressHash(address));
return terms;
}

if let Ok(block_number) = query.parse::<alloy_primitives::BlockNumber>() {
if let Some(block_number) = SearchTerm::try_parse_block_number(query) {
terms.push(SearchTerm::BlockNumber(block_number));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,41 @@ impl TryFrom<QuickSearchResult> for cluster_proto::ClusterQuickSearchResponse {
}
}

#[derive(strum::Display)]
#[strum(serialize_all = "lowercase")]
pub enum RedirectType {
Block,
Transaction,
Address,
}

pub struct Redirect {
ty: RedirectType,
parameter: String,
chain_id: Option<ChainId>,
}

impl Redirect {
pub fn new(ty: RedirectType, parameter: impl ToString, chain_id: Option<ChainId>) -> Self {
Self {
ty,
parameter: parameter.to_string(),
chain_id,
}
}
}

impl From<Redirect> for cluster_proto::CheckRedirectResponse {
fn from(v: Redirect) -> Self {
Self {
redirect: true,
r#type: Some(v.ty.to_string()),
parameter: Some(v.parameter),
chain_id: v.chain_id.map(|c| c.to_string()),
}
}
}

fn evenly_take_elements<const N: usize>(
mut lengths: [usize; N],
mut remained: usize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ http:
- selector: blockscout.clusterExplorer.v1.ClusterExplorerService.QuickSearch
get: /api/v1/clusters/{cluster_id}/search:quick

- selector: blockscout.clusterExplorer.v1.ClusterExplorerService.CheckRedirect
get: /api/v1/clusters/{cluster_id}/search:check-redirect

#################### Health ####################

- selector: blockscout.multichainAggregator.v1.Health.Check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ service ClusterExplorerService {
rpc SearchDomains(SearchByQueryRequest) returns (SearchDomainsResponse) {}
rpc SearchDapps(SearchByQueryRequest) returns (SearchDappsResponse) {}
rpc QuickSearch(ClusterQuickSearchRequest) returns (ClusterQuickSearchResponse) {}
rpc CheckRedirect(CheckRedirectRequest) returns (CheckRedirectResponse) {}
}

message AddressHash {
Expand Down Expand Up @@ -322,3 +323,15 @@ message ClusterQuickSearchResponse {
repeated AggregatedTokenInfo nfts = 7;
repeated blockscout.multichainAggregator.v1.Domain domains = 8;
}

message CheckRedirectRequest {
string cluster_id = 1;
string q = 2;
}

message CheckRedirectResponse {
bool redirect = 1;
optional string type = 2;
optional string parameter = 3;
optional string chain_id = 4;
}
Loading
Loading