Skip to content

Commit

Permalink
initial useSwapAssets work
Browse files Browse the repository at this point in the history
  • Loading branch information
walmat committed Mar 22, 2024
1 parent c0f41c7 commit 52e52cc
Show file tree
Hide file tree
Showing 25 changed files with 1,106 additions and 3 deletions.
Binary file added src/__swaps__/assets/aggregators/0x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/__swaps__/assets/aggregators/1inch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/__swaps__/assets/aggregators/rainbow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/__swaps__/screens/Swap/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';

export function useDebounce<T>(value: T, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
11 changes: 11 additions & 0 deletions src/__swaps__/screens/Swap/hooks/usePrevious.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';

export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();

useEffect(() => {
ref.current = value;
}, [value]);

return ref.current;
}
167 changes: 167 additions & 0 deletions src/__swaps__/screens/Swap/hooks/useSwapAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { useCallback, useMemo, useState } from 'react';
import { Hex } from 'viem';

import { selectUserAssetsList, selectUserAssetsListByChainId } from '../resources/_selectors/assets';

import { useUserAssets, useAssets } from '@/__swaps__/screens/Swap/resources/assets';
import { ParsedAsset, ParsedAssetsDictByChain, ParsedSearchAsset } from '@/__swaps__/screens/Swap/types/assets';
import { ChainId } from '@/__swaps__/screens/Swap/types/chains';
import { SearchAsset } from '@/__swaps__/screens/Swap/types/search';
import { parseSearchAsset } from '@/__swaps__/screens/Swap/utils/assets';
import { isLowerCaseMatch } from '@/__swaps__/screens/Swap/utils/strings';
import type { SortMethod } from '@/__swaps__/screens/Swap/types/swap';
import { useDebounce } from '@/__swaps__/screens/Swap/hooks/useDebounce';
import { usePrevious } from '@/__swaps__/screens/Swap/hooks/usePrevious';
// import { useSearchCurrencyLists } from '../useSearchCurrencyLists';
import { useAccountSettings } from '@/hooks';

const sortBy = (by: SortMethod) => {
switch (by) {
case 'token':
return selectUserAssetsList;
case 'chain':
return selectUserAssetsListByChainId;
}
};

export const isSameAsset = (a1: Pick<ParsedAsset, 'chainId' | 'address'>, a2: Pick<ParsedAsset, 'chainId' | 'address'>) =>
+a1.chainId === +a2.chainId && isLowerCaseMatch(a1.address, a2.address);

const isSameAssetInDiffChains = (a1?: Pick<ParsedAsset, 'address' | 'networks'> | null, a2?: Pick<ParsedAsset, 'address'> | null) => {
if (!a1?.networks || !a2) return false;
return Object.values(a1.networks).some(assetInNetwork => assetInNetwork?.address === a2.address);
};

