diff --git a/src/app/components/Search/search-utils.ts b/src/app/components/Search/search-utils.ts index 752c2fc784..c097b1eee9 100644 --- a/src/app/components/Search/search-utils.ts +++ b/src/app/components/Search/search-utils.ts @@ -112,6 +112,11 @@ export const validateAndNormalize = { return searchTerm.toLowerCase() } }, + accountNameFragment: (searchTerm: string) => { + if (searchTerm?.length >= textSearchMininumLength) { + return searchTerm.toLowerCase() + } + }, } satisfies { [name: string]: (searchTerm: string) => string | undefined } export function isSearchValid(searchTerm: string) { diff --git a/src/app/data/pontusx-account-names.ts b/src/app/data/pontusx-account-names.ts index 56f36bdfee..b5474f0c96 100644 --- a/src/app/data/pontusx-account-names.ts +++ b/src/app/data/pontusx-account-names.ts @@ -1,6 +1,9 @@ import axios from 'axios' import { useQuery } from '@tanstack/react-query' import type { AccountNameInfo } from '../hooks/useAccountName' +import { Layer } from '../../oasis-nexus/api' +import { Network } from '../../types/network' +import { findTextMatch } from '../components/HighlightedText/text-matching' import * as process from 'process' const DATA_SOURCE_URL = 'https://raw.githubusercontent.com/deltaDAO/mvg-portal/main/pontusxAddresses.json' @@ -63,3 +66,29 @@ export const usePontusXAccountName = (address: string, enabled: boolean): Accoun loading: isLoading, } } + +export const useSearchForPontusXAccountsByName = ( + network: Network, + nameFragment: string, + enabled: boolean, +) => { + const { isLoading, error, data: allNames } = usePontusXAccountNames(enabled) + if (error) { + console.log('Failed to load Pontus-X account names', error) + } + + const textMatcher = + nameFragment && enabled + ? (entry: AccountEntry): boolean => { + return !!findTextMatch(entry.name, [nameFragment]) + } + : () => false + return { + results: (allNames?.list || []).filter(textMatcher).map(entry => ({ + network, + layer: Layer.pontusx, + address: entry.address, + })), + isLoading, + } +} diff --git a/src/app/hooks/useAccountName.ts b/src/app/hooks/useAccountName.ts index 20e052bed9..fe7a95ea77 100644 --- a/src/app/hooks/useAccountName.ts +++ b/src/app/hooks/useAccountName.ts @@ -1,7 +1,7 @@ import { SearchScope } from '../../types/searchScope' import Chance from 'chance' import { Layer } from '../../oasis-nexus/api' -import { usePontusXAccountName } from '../data/pontusx-account-names' +import { usePontusXAccountName, useSearchForPontusXAccountsByName } from '../data/pontusx-account-names' const NO_MATCH = '__no_match__' @@ -59,3 +59,14 @@ export const useAccountName = (scope: SearchScope, address: string, dropCache = loading: false, } } + +export const useSearchForAccountsByName = (scope: SearchScope, nameFragment = '') => { + const isValidPontusXSearch = scope.layer === Layer.pontusx && !!nameFragment + const pontusXResults = useSearchForPontusXAccountsByName(scope.network, nameFragment, isValidPontusXSearch) + return isValidPontusXSearch + ? pontusXResults + : { + isLoading: false, + results: [], + } +} diff --git a/src/app/pages/AccountDetailsPage/DeferredConsensusAccountDetails.tsx b/src/app/pages/AccountDetailsPage/DeferredConsensusAccountDetails.tsx new file mode 100644 index 0000000000..b3e3ee690d --- /dev/null +++ b/src/app/pages/AccountDetailsPage/DeferredConsensusAccountDetails.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react' +import { AllTokenPrices } from '../../../coin-gecko/api' +import { Network } from '../../../types/network' + +/** + * Load and display details of a RuntimeAccount + */ +export const DeferredConsensusAccountDetails: FC<{ + network: Network + address: string + tokenPrices: AllTokenPrices + showLayer?: boolean +}> = () => + // { + // network, address, tokenPrices, highlightedPartOfName, showLayer + // }, + { + // TODO: load and display consensus account details when API and component becomes available + return null + } diff --git a/src/app/pages/AccountDetailsPage/DeferredRuntimeAccountDetails.tsx b/src/app/pages/AccountDetailsPage/DeferredRuntimeAccountDetails.tsx new file mode 100644 index 0000000000..e00cdd468d --- /dev/null +++ b/src/app/pages/AccountDetailsPage/DeferredRuntimeAccountDetails.tsx @@ -0,0 +1,27 @@ +import { FC } from 'react' +import { Runtime, useGetRuntimeAccountsAddress } from '../../../oasis-nexus/api' +import { AllTokenPrices } from '../../../coin-gecko/api' +import { AccountDetailsView } from './AccountDetailsView' +import { Network } from '../../../types/network' + +/** + * Load and display details of a RuntimeAccount + */ +export const DeferredRuntimeAccountDetails: FC<{ + network: Network + layer: Runtime + address: string + tokenPrices: AllTokenPrices + showLayer?: boolean +}> = ({ network, layer, address, tokenPrices, showLayer }) => { + const { data, isLoading, isError } = useGetRuntimeAccountsAddress(network, layer, address) + return ( + + ) +} diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 366259b74c..bf3eecd5d6 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -7,6 +7,7 @@ import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage' import { AccountDetailsView } from '../AccountDetailsPage/AccountDetailsView' import { AccountResult, + AccountAddressResult, BlockResult, ContractResult, ProposalResult, @@ -21,6 +22,9 @@ import { AllTokenPrices } from '../../../coin-gecko/api' import { ResultListFrame } from './ResultListFrame' import { TokenDetails } from '../../components/Tokens/TokenDetails' import { ProposalDetailView } from '../ProposalDetailsPage' +import { DeferredRuntimeAccountDetails } from '../AccountDetailsPage/DeferredRuntimeAccountDetails' +import { Layer } from '../../../oasis-nexus/api' +import { DeferredConsensusAccountDetails } from '../AccountDetailsPage/DeferredConsensusAccountDetails' /** * Component for displaying a list of search results @@ -94,6 +98,33 @@ export const SearchResultsList: FC<{ linkLabel={t('search.results.accounts.viewLink')} /> + item.resultType === 'accountAddress', + )} + resultComponent={item => + item.layer === Layer.consensus ? ( + + ) : ( + + ) + } + link={acc => RouteUtils.getAccountRoute(acc, acc.address)} + linkLabel={t('search.results.accounts.viewLink')} + /> + item.resultType === 'contract')} diff --git a/src/app/pages/SearchResultsPage/hooks.ts b/src/app/pages/SearchResultsPage/hooks.ts index 395a96cbfb..b5450202e5 100644 --- a/src/app/pages/SearchResultsPage/hooks.ts +++ b/src/app/pages/SearchResultsPage/hooks.ts @@ -19,6 +19,7 @@ import { import { RouteUtils } from '../../utils/route-utils' import { SearchParams } from '../../components/Search/search-utils' import { SearchScope } from '../../../types/searchScope' +import { useSearchForAccountsByName } from '../../hooks/useAccountName' function isDefined(item: T): item is NonNullable { return item != null @@ -27,7 +28,7 @@ function isDefined(item: T): item is NonNullable { export type ConditionalResults = { isLoading: boolean; results: T[] } type SearchResultItemCore = HasScope & { - resultType: 'block' | 'transaction' | 'account' | 'contract' | 'token' | 'proposal' + resultType: 'block' | 'transaction' | 'account' | 'accountAddress' | 'contract' | 'token' | 'proposal' } export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'block' } @@ -36,6 +37,10 @@ export type TransactionResult = SearchResultItemCore & RuntimeTransaction & { re export type AccountResult = SearchResultItemCore & RuntimeAccount & { resultType: 'account' } +export type AccountAddressResult = SearchResultItemCore & { address: string } & { + resultType: 'accountAddress' +} + export type ContractResult = SearchResultItemCore & RuntimeAccount & { resultType: 'contract' } export type TokenResult = SearchResultItemCore & EvmToken & { resultType: 'token' } @@ -46,6 +51,7 @@ export type SearchResultItem = | BlockResult | TransactionResult | AccountResult + | AccountAddressResult | ContractResult | TokenResult | ProposalResult @@ -193,6 +199,25 @@ export function useNetworkProposalsConditionally( } } +type AccountAddressInfo = Pick + +export function useNamedAccountConditionally( + currentScope: SearchScope | undefined, + nameFragment: string | undefined, +): ConditionalResults { + const queries = RouteUtils.getVisibleScopes(currentScope).map(scope => + // eslint-disable-next-line react-hooks/rules-of-hooks + useSearchForAccountsByName(scope, nameFragment), + ) + return { + isLoading: queries.some(query => query.isLoading), + results: queries + .map(query => query.results) + .filter(isDefined) + .flat(), + } +} + export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams) => { const queries = { blockHeight: useBlocksByHeightConditionally(currentScope, q.blockHeight), @@ -201,6 +226,7 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams oasisAccount: useRuntimeAccountConditionally(currentScope, q.consensusAccount), // TODO: remove evmBech32Account and use evmAccount when API is ready evmBech32Account: useRuntimeAccountConditionally(currentScope, q.evmBech32Account), + accountsByName: useNamedAccountConditionally(currentScope, q.accountNameFragment), tokens: useRuntimeTokenConditionally(currentScope, q.evmTokenNameFragment), proposals: useNetworkProposalsConditionally(q.networkProposalNameFragment), } @@ -211,6 +237,7 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams ...(queries.oasisAccount.results || []), ...(queries.evmBech32Account.results || []), ].filter(isAccountNonEmpty) + const accountAddresses = queries.accountsByName.results || [] const tokens = queries.tokens.results .map(l => l.evm_tokens) .flat() @@ -228,6 +255,9 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams ...accounts .filter(account => account.evm_contract) .map((account): ContractResult => ({ ...account, resultType: 'contract' })), + ...accountAddresses.map( + (account): AccountAddressResult => ({ ...account, resultType: 'accountAddress' }), + ), ...tokens.map((token): TokenResult => ({ ...token, resultType: 'token' })), ...proposals.map((proposal): ProposalResult => ({ ...proposal, resultType: 'proposal' })), ] diff --git a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts index ebcb816b8c..e3e704e029 100644 --- a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts +++ b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts @@ -35,6 +35,9 @@ export function useRedirectIfSingleResult( case 'account': redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address) break + case 'accountAddress': + redirectTo = `${RouteUtils.getAccountRoute(item, item.address)}?q=${searchTerm}` + break case 'contract': redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address) break