Skip to content

Commit

Permalink
Support for searching for accounts by name
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed Apr 30, 2024
1 parent ecad3f2 commit fad05ea
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/app/components/Search/search-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,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) {
Expand Down
29 changes: 29 additions & 0 deletions src/app/data/pontusx-account-names.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
}
}
13 changes: 12 additions & 1 deletion src/app/hooks/useAccountName.ts
Original file line number Diff line number Diff line change
@@ -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__'

Expand Down Expand Up @@ -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: [],
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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 (
<AccountDetailsView
isLoading={isLoading}
isError={isError}
account={data?.data}
tokenPrices={tokenPrices}
showLayer={showLayer}
/>
)
}
31 changes: 31 additions & 0 deletions src/app/pages/SearchResultsPage/SearchResultsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage'
import { AccountDetailsView } from '../RuntimeAccountDetailsPage/AccountDetailsView'
import {
AccountResult,
AccountAddressResult,
BlockResult,
ContractResult,
ProposalResult,
Expand All @@ -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 '../RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails'
import { Layer } from '../../../oasis-nexus/api'
import { DeferredConsensusAccountDetails } from '../ConsensusAccountDetailsPage/DeferredConsensusAccountDetails'

/**
* Component for displaying a list of search results
Expand Down Expand Up @@ -94,6 +98,33 @@ export const SearchResultsList: FC<{
linkLabel={t('search.results.accounts.viewLink')}
/>

<ResultsGroupByType
title={t('search.results.accounts.title')}
results={searchResults.filter(
(item): item is AccountAddressResult => item.resultType === 'accountAddress',
)}
resultComponent={item =>
item.layer === Layer.consensus ? (
<DeferredConsensusAccountDetails
network={item.network}
address={item.address}
tokenPrices={tokenPrices}
showLayer={true}
/>
) : (
<DeferredRuntimeAccountDetails
network={item.network}
layer={item.layer}
address={item.address}
tokenPrices={tokenPrices}
showLayer={true}
/>
)
}
link={acc => RouteUtils.getAccountRoute(acc, acc.address)}
linkLabel={t('search.results.accounts.viewLink')}
/>

<ResultsGroupByType
title={t('search.results.contracts.title')}
results={searchResults.filter((item): item is ContractResult => item.resultType === 'contract')}
Expand Down
32 changes: 31 additions & 1 deletion src/app/pages/SearchResultsPage/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(item: T): item is NonNullable<T> {
return item != null
Expand All @@ -27,7 +28,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' | 'contract' | 'token' | 'proposal'
resultType: 'block' | 'transaction' | 'account' | 'accountAddress' | 'contract' | 'token' | 'proposal'
}

export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'block' }
Expand All @@ -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' }
Expand All @@ -46,6 +51,7 @@ export type SearchResultItem =
| BlockResult
| TransactionResult
| AccountResult
| AccountAddressResult
| ContractResult
| TokenResult
| ProposalResult
Expand Down Expand Up @@ -193,6 +199,25 @@ export function useNetworkProposalsConditionally(
}
}

type AccountAddressInfo = Pick<RuntimeAccount, 'network' | 'layer' | 'address'>

export function useNamedAccountConditionally(
currentScope: SearchScope | undefined,
nameFragment: string | undefined,
): ConditionalResults<AccountAddressInfo> {
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),
Expand All @@ -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),
}
Expand All @@ -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()
Expand All @@ -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' })),
]
Expand Down
3 changes: 3 additions & 0 deletions src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fad05ea

Please sign in to comment.