export const useSwapAssets = ({ bridge }: { bridge: boolean }) => {
const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings();

const [assetToSell, setAssetToSellState] = useState<ParsedSearchAsset | SearchAsset | null>(null);
const [assetToBuy, setAssetToBuyState] = useState<ParsedSearchAsset | SearchAsset | null>(null);

const prevAssetToSell = usePrevious<ParsedSearchAsset | SearchAsset | null>(assetToSell);

const [outputChainId, setOutputChainId] = useState(ChainId.mainnet);

const [sortMethod, setSortMethod] = useState<SortMethod>('token');

const [assetToSellFilter, setAssetToSellFilter] = useState('');
const [assetToBuyFilter, setAssetToBuyFilter] = useState('');

const debouncedAssetToSellFilter = useDebounce(assetToSellFilter, 200);
const debouncedAssetToBuyFilter = useDebounce(assetToBuyFilter, 200);

const { data: userAssets = [] } = useUserAssets(
{
address: currentAddress as Hex,
currency: currentCurrency,
},
{
select: data => {
const filteredAssetsDictByChain = Object.keys(data).reduce((acc, key) => {
const chainKey = Number(key);
acc[chainKey] = data[chainKey];
return acc;
}, {} as ParsedAssetsDictByChain);
return sortBy(sortMethod)(filteredAssetsDictByChain);
},
}
);

const filteredAssetsToSell = useMemo(() => {
return debouncedAssetToSellFilter
? userAssets.filter(({ name, symbol, address }) =>
[name, symbol, address].reduce(
(res, param) => res || param.toLowerCase().startsWith(debouncedAssetToSellFilter.toLowerCase()),
false
)
)
: userAssets;
}, [debouncedAssetToSellFilter, userAssets]) as ParsedSearchAsset[];

// const { results: searchAssetsToBuySections } = useSearchCurrencyLists({
// inputChainId: assetToSell?.chainId,
// outputChainId,
// assetToSell,
// searchQuery: debouncedAssetToBuyFilter,
// bridge,
// });

const { data: buyPriceData = [] } = useAssets({
assetAddresses: assetToBuy ? [assetToBuy?.address] : [],
chainId: outputChainId,
currency: currentCurrency,
});

const { data: sellPriceData = [] } = useAssets({
assetAddresses: assetToSell ? [assetToSell?.address] : [],
chainId: outputChainId,
currency: currentCurrency,
});

const assetToBuyWithPrice = useMemo(
() => Object.values(buyPriceData || {})?.find(asset => asset.uniqueId === assetToBuy?.uniqueId),
[assetToBuy, buyPriceData]
);

const assetToSellWithPrice = useMemo(
() => Object.values(sellPriceData || {})?.find(asset => asset.uniqueId === assetToBuy?.uniqueId),
[assetToBuy, sellPriceData]
);

const parsedAssetToBuy = useMemo(() => {
if (!assetToBuy) return null;
const userAsset = userAssets.find(userAsset => isSameAsset(userAsset, assetToBuy));
return parseSearchAsset({
assetWithPrice: assetToBuyWithPrice,
searchAsset: assetToBuy,
userAsset,
});
}, [assetToBuy, assetToBuyWithPrice, userAssets]);

const parsedAssetToSell = useMemo(() => {
if (!assetToSell) return null;
const userAsset = userAssets.find(userAsset => isSameAsset(userAsset, assetToSell));
return parseSearchAsset({
assetWithPrice: assetToSellWithPrice,
searchAsset: assetToSell,
userAsset,
});
}, [assetToSell, assetToSellWithPrice, userAssets]);

const setAssetToBuy = useCallback((asset: ParsedSearchAsset | null) => {
setAssetToBuyState(asset);
}, []);

const setAssetToSell = useCallback(
(asset: ParsedSearchAsset | null) => {
if (assetToBuy && asset && assetToBuy?.address === asset?.address && assetToBuy?.chainId === asset?.chainId) {
setAssetToBuyState(prevAssetToSell === undefined ? null : prevAssetToSell);
}
// if it's in bridge mode, the asset to sell changes, and it's not the same asset in different chains,
// we clear the asset to buy (because that would be a crosschain swap)
if (bridge && !isSameAssetInDiffChains(asset, assetToBuy)) {
setAssetToBuyState(null);
}
setAssetToSellState(asset);
asset?.chainId && setOutputChainId(asset?.chainId);
},
[assetToBuy, prevAssetToSell, bridge]
);

return {
assetsToSell: filteredAssetsToSell,
assetToSellFilter,
// assetsToBuy: searchAssetsToBuySections,
assetToBuyFilter,
sortMethod,
assetToSell: parsedAssetToSell,
assetToBuy: parsedAssetToBuy,
outputChainId: bridge ? undefined : outputChainId,
setSortMethod,
setAssetToSell,
setAssetToBuy,
setOutputChainId: bridge ? undefined : setOutputChainId,
setAssetToSellFilter,
setAssetToBuyFilter,
};
};
74 changes: 74 additions & 0 deletions src/__swaps__/screens/Swap/resources/_selectors/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ParsedAssetsDict, ParsedAssetsDictByChain, ParsedUserAsset, UniqueId } from '../../types/assets';
import { ChainId } from '../../types/chains';
import { deriveAddressAndChainWithUniqueId } from '../../utils/address';
import { add } from '../../utils/numbers';

