From e1241f16adf1cd928dceca087503a0dcacc7f100 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Fri, 30 Jun 2023 21:28:05 +0200 Subject: [PATCH 1/3] As part of search, run queries to search for tokens by name --- .../Search/__tests__/search-utils.test.ts | 3 +- src/app/components/Search/search-utils.ts | 5 +++ src/app/pages/SearchResultsPage/hooks.ts | 45 ++++++++++++++++++- .../useRedirectIfSingleResult.ts | 3 ++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/app/components/Search/__tests__/search-utils.test.ts b/src/app/components/Search/__tests__/search-utils.test.ts index 3244586c3..fedc60ff9 100644 --- a/src/app/components/Search/__tests__/search-utils.test.ts +++ b/src/app/components/Search/__tests__/search-utils.test.ts @@ -94,6 +94,7 @@ describe('search-utils validateAndNormalize', () => { it('isSearchValid', () => { expect(isSearchValid(consensusTxHash)).toBe(true) expect(isSearchValid(blockHeight)).toBe(true) - expect(isSearchValid(blockHeight.replace('1', 'a'))).toBe(false) + expect(isSearchValid('yu')).toBe(false) + expect(isSearchValid('yuz')).toBe(true) }) }) diff --git a/src/app/components/Search/search-utils.ts b/src/app/components/Search/search-utils.ts index e6cb603ad..ac5098760 100644 --- a/src/app/components/Search/search-utils.ts +++ b/src/app/components/Search/search-utils.ts @@ -80,6 +80,11 @@ export const validateAndNormalize = { return searchTerm.toLowerCase() } }, + evmTokenNameFragment: (searchTerm: string) => { + if (searchTerm?.length > 2) { + return searchTerm.toLowerCase() + } + }, } satisfies { [name: string]: (searchTerm: string) => string | undefined } export function isSearchValid(searchTerm: string) { diff --git a/src/app/pages/SearchResultsPage/hooks.ts b/src/app/pages/SearchResultsPage/hooks.ts index 88485d197..422e08e3c 100644 --- a/src/app/pages/SearchResultsPage/hooks.ts +++ b/src/app/pages/SearchResultsPage/hooks.ts @@ -9,6 +9,9 @@ import { Layer, isAccountNonEmpty, HasScope, + useGetRuntimeEvmTokens, + EvmTokenList, + EvmToken, } from '../../../oasis-nexus/api' import { RouteUtils } from '../../utils/route-utils' import { SearchParams } from '../../components/Search/search-utils' @@ -20,7 +23,7 @@ function isDefined(item: T): item is NonNullable { export type ConditionalResults = { isLoading: boolean; results: T[] } type SearchResultItemCore = HasScope & { - resultType: 'block' | 'transaction' | 'account' + resultType: 'block' | 'transaction' | 'account' | 'token' } export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'block' } @@ -29,7 +32,9 @@ export type TransactionResult = SearchResultItemCore & RuntimeTransaction & { re export type AccountResult = SearchResultItemCore & RuntimeAccount & { resultType: 'account' } -export type SearchResultItem = BlockResult | TransactionResult | AccountResult +export type TokenResult = SearchResultItemCore & EvmToken & { resultType: 'token' } + +export type SearchResultItem = BlockResult | TransactionResult | AccountResult | TokenResult export type SearchResults = SearchResultItem[] @@ -96,6 +101,35 @@ export function useRuntimeAccountConditionally( } } +export function useRuntimeTokenConditionally( + nameFragment: string | undefined, +): ConditionalResults { + const queries = RouteUtils.getEnabledScopes() + .filter(scope => scope.layer !== Layer.consensus) + .map(scope => + // See explanation above + // eslint-disable-next-line react-hooks/rules-of-hooks + useGetRuntimeEvmTokens( + scope.network, + scope.layer as Runtime, + { + name: nameFragment, + limit: 10, + }, + { + query: { + enabled: !!nameFragment, + }, + }, + ), + ) + + return { + isLoading: queries.some(query => query.isInitialLoading), + results: queries.map(query => query.data?.data).filter(isDefined), + } +} + export const useSearch = (q: SearchParams) => { const queries = { blockHeight: useBlocksConditionally(q.blockHeight), @@ -104,6 +138,7 @@ export const useSearch = (q: SearchParams) => { oasisAccount: useRuntimeAccountConditionally(q.consensusAccount), // TODO: remove evmBech32Account and use evmAccount when API is ready evmBech32Account: useRuntimeAccountConditionally(q.evmBech32Account), + tokens: useRuntimeTokenConditionally(q.evmTokenNameFragment), } const isLoading = Object.values(queries).some(query => query.isLoading) const blocks = queries.blockHeight.results || [] @@ -112,12 +147,18 @@ export const useSearch = (q: SearchParams) => { ...(queries.oasisAccount.results || []), ...(queries.evmBech32Account.results || []), ].filter(isAccountNonEmpty) + const tokens = queries.tokens.results + .map(l => l.evm_tokens) + .flat() + .sort((t1, t2) => t2.num_holders - t1.num_holders) + const results: SearchResultItem[] = isLoading ? [] : [ ...blocks.map((block): BlockResult => ({ ...block, resultType: 'block' })), ...transactions.map((tx): TransactionResult => ({ ...tx, resultType: 'transaction' })), ...accounts.map((account): AccountResult => ({ ...account, resultType: 'account' })), + ...tokens.map((token): TokenResult => ({ ...token, resultType: 'token' })), ] return { isLoading, diff --git a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts index 63524c8ef..1b8d4c3db 100644 --- a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts +++ b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts @@ -30,6 +30,9 @@ export function useRedirectIfSingleResult(scope: SearchScope | undefined, result case 'account': redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address) break + case 'token': + redirectTo = RouteUtils.getTokenRoute(item, item.eth_contract_addr || item.contract_addr) + break default: exhaustedTypeWarning('Unexpected result type', item) } From b1c8d2ae58afcb211d44385a480a2970fceb218b Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Fri, 30 Jun 2023 21:42:21 +0200 Subject: [PATCH 2/3] Add support for displaying token search results --- .changelog/637.feature.md | 1 + src/app/pages/SearchResultsPage/SearchResultsList.tsx | 11 ++++++++++- src/locales/en/translation.json | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changelog/637.feature.md diff --git a/.changelog/637.feature.md b/.changelog/637.feature.md new file mode 100644 index 000000000..040655595 --- /dev/null +++ b/.changelog/637.feature.md @@ -0,0 +1 @@ +Support searching for tokens by name diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 359b80a8b..4bef49c21 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -5,12 +5,13 @@ import { BlockDetailView } from '../BlockDetailPage' import { RouteUtils } from '../../utils/route-utils' import { TransactionDetailView } from '../TransactionDetailPage' import { AccountDetailsView } from '../AccountDetailsPage' -import { AccountResult, BlockResult, SearchResults, TransactionResult } from './hooks' +import { AccountResult, BlockResult, SearchResults, TokenResult, TransactionResult } from './hooks' import { getThemesForNetworks } from '../../../styles/theme' import { Network } from '../../../types/network' import { SubPageCard } from '../../components/SubPageCard' import { AllTokenPrices } from '../../../coin-gecko/api' import { ResultListFrame } from './ResultListFrame' +import { TokenDetails } from '../../components/Tokens/TokenDetails' /** * Component for displaying a list of search results @@ -82,6 +83,14 @@ export const SearchResultsList: FC<{ link={acc => RouteUtils.getAccountRoute(acc, acc.address_eth ?? acc.address)} linkLabel={t('search.results.accounts.viewLink')} /> + + item.resultType === 'token')} + resultComponent={item => } + link={token => RouteUtils.getTokenRoute(token, token.eth_contract_addr ?? token.contract_addr)} + linkLabel={t('search.results.tokens.viewLink')} + /> ) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 3a82cefe4..e2ac7a534 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -379,6 +379,10 @@ "title": "Blocks", "viewLink": "View Block" }, + "tokens": { + "title": "Tokens", + "viewLink": "View Token" + }, "transactions": { "title": "Transactions", "viewLink": "View Transaction" From 1f5b7447d2df15a0a4d1361d8d19d237cfe42362 Mon Sep 17 00:00:00 2001 From: Csillag Kristof Date: Sat, 1 Jul 2023 05:33:00 +0200 Subject: [PATCH 3/3] Include tokens in search suggestions --- src/app/components/Search/SearchSuggestionsButtons.tsx | 5 ++++- src/app/components/Search/SearchSuggestionsLinks.tsx | 6 +++++- src/app/components/Search/search-utils.ts | 4 ++++ src/locales/en/translation.json | 6 +++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/app/components/Search/SearchSuggestionsButtons.tsx b/src/app/components/Search/SearchSuggestionsButtons.tsx index 49587f19a..16684a0e2 100644 --- a/src/app/components/Search/SearchSuggestionsButtons.tsx +++ b/src/app/components/Search/SearchSuggestionsButtons.tsx @@ -7,6 +7,7 @@ import { COLORS } from '../../../styles/theme/colors' import WidgetsIcon from '@mui/icons-material/Widgets' import RepeatIcon from '@mui/icons-material/Repeat' import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet' +import TokenIcon from '@mui/icons-material/Token' import { searchSuggestionTerms } from './search-utils' import { OptionalBreak } from '../OptionalBreak' import { SearchScope } from '../../../types/searchScope' @@ -35,7 +36,7 @@ interface Props { export const SearchSuggestionsButtons: FC = ({ scope, onClickSuggestion }) => { const { t } = useTranslation() - const { suggestedBlock, suggestedTransaction, suggestedAccount } = + const { suggestedBlock, suggestedTransaction, suggestedAccount, suggestedTokenFragment } = (scope?.network && scope?.layer && searchSuggestionTerms[scope.network][scope.layer]) ?? searchSuggestionTerms['mainnet']['sapphire']! @@ -53,6 +54,8 @@ export const SearchSuggestionsButtons: FC = ({ scope, onClickSuggestion } TransactionLink: onClickSuggestion(suggestedTransaction)} />, AccountIcon: , AccountLink: onClickSuggestion(suggestedAccount)} />, + TokenIcon: , + TokenLink: onClickSuggestion(suggestedTokenFragment)} />, }} /> diff --git a/src/app/components/Search/SearchSuggestionsLinks.tsx b/src/app/components/Search/SearchSuggestionsLinks.tsx index 280c19e5f..78de7ae71 100644 --- a/src/app/components/Search/SearchSuggestionsLinks.tsx +++ b/src/app/components/Search/SearchSuggestionsLinks.tsx @@ -13,7 +13,7 @@ interface Props { export const SearchSuggestionsLinks: FC = ({ scope }) => { const { t } = useTranslation() - const { suggestedBlock, suggestedTransaction, suggestedAccount } = + const { suggestedBlock, suggestedTransaction, suggestedAccount, suggestedTokenFragment } = (scope?.network && scope?.layer && searchSuggestionTerms[scope.network][scope.layer]) ?? searchSuggestionTerms['mainnet']['sapphire']! @@ -31,6 +31,10 @@ export const SearchSuggestionsLinks: FC = ({ scope }) => { ), AccountIcon: <>, AccountLink: , + TokenIcon: <>, + TokenLink: ( + + ), }} /> ) diff --git a/src/app/components/Search/search-utils.ts b/src/app/components/Search/search-utils.ts index ac5098760..a54fe68b0 100644 --- a/src/app/components/Search/search-utils.ts +++ b/src/app/components/Search/search-utils.ts @@ -16,6 +16,7 @@ type LayerSuggestions = { suggestedBlock: string suggestedTransaction: string suggestedAccount: string + suggestedTokenFragment: string } export const searchSuggestionTerms: Record>> = { @@ -24,11 +25,13 @@ export const searchSuggestionTerms: Record Block, Transaction, Address", + "searchSuggestions": "Not sure what to look for? Try out a search: Block, Transaction, Address, Token ", "sectionHeader": "Results on {{ scope }}" } }