Skip to content

Commit

Permalink
Incomplete: move useSearchCurrencyList to new searchV2
Browse files Browse the repository at this point in the history
  • Loading branch information
jinchung committed Feb 5, 2025
1 parent 659dd77 commit 2b83d50
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 115 deletions.
167 changes: 167 additions & 0 deletions src/__swaps__/screens/Swap/resources/search/searchV2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import qs from 'qs';
import { getProvider } from '@/handlers/web3';
import { groupBy } from 'lodash';
import { RainbowError, logger } from '@/logger';
import { RainbowFetchClient } from '@/rainbow-fetch';
import { getAddress, isAddress } from '@ethersproject/address';
Expand All @@ -14,6 +15,10 @@ import { SearchAsset, TokenSearchAssetKey } from '@/__swaps__/types/search';
import { time } from '@/utils';
import { parseTokenSearchResults } from './utils';

type SearchItemWithRelevance = SearchAsset & {
relevance: number;
};

const tokenSearchClient = new RainbowFetchClient({
baseURL: 'https://token-search.rainbow.me/v3/tokens',
headers: {
Expand All @@ -29,6 +34,11 @@ type TokenSearchParams = {
query: string | undefined;
};

type DiscoverSearchParams = {
list?: string;
query: string;
};

type TokenSearchState = {
bridgeAsset: SearchAsset | null;
};
Expand All @@ -44,6 +54,12 @@ type VerifiedTokenData = {
unverifiedAssets: SearchAsset[];
};

type DiscoverSearchResults = {
verifiedAssets: SearchAsset[];
highLiquidityAssets: SearchAsset[];
lowLiquidityAssets: SearchAsset[];
};

enum TokenLists {
HighLiquidity = 'highLiquidityAssets',
LowLiquidity = 'lowLiquidityAssets',
Expand All @@ -55,6 +71,7 @@ const MAX_UNVERIFIED_RESULTS = 6;
const MAX_CROSSCHAIN_RESULTS = 3;

const NO_RESULTS: VerifiedTokenData = { bridgeAsset: null, crosschainResults: [], verifiedAssets: [], unverifiedAssets: [] };
const NO_DISCOVER_RESULTS: DiscoverSearchResults = { verifiedAssets: [], highLiquidityAssets: [], lowLiquidityAssets: [] };

export const useSwapsSearchStore = createRainbowStore<SearchQueryState>(() => ({ searchQuery: '' }));

Expand All @@ -77,6 +94,120 @@ export const useTokenSearchStore = createQueryStore<VerifiedTokenData, TokenSear
{ persistThrottleMs: time.seconds(8), storageKey: 'verifiedTokenSearch' }
);

export const useDiscoverSearchStore = createQueryStore<DiscoverSearchResults, DiscoverSearchParams>(
{
fetcher: (params, abortController) => discoverSearchQueryFunction(params, abortController),
cacheTime: params => (params.query?.length ? time.seconds(15) : time.hours(1)),
disableAutoRefetching: true,
keepPreviousData: true,
params: {
query: $ => $(useSwapsSearchStore, state => (state.searchQuery.trim().length ? state.searchQuery.trim() : '')),
},
staleTime: time.minutes(2),
},

() => ({ bridgeAsset: null }),

{ persistThrottleMs: time.seconds(8), storageKey: 'discoverTokenSearch' }
);

const sortForDefaultList = (tokens: SearchAsset[]) => {
const curated = tokens.filter(asset => asset.highLiquidity && asset.isRainbowCurated && asset.icon_url);
return curated.sort((a, b) => (b.market?.market_cap?.value || 0) - (a.market?.market_cap?.value || 0));
};

const sortTokensByRelevance = (tokens: SearchAsset[], query: string): SearchItemWithRelevance[] => {
const normalizedQuery = query.toLowerCase().trim();
const tokenWithRelevance: SearchItemWithRelevance[] = tokens.map(token => {
const normalizedTokenName = token.name.toLowerCase();

const normalizedTokenSymbol = token.symbol.toLowerCase();
const tokenNameWords = normalizedTokenName.split(' ');
const relevance = getTokenRelevance({
token,
normalizedTokenName,
normalizedQuery,
normalizedTokenSymbol,
tokenNameWords,
});
return { ...token, relevance };
});

return tokenWithRelevance.sort((a, b) => b.relevance - a.relevance);
};

// higher number indicates higher relevance
const getTokenRelevance = ({
token,
normalizedTokenName,
normalizedQuery,
normalizedTokenSymbol,
tokenNameWords,
}: {
token: SearchAsset;
normalizedTokenName: string;
normalizedQuery: string;
normalizedTokenSymbol?: string;
tokenNameWords: string[];
}) => {
// High relevance: Leading word in token name starts with query or exact match on symbol
if (normalizedTokenName.startsWith(normalizedQuery) || (normalizedTokenSymbol && normalizedTokenSymbol === normalizedQuery)) {
return 5;
}

// Medium relevance: Non-leading word in token name starts with query
if (tokenNameWords.some((word, index) => index !== 0 && word.startsWith(normalizedQuery))) {
return 4;
}

// Low relevance: Token name contains query
if (tokenNameWords.some(word => word.includes(normalizedQuery))) {
return 3;
}

return 0;
};

const selectTopDiscoverSearchResults = ({
abortController,
searchQuery,
data,
}: {
abortController: AbortController | null;
searchQuery: string;
data: SearchAsset[];
}): DiscoverSearchResults => {
if (abortController?.signal.aborted) return NO_DISCOVER_RESULTS;
const results = data.filter(asset => {
const hasIcon = asset.icon_url;
const isMatch = hasIcon || searchQuery.length > 2;

if (!isMatch) {
const crosschainMatch = getExactMatches([asset], searchQuery);
return crosschainMatch.length > 0;
}

return isMatch;
});
if (abortController?.signal.aborted) return NO_DISCOVER_RESULTS;
const topResults = searchQuery === '' ? sortForDefaultList(results) : sortTokensByRelevance(results, searchQuery);
const { verifiedAssets, highLiquidityAssets, lowLiquidityAssets } = groupBy(topResults, searchResult => {
if (searchResult.isVerified) {
return 'verifiedAssets';
} else if (!searchResult.isVerified && searchResult.highLiquidity) {
return 'highLiquidityAssets';
} else {
return 'lowLiquidityAssets';
}
});

return {
verifiedAssets,
highLiquidityAssets,
lowLiquidityAssets,
};
};

function selectTopSearchResults({
abortController,
data,
Expand Down Expand Up @@ -201,6 +332,42 @@ const getImportedAsset = async (searchQuery: string, chainId: number = ChainId.m
return [];
};

export async function discoverSearchQueryFunction(
{ query }: DiscoverSearchParams,
abortController: AbortController | null
): Promise<DiscoverSearchResults> {
const queryParams: DiscoverSearchParams = {
query,
};

const isAddressSearch = query && isAddress(query);

const searchDefaultVerifiedList = query === '';
if (searchDefaultVerifiedList) {
queryParams.list = 'verifiedAssets';
}

const url = `${searchDefaultVerifiedList ? `/${ChainId.mainnet}` : ''}/?${qs.stringify(queryParams)}`;

try {
const tokenSearch = await tokenSearchClient.get<{ data: SearchAsset[] }>(url);

if (isAddressSearch && (tokenSearch?.data?.data?.length || 0) === 0) {
const result = await getImportedAsset(query);
return {
verifiedAssets: [],
highLiquidityAssets: [],
lowLiquidityAssets: result,
};
}

return selectTopDiscoverSearchResults({ abortController, data: parseTokenSearchResults(tokenSearch.data.data), searchQuery: query });
} catch (e) {
logger.error(new RainbowError('[tokenSearchQueryFunction]: Token search failed'), { url });
return NO_DISCOVER_RESULTS;
}
}

export async function tokenSearchQueryFunction(
{ chainId, query }: TokenSearchParams,
abortController: AbortController | null
Expand Down
122 changes: 7 additions & 115 deletions src/hooks/useSearchCurrencyList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import lang from 'i18n-js';
import { rankings } from 'match-sorter';
import { groupBy } from 'lodash';
import { useCallback, useMemo } from 'react';
import { useTheme } from '../theme/ThemeContext';
import { addHexPrefix } from '@/handlers/web3';
Expand All @@ -10,91 +9,18 @@ import { IS_TEST } from '@/env';
import { useFavorites } from '@/resources/favorites';
import { ChainId } from '@/state/backendNetworks/types';
import { getUniqueId } from '@/utils/ethereumUtils';
import { TokenSearchResult, useTokenSearchAllNetworks } from '@/__swaps__/screens/Swap/resources/search/search';
import { useSwapsSearchStore, useDiscoverSearchStore } from '@/__swaps__/screens/Swap/resources/search/searchV2';
import { SearchAsset, TokenSearchAssetKey, TokenSearchThreshold } from '@/__swaps__/types/search';
import { isAddress } from '@ethersproject/address';

const MAX_VERIFIED_RESULTS = 48;

const getExactMatches = (data: TokenSearchResult, query: string) => {
const isQueryAddress = isAddress(query.trim());
return data.filter(asset => {
if (isQueryAddress) {
return !!(asset.address?.toLowerCase() === query.trim().toLowerCase());
}
const symbolMatch = isLowerCaseMatch(asset.symbol, query);
const nameMatch = isLowerCaseMatch(asset.name, query);
return symbolMatch || nameMatch;
});
};

const abcSort = (list: any[], key?: string) => {
return list.sort((a, b) => {
return key ? a[key]?.localeCompare(b[key]) : a?.localeCompare(b);
});
};

type SearchItemWithRelevance = SearchAsset & {
relevance: number;
};

const sortForDefaultList = (tokens: SearchAsset[]) => {
const curated = tokens.filter(asset => asset.highLiquidity && asset.isRainbowCurated && asset.icon_url);
return curated.sort((a, b) => (b.market?.market_cap?.value || 0) - (a.market?.market_cap?.value || 0));
};

const sortTokensByRelevance = (tokens: SearchAsset[], query: string): SearchItemWithRelevance[] => {
const normalizedQuery = query.toLowerCase().trim();
const tokenWithRelevance: SearchItemWithRelevance[] = tokens.map(token => {
const normalizedTokenName = token.name.toLowerCase();

const normalizedTokenSymbol = token.symbol.toLowerCase();
const tokenNameWords = normalizedTokenName.split(' ');
const relevance = getTokenRelevance({
token,
normalizedTokenName,
normalizedQuery,
normalizedTokenSymbol,
tokenNameWords,
});
return { ...token, relevance };
});

return tokenWithRelevance.sort((a, b) => b.relevance - a.relevance);
};

// higher number indicates higher relevance
const getTokenRelevance = ({
token,
normalizedTokenName,
normalizedQuery,
normalizedTokenSymbol,
tokenNameWords,
}: {
token: SearchAsset;
normalizedTokenName: string;
normalizedQuery: string;
normalizedTokenSymbol?: string;
tokenNameWords: string[];
}) => {
// High relevance: Leading word in token name starts with query or exact match on symbol
if (normalizedTokenName.startsWith(normalizedQuery) || (normalizedTokenSymbol && normalizedTokenSymbol === normalizedQuery)) {
return 5;
}

// Medium relevance: Non-leading word in token name starts with query
if (tokenNameWords.some((word, index) => index !== 0 && word.startsWith(normalizedQuery))) {
return 4;
}

// Low relevance: Token name contains query
if (tokenNameWords.some(word => word.includes(normalizedQuery))) {
return 3;
}

return 0;
};

const useSearchCurrencyList = (searchQuery: string) => {
const searching = useMemo(() => searchQuery !== '', [searchQuery]);

Expand Down Expand Up @@ -141,49 +67,15 @@ const useSearchCurrencyList = (searchQuery: string) => {

const { colors } = useTheme();

const selectTopSearchResults = useCallback(
(data: TokenSearchResult) => {
const results = data.filter(asset => {
const isFavorite = favoriteAddresses.map(a => a?.toLowerCase()).includes(asset.uniqueId?.toLowerCase());
if (isFavorite) return false;

const hasIcon = asset.icon_url;
const isMatch = hasIcon || searchQuery.length > 2;

if (!isMatch) {
const crosschainMatch = getExactMatches([asset], searchQuery);
return crosschainMatch.length > 0;
}

return isMatch;
});
const topResults = searchQuery === '' ? sortForDefaultList(results) : sortTokensByRelevance(results, searchQuery);
return topResults.slice(0, MAX_VERIFIED_RESULTS);
},
[searchQuery, favoriteAddresses]
);

const { data: searchResultAssets, isFetching: loading } = useTokenSearchAllNetworks(
{
query: searchQuery,
},
{
select: selectTopSearchResults,
staleTime: 10 * 60 * 1_000, // 10 min
}
);
// TODO JIN searchQuery, loading
const searchResultAssets = useDiscoverSearchStore(state => state.getData());

// TODO JIN - need to filter out favorites from these results and then slice
const currencyList = useMemo(() => {
const list = [];
const { verifiedAssets, highLiquidityAssets, lowLiquidityAssets } = groupBy(searchResultAssets, searchResult => {
if (searchResult.isVerified) {
return 'verifiedAssets';
} else if (!searchResult.isVerified && searchResult.highLiquidity) {
return 'highLiquidityAssets';
} else {
return 'lowLiquidityAssets';
}
});
const verifiedAssets = searchResultAssets?.verifiedAssets || [];
const highLiquidityAssets = searchResultAssets?.highLiquidityAssets || [];
const lowLiquidityAssets = searchResultAssets?.lowLiquidityAssets || [];

if (searching) {
if (favoriteAssets?.length) {
Expand Down

0 comments on commit 2b83d50

Please sign in to comment.