diff --git a/apps/withdraw-uni-v2/src/app/page.tsx b/apps/withdraw-uni-v2/src/app/page.tsx index 82dccb6..b2f5736 100644 --- a/apps/withdraw-uni-v2/src/app/page.tsx +++ b/apps/withdraw-uni-v2/src/app/page.tsx @@ -17,12 +17,13 @@ import { PoolForm } from "#/components/PoolForm"; import { useFetchNewPoolCallback } from "#/hooks/useFetchNewPoolCallback"; import { useSelectedPool } from "#/hooks/useSelectedPool"; import { useUserPools } from "#/hooks/useUserPools"; +import { combineTokenLists } from "#/utils/combineTokenLists"; import { isChainIdSupported } from "#/utils/uniswapSupportedChains"; export default function Page() { const [isEditHookLoading, setIsEditHookLoading] = useState(true); const { context } = useIFrameContext(); - const { data: pools, isLoading: isLoadingPools } = useUserPools(); + const { data: pools, isLoading: isLoadingPools, mutate } = useUserPools(); const { setValue, control } = useFormContext(); const poolId = useWatch({ control, name: "poolId" }); @@ -41,11 +42,7 @@ export default function Page() { } }, [context?.account, context?.hookToEdit, setValue]); - const { - data: selectedPool, - isLoading: isLoadingSelectedPool, - mutate: mutateSelectedPool, - } = useSelectedPool(poolId); + const selectedPool = useSelectedPool(poolId); useEffect(() => { if (poolId) { @@ -67,11 +64,7 @@ export default function Page() { return Unsupported chain; } - if ( - isLoadingPools || - (isEditHookLoading && context.hookToEdit) || - isLoadingSelectedPool - ) { + if (isLoadingPools || (isEditHookLoading && context.hookToEdit)) { return (
@@ -84,7 +77,11 @@ export default function Page() { { setValue("poolId", pool.id); - mutateSelectedPool(pool); + const chainId = context.chainId; + const poolWithChainId = { ...pool, chainId }; + const poolsWithChainId = pools?.map((p) => ({ ...p, chainId })) || []; + + mutate(combineTokenLists([poolWithChainId], poolsWithChainId)); }} pools={pools || []} PoolItemInfo={PoolItemInfo} diff --git a/apps/withdraw-uni-v2/src/context/withdrawHookForm.tsx b/apps/withdraw-uni-v2/src/context/withdrawHookForm.tsx index d259502..3d2de24 100644 --- a/apps/withdraw-uni-v2/src/context/withdrawHookForm.tsx +++ b/apps/withdraw-uni-v2/src/context/withdrawHookForm.tsx @@ -30,7 +30,7 @@ export function WithdrawFormContextProvider({ const poolId = useWatch({ control, name: "poolId" }); - const { data: selectedPool } = useSelectedPool(poolId); + const selectedPool = useSelectedPool(poolId); const router = useRouter(); diff --git a/apps/withdraw-uni-v2/src/hooks/useFetchNewPoolCallback.ts b/apps/withdraw-uni-v2/src/hooks/useFetchNewPoolCallback.ts index 5d31969..2e8d148 100644 --- a/apps/withdraw-uni-v2/src/hooks/useFetchNewPoolCallback.ts +++ b/apps/withdraw-uni-v2/src/hooks/useFetchNewPoolCallback.ts @@ -1,6 +1,6 @@ import type { IPool } from "@bleu/cow-hooks-ui"; import { useIFrameContext } from "@bleu/cow-hooks-ui"; -import type { Address, PublicClient } from "viem"; +import { type Address, type PublicClient, formatUnits } from "viem"; import { getTokensInfo } from "#/utils/getTokensInfo"; import { readPairData } from "#/utils/readPairsData"; import { storeExtraTokens } from "#/utils/storage"; @@ -11,12 +11,14 @@ async function fetchNewPool({ poolAddress, client, balancesDiff, + saveOnStore, }: { chainId: number; account: string; poolAddress: Address; client: PublicClient; balancesDiff: Record; + saveOnStore: boolean; }): Promise { // Get lists of tokens const lpToken = await readPairData( @@ -32,13 +34,11 @@ async function fetchNewPool({ const userBalance0 = lpToken.userBalance .mul(lpToken.reserve0) .div(lpToken.totalSupply) - .mul(99) - .div(100); // 1% slippage + .toBigInt(); const userBalance1 = lpToken.userBalance .mul(lpToken.reserve1) .div(lpToken.totalSupply) - .mul(99) - .div(100); // 1% slippage; + .toBigInt(); const token0 = tokens.find((token) => token.address === lpToken.tokens[0]); const token1 = tokens.find((token) => token.address === lpToken.tokens[1]); @@ -48,30 +48,33 @@ async function fetchNewPool({ "Unexpected error in fetchNewPool: some of tokens are undefined", ); - try { - storeExtraTokens( - [ - { - chainId, - address: poolAddress, - name: `Uniswap V2 ${token0.symbol}/${token1.symbol}`, - symbol: lpToken.symbol, - decimals: 18, // UniV2 are always 18 decimals - extensions: { tokens: lpToken.tokens.join(",") }, - }, - ], - chainId, - account, - ); - } catch (e) { - console.error("Error caching new LP token:", e); + if (saveOnStore) { + try { + storeExtraTokens( + [ + { + chainId, + address: poolAddress, + name: `Uniswap V2 ${token0.symbol}/${token1.symbol}`, + symbol: lpToken.symbol, + decimals: 18, // UniV2 are always 18 decimals + extensions: { tokens: lpToken.tokens.join(",") }, + }, + ], + chainId, + account, + ); + } catch (e) { + console.error("Error caching new LP token:", e); + } } - const userBalanceUsd0 = - (token0.priceUsd * userBalance0.toNumber()) / 10 ** token0.decimals; + const userBalance0Number = Number(formatUnits(userBalance0, token0.decimals)); + const userBalance1Number = Number(formatUnits(userBalance1, token1.decimals)); + + const userBalanceUsd0 = token0.priceUsd * userBalance0Number; - const userBalanceUsd1 = - (token1.priceUsd * userBalance1.toNumber()) / 10 ** token1.decimals; + const userBalanceUsd1 = token1.priceUsd * userBalance1Number; return { id: lpToken.address as Address, @@ -109,7 +112,7 @@ async function fetchNewPool({ }; } -export function useFetchNewPoolCallback() { +export function useFetchNewPoolCallback(saveOnStore = true) { const { context, publicClient } = useIFrameContext(); //@ts-ignore const { account, chainId, balancesDiff } = context ?? { @@ -130,6 +133,7 @@ export function useFetchNewPoolCallback() { poolAddress, client: publicClient, balancesDiff: userBalancesDiff as Record, + saveOnStore, }); }; } diff --git a/apps/withdraw-uni-v2/src/hooks/usePools.ts b/apps/withdraw-uni-v2/src/hooks/usePools.ts index 9a4d84b..8926cc0 100644 --- a/apps/withdraw-uni-v2/src/hooks/usePools.ts +++ b/apps/withdraw-uni-v2/src/hooks/usePools.ts @@ -1,7 +1,7 @@ import type { IPool } from "@bleu/cow-hooks-ui"; import type { SupportedChainId } from "@cowprotocol/cow-sdk"; import useSWR from "swr"; -import type { Address, PublicClient } from "viem"; +import { type Address, type PublicClient, formatUnits } from "viem"; import { getLpTokensList } from "#/utils/getLpTokensList"; import { getTokensInfo } from "#/utils/getTokensInfo"; import { getTokensList } from "#/utils/getTokensList"; @@ -11,7 +11,6 @@ import { isChainIdSupported } from "#/utils/uniswapSupportedChains"; async function getUserPools( ownerAddress: string, chainId: SupportedChainId, - token: string, client: PublicClient, balancesDiff: Record, ): Promise { @@ -21,9 +20,7 @@ async function getUserPools( getTokensList(chainId), ]); - const lpTokens = allLpTokens.filter( - (lpToken) => lpToken.chainId === chainId && lpToken.tokens.includes(token), - ); + const lpTokens = allLpTokens.filter((lpToken) => lpToken.chainId === chainId); // Read possibly missing tokens on chain and add price Usd const tokens = await getTokensInfo( @@ -46,94 +43,83 @@ async function getUserPools( return { ...lpToken, ...lpTokensInfo[idx] }; }); - const userPools: IPool[] = lpTokensWithInfo - .map((lpToken) => { - const userBalance0 = lpToken.userBalance - .mul(lpToken.reserve0) - .div(lpToken.totalSupply) - .mul(99) - .div(100); // 1% slippage - const userBalance1 = lpToken.userBalance - .mul(lpToken.reserve1) - .div(lpToken.totalSupply) - .mul(99) - .div(100); // 1% slippage; - - const token0 = tokens.find( - (token) => token.address === lpToken.tokens[0], - ); - const token1 = tokens.find( - (token) => token.address === lpToken.tokens[1], - ); - - if (!token0 || !token1) return; - - const userBalanceUsd0 = - (token0.priceUsd * userBalance0.toNumber()) / 10 ** token0.decimals; - - const userBalanceUsd1 = - (token1.priceUsd * userBalance1.toNumber()) / 10 ** token1.decimals; - - return { - id: lpToken.address as Address, - chain: String(chainId), - decimals: 18, - symbol: lpToken.symbol, - address: lpToken.address as Address, - type: "Uniswap v2", - protocolVersion: 2 as const, - totalSupply: lpToken.totalSupply, - allTokens: [ - { - address: token0.address as Address, - symbol: token0.symbol, - decimals: token0.decimals, - userBalance: userBalance0, - userBalanceUsd: userBalanceUsd0, - reserve: lpToken.reserve0, - weight: 0.5, - }, - { - address: token1.address as Address, - symbol: token1.symbol, - decimals: token1.decimals, - userBalance: userBalance1, - userBalanceUsd: userBalanceUsd1, - reserve: lpToken.reserve1, - weight: 0.5, - }, - ], - userBalance: { - walletBalance: lpToken.userBalance, - walletBalanceUsd: userBalanceUsd0 + userBalanceUsd1, + const allPools: (IPool | undefined)[] = lpTokensWithInfo.map((lpToken) => { + const userBalance0 = lpToken.userBalance + .mul(lpToken.reserve0) + .div(lpToken.totalSupply) + .toBigInt(); + const userBalance1 = lpToken.userBalance + .mul(lpToken.reserve1) + .div(lpToken.totalSupply) + .toBigInt(); + + const token0 = tokens.find((token) => token.address === lpToken.tokens[0]); + const token1 = tokens.find((token) => token.address === lpToken.tokens[1]); + + if (!token0 || !token1) return; + + const userBalance0Number = Number( + formatUnits(userBalance0, token0.decimals), + ); + const userBalance1Number = Number( + formatUnits(userBalance1, token1.decimals), + ); + + const userBalanceUsd0 = token0.priceUsd * userBalance0Number; + + const userBalanceUsd1 = token1.priceUsd * userBalance1Number; + + return { + id: lpToken.address as Address, + chain: String(chainId), + decimals: 18, + symbol: lpToken.symbol, + address: lpToken.address as Address, + type: "Uniswap v2", + protocolVersion: 2 as const, + totalSupply: lpToken.totalSupply, + allTokens: [ + { + address: token0.address as Address, + symbol: token0.symbol, + decimals: token0.decimals, + userBalance: userBalance0, + userBalanceUsd: userBalanceUsd0, + reserve: lpToken.reserve0, + weight: 0.5, }, - }; - }) + { + address: token1.address as Address, + symbol: token1.symbol, + decimals: token1.decimals, + userBalance: userBalance1, + userBalanceUsd: userBalanceUsd1, + reserve: lpToken.reserve1, + weight: 0.5, + }, + ], + userBalance: { + walletBalance: lpToken.userBalance, + walletBalanceUsd: userBalanceUsd0 + userBalanceUsd1, + }, + }; + }); + + return allPools .filter((pool) => pool !== undefined) .filter((pool) => pool.userBalance.walletBalance.toString() !== "0"); - - return userPools; } export function usePools( ownerAddress: string | undefined, chainId: SupportedChainId | undefined, - token: string | undefined, client: PublicClient | undefined, balancesDiff: Record>, ) { return useSWR( - [ownerAddress, chainId, token, client, balancesDiff], - async ([ownerAddress, chainId, token, client, balancesDiff]): Promise< - IPool[] - > => { - if ( - !ownerAddress || - !chainId || - !token || - !client || - balancesDiff === undefined - ) + [ownerAddress, chainId, client, balancesDiff], + async ([ownerAddress, chainId, client, balancesDiff]): Promise => { + if (!ownerAddress || !chainId || !client || balancesDiff === undefined) return []; if (!isChainIdSupported(chainId)) { @@ -146,7 +132,6 @@ export function usePools( return await getUserPools( ownerAddress, chainId, - token, client, userBalancesDiff, ); diff --git a/apps/withdraw-uni-v2/src/hooks/useSelectedPool.ts b/apps/withdraw-uni-v2/src/hooks/useSelectedPool.ts index 780808c..920a288 100644 --- a/apps/withdraw-uni-v2/src/hooks/useSelectedPool.ts +++ b/apps/withdraw-uni-v2/src/hooks/useSelectedPool.ts @@ -1,21 +1,10 @@ -import { useCallback } from "react"; -import useSWR from "swr"; -import { isAddress } from "viem"; -import { useFetchNewPoolCallback } from "./useFetchNewPoolCallback"; +import { useMemo } from "react"; +import { useUserPools } from "./useUserPools"; export function useSelectedPool(poolId: string) { - const fetchNewPoolCallback = useFetchNewPoolCallback(); - - const getSelectedPoolCallback = useCallback( - async (_poolId: string) => { - if (!fetchNewPoolCallback) return; - if (!isAddress(_poolId)) return; - - const fetchedNewPool = await fetchNewPoolCallback(_poolId); - return fetchedNewPool; - }, - [fetchNewPoolCallback], - ); - - return useSWR(poolId, getSelectedPoolCallback); + const { data: pools } = useUserPools(); + return useMemo(() => { + if (!pools) return; + return pools.find((pool) => pool.id.toLowerCase() === poolId.toLowerCase()); + }, [pools, poolId]); } diff --git a/apps/withdraw-uni-v2/src/hooks/useUserPools.ts b/apps/withdraw-uni-v2/src/hooks/useUserPools.ts index 41691f7..d1cbf24 100644 --- a/apps/withdraw-uni-v2/src/hooks/useUserPools.ts +++ b/apps/withdraw-uni-v2/src/hooks/useUserPools.ts @@ -1,29 +1,13 @@ import { useIFrameContext } from "@bleu/cow-hooks-ui"; -import type { WithdrawSchemaType } from "@bleu/utils"; -import { useFormContext, useWatch } from "react-hook-form"; import { usePools } from "./usePools"; -import { useSelectedPool } from "./useSelectedPool"; export function useUserPools() { const { context, publicClient } = useIFrameContext(); - const { control } = useFormContext(); - const poolId = useWatch({ control, name: "poolId" }); - const { data: selectedPool } = useSelectedPool(poolId); - const usePoolsReturn = usePools( + return usePools( context?.account, context?.chainId, - context?.orderParams?.sellTokenAddress, publicClient, //@ts-ignore context?.balancesDiff as Record>, ); - - const basePools = usePoolsReturn?.data || []; - const allPools = - selectedPool && - !basePools.map((pool) => pool.address).includes(selectedPool?.address) - ? [selectedPool, ...basePools] - : basePools; - - return { ...usePoolsReturn, data: allPools }; } diff --git a/apps/withdraw-uni-v2/src/utils/combineTokenLists.ts b/apps/withdraw-uni-v2/src/utils/combineTokenLists.ts index 6355cc7..044b094 100644 --- a/apps/withdraw-uni-v2/src/utils/combineTokenLists.ts +++ b/apps/withdraw-uni-v2/src/utils/combineTokenLists.ts @@ -11,16 +11,19 @@ function isTokenInList(token: TokenId, list: TokenId[]) { ); } -export function combineTokenLists( - oldList: T[], - newList: T[], -): T[] { - // Avoid storing the same token many times - const oldListFiltered = oldList.filter( - (token) => !isTokenInList(token, newList), - ); +export function combineTokenLists(...lists: T[][]): T[] { + if (lists.length === 0) return []; + if (lists.length === 1) return lists[0]; - const resultList = [...oldListFiltered, ...newList]; + // Start with the last list and work backwards + return lists.reduceRight((accumulated: T[], current: T[]) => { + // Filter out tokens from the current list that already exist in accumulated + const uniqueTokens = current.filter( + (token) => !isTokenInList(token, accumulated), + ); - return resultList; + // biome-ignore lint: + uniqueTokens.forEach((token) => accumulated.unshift(token)); + return accumulated; + }); } diff --git a/apps/withdraw-uni-v2/src/utils/getLpTokensList.ts b/apps/withdraw-uni-v2/src/utils/getLpTokensList.ts index b2a7c47..3d12d1d 100644 --- a/apps/withdraw-uni-v2/src/utils/getLpTokensList.ts +++ b/apps/withdraw-uni-v2/src/utils/getLpTokensList.ts @@ -1,5 +1,6 @@ import type { SupportedChainId } from "@cowprotocol/cow-sdk"; import type { RawTokenData } from "#/types"; +import { combineTokenLists } from "./combineTokenLists"; import { getExtraTokens } from "./storage"; interface TokenData extends Omit { @@ -23,9 +24,11 @@ export async function getLpTokensList( const cachedLpTokens = getExtraTokens(chainId, account); + const allTokens = combineTokenLists(data.tokens, cachedLpTokens); + // Transform the tokens array const allLpTokens: TokenData[] = { - tokens: [...data.tokens, ...cachedLpTokens], // TODO: Remove this test token after review + tokens: allTokens, }.tokens.map((token) => ({ chainId: token.chainId, address: token.address, diff --git a/apps/withdraw-uni-v2/src/utils/getTokensInfo.ts b/apps/withdraw-uni-v2/src/utils/getTokensInfo.ts index 74fd5ee..7f9a4a5 100644 --- a/apps/withdraw-uni-v2/src/utils/getTokensInfo.ts +++ b/apps/withdraw-uni-v2/src/utils/getTokensInfo.ts @@ -39,12 +39,11 @@ export async function getTokensInfo( } // Fetch USD prices - const prices = await getTokensPrices(Object.keys(tokens), chainId); - const tokensWithPrices: TokenWithPrice[] = Object.values(tokens).map( - (token) => { - return { ...token, priceUsd: prices[token.address] }; - }, - ); + const tokenList = Object.values(tokens); + const prices = await getTokensPrices(tokenList, chainId); + const tokensWithPrices: TokenWithPrice[] = tokenList.map((token, i) => { + return { ...token, priceUsd: prices[i] }; + }); return tokensWithPrices; } diff --git a/apps/withdraw-uni-v2/src/utils/getTokensList.ts b/apps/withdraw-uni-v2/src/utils/getTokensList.ts index f62b29e..f478559 100644 --- a/apps/withdraw-uni-v2/src/utils/getTokensList.ts +++ b/apps/withdraw-uni-v2/src/utils/getTokensList.ts @@ -1,5 +1,6 @@ import { SupportedChainId } from "@cowprotocol/cow-sdk"; import type { TokenData } from "#/types"; +import { combineTokenLists } from "./combineTokenLists"; /** * #CHAIN-INTEGRATION @@ -23,19 +24,6 @@ const tokenListUrlMap = { ], }; -const filterUniqueAddresses = (tokens: TokenData[]) => { - const seen = new Set(); - - return tokens.filter((token) => { - const lowercaseAddress = token.address.toLowerCase(); - if (seen.has(lowercaseAddress)) { - return false; - } - seen.add(lowercaseAddress); - return true; - }); -}; - export async function getTokensList( chainId: SupportedChainId, ): Promise { @@ -54,9 +42,9 @@ export async function getTokensList( response.map((res) => res.json()), )) as { tokens: TokenData[] }[]; - const allTokens = allJsonFiles.flatMap(({ tokens }) => tokens); + const allTokens = allJsonFiles.map(({ tokens }) => tokens); - return filterUniqueAddresses(allTokens); + return combineTokenLists(...allTokens); } catch (error) { console.error("Error fetching token list:", error); throw error; diff --git a/apps/withdraw-uni-v2/src/utils/getTokensPrices.ts b/apps/withdraw-uni-v2/src/utils/getTokensPrices.ts index a12a3db..224c9a4 100644 --- a/apps/withdraw-uni-v2/src/utils/getTokensPrices.ts +++ b/apps/withdraw-uni-v2/src/utils/getTokensPrices.ts @@ -1,56 +1,24 @@ -import { SupportedChainId } from "@cowprotocol/cow-sdk"; +import { getCowProtocolUsdPrice } from "@bleu/utils"; +import type { SupportedChainId } from "@cowprotocol/cow-sdk"; +import type { Address } from "viem"; +import type { TokenData } from "#/types"; import { isChainIdSupported } from "./uniswapSupportedChains"; -/** - * #CHAIN-INTEGRATION - * This needs to be changed if you want to support a new chain - */ -const coingeckoPlatfromMap = { - [SupportedChainId.MAINNET]: "ethereum", - [SupportedChainId.ARBITRUM_ONE]: "arbitrum-one", - [SupportedChainId.GNOSIS_CHAIN]: "", - [SupportedChainId.SEPOLIA]: "", -}; - export async function getTokensPrices( - addresses: string[], + tokens: TokenData[], chainId: SupportedChainId, -): Promise> { +): Promise { if (!isChainIdSupported(chainId)) { throw new Error(`ChainId ${chainId} is not supported`); } - if (chainId === SupportedChainId.SEPOLIA) { - return addresses.reduce( - (acc, address) => { - acc[address] = 0; - return acc; - }, - {} as Record, - ); - } - - const baseUrl = "https://api.coingecko.com/api/v3/simple/token_price"; - const getUrl = (address: string) => - `${baseUrl}/${coingeckoPlatfromMap[chainId]}?contract_addresses=${address}&vs_currencies=usd`; - - const responses = await Promise.all( - addresses.map((address) => fetch(getUrl(address))), + return await Promise.all( + tokens.map((token) => + getCowProtocolUsdPrice({ + chainId, + tokenAddress: token.address as Address, + tokenDecimals: token.decimals, + }), + ), ); - - const pricesData = await Promise.all( - responses.map((response) => response.json()), - ); - - const tokensPrices: Record = {}; - pricesData.forEach((priceData, idx) => { - if (!priceData) { - tokensPrices[addresses[idx]] = 0; - } else { - tokensPrices[addresses[idx]] = priceData[addresses[idx].toLowerCase()] - .usd as number; - } - }); - - return tokensPrices; } diff --git a/packages/cow-hooks-ui/src/PoolsDropdownMenu.tsx b/packages/cow-hooks-ui/src/PoolsDropdownMenu.tsx index 05c5bc4..22043db 100644 --- a/packages/cow-hooks-ui/src/PoolsDropdownMenu.tsx +++ b/packages/cow-hooks-ui/src/PoolsDropdownMenu.tsx @@ -104,7 +104,7 @@ export function PoolsDropdownMenu({ ); return ( <> -

No results found.

+

No results found and invalid address format to import pool.

Try placing your LP token address on the search bar.

); @@ -145,7 +145,8 @@ export function PoolsDropdownMenu({ setTypedAddress(e)} + onValueChange={(e) => setTypedAddress(e.trim())} + value={typedAddress} />
diff --git a/packages/utils/cowApi.ts b/packages/utils/cowApi.ts new file mode 100644 index 0000000..c354f48 --- /dev/null +++ b/packages/utils/cowApi.ts @@ -0,0 +1,98 @@ +import { SupportedChainId } from "@cowprotocol/cow-sdk"; +import type { Address } from "viem"; + +const COW_API_BASE_URL = "https://api.cow.fi/"; + +export const COW_API_URL_BY_CHAIN_ID = { + /** + * #CHAIN-INTEGRATION + * This needs to be changed if you want to support a new chain + */ + [SupportedChainId.MAINNET]: `${COW_API_BASE_URL}mainnet`, + [SupportedChainId.GNOSIS_CHAIN]: `${COW_API_BASE_URL}xdai`, + [SupportedChainId.SEPOLIA]: `${COW_API_BASE_URL}sepolia`, + [SupportedChainId.ARBITRUM_ONE]: `${COW_API_BASE_URL}arbitrum_one`, +}; + +export interface INativePrice { + price: number; +} + +export async function getNativePrice( + tokenAddress: Address, + chainId: SupportedChainId, +): Promise { + const url = COW_API_URL_BY_CHAIN_ID[chainId]; + + return fetch(`${url}/api/v1/token/${tokenAddress}/native_price`, { + headers: { + Accept: "application/json", + }, + }) + .then((response) => response.json() as Promise) + .then((data) => data.price); +} + +export const USDC: Record< + SupportedChainId, + { address: Address; decimals: number } +> = { + [SupportedChainId.MAINNET]: { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + decimals: 6, + }, + [SupportedChainId.GNOSIS_CHAIN]: { + address: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", + decimals: 6, + }, + [SupportedChainId.SEPOLIA]: { + address: "0xbe72E441BF55620febc26715db68d3494213D8Cb", + decimals: 18, + }, + [SupportedChainId.ARBITRUM_ONE]: { + address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + decimals: 6, + }, +}; + +export async function getCowProtocolUsdPrice({ + chainId, + tokenAddress, + tokenDecimals, +}: { + chainId: SupportedChainId; + tokenAddress: Address; + tokenDecimals: number; +}): Promise { + const usdcToken = USDC[chainId]; + const [usdNativePrice, tokenNativePrice] = await Promise.all([ + getNativePrice(USDC[chainId].address as Address, chainId), + getNativePrice(tokenAddress, chainId), + ]); + + if (usdNativePrice && tokenNativePrice) { + const usdPrice = invertNativeToTokenPrice( + usdNativePrice, + usdcToken.decimals, + ); + const tokenPrice = invertNativeToTokenPrice( + tokenNativePrice, + tokenDecimals, + ); + + if (!tokenPrice) throw new Error("Token price is 0"); + + return usdPrice / tokenPrice; + } + + return 0; +} + +/** + * API response value represents the amount of native token atoms needed to buy 1 atom of the specified token + * This function inverts the price to represent the amount of specified token atoms needed to buy 1 atom of the native token + */ +function invertNativeToTokenPrice(value: number, decimals: number): number { + const inverted = 1 / value; + return inverted * 10 ** (18 - decimals); +} diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 6f1e468..de2ffe6 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -6,6 +6,7 @@ export * from "./balancerApi"; export * from "./math"; export * from "./schema"; export * from "./decode"; +export * from "./cowApi"; export function truncateAddress(address?: string | null) { if (!address) return address;