-
Notifications
You must be signed in to change notification settings - Fork 637
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
1,106 additions
and
3 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
Oops, something went wrong.