zetachain external search refactoring (#3020)

This commit is contained in:
Igor Stuev
2025-09-24 12:59:05 +02:00
committed by GitHub
parent 83ec8774db
commit 08829c6904
15 changed files with 111 additions and 105 deletions
+7 -7
View File
@@ -1,22 +1,22 @@
import type { Feature } from './types';
import apis from '../apis';
import { getEnvValue, getExternalAssetFilePath } from '../utils';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from '../utils';
const title = 'ZetaChain transactions';
const chainsConfigUrl = getExternalAssetFilePath('NEXT_PUBLIC_ZETACHAIN_SERVICE_CHAINS_CONFIG_URL');
const cosmosTxUrlTemplate = getEnvValue('NEXT_PUBLIC_ZETACHAIN_COSMOS_TX_URL_TEMPLATE');
const cosmosAddressUrlTemplate = getEnvValue('NEXT_PUBLIC_ZETACHAIN_COSMOS_ADDRESS_URL_TEMPLATE');
const externalSearchConfig = parseEnvJson<Array<{ regex: string; template: string; name: string }>>(
getEnvValue('NEXT_PUBLIC_ZETACHAIN_EXTERNAL_SEARCH_CONFIG'),
);
const config: Feature<{ chainsConfigUrl: string; cosmosTxUrlTemplate: string; cosmosAddressUrlTemplate: string }> = (() => {
if (apis.zetachain && chainsConfigUrl && cosmosTxUrlTemplate && cosmosAddressUrlTemplate) {
const config: Feature<{ chainsConfigUrl: string; externalSearchConfig: Array<{ regex: string; template: string; name: string }> }> = (() => {
if (apis.zetachain && chainsConfigUrl && externalSearchConfig) {
return Object.freeze({
title,
isEnabled: true,
chainsConfigUrl,
cosmosTxUrlTemplate,
cosmosAddressUrlTemplate,
externalSearchConfig,
});
}
+1 -2
View File
@@ -60,5 +60,4 @@ NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST=https://zetachain-cctx-mainnet.k8s-prod-2.blockscout.com
NEXT_PUBLIC_ZETACHAIN_SERVICE_CHAINS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/cross-chain/zetachain-mainnet.json
NEXT_PUBLIC_ZETACHAIN_COSMOS_TX_URL_TEMPLATE=https://zetachain.exploreme.pro/transactions/{hash}
NEXT_PUBLIC_ZETACHAIN_COSMOS_ADDRESS_URL_TEMPLATE=https://zetachain.exploreme.pro/account/{hash}
NEXT_PUBLIC_ZETACHAIN_EXTERNAL_SEARCH_CONFIG=[{"regex":"^[Cc][Oo][Ss][Mm][Oo][Ss][a-z0-9]{39}\\$","template":"https://example.com/cosmos/address/{hash}","name":"Cosmos SDK style address"}]
+1 -2
View File
@@ -50,6 +50,5 @@ NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST=https://zetachain-cctx-testnet.k8s-prod-1.blockscout.com
NEXT_PUBLIC_ZETACHAIN_SERVICE_CHAINS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/cross-chain/zetachain-testnet.json
NEXT_PUBLIC_ZETACHAIN_COSMOS_TX_URL_TEMPLATE=https://testnet.zetachain.exploreme.pro/transactions/{hash}
NEXT_PUBLIC_ZETACHAIN_COSMOS_ADDRESS_URL_TEMPLATE=https://testnet.zetachain.exploreme.pro/account/{hash}
NEXT_PUBLIC_ZETACHAIN_EXTERNAL_SEARCH_CONFIG=[{"regex":"^[Cc][Oo][Ss][Mm][Oo][Ss][a-z0-9]{39}\\$","template":"https://example.com/cosmos/address/{hash}","name":"Cosmos SDK style address"}]
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
+12 -14
View File
@@ -661,25 +661,23 @@ const zetaChainSchema = yup
value => value === undefined,
),
}),
NEXT_PUBLIC_ZETACHAIN_COSMOS_TX_URL_TEMPLATE: yup
.string()
NEXT_PUBLIC_ZETACHAIN_EXTERNAL_SEARCH_CONFIG: yup
.array()
.transform(replaceQuotes)
.json()
.of(
yup.object({
regex: yup.string().required(),
template: yup.string().required(),
name: yup.string().required(),
})
)
.when('NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST', {
is: (value: string) => Boolean(value),
then: (schema) => schema,
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_ZETACHAIN_COSMOS_TX_URL_TEMPLATE cannot be used if NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST is not set',
value => value === undefined,
),
}),
NEXT_PUBLIC_ZETACHAIN_COSMOS_ADDRESS_URL_TEMPLATE: yup
.string()
.when('NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST', {
is: (value: string) => Boolean(value),
then: (schema) => schema,
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_ZETACHAIN_COSMOS_ADDRESS_URL_TEMPLATE cannot be used if NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST is not set',
'NEXT_PUBLIC_ZETACHAIN_EXTERNAL_SEARCH_CONFIG cannot be used if NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST is not set',
value => value === undefined,
),
}),
+1 -2
View File
@@ -987,8 +987,7 @@ This feature enables cross-chain transactions pages and views on ZetaChain insta
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST | `string` | ZetaChain cross-chain transactions service API endpoint url | - | - | `https://zetachain-cctx.services.blockscout.com` | v2.3.2+ |
| NEXT_PUBLIC_ZETACHAIN_SERVICE_CHAINS_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains chains info for the supported chains. | - | - | `https://example.com/zetachain_chains_config.json` | v2.3.2+ |
| NEXT_PUBLIC_ZETACHAIN_COSMOS_TX_URL_TEMPLATE | `string` | URL template to redirect cosmos tx search. | - | - | `https://example.com/cosmos/tx/{hash}` | v2.3.2+ |
| NEXT_PUBLIC_ZETACHAIN_COSMOS_ADDRESS_URL_TEMPLATE | `string` URL template to redirect cosmos address search. | - | - | `https://example.com/cosmos/address/{hash}` | v2.3.2+ |
| NEXT_PUBLIC_ZETACHAIN_EXTERNAL_SEARCH_CONFIG | `Array<{ regex: string; template: string }>` | List of objects with regex, template and name fields to build redirect links for external searches. The first matching regex will be used to generate the URL from the provided hash. | - | - | `'[{"regex":"^0x[0-9a-fA-F]{64}$","template":"https://example.com/cosmos/tx/{hash}","name":"Cosmos SDK style transaction"}]'` | v2.4.0+ |
#### ZetaChain supported cain configuration properties
-20
View File
@@ -1,20 +0,0 @@
import config from 'configs/app';
const COSMOS_TX_HASH_REGEXP = /^[A-F0-9]{64}$/i;
const COSMOS_ADDRESS_REGEXP = /^cosmos[a-z0-9]{39}$/i;
export type CosmosHashType = 'tx' | 'address' | null;
export function checkCosmosHash(hash: string): CosmosHashType {
if (config.features.zetachain.isEnabled) {
if (COSMOS_TX_HASH_REGEXP.test(hash)) {
return 'tx';
}
if (COSMOS_ADDRESS_REGEXP.test(hash)) {
return 'address';
}
}
return null;
}
+32
View File
@@ -0,0 +1,32 @@
import config from 'configs/app';
const zetaChainFeature = config.features.zetachain;
export type ExternalSearchItem = {
url: string;
name: string;
} | null;
export function getExternalSearchItem(hash: string): ExternalSearchItem {
if (!zetaChainFeature.isEnabled) {
return null;
}
const externalSearchConfig = zetaChainFeature.externalSearchConfig;
const externalSearchConfigItem = externalSearchConfig.find((item) => {
try {
return new RegExp(item.regex).test(hash);
} catch {
return false;
}
});
if (externalSearchConfigItem) {
return {
url: externalSearchConfigItem.template.replace('{hash}', hash),
name: externalSearchConfigItem.name,
};
}
return null;
}
+1 -2
View File
@@ -123,8 +123,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
zetaChain: [
[ 'NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST', 'http://localhost:3111' ],
[ 'NEXT_PUBLIC_ZETACHAIN_SERVICE_CHAINS_CONFIG_URL', 'http://localhost:3000/zeta-config.json' ],
[ 'NEXT_PUBLIC_ZETACHAIN_COSMOS_TX_URL_TEMPLATE', 'https://example.com/cosmos/tx/{hash}' ],
[ 'NEXT_PUBLIC_ZETACHAIN_COSMOS_ADDRESS_URL_TEMPLATE', 'https://example.com/cosmos/address/{hash}' ],
[ 'NEXT_PUBLIC_ZETACHAIN_EXTERNAL_SEARCH_CONFIG', '[{"regex":"^0x[0-9a-fA-F]{64}$","template":"https://example.com/tx/{hash}","name":"Cosmos SDK style transaction"}]' ],
],
navigationPromoBannerText: [
[ 'NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG', '{"img_url": "http://localhost:3000/image.svg", "text": "Try the DUCK!", "bg_color": {"light": "rgb(150, 211, 255)", "dark": "rgb(68, 51, 122)"}, "text_color": {"light": "rgb(69, 69, 69)", "dark": "rgb(233, 216, 253)"}, "link_url": "https://example.com"}' ],
+4 -4
View File
@@ -24,7 +24,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import * as Layout from 'ui/shared/layout/components';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import SearchCosmosNotice from 'ui/shared/search/SearchCosmosNotice';
import ExternalSearchItem from 'ui/shared/search/ExternalSearchItem';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import HeaderAlert from 'ui/snippets/header/HeaderAlert';
import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
@@ -42,7 +42,7 @@ const SearchResultsPageContent = () => {
debouncedSearchTerm,
handleSearchTermChange,
zetaChainCCTXQuery,
cosmosHashType,
externalSearchItem,
} = useSearchQuery(withRedirectCheck);
const { data, isError, isPlaceholderData, pagination } = query;
const [ showContent, setShowContent ] = React.useState(!withRedirectCheck);
@@ -220,8 +220,8 @@ const SearchResultsPageContent = () => {
return <Skeleton loading h={ 6 } w="280px" borderRadius="full" mb={ pagination.isVisible ? 0 : 6 }/>;
}
if (resultsCount === 0 && cosmosHashType) {
return <SearchCosmosNotice cosmosHash={ debouncedSearchTerm } type={ cosmosHashType }/>;
if (resultsCount === 0 && externalSearchItem) {
return <ExternalSearchItem item={ externalSearchItem }/>;
}
return (
+40
View File
@@ -0,0 +1,40 @@
import { Text } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import type { ExternalSearchItem as ExternalSearchItemType } from 'lib/search/externalSearch';
import { Link } from 'toolkit/chakra/link';
const zetaChainFeature = config.features.zetachain;
function getIndefiniteArticle(phrase: string): 'a' | 'an' {
const trimmed = (phrase || '').trim().toLowerCase();
if (!trimmed) return 'a';
return 'aeiou'.includes(trimmed[0]) ? 'an' : 'a';
}
interface Props {
item: ExternalSearchItemType;
}
const ExternalSearchItem = ({ item }: Props) => {
if (!zetaChainFeature.isEnabled || !item) {
return null;
}
const url = item.url;
return (
<>
<Text color="text.secondary">
It looks like you are searching for { getIndefiniteArticle(item.name) } { item.name }. This information is best served by the external explorer.
</Text>
<Link href={ url } external mt={ 4 }>
Click here to be redirected
</Link>
</>
);
};
export default React.memo(ExternalSearchItem);
-40
View File
@@ -1,40 +0,0 @@
import { Text } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import type { CosmosHashType } from 'lib/address/cosmos';
import { Link } from 'toolkit/chakra/link';
const zetaChainFeature = config.features.zetachain;
interface Props {
cosmosHash: string;
type: CosmosHashType;
}
const SearchCosmosNotice = ({ cosmosHash, type }: Props) => {
if (!zetaChainFeature.isEnabled || !type) {
return null;
}
const url = (() => {
if (type === 'tx') {
return zetaChainFeature.cosmosTxUrlTemplate.replace('{hash}', cosmosHash);
}
return zetaChainFeature.cosmosAddressUrlTemplate.replace('{hash}', cosmosHash);
})();
return (
<>
<Text color="text.secondary">
It looks like you are searching for a Cosmos SDK style hash. This information is best served by the external explorer.
</Text>
<Link href={ url } external mt={ 4 }>
Click here to be redirected
</Link>
</>
);
};
export default React.memo(SearchCosmosNotice);
+2 -2
View File
@@ -38,7 +38,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const recentSearchKeywords = getRecentSearchKeywords();
const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query, zetaChainCCTXQuery, cosmosHashType } = useSearchWithClusters();
const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query, zetaChainCCTXQuery, externalSearchItem } = useSearchWithClusters();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -188,7 +188,7 @@ const SearchBar = ({ isHomepage }: Props) => {
onItemClick={ handleItemClick }
containerId={ SCROLL_CONTAINER_ID }
zetaChainCCTXQuery={ zetaChainCCTXQuery }
cosmosHashType={ cosmosHashType }
externalSearchItem={ externalSearchItem }
/>
) }
</Box>
@@ -8,16 +8,16 @@ import type { ListCctxsResponse } from '@blockscout/zetachain-cctx-types';
import type { SearchResultItem } from 'types/api/search';
import config from 'configs/app';
import type { CosmosHashType } from 'lib/address/cosmos';
import type { ResourceError } from 'lib/api/resources';
import { useSettingsContext } from 'lib/contexts/settings';
import useIsMobile from 'lib/hooks/useIsMobile';
import type { ExternalSearchItem as ExternalSearchItemType } from 'lib/search/externalSearch';
import { TabsList, TabsRoot, TabsTrigger } from 'toolkit/chakra/tabs';
import * as regexp from 'toolkit/utils/regexp';
import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps';
import TextAd from 'ui/shared/ad/TextAd';
import ContentLoader from 'ui/shared/ContentLoader';
import SearchCosmosNotice from 'ui/shared/search/SearchCosmosNotice';
import ExternalSearchItem from 'ui/shared/search/ExternalSearchItem';
import type { ApiCategory, Category, ItemsCategoriesMap } from 'ui/shared/search/utils';
import { getItemCategory, searchCategories } from 'ui/shared/search/utils';
@@ -29,13 +29,13 @@ import SearchBarSuggestZetaChainCCTX from './SearchBarSuggestZetaChainCCTX';
interface Props {
query: UseQueryResult<Array<SearchResultItem>, ResourceError<unknown>>;
zetaChainCCTXQuery: UseQueryResult<ListCctxsResponse, ResourceError<unknown>>;
cosmosHashType: CosmosHashType;
externalSearchItem: ExternalSearchItemType;
searchTerm: string;
onItemClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
containerId: string;
}
const SearchBarSuggest = ({ query, zetaChainCCTXQuery, cosmosHashType, searchTerm, onItemClick, containerId }: Props) => {
const SearchBarSuggest = ({ query, zetaChainCCTXQuery, externalSearchItem, searchTerm, onItemClick, containerId }: Props) => {
const isMobile = useIsMobile();
const marketplaceApps = useMarketplaceApps(searchTerm);
@@ -152,7 +152,7 @@ const SearchBarSuggest = ({ query, zetaChainCCTXQuery, cosmosHashType, searchTer
const resultCategories = searchCategories.filter(cat => itemsGroups[cat.id]);
if (resultCategories.length === 0 && !cosmosHashType) {
if (resultCategories.length === 0 && !externalSearchItem) {
if (regexp.BLOCK_HEIGHT.test(searchTerm)) {
return <SearchBarSuggestBlockCountdown blockHeight={ searchTerm } onClick={ onItemClick }/>;
}
@@ -218,7 +218,7 @@ const SearchBarSuggest = ({ query, zetaChainCCTXQuery, cosmosHashType, searchTer
</Element>
);
}) }
{ cosmosHashType && <SearchCosmosNotice cosmosHash={ searchTerm } type={ cosmosHashType }/> }
{ externalSearchItem && <ExternalSearchItem item={ externalSearchItem }/> }
</>
);
})();
@@ -2,9 +2,9 @@ import React from 'react';
import config from 'configs/app';
import { isBech32Address, fromBech32Address } from 'lib/address/bech32';
import { checkCosmosHash } from 'lib/address/cosmos';
import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
import { getExternalSearchItem } from 'lib/search/externalSearch';
export default function useQuickSearchQuery() {
const [ searchTerm, setSearchTerm ] = React.useState('');
@@ -39,7 +39,7 @@ export default function useQuickSearchQuery() {
handleSearchTermChange: setSearchTerm,
query,
redirectCheckQuery,
cosmosHashType: checkCosmosHash(debouncedSearchTerm),
externalSearchItem: getExternalSearchItem(debouncedSearchTerm),
zetaChainCCTXQuery,
}), [ debouncedSearchTerm, query, redirectCheckQuery, searchTerm, zetaChainCCTXQuery ]);
}
+2 -2
View File
@@ -3,11 +3,11 @@ import React from 'react';
import config from 'configs/app';
import { fromBech32Address, isBech32Address } from 'lib/address/bech32';
import { checkCosmosHash } from 'lib/address/cosmos';
import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
import getQueryParamString from 'lib/router/getQueryParamString';
import { getExternalSearchItem } from 'lib/search/externalSearch';
import { SEARCH_RESULT_ITEM, SEARCH_RESULT_NEXT_PAGE_PARAMS } from 'stubs/search';
import { generateListStub } from 'stubs/utils';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
@@ -59,6 +59,6 @@ export default function useSearchQuery(withRedirectCheck?: boolean) {
redirectCheckQuery,
pathname,
zetaChainCCTXQuery,
cosmosHashType: checkCosmosHash(debouncedSearchTerm),
externalSearchItem: getExternalSearchItem(debouncedSearchTerm),
}), [ debouncedSearchTerm, pathname, query, redirectCheckQuery, searchTerm, zetaChainCCTXQuery ]);
}