// selectors
export function selectUserAssetsList(assets: ParsedAssetsDictByChain) {
return Object.values(assets)
.map(chainAssets => Object.values(chainAssets))
.flat()
.sort((a: ParsedUserAsset, b: ParsedUserAsset) => parseFloat(b?.native?.balance?.amount) - parseFloat(a?.native?.balance?.amount));
}

export function selectUserAssetsFilteringSmallBalancesList(assets: ParsedAssetsDictByChain) {
return selectUserAssetsList(assets).filter(a => !a.smallBalance);
}

export function selectUserAssetsDictByChain(assets: ParsedAssetsDictByChain) {
return assets;
}

export function selectUserAssetsListByChainId(assets: ParsedAssetsDictByChain) {
const assetsByNetwork = [
assets?.[ChainId.mainnet],
assets?.[ChainId.optimism],
assets?.[ChainId.polygon],
assets?.[ChainId.arbitrum],
assets?.[ChainId.base],
assets?.[ChainId.zora],
assets?.[ChainId.bsc],
assets?.[ChainId.avalanche],
].flat();
return assetsByNetwork
.map(chainAssets =>
Object.values(chainAssets).sort(
(a: ParsedUserAsset, b: ParsedUserAsset) => parseFloat(b?.native?.balance?.amount) - parseFloat(a?.native?.balance?.amount)
)
)
.flat();
}

export function selectUserAssetAddressMapByChainId(assets: ParsedAssetsDictByChain) {
const mapAddresses = (list: ParsedAssetsDict = {}) => Object.values(list).map(i => i.address);
return {
[ChainId.mainnet]: mapAddresses(assets[ChainId.mainnet]) || [],
[ChainId.optimism]: mapAddresses(assets[ChainId.optimism]) || [],
[ChainId.bsc]: mapAddresses(assets[ChainId.bsc]) || [],
[ChainId.polygon]: mapAddresses(assets[ChainId.polygon]) || [],
[ChainId.arbitrum]: mapAddresses(assets[ChainId.arbitrum]) || [],
[ChainId.base]: mapAddresses(assets[ChainId.base]) || [],
[ChainId.zora]: mapAddresses(assets[ChainId.zora]) || [],
[ChainId.avalanche]: mapAddresses(assets[ChainId.avalanche]) || [],
};
}

// selector generators
export function selectUserAssetWithUniqueId(uniqueId: UniqueId) {
return (assets: ParsedAssetsDictByChain) => {
const { chain } = deriveAddressAndChainWithUniqueId(uniqueId);
return assets?.[chain]?.[uniqueId];
};
}

export function selectUserAssetsBalance(assets: ParsedAssetsDictByChain) {
const networksTotalBalance = Object.values(assets).map(assetsOnject => {
const assetsNetwork = Object.values(assetsOnject);
const networkBalance = assetsNetwork
.map(asset => asset.native.balance.amount)
.reduce((prevBalance, currBalance) => add(prevBalance, currBalance), '0');
return networkBalance;
});
const totalAssetsBalance = networksTotalBalance.reduce((prevBalance, currBalance) => add(prevBalance, currBalance), '0');
return totalAssetsBalance;
}
103 changes: 103 additions & 0 deletions src/__swaps__/screens/Swap/resources/assets/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useQuery } from '@tanstack/react-query';

