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

Reintroduce support for crosschain search results in swaps search #6440

Merged
Merged
32 changes: 15 additions & 17 deletions src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
NAME_SYMBOL_SEARCH_KEYS,
useSwapsSearchStore,
useTokenSearchStore,
useUnverifiedTokenSearchStore,
} from '@/__swaps__/screens/Swap/resources/search/searchV2';
import { AssetToBuySectionId, SearchAsset, TokenToBuyListItem } from '@/__swaps__/types/search';
import { RecentSwap } from '@/__swaps__/types/swap';
Expand All @@ -25,8 +24,7 @@ const MAX_POPULAR_RESULTS = 3;

export function useSearchCurrencyLists() {
const lastTrackedTimeRef = useRef<number | null>(null);
const verifiedAssets = useTokenSearchStore(state => state.getData());
const unverifiedAssets = useUnverifiedTokenSearchStore(state => state.getData());
const searchResults = useTokenSearchStore(state => state.getData());
const popularAssets = usePopularTokensStore(state => state.getData());
const { favoritesMetadata: favorites } = useFavorites();

Expand Down Expand Up @@ -58,28 +56,28 @@ export function useSearchCurrencyLists() {
}, [favorites, toChainId]);

const filteredBridgeAsset = useDeepCompareMemo(() => {
if (!verifiedAssets?.bridgeAsset) return null;
if (!searchResults?.bridgeAsset) return null;

const inputAssetBridgedToSelectedChainAddress = useSwapsStore.getState().inputAsset?.networks?.[toChainId]?.address;

const shouldShowBridgeResult =
isCrosschainSearch &&
inputAssetBridgedToSelectedChainAddress &&
inputAssetBridgedToSelectedChainAddress === verifiedAssets?.bridgeAsset?.networks?.[toChainId]?.address &&
filterBridgeAsset({ asset: verifiedAssets?.bridgeAsset, filter: query });
inputAssetBridgedToSelectedChainAddress === searchResults?.bridgeAsset?.networks?.[toChainId]?.address &&
filterBridgeAsset({ asset: searchResults?.bridgeAsset, filter: query });

return shouldShowBridgeResult && verifiedAssets.bridgeAsset
return shouldShowBridgeResult && searchResults.bridgeAsset
? {
...verifiedAssets.bridgeAsset,
...searchResults.bridgeAsset,
chainId: toChainId,
favorite: unfilteredFavorites.some(
fav =>
fav.networks?.[toChainId]?.address ===
(verifiedAssets?.bridgeAsset?.networks?.[toChainId]?.address || inputAssetBridgedToSelectedChainAddress)
(searchResults?.bridgeAsset?.networks?.[toChainId]?.address || inputAssetBridgedToSelectedChainAddress)
),
}
: null;
}, [isCrosschainSearch, query, toChainId, unfilteredFavorites, verifiedAssets?.bridgeAsset]);
}, [isCrosschainSearch, query, toChainId, unfilteredFavorites, searchResults?.bridgeAsset]);

