Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support searching for tokens by name or symbol #637

Merged
merged 3 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .changelog/637.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support searching for tokens by name
5 changes: 4 additions & 1 deletion src/app/components/Search/SearchSuggestionsButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -35,7 +36,7 @@ interface Props {

export const SearchSuggestionsButtons: FC<Props> = ({ 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']!

Expand All @@ -53,6 +54,8 @@ export const SearchSuggestionsButtons: FC<Props> = ({ scope, onClickSuggestion }
TransactionLink: <SuggestionButton onClick={() => onClickSuggestion(suggestedTransaction)} />,
AccountIcon: <AccountBalanceWalletIcon sx={{ fontSize: '18px' }} />,
AccountLink: <SuggestionButton onClick={() => onClickSuggestion(suggestedAccount)} />,
TokenIcon: <TokenIcon sx={{ fontSize: '18px' }} />,
TokenLink: <SuggestionButton onClick={() => onClickSuggestion(suggestedTokenFragment)} />,
}}
/>
</Typography>
Expand Down
6 changes: 5 additions & 1 deletion src/app/components/Search/SearchSuggestionsLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface Props {

export const SearchSuggestionsLinks: FC<Props> = ({ 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']!

Expand All @@ -31,6 +31,10 @@ export const SearchSuggestionsLinks: FC<Props> = ({ scope }) => {
),
AccountIcon: <></>,
AccountLink: <Link component={RouterLink} to={RouteUtils.getSearchRoute(scope, suggestedAccount)} />,
TokenIcon: <></>,
TokenLink: (
<Link component={RouterLink} to={RouteUtils.getSearchRoute(scope, suggestedTokenFragment)} />
),
}}
/>
)
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/Search/__tests__/search-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
9 changes: 9 additions & 0 deletions src/app/components/Search/search-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type LayerSuggestions = {
suggestedBlock: string
suggestedTransaction: string
suggestedAccount: string
suggestedTokenFragment: string
}

export const searchSuggestionTerms: Record<Network, Partial<Record<Layer, LayerSuggestions>>> = {
Expand All @@ -24,18 +25,21 @@ export const searchSuggestionTerms: Record<Network, Partial<Record<Layer, LayerS
suggestedBlock: '4260',
suggestedTransaction: '0x2f461f83745e1fa1177138aa815e210e1c69305db8065af9015b2e490a5033f1',
suggestedAccount: '0x0266562AB0aE2a80C14373029a70F73A9A3dB9d3',
suggestedTokenFragment: 'yuzu',
},
sapphire: {
suggestedBlock: '4260',
suggestedTransaction: '0x5900415a3fbb39325d5dfe145d1eccd1586a2afe12a204de34ecac0c808ac3f7',
suggestedAccount: '0x90adE3B7065fa715c7a150313877dF1d33e777D5',
suggestedTokenFragment: 'mock',
},
},
testnet: {
sapphire: {
suggestedBlock: '4260',
suggestedTransaction: '0xd9b5c08be1cb74229abedd9b3e1afb8b43228085a6abf72993db415959ab6b35',
suggestedAccount: '0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204',
suggestedTokenFragment: 'USD',
},
},
} satisfies SpecifiedPerEnabledLayer
Expand Down Expand Up @@ -80,6 +84,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) {
Expand Down
11 changes: 10 additions & 1 deletion src/app/pages/SearchResultsPage/SearchResultsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,6 +83,14 @@ export const SearchResultsList: FC<{
link={acc => RouteUtils.getAccountRoute(acc, acc.address_eth ?? acc.address)}
linkLabel={t('search.results.accounts.viewLink')}
/>

<ResultsGroupByType
title={t('search.results.tokens.title')}
results={searchResults.filter((item): item is TokenResult => item.resultType === 'token')}
resultComponent={item => <TokenDetails token={item} showLayer />}
link={token => RouteUtils.getTokenRoute(token, token.eth_contract_addr ?? token.contract_addr)}
linkLabel={t('search.results.tokens.viewLink')}
/>
</SubPageCard>
</ResultListFrame>
)
Expand Down
45 changes: 43 additions & 2 deletions src/app/pages/SearchResultsPage/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -20,7 +23,7 @@ function isDefined<T>(item: T): item is NonNullable<T> {
export type ConditionalResults<T> = { isLoading: boolean; results: T[] }

type SearchResultItemCore = HasScope & {
resultType: 'block' | 'transaction' | 'account'
resultType: 'block' | 'transaction' | 'account' | 'token'
}

export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'block' }
Expand All @@ -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[]

Expand Down Expand Up @@ -96,6 +101,35 @@ export function useRuntimeAccountConditionally(
}
}

export function useRuntimeTokenConditionally(
nameFragment: string | undefined,
): ConditionalResults<EvmTokenList> {
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),
Expand All @@ -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 || []
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
10 changes: 7 additions & 3 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@
"selectNetworkAria": "Toggle network selector"
},
"search": {
"placeholder": "Address, Block, Contract, Txn Hash, Transaction ID, etc",
"mobilePlaceholder": "Search Address, Block, Txn, etc",
"placeholder": "Address, Block, Contract, Txn Hash, Transaction ID, Token name, etc",
"mobilePlaceholder": "Search Address, Block, Txn, Token, etc",
"noResults": {
"header": "No results found",
"scopeHeader": "No results found on {{scope}}",
Expand All @@ -379,6 +379,10 @@
"title": "Blocks",
"viewLink": "View Block"
},
"tokens": {
"title": "Tokens",
"viewLink": "View Token"
},
"transactions": {
"title": "Transactions",
"viewLink": "View Transaction"
Expand All @@ -389,7 +393,7 @@
"moreCount_other": "{{ count }} more results"
},
"searchBtnText": "Search",
"searchSuggestions": "Not sure what to look for? Try out a search: <OptionalBreak><BlockLink><BlockIcon/> Block</BlockLink>, <TransactionLink><TransactionIcon/> Transaction</TransactionLink>, <AccountLink><AccountIcon/> Address</AccountLink></OptionalBreak>",
"searchSuggestions": "Not sure what to look for? Try out a search: <OptionalBreak><BlockLink><BlockIcon/> Block</BlockLink>, <TransactionLink><TransactionIcon/> Transaction</TransactionLink>, <AccountLink><AccountIcon/> Address</AccountLink>, <TokenLink><TokenIcon/> Token</TokenLink> </OptionalBreak>",
"sectionHeader": "Results on {{ scope }}"
}
}