import { requestMetadata } from '@/graphql';
import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query';
import { SupportedCurrencyKey } from '@/references';
import { AddressOrEth, AssetMetadata, ParsedAsset, UniqueId } from '../../types/assets';
import { ChainId } from '../../types/chains';
import { chunkArray, createAssetQuery, parseAssetMetadata } from '../../utils/assets';
import { RainbowError, logger } from '@/logger';

export const ASSETS_TIMEOUT_DURATION = 10000;
const ASSETS_REFETCH_INTERVAL = 60000;

// ///////////////////////////////////////////////
// Query Types

export type AssetsQueryArgs = {
assetAddresses: AddressOrEth[];
chainId: ChainId;
currency: SupportedCurrencyKey;
};

// ///////////////////////////////////////////////
// Query Key

const assetsQueryKey = ({ assetAddresses, chainId, currency }: AssetsQueryArgs) =>
createQueryKey('assets', { assetAddresses, chainId, currency }, { persisterVersion: 2 });

type AssetsQueryKey = ReturnType<typeof assetsQueryKey>;

// ///////////////////////////////////////////////
// Query Function

export async function assetsQueryFunction({
queryKey: [{ assetAddresses, chainId, currency }],
}: QueryFunctionArgs<typeof assetsQueryKey>): Promise<{
[key: UniqueId]: ParsedAsset;
}> {
try {
if (!assetAddresses || !assetAddresses.length) return {};
const batches = chunkArray([...assetAddresses], 10); // chunking because a full batch would throw 413
const batchResults = batches.map(batchedQuery =>
requestMetadata(createAssetQuery(batchedQuery, chainId, currency, true), {
timeout: ASSETS_TIMEOUT_DURATION,
})
) as Promise<Record<string, AssetMetadata>[]>[];
const results = (await Promise.all(batchResults))
.flat()
.map(r => Object.values(r))
.flat();
const parsedAssets = parseAssets(results, chainId, currency);
return parsedAssets;
} catch (e) {
logger.error(new RainbowError('assetsQueryFunction: '), {
message: (e as Error)?.message,
});
return {};
}
}

type AssetsQueryResult = QueryFunctionResult<typeof assetsQueryFunction>;

// ///////////////////////////////////////////////
// Query Fetcher

export async function fetchAssets(
{ assetAddresses, chainId, currency }: AssetsQueryArgs,
config: QueryConfigWithSelect<AssetsQueryResult, Error, AssetsQueryResult, AssetsQueryKey> = {}
) {
return await queryClient.fetchQuery(assetsQueryKey({ assetAddresses, chainId, currency }), assetsQueryFunction, config);
}

function parseAssets(assets: AssetMetadata[], chainId: ChainId, currency: SupportedCurrencyKey) {
return assets.reduce(
(assetsDict, asset) => {
const address = asset.networks?.[chainId]?.address;
if (address) {
const parsedAsset = parseAssetMetadata({
address,
asset,
chainId,
currency,
});
assetsDict[parsedAsset?.uniqueId] = parsedAsset;
}
return assetsDict;
},
{} as Record<UniqueId, ParsedAsset>
);
}

// ///////////////////////////////////////////////
// Query Hook

export function useAssets<TSelectData = AssetsQueryResult>(
{ assetAddresses, chainId, currency }: AssetsQueryArgs,
config: QueryConfigWithSelect<AssetsQueryResult, Error, TSelectData, AssetsQueryKey> = {}
) {
return useQuery(assetsQueryKey({ assetAddresses, chainId, currency }), assetsQueryFunction, {
...config,
refetchInterval: ASSETS_REFETCH_INTERVAL,
});
}
5 changes: 5 additions & 0 deletions src/__swaps__/screens/Swap/resources/assets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { useAssets } from './assets';
export { useUserAssets } from './userAssets';
export type { UserAssetsArgs } from './userAssets';
export { useUserAssetsByChain } from './userAssetsByChain';
export type { UserAssetsByChainArgs } from './userAssetsByChain';
Loading

0 comments on commit 52e52cc

Please sign in to comment.