const favoritesList = useDeepCompareMemo(() => {
if (query === '') return unfilteredFavorites;
Expand Down Expand Up @@ -110,11 +108,11 @@ export function useSearchCurrencyLists() {
results: buildListSectionsData({
combinedData: {
bridgeAsset: filteredBridgeAsset,
crosschainExactMatches: verifiedAssets?.crosschainResults,
crosschainExactMatches: searchResults?.crosschainResults,
popularAssets: popularAssetsForChain,
recentSwaps: recentsForChain,
unverifiedAssets: unverifiedAssets,
verifiedAssets: verifiedAssets?.results,
unverifiedAssets: searchResults?.unverifiedAssets,
verifiedAssets: searchResults?.verifiedAssets,
},
favoritesList,
filteredBridgeAssetAddress: filteredBridgeAsset?.address,
Expand All @@ -125,9 +123,9 @@ export function useSearchCurrencyLists() {
filteredBridgeAsset,
popularAssetsForChain,
recentsForChain,
unverifiedAssets,
verifiedAssets?.crosschainResults,
verifiedAssets?.results,
searchResults?.crosschainResults,
searchResults?.verifiedAssets,
searchResults?.unverifiedAssets,
]);

useEffect(() => {
Expand Down Expand Up @@ -255,7 +253,7 @@ const buildListSectionsData = ({
combinedData: {
bridgeAsset: SearchAsset | null;
verifiedAssets: SearchAsset[] | undefined;
unverifiedAssets: SearchAsset[] | null;
unverifiedAssets: SearchAsset[] | undefined;
crosschainExactMatches: SearchAsset[] | undefined;
recentSwaps: RecentSwap[] | undefined;
popularAssets: SearchAsset[] | undefined;
Expand Down
139 changes: 35 additions & 104 deletions src/__swaps__/screens/Swap/resources/search/searchV2.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { isAddress } from '@ethersproject/address';
import qs from 'qs';
import { RainbowError, logger } from '@/logger';
import { RainbowFetchClient } from '@/rainbow-fetch';
import { ChainId } from '@/state/backendNetworks/types';
import { useSwapsStore } from '@/state/swaps/swapsStore';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { createQueryStore } from '@/state/internal/createQueryStore';
import { SearchAsset, TokenSearchAssetKey, TokenSearchThreshold } from '@/__swaps__/types/search';
import { SearchAsset, TokenSearchAssetKey } from '@/__swaps__/types/search';
import { time } from '@/utils';
import { parseTokenSearch } from './utils';
import { parseTokenSearchResults } from './utils';

const tokenSearchClient = new RainbowFetchClient({
baseURL: 'https://token-search.rainbow.me/v3/tokens',
Expand All @@ -19,13 +18,10 @@ const tokenSearchClient = new RainbowFetchClient({
timeout: time.seconds(15),
});

type TokenSearchParams<List extends TokenLists = TokenLists> = {
type TokenSearchParams = {
list?: string;
chainId: ChainId;
fromChainId?: ChainId;
keys: TokenSearchAssetKey[];
list: List;
query: string | undefined;
threshold: TokenSearchThreshold;
};

type TokenSearchState = {
Expand All @@ -39,7 +35,8 @@ type SearchQueryState = {
type VerifiedTokenData = {
bridgeAsset: SearchAsset | null;
crosschainResults: SearchAsset[];
results: SearchAsset[];
verifiedAssets: SearchAsset[];
unverifiedAssets: SearchAsset[];
};

enum TokenLists {
Expand All @@ -50,24 +47,22 @@ enum TokenLists {

const MAX_VERIFIED_RESULTS = 24;
const MAX_UNVERIFIED_RESULTS = 6;
const NO_RESULTS: SearchAsset[] = [];
const NO_VERIFIED_RESULTS: VerifiedTokenData = { bridgeAsset: null, crosschainResults: [], results: [] };
const MAX_CROSSCHAIN_RESULTS = 3;

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

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

export const useTokenSearchStore = createQueryStore<VerifiedTokenData, TokenSearchParams<TokenLists.Verified>, TokenSearchState>(
export const useTokenSearchStore = createQueryStore<VerifiedTokenData, TokenSearchParams, TokenSearchState>(
{
fetcher: (params, abortController) => tokenSearchQueryFunction(params, abortController),

cacheTime: params => (params.query?.length ? time.seconds(15) : time.hours(1)),
disableAutoRefetching: true,
keepPreviousData: true,
params: {
list: TokenLists.Verified,
chainId: $ => $(useSwapsStore).selectedOutputChainId,
keys: $ => $(useSwapsSearchStore, state => getSearchKeys(state.searchQuery.trim())),
query: $ => $(useSwapsSearchStore, state => (state.searchQuery.trim().length ? state.searchQuery.trim() : undefined)),
threshold: $ => $(useSwapsSearchStore, state => getSearchThreshold(state.searchQuery.trim())),
},
staleTime: time.minutes(2),
},
Expand All @@ -77,29 +72,6 @@ export const useTokenSearchStore = createQueryStore<VerifiedTokenData, TokenSear
{ persistThrottleMs: time.seconds(8), storageKey: 'verifiedTokenSearch' }
);

export const useUnverifiedTokenSearchStore = createQueryStore<SearchAsset[], TokenSearchParams<TokenLists.HighLiquidity>>(
{
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the removal of the "extra" call for assets on the unverified list - these results will now show up as part of the main call to tokenSearchQueryFunction

fetcher: (params, abortController) =>
(params.query?.length ?? 0) > 2 ? tokenSearchQueryFunction(params, abortController) : NO_RESULTS,
transform: (data, { query }) =>
query && isAddress(query) ? getExactMatches(data, query, MAX_UNVERIFIED_RESULTS) : data.slice(0, MAX_UNVERIFIED_RESULTS),

cacheTime: params => ((params.query?.length ?? 0) > 2 ? time.seconds(15) : time.zero),
disableAutoRefetching: true,
keepPreviousData: true,
params: {
list: TokenLists.HighLiquidity,
chainId: $ => $(useSwapsStore).selectedOutputChainId,
keys: $ => $(useSwapsSearchStore, state => getSearchKeys(state.searchQuery.trim())),
query: $ => $(useSwapsSearchStore, state => state.searchQuery.trim()),
threshold: $ => $(useSwapsSearchStore, state => getSearchThreshold(state.searchQuery.trim())),
},
staleTime: time.minutes(2),
},

{ persistThrottleMs: time.seconds(12), storageKey: 'unverifiedTokenSearch' }
);

function selectTopSearchResults({
abortController,
data,
Expand All @@ -113,7 +85,8 @@ function selectTopSearchResults({
}): VerifiedTokenData {
const normalizedQuery = query?.trim().toLowerCase();
const queryHasMultipleChars = !!(normalizedQuery && normalizedQuery.length > 1);
const currentChainResults: SearchAsset[] = [];
const currentChainVerifiedResults: SearchAsset[] = [];
const currentChainUnverifiedResults: SearchAsset[] = [];
const crosschainResults: SearchAsset[] = [];
let bridgeAsset: SearchAsset | null = null;

Expand All @@ -135,25 +108,31 @@ function selectTopSearchResults({

const isMatch = isCurrentNetwork && (!!asset.icon_url || queryHasMultipleChars);

if (isMatch) currentChainResults.push(asset);
else {
if (isMatch) {
if (asset.isVerified) {
currentChainVerifiedResults.push(asset);
} else {
currentChainUnverifiedResults.push(asset);
}
} else {
const isCrosschainMatch = (!isCurrentNetwork && queryHasMultipleChars && isExactMatch(asset, normalizedQuery)) || asset.isNativeAsset;
if (isCrosschainMatch) crosschainResults.push(asset);
}
}

if (abortController?.signal.aborted) return NO_VERIFIED_RESULTS;
if (abortController?.signal.aborted) return NO_RESULTS;

currentChainResults.sort((a, b) => {
currentChainVerifiedResults.sort((a, b) => {
if (a.isNativeAsset !== b.isNativeAsset) return a.isNativeAsset ? -1 : 1;
if (a.highLiquidity !== b.highLiquidity) return a.highLiquidity ? -1 : 1;
return Object.keys(b.networks).length - Object.keys(a.networks).length;
});

return {
bridgeAsset,
crosschainResults: crosschainResults,
results: currentChainResults.slice(0, MAX_VERIFIED_RESULTS),
crosschainResults: crosschainResults.slice(0, MAX_CROSSCHAIN_RESULTS),
verifiedAssets: currentChainVerifiedResults.slice(0, MAX_VERIFIED_RESULTS),
unverifiedAssets: currentChainUnverifiedResults.slice(0, MAX_UNVERIFIED_RESULTS),
};
}

Expand All @@ -176,74 +155,26 @@ function getExactMatches(data: SearchAsset[], query: string, slice?: number): Se
export const ADDRESS_SEARCH_KEY: TokenSearchAssetKey[] = ['address'];
export const NAME_SYMBOL_SEARCH_KEYS: TokenSearchAssetKey[] = ['name', 'symbol'];

function getSearchKeys(query: string): TokenSearchAssetKey[] {
return isAddress(query) ? ADDRESS_SEARCH_KEY : NAME_SYMBOL_SEARCH_KEYS;
}

const CASE_SENSITIVE_EQUAL_THRESHOLD: TokenSearchThreshold = 'CASE_SENSITIVE_EQUAL';
const CONTAINS_THRESHOLD: TokenSearchThreshold = 'CONTAINS';

function getSearchThreshold(query: string): TokenSearchThreshold {
return isAddress(query) ? CASE_SENSITIVE_EQUAL_THRESHOLD : CONTAINS_THRESHOLD;
}

const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets';

/** Unverified token search */
async function tokenSearchQueryFunction(
{ chainId, fromChainId, keys, list, query, threshold }: TokenSearchParams<TokenLists.HighLiquidity>,
abortController: AbortController | null
): Promise<SearchAsset[]>;

/** Verified token search */
async function tokenSearchQueryFunction(
{ chainId, fromChainId, keys, list, query, threshold }: TokenSearchParams<TokenLists.Verified>,
abortController: AbortController | null
): Promise<VerifiedTokenData>;

async function tokenSearchQueryFunction(
{ chainId, fromChainId, keys, list, query, threshold }: TokenSearchParams,
{ chainId, query }: TokenSearchParams,
abortController: AbortController | null
): Promise<SearchAsset[] | VerifiedTokenData> {
const queryParams: Omit<TokenSearchParams, 'chainId' | 'keys'> & { keys: string } = {
fromChainId,
keys: keys?.join(','),
list,
): Promise<VerifiedTokenData> {
const queryParams: Omit<TokenSearchParams, 'chainId'> = {
query,
threshold,
};

const isAddressSearch = query && isAddress(query);
if (isAddressSearch) queryParams.keys = `networks.${chainId}.address`;
const searchDefaultVerifiedList = query === '';
if (searchDefaultVerifiedList) {
queryParams.list = 'verifiedAssets';
}

const url = `${chainId ? `/${chainId}` : ''}/?${qs.stringify(queryParams)}`;
const isSearchingVerifiedAssets = queryParams.list === 'verifiedAssets';
const url = `${searchDefaultVerifiedList ? `/${chainId}` : ''}/?${qs.stringify(queryParams)}`;

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

if (tokenSearch && tokenSearch.data.data.length > 0) {
return parseTokenSearch(tokenSearch.data.data, chainId);
}

// Search for token contract address on other chains
const allVerifiedTokens = await tokenSearchClient.get<{ data: SearchAsset[] }>(ALL_VERIFIED_TOKENS_PARAM);

const addressQuery = query.trim().toLowerCase();
const addressMatchesOnOtherChains = allVerifiedTokens.data.data.filter(a =>
Object.values(a.networks).some(n => n?.address === addressQuery)
);

return parseTokenSearch(addressMatchesOnOtherChains);
} else {
const tokenSearch = await tokenSearchClient.get<{ data: SearchAsset[] }>(url);
return list === TokenLists.Verified
? selectTopSearchResults({ abortController, data: parseTokenSearch(tokenSearch.data.data, chainId), query, toChainId: chainId })
: parseTokenSearch(tokenSearch.data.data, chainId);
}
const tokenSearch = await tokenSearchClient.get<{ data: SearchAsset[] }>(url);
return selectTopSearchResults({ abortController, data: parseTokenSearchResults(tokenSearch.data.data), query, toChainId: chainId });
} catch (e) {
logger.error(new RainbowError('[tokenSearchQueryFunction]: Token search failed'), { url });
return [];
return NO_RESULTS;
}
}
21 changes: 21 additions & 0 deletions src/__swaps__/screens/Swap/resources/search/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ import { SearchAsset } from '@/__swaps__/types/search';
import { Address } from 'viem';
import { isNativeAsset } from '@/handlers/assets';

export function parseTokenSearchResults(assets: SearchAsset[]): SearchAsset[] {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a new function here that will eventually replace / consolidate the other parsers in this file once I finish moving DiscoverSearch over to the new query store.

const results: SearchAsset[] = [];

for (const asset of assets) {
const assetNetworks = asset.networks;
const mainnetInfo = assetNetworks[ChainId.mainnet];
const address = asset.address;
const chainId = asset.chainId;
const uniqueId = `${address}_${chainId}`;

results.push({
...asset,
isNativeAsset: isNativeAsset(address, chainId),
mainnetAddress: mainnetInfo ? mainnetInfo.address : chainId === ChainId.mainnet ? address : ('' as Address),
uniqueId,
});
}

return results;
}

export function parseTokenSearch(assets: SearchAsset[], chainId?: ChainId): SearchAsset[] {
const results: SearchAsset[] = [];

Expand Down
Loading