From a61db52f15c4a0397019ea7a34f68aa364a12636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Wed, 29 May 2024 09:53:43 -0300 Subject: [PATCH] refactor data fetching --- .../(components)/BalancerPriceInformation.tsx | 42 -- .../ChainlinkPriceInformation.tsx | 103 ---- .../(components)/CustomPriceInformation.tsx | 27 - .../(components)/PoolCompositionTable.tsx | 3 +- .../[id]/(components)/PriceInformation.tsx | 70 +-- .../(components)/SushiV2PriceInformation.tsx | 38 -- .../UniswapV2PriceInformation.tsx | 44 -- .../src/app/amms/[id]/page.tsx | 52 +- .../src/app/new/(components)/AmmForm.tsx | 14 +- .../app/new/(components)/TokenAmountInput.tsx | 2 +- apps/cow-amm-deployer/src/app/page.tsx | 149 +++++- .../src/components/HomeWrapper.tsx | 160 ------ .../src/components/TokenSelect.tsx | 2 +- .../src/hooks/useDecodedPriceOracleData.ts | 37 -- .../src/hooks/useRunningAmmInfo.ts | 472 +++++++++--------- .../src/hooks/useStandaloneAmm/index.ts | 131 ----- .../src/lib/chainlinkPriceFeedRouter.ts | 2 +- apps/cow-amm-deployer/src/lib/fetchAmmData.ts | 207 ++++++++ .../getBalancesFromContract.ts | 2 +- .../src/lib/getCowProtocolUsdPrice.ts | 2 +- .../getTokensExternalPrices.ts} | 2 +- apps/cow-amm-deployer/src/lib/tokenUtils.ts | 2 +- apps/cow-amm-deployer/src/lib/types.ts | 21 - packages/utils/index.ts | 11 +- 24 files changed, 667 insertions(+), 928 deletions(-) delete mode 100644 apps/cow-amm-deployer/src/app/amms/[id]/(components)/BalancerPriceInformation.tsx delete mode 100644 apps/cow-amm-deployer/src/app/amms/[id]/(components)/ChainlinkPriceInformation.tsx delete mode 100644 apps/cow-amm-deployer/src/app/amms/[id]/(components)/CustomPriceInformation.tsx delete mode 100644 apps/cow-amm-deployer/src/app/amms/[id]/(components)/SushiV2PriceInformation.tsx delete mode 100644 apps/cow-amm-deployer/src/app/amms/[id]/(components)/UniswapV2PriceInformation.tsx delete mode 100644 apps/cow-amm-deployer/src/components/HomeWrapper.tsx delete mode 100644 apps/cow-amm-deployer/src/hooks/useDecodedPriceOracleData.ts delete mode 100644 apps/cow-amm-deployer/src/hooks/useStandaloneAmm/index.ts create mode 100644 apps/cow-amm-deployer/src/lib/fetchAmmData.ts rename apps/cow-amm-deployer/src/{hooks/useStandaloneAmm => lib}/getBalancesFromContract.ts (93%) rename apps/cow-amm-deployer/src/{hooks/useStandaloneAmm/getTokesFrom.ts => lib/getTokensExternalPrices.ts} (94%) diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/BalancerPriceInformation.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/BalancerPriceInformation.tsx deleted file mode 100644 index 296d1e71f..000000000 --- a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/BalancerPriceInformation.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { NetworkFromNetworkChainId } from "@bleu/utils"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { ArrowTopRightIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; - -import { useDecodedPriceOracleData } from "#/hooks/useDecodedPriceOracleData"; -import { ICowAmm } from "#/lib/types"; -import { ChainId } from "#/utils/chainsPublicClients"; - -export function BalancerPriceInformation({ cowAmm }: { cowAmm: ICowAmm }) { - const { safe } = useSafeAppsSDK(); - - const { isLoading, decodedData } = useDecodedPriceOracleData({ - priceOracleAddress: cowAmm.priceOracleAddress, - priceOracleData: cowAmm.priceOracleData, - chainId: safe.chainId as ChainId, - }); - - if (isLoading || !decodedData) { - return Loading price information...; - } - - const priceOracleLink = getBalancerPoolUrl( - safe.chainId as ChainId, - decodedData[1].balancerPoolId, - ); - - return ( -
- Using price information from Balancer V2 - {priceOracleLink && ( - - - - )} -
- ); -} - -export function getBalancerPoolUrl(chainId: ChainId, poolId?: string) { - return `https://app.balancer.fi/#/${NetworkFromNetworkChainId[chainId]}-chain/pool/${poolId}`; -} diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/ChainlinkPriceInformation.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/ChainlinkPriceInformation.tsx deleted file mode 100644 index ad550328f..000000000 --- a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/ChainlinkPriceInformation.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { ResultOf } from "gql.tada"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { Address } from "viem"; -import { gnosis, sepolia } from "viem/chains"; - -import { useDecodedPriceOracleData } from "#/hooks/useDecodedPriceOracleData"; -import { AMM_QUERY } from "#/hooks/useStandaloneAmm"; -import { priceFeedAbi } from "#/lib/abis/priceFeed"; -import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients"; - -function usePriceFeedLinks(cowAmm: ResultOf) { - const { safe } = useSafeAppsSDK(); - const [priceFeed0Link, setPriceFeed0Link] = useState(); - const [priceFeed1Link, setPriceFeed1Link] = useState(); - - if (!cowAmm || !cowAmm.constantProductData) - return { error: "No price oracle data" }; - - const { isLoading, decodedData } = useDecodedPriceOracleData({ - priceOracleAddress: cowAmm.constantProductData.priceOracle as Address, - priceOracleData: cowAmm.constantProductData.priceOracleData as Address, - chainId: safe.chainId as ChainId, - }); - - if (isLoading || !decodedData) { - return { isLoading }; - } - - useEffect(() => { - const fetchPriceFeedLinks = async () => { - const [priceFeed0Link, priceFeed1Link] = await Promise.all([ - getPriceFeedLink( - safe.chainId as ChainId, - decodedData[1].chainlinkPriceFeed0, - ), - getPriceFeedLink( - safe.chainId as ChainId, - decodedData[1].chainlinkPriceFeed1, - ), - ]); - setPriceFeed0Link(priceFeed0Link); - setPriceFeed1Link(priceFeed1Link); - }; - - fetchPriceFeedLinks(); - }, [cowAmm]); - - return { isLoading, priceFeed0Link, priceFeed1Link }; -} - -export function ChainlinkPriceInformation({ - cowAmm, -}: { - cowAmm: ResultOf; -}) { - const { isLoading, priceFeed0Link, priceFeed1Link } = - usePriceFeedLinks(cowAmm); - - if (isLoading) return Loading price information...; - - return ( -
- Using price information from Chainlink - {priceFeed0Link && ( - - 1 - - )} - {priceFeed1Link && ( - - 2 - - )} -
- ); -} - -export async function getPriceFeedLink(chainId: ChainId, address?: Address) { - if (!address) return; - if (chainId === sepolia.id) return; - const publicClient = publicClientsFromIds[chainId]; - const priceFeedDescription = (await publicClient.readContract({ - address: address, - abi: priceFeedAbi, - functionName: "description", - })) as string; - const priceFeedPageName = priceFeedDescription - .replace(" / ", "-") - .toLowerCase(); - const chainName = chainId === gnosis.id ? "xdai" : "ethereum"; - - return `https://data.chain.link/feeds/${chainName}/mainnet/${priceFeedPageName}`; -} diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/CustomPriceInformation.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/CustomPriceInformation.tsx deleted file mode 100644 index de958c513..000000000 --- a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/CustomPriceInformation.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { buildBlockExplorerAddressURL } from "@bleu/utils"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { ArrowTopRightIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; - -import { ICowAmm } from "#/lib/types"; -import { ChainId } from "#/utils/chainsPublicClients"; - -export function CustomPriceInformation({ cowAmm }: { cowAmm: ICowAmm }) { - const { safe } = useSafeAppsSDK(); - - const priceOracleLink = buildBlockExplorerAddressURL({ - chainId: safe.chainId as ChainId, - address: cowAmm.priceOracleAddress, - }); - - return ( -
- Using price information from custom contract - {priceOracleLink && ( - - - - )} -
- ); -} diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PoolCompositionTable.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PoolCompositionTable.tsx index 103188a47..18c5f9de5 100644 --- a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PoolCompositionTable.tsx +++ b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PoolCompositionTable.tsx @@ -1,10 +1,11 @@ +"use client"; import { formatNumber } from "@bleu/utils/formatNumber"; import { tomatoDark } from "@radix-ui/colors"; import { InfoCircledIcon } from "@radix-ui/react-icons"; import Table from "#/components/Table"; import { Tooltip } from "#/components/Tooltip"; -import { ICowAmm } from "#/lib/types"; +import { ICowAmm } from "#/lib/fetchAmmData"; import { TokenInfo } from "./TokenInfo"; diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PriceInformation.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PriceInformation.tsx index 6828c0ea4..988b4481c 100644 --- a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PriceInformation.tsx +++ b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/PriceInformation.tsx @@ -1,42 +1,44 @@ -import { Address } from "@bleu/utils"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { ResultOf } from "gql.tada"; +"use client"; -import { useDecodedPriceOracleData } from "#/hooks/useDecodedPriceOracleData"; -import { AMM_QUERY } from "#/hooks/useStandaloneAmm"; -import { PRICE_ORACLES } from "#/lib/types"; -import { ChainId } from "#/utils/chainsPublicClients"; +import { ArrowTopRightIcon } from "@radix-ui/react-icons"; +import Link from "next/link"; -import { BalancerPriceInformation } from "./BalancerPriceInformation"; -import { ChainlinkPriceInformation } from "./ChainlinkPriceInformation"; -import { CustomPriceInformation } from "./CustomPriceInformation"; -import { SushiV2PriceInformation } from "./SushiV2PriceInformation"; -import { UniswapV2PriceInformation } from "./UniswapV2PriceInformation"; +import { ICowAmm } from "#/lib/fetchAmmData"; +import { PRICE_ORACLES } from "#/lib/types"; -export function PriceInformation({ - cowAmm, +function PriceInformationComponent({ + label, + urls, }: { - cowAmm: ResultOf; + label: string; + urls: string[]; }) { - const { safe } = useSafeAppsSDK(); - const { isLoading, decodedData } = useDecodedPriceOracleData({ - priceOracleAddress: cowAmm.constantProductData?.priceOracle as Address, - priceOracleData: cowAmm.constantProductData?.priceOracleData as Address, - chainId: safe.chainId as ChainId, - }); + return ( +
+ {label} + {urls.map((url, index) => ( + + + + ))} +
+ ); +} + +export function PriceInformation({ cowAmm }: { cowAmm: ICowAmm }) { + const { decodedPriceOracleData, priceFeedLinks } = cowAmm; + + const labels = { + [PRICE_ORACLES.UNI]: "Using price information from Uniswap V2", + [PRICE_ORACLES.BALANCER]: "Using price information from Balancer V2", + [PRICE_ORACLES.SUSHI]: "Using price information from Sushi V2", + [PRICE_ORACLES.CHAINLINK]: "Using price information from Chainlink", + default: "Using price information from custom contract", + } as const; - if (isLoading || !decodedData) return <>Loading...; + const priceOracle = decodedPriceOracleData[0]; + // @ts-ignore + const label = labels[priceOracle] || labels.default; - switch (decodedData[0]) { - case PRICE_ORACLES.UNI: - return ; - case PRICE_ORACLES.BALANCER: - return ; - case PRICE_ORACLES.SUSHI: - return ; - case PRICE_ORACLES.CHAINLINK: - return ; - default: - return ; - } + return ; } diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/SushiV2PriceInformation.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/SushiV2PriceInformation.tsx deleted file mode 100644 index a231908d9..000000000 --- a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/SushiV2PriceInformation.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { ArrowTopRightIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; - -import { useDecodedPriceOracleData } from "#/hooks/useDecodedPriceOracleData"; -import { ICowAmm } from "#/lib/types"; -import { ChainId } from "#/utils/chainsPublicClients"; - -export function SushiV2PriceInformation({ cowAmm }: { cowAmm: ICowAmm }) { - const { safe } = useSafeAppsSDK(); - const { isLoading, decodedData } = useDecodedPriceOracleData({ - priceOracleAddress: cowAmm.priceOracleAddress, - priceOracleData: cowAmm.priceOracleData, - chainId: safe.chainId as ChainId, - }); - - if (isLoading || !decodedData) return <>Loading...; - - const priceOracleLink = getSushiV2Pair( - safe.chainId as ChainId, - decodedData[1].sushiSwapPairAddress, - ); - - return ( -
- Using price information from Sushi V2 - {priceOracleLink && ( - - - - )} -
- ); -} - -export function getSushiV2Pair(chainId: ChainId, referencePair?: string) { - return `https://www.sushi.com/pool/${[chainId]}%3A${referencePair}`; -} diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/UniswapV2PriceInformation.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/(components)/UniswapV2PriceInformation.tsx deleted file mode 100644 index b91aae5de..000000000 --- a/apps/cow-amm-deployer/src/app/amms/[id]/(components)/UniswapV2PriceInformation.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { NetworkFromNetworkChainId } from "@bleu/utils"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { ArrowTopRightIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; -import { gnosis } from "viem/chains"; - -import { useDecodedPriceOracleData } from "#/hooks/useDecodedPriceOracleData"; -import { ICowAmm } from "#/lib/types"; -import { ChainId } from "#/utils/chainsPublicClients"; - -export function UniswapV2PriceInformation({ cowAmm }: { cowAmm: ICowAmm }) { - const { safe } = useSafeAppsSDK(); - - const { isLoading, decodedData } = useDecodedPriceOracleData({ - priceOracleAddress: cowAmm.priceOracleAddress, - priceOracleData: cowAmm.priceOracleData, - chainId: safe.chainId as ChainId, - }); - - if (isLoading || !decodedData) return <>Loading...; - - const priceOracleLink = getUniV2PairUrl( - safe.chainId as ChainId, - decodedData[1].uniswapV2PairAddress, - ); - - return ( -
- Using price information from Uniswap V2 - {priceOracleLink && ( - - - - )} -
- ); -} - -export function getUniV2PairUrl(chainId: ChainId, referencePair?: string) { - if (chainId === gnosis.id) { - return; - } - return `https://info.uniswap.org/#/${NetworkFromNetworkChainId[chainId]}/pools/${referencePair}`; -} diff --git a/apps/cow-amm-deployer/src/app/amms/[id]/page.tsx b/apps/cow-amm-deployer/src/app/amms/[id]/page.tsx index b23145b9e..0a381f4a5 100644 --- a/apps/cow-amm-deployer/src/app/amms/[id]/page.tsx +++ b/apps/cow-amm-deployer/src/app/amms/[id]/page.tsx @@ -1,7 +1,4 @@ -"use client"; - import { formatNumber } from "@bleu/utils/formatNumber"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; import { ArrowTopRightIcon, Pencil2Icon, @@ -11,32 +8,20 @@ import Link from "next/link"; import { Address } from "viem"; import { Button } from "#/components/Button"; -import { Spinner } from "#/components/Spinner"; -import { UnsuportedChain } from "#/components/UnsuportedChain"; -import WalletNotConnected from "#/components/WalletNotConnected"; -import { useStandaloneAMM } from "#/hooks/useStandaloneAmm"; import { buildAccountCowExplorerUrl } from "#/lib/cowExplorer"; -import { ChainId, supportedChainIds } from "#/utils/chainsPublicClients"; +import { fetchAmmData } from "#/lib/fetchAmmData"; +import { ChainId } from "#/utils/chainsPublicClients"; import { PoolCompositionTable } from "./(components)/PoolCompositionTable"; import { PriceInformation } from "./(components)/PriceInformation"; -export default function Page({ params }: { params: { id: `0x${string}` } }) { - const { data: cowAmm, loading } = useStandaloneAMM(params.id); - - const { safe, connected } = useSafeAppsSDK(); - - if (!connected) { - return ; - } - - if (!cowAmm || loading) { - return ; - } +async function getData({ params }: { params: { id: string } }) { + const ammData = await fetchAmmData(params.id); + return ammData; +} - if (!supportedChainIds.includes(safe.chainId)) { - return ; - } +export default async function Page({ params }: { params: { id: string } }) { + const ammData = await getData({ params }); return (
@@ -45,21 +30,20 @@ export default function Page({ params }: { params: { id: `0x${string}` } }) {

The first MEV-Capturing AMM, - brought to you by CoW DAO{" "} + brought to you by CoW DAO

- {/* @ts-ignore */} - +
Total Value ${" "} {formatNumber( - cowAmm.totalUsdValue, + ammData.totalUsdValue, 2, "decimal", "compact", - 0.01, + 0.01 )}
@@ -73,9 +57,9 @@ export default function Page({ params }: { params: { id: `0x${string}` } }) { href={ new URL( buildAccountCowExplorerUrl({ - chainId: safe.chainId as ChainId, - address: safe.safeAddress as Address, - }), + chainId: ammData.order.chainId as ChainId, + address: ammData.order.handler as Address, + }) ) } rel="noreferrer noopener" @@ -85,10 +69,10 @@ export default function Page({ params }: { params: { id: `0x${string}` } }) {
- -
+ +
+ } + /> +
+
+ + + + Token0 + Token1 + State + Link + Updated At + Actions + + + + {rows?.length ? ( + rows.map((row, index) => { + return ( + + {row.token0} + {row.token1} + {row.state} + + Link + + {formatDate(row.createdAt)} + + + Edit + Delete + + + ); + }) + ) : ( +
+ Nenhum resultado encontrado. +
+ )} +
+
+
{" "} +
+ + +
+
+ ); } diff --git a/apps/cow-amm-deployer/src/components/HomeWrapper.tsx b/apps/cow-amm-deployer/src/components/HomeWrapper.tsx deleted file mode 100644 index 6cec0c7bd..000000000 --- a/apps/cow-amm-deployer/src/components/HomeWrapper.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@bleu/ui"; -import { formatDate } from "@bleu/utils"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { graphql } from "gql.tada"; -import request from "graphql-request"; -import Image from "next/image"; -import Link from "next/link"; -import useSWR from "swr"; - -import { NEXT_PUBLIC_API_URL } from "#/lib/ponderApi"; - -import { Button } from "./Button"; -import Fathom from "./Fathom"; -import { LinkComponent } from "./Link"; - -const CREATED_AMMS_FOR_USER_QUERY = graphql(` - query ($userId: String!) { - constantProductDatas(where: { userId: $userId, version: "Standalone" }) { - items { - id - disabled - token0 { - address - decimals - symbol - } - token1 { - address - decimals - symbol - } - order { - blockTimestamp - } - } - } - } -`); - -export function HomeWrapper({ goToSafe = false }: { goToSafe?: boolean }) { - const title = goToSafe ? "Open App in Safe" : "Create a CoW AMM"; - - const { safe } = useSafeAppsSDK(); - const userId = `${safe.safeAddress}-${safe.chainId}`; - - const { data, isLoading } = useSWR(CREATED_AMMS_FOR_USER_QUERY, (query) => - request(NEXT_PUBLIC_API_URL, query, { userId }), - ); - - if (isLoading || !data) return <>Loading...; - - const rows = data.constantProductDatas.items.map((item) => ({ - id: item.id, - token0: item.token0.symbol, - token1: item.token1.symbol, - state: item.disabled ? "Stopped" : "Running", - link: `/amms/${item.id}`, - createdAt: new Date((item.order.blockTimestamp as number) * 1000), - })); - - return ( -
-
- CoW AMM Logo -

- The first MEV-Capturing AMM, -
brought to you by CoW DAO -

- - CoW AMM is a production-ready implementation of an FM-AMM that - supplies liquidity for trades made on CoW Protocol. Solvers compete - with each other for the right to trade against the AMM - - - {title} - - } - /> -
-
- - - - Token0 - Token1 - State - Link - Updated At - Actions - - - - {rows?.length ? ( - rows.map((row, index) => { - return ( - - {row.token0} - {row.token1} - {row.state} - - Link - - {formatDate(row.createdAt)} - - - Edit - Delete - - - ); - }) - ) : ( -
- Nenhum resultado encontrado. -
- )} -
-
-
{" "} -
- - -
-
- ); -} diff --git a/apps/cow-amm-deployer/src/components/TokenSelect.tsx b/apps/cow-amm-deployer/src/components/TokenSelect.tsx index 0b4df9a9c..1ab9814a1 100644 --- a/apps/cow-amm-deployer/src/components/TokenSelect.tsx +++ b/apps/cow-amm-deployer/src/components/TokenSelect.tsx @@ -11,7 +11,7 @@ import { PopoverTrigger, } from "#/components/ui/popover"; import { useSafeBalances } from "#/hooks/useSafeBalances"; -import { IToken } from "#/lib/types"; +import { IToken } from "#/lib/fetchAmmData"; import { ChainId } from "#/utils/chainsPublicClients"; import { Button } from "./Button"; diff --git a/apps/cow-amm-deployer/src/hooks/useDecodedPriceOracleData.ts b/apps/cow-amm-deployer/src/hooks/useDecodedPriceOracleData.ts deleted file mode 100644 index a80a18016..000000000 --- a/apps/cow-amm-deployer/src/hooks/useDecodedPriceOracleData.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Address } from "@bleu/utils"; -import { useEffect, useState } from "react"; - -import { decodePriceOracleWithData } from "#/lib/decodePriceOracle"; -import { PRICE_ORACLES, PriceOracleData } from "#/lib/types"; -import { ChainId } from "#/utils/chainsPublicClients"; - -type DecodedPriceOracleData = [PRICE_ORACLES, PriceOracleData]; - -interface UseDecodedPriceOracleWithDataState { - isLoading: boolean; - decodedData?: DecodedPriceOracleData; -} - -export function useDecodedPriceOracleData({ - priceOracleAddress, - priceOracleData, - chainId, -}: { - priceOracleAddress: Address; - priceOracleData: Address; - chainId: ChainId; -}): UseDecodedPriceOracleWithDataState { - const [state, setState] = useState({ - isLoading: true, - }); - - useEffect(() => { - decodePriceOracleWithData({ - address: priceOracleAddress, - priceOracleData, - chainId, - }).then((data) => setState({ isLoading: false, decodedData: data })); - }, [priceOracleAddress, priceOracleData, chainId]); - - return state; -} diff --git a/apps/cow-amm-deployer/src/hooks/useRunningAmmInfo.ts b/apps/cow-amm-deployer/src/hooks/useRunningAmmInfo.ts index dbc725355..21edf418a 100644 --- a/apps/cow-amm-deployer/src/hooks/useRunningAmmInfo.ts +++ b/apps/cow-amm-deployer/src/hooks/useRunningAmmInfo.ts @@ -1,259 +1,259 @@ -// import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -// import { gql } from "graphql-tag"; -// import { useEffect, useState } from "react"; -// import { Address, formatUnits } from "viem"; +import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; +import { gql } from "graphql-tag"; +import { useEffect, useState } from "react"; +import { Address, formatUnits } from "viem"; -// import { composableCowAbi } from "#/lib/abis/composableCow"; -// import { cowAmmModuleAbi } from "#/lib/abis/cowAmmModule"; -// import { -// COMPOSABLE_COW_ADDRESS, -// COW_AMM_HANDLER_ADDRESS, -// COW_AMM_MODULE_ADDRESS, -// } from "#/lib/contracts"; -// import { decodePriceOracleWithData } from "#/lib/decodePriceOracle"; -// import { UserCurrentAmmQuery } from "#/lib/gqlComposableCow/generated"; -// import { composableCowApi } from "#/lib/gqlComposableCow/sdk"; -// import { fetchTokenUsdPrice } from "#/lib/tokenUtils"; -// import { ICowAmm, PRICE_ORACLES, PriceOracleData } from "#/lib/types"; -// import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients"; +import { composableCowAbi } from "#/lib/abis/composableCow"; +import { cowAmmModuleAbi } from "#/lib/abis/cowAmmModule"; +import { + COMPOSABLE_COW_ADDRESS, + COW_AMM_HANDLER_ADDRESS, + COW_AMM_MODULE_ADDRESS, +} from "#/lib/contracts"; +import { decodePriceOracleWithData } from "#/lib/decodePriceOracle"; +import { UserCurrentAmmQuery } from "#/lib/gqlComposableCow/generated"; +import { composableCowApi } from "#/lib/gqlComposableCow/sdk"; +import { fetchTokenUsdPrice } from "#/lib/tokenUtils"; +import { ICowAmm, PRICE_ORACLES, PriceOracleData } from "#/lib/types"; +import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients"; -// import { useSafeBalances } from "./useSafeBalances"; +import { useSafeBalances } from "./useSafeBalances"; -// gql(` -// query UserCurrentAmm($userId: String!, $handlerId: String!) { -// orders( -// where: {userId: $userId, orderHandlerId: $handlerId} -// limit: 1 -// orderBy: "blockNumber" -// orderDirection: "desc" -// ) { -// items { -// id -// chainId -// blockNumber -// blockTimestamp -// hash -// orderHandlerId -// decodedSuccess -// staticInput -// constantProductData { -// id -// token0 { -// id -// address -// symbol -// decimals -// } -// token1 { -// id -// address -// symbol -// decimals -// } -// minTradedToken0 -// priceOracle -// priceOracleData -// appData -// } -// } -// } -// } -// `); +gql(` +query UserCurrentAmm($userId: String!, $handlerId: String!) { + orders( + where: {userId: $userId, orderHandlerId: $handlerId} + limit: 1 + orderBy: "blockNumber" + orderDirection: "desc" + ) { + items { + id + chainId + blockNumber + blockTimestamp + hash + orderHandlerId + decodedSuccess + staticInput + constantProductData { + id + token0 { + id + address + symbol + decimals + } + token1 { + id + address + symbol + decimals + } + minTradedToken0 + priceOracle + priceOracleData + appData + } + } + } +} +`); -// export async function fetchLastAmmInfo({ -// chainId, -// safeAddress, -// }: { -// safeAddress: Address; -// chainId: ChainId; -// }) { -// const { orders } = await composableCowApi.UserCurrentAmm({ -// userId: `${safeAddress}-${chainId}`, -// handlerId: `${COW_AMM_HANDLER_ADDRESS[chainId as ChainId]}-${chainId}`, -// }); +export async function fetchLastAmmInfo({ + chainId, + safeAddress, +}: { + safeAddress: Address; + chainId: ChainId; +}) { + const { orders } = await composableCowApi.UserCurrentAmm({ + userId: `${safeAddress}-${chainId}`, + handlerId: `${COW_AMM_HANDLER_ADDRESS[chainId as ChainId]}-${chainId}`, + }); -// const order = orders?.items?.[0]; -// if (!order || !order.decodedSuccess) return; + const order = orders?.items?.[0]; + if (!order || !order.decodedSuccess) return; -// return decodePriceOracle({ -// constantProductData: order.constantProductData, -// hash: order.hash as `0x${string}`, -// chainId: chainId as ChainId, -// }); -// } + return decodePriceOracle({ + constantProductData: order.constantProductData, + hash: order.hash as `0x${string}`, + chainId: chainId as ChainId, + }); +} -// export async function decodePriceOracle({ -// constantProductData, -// hash, -// chainId, -// }: { -// // @ts-ignore -// constantProductData: UserCurrentAmmQuery["orders"]["items"][0]["constantProductData"]; -// hash: `0x${string}`; -// chainId: ChainId; -// }) { -// const [priceOracle, priceOracleDataDecoded] = await decodePriceOracleWithData( -// { -// address: constantProductData.priceOracle, -// priceOracleData: constantProductData.priceOracleData, -// chainId: chainId as ChainId, -// } -// ); +export async function decodePriceOracle({ + constantProductData, + hash, + chainId, +}: { + // @ts-ignore + constantProductData: UserCurrentAmmQuery["orders"]["items"][0]["constantProductData"]; + hash: `0x${string}`; + chainId: ChainId; +}) { + const [priceOracle, priceOracleDataDecoded] = await decodePriceOracleWithData( + { + address: constantProductData.priceOracle, + priceOracleData: constantProductData.priceOracleData, + chainId: chainId as ChainId, + }, + ); -// return { -// ...constantProductData, -// hash: hash, -// priceOracleType: priceOracle, -// priceOracleDataDecoded, -// }; -// } + return { + ...constantProductData, + hash: hash, + priceOracleType: priceOracle, + priceOracleDataDecoded, + }; +} -// export async function checkIsAmmRunning( -// chainId: ChainId, -// safeAddress: Address, -// orderHash: `0x${string}` -// ) { -// const publicClient = publicClientsFromIds[chainId]; -// return publicClient.readContract({ -// address: COMPOSABLE_COW_ADDRESS, -// abi: composableCowAbi, -// functionName: "singleOrders", -// args: [safeAddress, orderHash], -// }); -// } +export async function checkIsAmmRunning( + chainId: ChainId, + safeAddress: Address, + orderHash: `0x${string}`, +) { + const publicClient = publicClientsFromIds[chainId]; + return publicClient.readContract({ + address: COMPOSABLE_COW_ADDRESS, + abi: composableCowAbi, + functionName: "singleOrders", + args: [safeAddress, orderHash], + }); +} -// export async function checkAmmIsFromModule( -// chainId: ChainId, -// safeAddress: Address, -// orderHash: `0x${string}` -// ): Promise { -// const publicClient = publicClientsFromIds[chainId]; -// return publicClient -// .readContract({ -// address: COW_AMM_MODULE_ADDRESS[chainId], -// abi: cowAmmModuleAbi, -// functionName: "activeOrders", -// args: [safeAddress], -// }) -// .then((result) => result.toLowerCase() === orderHash.toLowerCase()); -// } +export async function checkAmmIsFromModule( + chainId: ChainId, + safeAddress: Address, + orderHash: `0x${string}`, +): Promise { + const publicClient = publicClientsFromIds[chainId]; + return publicClient + .readContract({ + address: COW_AMM_MODULE_ADDRESS[chainId], + abi: cowAmmModuleAbi, + functionName: "activeOrders", + args: [safeAddress], + }) + .then((result) => result.toLowerCase() === orderHash.toLowerCase()); +} -// export function useRunningAMM(): { -// cowAmm?: ICowAmm; -// loaded: boolean; -// isAmmRunning: boolean; -// error: boolean; -// updateAmmInfo: () => Promise; -// isAmmFromModule: boolean; -// } { -// const { -// safe: { safeAddress, chainId }, -// } = useSafeAppsSDK(); +export function useRunningAMM(): { + cowAmm?: ICowAmm; + loaded: boolean; + isAmmRunning: boolean; + error: boolean; + updateAmmInfo: () => Promise; + isAmmFromModule: boolean; +} { + const { + safe: { safeAddress, chainId }, + } = useSafeAppsSDK(); -// const [cowAmm, setCowAmm] = useState(); -// const [isAmmRunning, setIsAmmRunning] = useState(false); -// const [isAmmFromModule, setIsAmmFromModule] = useState(false); -// const [loaded, setLoaded] = useState(false); -// const [error, setError] = useState(false); -// const { assets, loaded: assetLoaded } = useSafeBalances(); + const [cowAmm, setCowAmm] = useState(); + const [isAmmRunning, setIsAmmRunning] = useState(false); + const [isAmmFromModule, setIsAmmFromModule] = useState(false); + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(false); + const { assets, loaded: assetLoaded } = useSafeBalances(); -// async function loadCoWAmmRunning(hash: `0x${string}`) { -// const [newIsAmmRunning, newIsAmmFromModule] = await Promise.all([ -// checkIsAmmRunning(chainId as ChainId, safeAddress as Address, hash), -// checkAmmIsFromModule(chainId as ChainId, safeAddress as Address, hash), -// ]); -// setIsAmmRunning(newIsAmmRunning); -// setIsAmmFromModule(newIsAmmFromModule); -// } + async function loadCoWAmmRunning(hash: `0x${string}`) { + const [newIsAmmRunning, newIsAmmFromModule] = await Promise.all([ + checkIsAmmRunning(chainId as ChainId, safeAddress as Address, hash), + checkAmmIsFromModule(chainId as ChainId, safeAddress as Address, hash), + ]); + setIsAmmRunning(newIsAmmRunning); + setIsAmmFromModule(newIsAmmFromModule); + } -// async function loadCowAmm() { -// const gqlInfo = await fetchLastAmmInfo({ -// chainId: chainId as ChainId, -// safeAddress: safeAddress as Address, -// }); + async function loadCowAmm() { + const gqlInfo = await fetchLastAmmInfo({ + chainId: chainId as ChainId, + safeAddress: safeAddress as Address, + }); -// const token0 = assets.find( -// (asset) => -// asset.tokenInfo.address.toLowerCase() === -// gqlInfo?.token0?.address.toLowerCase() -// ); -// const token1 = assets.find( -// (asset) => -// asset.tokenInfo.address.toLowerCase() === -// gqlInfo?.token1?.address.toLowerCase() -// ); + const token0 = assets.find( + (asset) => + asset.tokenInfo.address.toLowerCase() === + gqlInfo?.token0?.address.toLowerCase(), + ); + const token1 = assets.find( + (asset) => + asset.tokenInfo.address.toLowerCase() === + gqlInfo?.token1?.address.toLowerCase(), + ); -// if (!token0 || !token1) { -// return; -// } + if (!token0 || !token1) { + return; + } -// const [token0ExternalUsdPrice, token1ExternalUsdPrice] = await Promise.all([ -// fetchTokenUsdPrice({ -// tokenAddress: token0.tokenInfo.address as Address, -// tokenDecimals: token0.tokenInfo.decimals, -// chainId: chainId as ChainId, -// }).catch(() => 0), -// fetchTokenUsdPrice({ -// tokenAddress: token1.tokenInfo.address as Address, -// tokenDecimals: token1.tokenInfo.decimals, -// chainId: chainId as ChainId, -// }).catch(() => 0), -// ]); + const [token0ExternalUsdPrice, token1ExternalUsdPrice] = await Promise.all([ + fetchTokenUsdPrice({ + tokenAddress: token0.tokenInfo.address as Address, + tokenDecimals: token0.tokenInfo.decimals, + chainId: chainId as ChainId, + }).catch(() => 0), + fetchTokenUsdPrice({ + tokenAddress: token1.tokenInfo.address as Address, + tokenDecimals: token1.tokenInfo.decimals, + chainId: chainId as ChainId, + }).catch(() => 0), + ]); -// const [token0ExternalUsdValue, token1ExternalUsdValue] = [ -// token0ExternalUsdPrice * -// Number(formatUnits(BigInt(token0.balance), token0.tokenInfo.decimals)), -// token1ExternalUsdPrice * -// Number(formatUnits(BigInt(token1.balance), token1.tokenInfo.decimals)), -// ]; + const [token0ExternalUsdValue, token1ExternalUsdValue] = [ + token0ExternalUsdPrice * + Number(formatUnits(BigInt(token0.balance), token0.tokenInfo.decimals)), + token1ExternalUsdPrice * + Number(formatUnits(BigInt(token1.balance), token1.tokenInfo.decimals)), + ]; -// const totalUsdValue = token0ExternalUsdValue + token1ExternalUsdValue; + const totalUsdValue = token0ExternalUsdValue + token1ExternalUsdValue; -// return { -// token0: { -// ...token0, -// externalUsdPrice: token0ExternalUsdPrice, -// externalUsdValue: token0ExternalUsdValue, -// }, -// hash: gqlInfo?.hash as `0x${string}`, -// token1: { -// ...token1, -// externalUsdPrice: token1ExternalUsdPrice, -// externalUsdValue: token1ExternalUsdValue, -// }, -// totalUsdValue, -// minTradedToken0: gqlInfo?.minTradedToken0 as number, -// priceOracle: gqlInfo?.priceOracleType as PRICE_ORACLES, -// priceOracleData: gqlInfo?.priceOracleDataDecoded as PriceOracleData, -// priceOracleAddress: gqlInfo?.priceOracle as Address, -// }; -// } + return { + token0: { + ...token0, + externalUsdPrice: token0ExternalUsdPrice, + externalUsdValue: token0ExternalUsdValue, + }, + hash: gqlInfo?.hash as `0x${string}`, + token1: { + ...token1, + externalUsdPrice: token1ExternalUsdPrice, + externalUsdValue: token1ExternalUsdValue, + }, + totalUsdValue, + minTradedToken0: gqlInfo?.minTradedToken0 as number, + priceOracle: gqlInfo?.priceOracleType as PRICE_ORACLES, + priceOracleData: gqlInfo?.priceOracleDataDecoded as PriceOracleData, + priceOracleAddress: gqlInfo?.priceOracle as Address, + }; + } -// async function updateAmmInfo(): Promise { -// setError(false); -// await loadCowAmm() -// .then(async (newCowAmm) => { -// if (!newCowAmm) return; -// setCowAmm(newCowAmm); -// await loadCoWAmmRunning(newCowAmm.hash); -// }) -// .catch(() => { -// setError(true); -// }); -// setLoaded(true); -// } + async function updateAmmInfo(): Promise { + setError(false); + await loadCowAmm() + .then(async (newCowAmm) => { + if (!newCowAmm) return; + setCowAmm(newCowAmm); + await loadCoWAmmRunning(newCowAmm.hash); + }) + .catch(() => { + setError(true); + }); + setLoaded(true); + } -// useEffect(() => { -// if (!assetLoaded) return; -// updateAmmInfo(); -// }, [safeAddress, chainId, assets]); + useEffect(() => { + if (!assetLoaded) return; + updateAmmInfo(); + }, [safeAddress, chainId, assets]); -// return { -// cowAmm, -// loaded, -// isAmmRunning, -// error, -// updateAmmInfo, -// isAmmFromModule, -// }; -// } + return { + cowAmm, + loaded, + isAmmRunning, + error, + updateAmmInfo, + isAmmFromModule, + }; +} diff --git a/apps/cow-amm-deployer/src/hooks/useStandaloneAmm/index.ts b/apps/cow-amm-deployer/src/hooks/useStandaloneAmm/index.ts deleted file mode 100644 index cab26deed..000000000 --- a/apps/cow-amm-deployer/src/hooks/useStandaloneAmm/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { graphql } from "gql.tada"; -import request from "graphql-request"; -import useSWR from "swr"; -import { Address } from "viem"; - -import { NEXT_PUBLIC_API_URL } from "#/lib/ponderApi"; -import { ICowAmm, IToken } from "#/lib/types"; -import { ChainId } from "#/utils/chainsPublicClients"; - -import { getBalancesFromContract } from "./getBalancesFromContract"; -import { getTokensExternalPrices } from "./getTokesFrom"; - -export const AMM_QUERY = graphql(` - query fetchAmmData($ammId: String!) { - constantProductData(id: $ammId) { - token0 { - address - decimals - symbol - } - token1 { - address - decimals - symbol - } - minTradedToken0 - priceOracleData - priceOracle - order { - handler - chainId - } - disabled - } - } -`); - -export function useStandaloneAMM(ammId: Address) { - const { - data: subgraphData, - error: subgraphError, - isLoading: isSubgraphLoading, - } = useSWR(ammId, (ammId) => - request(NEXT_PUBLIC_API_URL, AMM_QUERY, { ammId }), - ); - - const token0SubgraphData = subgraphData?.constantProductData?.token0; - const token1SubgraphData = subgraphData?.constantProductData?.token1; - const chainId = subgraphData?.constantProductData?.order?.chainId; - - const ammAddress = ammId.split("-")[0] as Address; - - const { - data: balancesData, - error: balancesError, - isLoading: isBalancesLoading, - } = useSWR( - [ - "balances", - chainId as ChainId, - ammAddress, - token0SubgraphData as IToken, - token1SubgraphData as IToken, - ], - getBalancesFromContract, - ); - - const { - data: pricesData, - error: pricesError, - isLoading: isPricesLoading, - } = useSWR( - [ - "prices", - chainId as ChainId, - token0SubgraphData as IToken, - token1SubgraphData as IToken, - ], - getTokensExternalPrices, - ); - - if ( - !subgraphData || - !subgraphData.constantProductData || - !token0SubgraphData || - !token1SubgraphData || - !chainId || - !balancesData || - !pricesData - ) { - return { - loading: true, - error: subgraphError, - data: null, - }; - } - - const token0 = { - ...token0SubgraphData, - balance: balancesData.token0.balance, - usdPrice: pricesData.token0.externalUsdPrice, - usdValue: - Number(balancesData.token0.balance) * pricesData.token0.externalUsdPrice, - }; - - const token1 = { - ...token1SubgraphData, - balance: balancesData.token1.balance, - usdPrice: pricesData.token1.externalUsdPrice, - usdValue: - Number(balancesData.token1.balance) * pricesData.token1.externalUsdPrice, - }; - - const error = subgraphError || balancesError || pricesError; - - return { - loading: isSubgraphLoading || isBalancesLoading || isPricesLoading, - error, - data: { - token0, - token1, - totalUsdValue: token0.usdValue + token1.usdValue, - minTradedToken0: subgraphData.constantProductData.minTradedToken0, - disabled: subgraphData.constantProductData.disabled, - priceOracleAddress: subgraphData.constantProductData - .priceOracle as Address, - priceOracleData: subgraphData.constantProductData - .priceOracleData as `0x${string}`, - } as ICowAmm, - }; -} diff --git a/apps/cow-amm-deployer/src/lib/chainlinkPriceFeedRouter.ts b/apps/cow-amm-deployer/src/lib/chainlinkPriceFeedRouter.ts index 3aa15fba9..1bbe65937 100644 --- a/apps/cow-amm-deployer/src/lib/chainlinkPriceFeedRouter.ts +++ b/apps/cow-amm-deployer/src/lib/chainlinkPriceFeedRouter.ts @@ -1,11 +1,11 @@ import { Address, formatUnits, PublicClient } from "viem"; import { gnosis, mainnet, sepolia } from "viem/chains"; +import { IToken } from "#/lib/fetchAmmData"; import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients"; import { priceFeedAbi } from "./abis/priceFeed"; import { priceFeedRegisterAbi } from "./abis/priceFeedRegister"; -import { IToken } from "./types"; export const PRICE_FEED_REGISTER_ADDRESS = { [mainnet.id]: "0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf", diff --git a/apps/cow-amm-deployer/src/lib/fetchAmmData.ts b/apps/cow-amm-deployer/src/lib/fetchAmmData.ts new file mode 100644 index 000000000..2747ef48b --- /dev/null +++ b/apps/cow-amm-deployer/src/lib/fetchAmmData.ts @@ -0,0 +1,207 @@ +import { + buildBlockExplorerAddressURL, + NetworkFromNetworkChainId, +} from "@bleu/utils"; +import { graphql, ResultOf } from "gql.tada"; +import request from "graphql-request"; +import { Address } from "viem"; +import { gnosis, sepolia } from "viem/chains"; + +import { decodePriceOracleWithData } from "#/lib/decodePriceOracle"; +import { NEXT_PUBLIC_API_URL } from "#/lib/ponderApi"; +import { PRICE_ORACLES, PriceOracleData } from "#/lib/types"; +import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients"; + +import { priceFeedAbi } from "./abis/priceFeed"; +import { getBalancesFromContract } from "./getBalancesFromContract"; +import { getTokensExternalPrices } from "./getTokensExternalPrices"; + +export const AMM_QUERY = graphql(` + query fetchAmmData($ammId: String!) { + constantProductData(id: $ammId) { + token0 { + address + decimals + symbol + } + token1 { + address + decimals + symbol + } + minTradedToken0 + priceOracleData + priceOracle + order { + handler + chainId + } + disabled + } + } +`); + +export type IToken = NonNullable< + ResultOf["constantProductData"] +>["token0"] & { + address: Address; +}; + +export interface ITokenExtended extends IToken { + balance: string; + usdPrice: number; + usdValue: number; +} + +export type ICowAmm = ResultOf["constantProductData"] & { + priceOracleData: `0x${string}`; + priceOracle: Address; + token0: ITokenExtended; + token1: ITokenExtended; + decodedPriceOracleData: [PRICE_ORACLES, PriceOracleData]; + totalUsdValue: number; + chainId: ChainId; + priceFeedLinks: string[]; +}; + +function validateAmmId(id: string) { + const parts = id.split("-"); + if (parts.length !== 3) { + throw new Error("Invalid AMM id"); + } + + return [ + parts[0] as Address, + parts[1] as Address, + parseInt(parts[2]) as ChainId, + ] as const; +} + +async function fetchPriceFeedLinks( + decodedData: [PRICE_ORACLES, PriceOracleData], + chainId: ChainId, +): Promise { + switch (decodedData[0]) { + case PRICE_ORACLES.UNI: { + const url = getUniV2PairUrl(chainId, decodedData[1].uniswapV2PairAddress); + return url ? [url] : []; + return []; + } + case PRICE_ORACLES.BALANCER: + return [getBalancerPoolUrl(chainId, decodedData[1].balancerPoolId)]; + case PRICE_ORACLES.SUSHI: + return [getSushiV2Pair(chainId, decodedData[1].sushiSwapPairAddress)]; + case PRICE_ORACLES.CHAINLINK: { + const [feed0Link, feed1Link] = await Promise.all([ + getPriceFeedLink(chainId, decodedData[1].chainlinkPriceFeed0), + getPriceFeedLink(chainId, decodedData[1].chainlinkPriceFeed1), + ]); + return [feed0Link, feed1Link].filter(Boolean) as string[]; + } + default: { + const url = buildBlockExplorerAddressURL({ + chainId, + address: decodedData[1].customPriceOracleAddress, + }); + return url ? [url.url] : []; + } + } +} + +export async function fetchAmmData(ammId: string): Promise { + const [ammAddress, _, chainId] = validateAmmId(ammId); + + const subgraphData = await request(NEXT_PUBLIC_API_URL, AMM_QUERY, { ammId }); + if (!subgraphData || !subgraphData.constantProductData) { + throw new Error("Failed to fetch AMM data"); + } + + const token0SubgraphData = subgraphData.constantProductData.token0; + const token1SubgraphData = subgraphData.constantProductData.token1; + + const [balancesData, pricesData, decodedPriceOracleData] = await Promise.all([ + getBalancesFromContract([ + "balances", + chainId as ChainId, + ammAddress, + token0SubgraphData as IToken, + token1SubgraphData as IToken, + ]), + getTokensExternalPrices([ + "prices", + chainId as ChainId, + token0SubgraphData as IToken, + token1SubgraphData as IToken, + ]), + decodePriceOracleWithData({ + address: subgraphData.constantProductData.priceOracle as Address, + priceOracleData: subgraphData.constantProductData + .priceOracleData as Address, + chainId: chainId as ChainId, + }), + ]); + + const priceFeedLinks = await fetchPriceFeedLinks( + decodedPriceOracleData, + chainId, + ); + + const token0 = { + ...token0SubgraphData, + balance: balancesData.token0.balance, + usdPrice: pricesData.token0.externalUsdPrice, + usdValue: + Number(balancesData.token0.balance) * pricesData.token0.externalUsdPrice, + }; + + const token1 = { + ...token1SubgraphData, + balance: balancesData.token1.balance, + usdPrice: pricesData.token1.externalUsdPrice, + usdValue: + Number(balancesData.token1.balance) * pricesData.token1.externalUsdPrice, + }; + + return { + ...subgraphData.constantProductData, + token0, + token1, + totalUsdValue: token0.usdValue + token1.usdValue, + decodedPriceOracleData, + chainId, + priceFeedLinks, + } as ICowAmm; +} + +export function getUniV2PairUrl(chainId: ChainId, referencePair?: string) { + if (chainId === gnosis.id) { + return; + } + + return `https://info.uniswap.org/#/${NetworkFromNetworkChainId[chainId]}/pools/${referencePair}`; +} + +export function getSushiV2Pair(chainId: ChainId, referencePair?: string) { + return `https://www.sushi.com/pool/${[chainId]}%3A${referencePair}`; +} + +export function getBalancerPoolUrl(chainId: ChainId, poolId?: string) { + return `https://app.balancer.fi/#/${NetworkFromNetworkChainId[chainId]}-chain/pool/${poolId}`; +} + +async function getPriceFeedLink(chainId: ChainId, address?: Address) { + if (!address) return; + if (chainId === sepolia.id) return; + const publicClient = publicClientsFromIds[chainId]; + const priceFeedDescription = (await publicClient.readContract({ + address: address, + abi: priceFeedAbi, + functionName: "description", + })) as string; + const priceFeedPageName = priceFeedDescription + .replace(" / ", "-") + .toLowerCase(); + const chainName = chainId === gnosis.id ? "xdai" : "ethereum"; + + return `https://data.chain.link/feeds/${chainName}/mainnet/${priceFeedPageName}`; +} diff --git a/apps/cow-amm-deployer/src/hooks/useStandaloneAmm/getBalancesFromContract.ts b/apps/cow-amm-deployer/src/lib/getBalancesFromContract.ts similarity index 93% rename from apps/cow-amm-deployer/src/hooks/useStandaloneAmm/getBalancesFromContract.ts rename to apps/cow-amm-deployer/src/lib/getBalancesFromContract.ts index ead51e786..fbe1201b7 100644 --- a/apps/cow-amm-deployer/src/hooks/useStandaloneAmm/getBalancesFromContract.ts +++ b/apps/cow-amm-deployer/src/lib/getBalancesFromContract.ts @@ -1,7 +1,7 @@ import { Address } from "viem"; +import { IToken } from "#/lib/fetchAmmData"; import { fetchWalletTokenBalance } from "#/lib/tokenUtils"; -import { IToken } from "#/lib/types"; import { ChainId } from "#/utils/chainsPublicClients"; export async function getBalancesFromContract([ diff --git a/apps/cow-amm-deployer/src/lib/getCowProtocolUsdPrice.ts b/apps/cow-amm-deployer/src/lib/getCowProtocolUsdPrice.ts index 0de798a50..8967e72e3 100644 --- a/apps/cow-amm-deployer/src/lib/getCowProtocolUsdPrice.ts +++ b/apps/cow-amm-deployer/src/lib/getCowProtocolUsdPrice.ts @@ -3,8 +3,8 @@ import { gnosis, mainnet, sepolia } from "viem/chains"; import { ChainId } from "#/utils/chainsPublicClients"; +import { IToken } from "./fetchAmmData"; import { getNativePrice } from "./orderBookApi/fetchNativePrice"; -import { IToken } from "./types"; export const USDC: Record = { [mainnet.id]: { diff --git a/apps/cow-amm-deployer/src/hooks/useStandaloneAmm/getTokesFrom.ts b/apps/cow-amm-deployer/src/lib/getTokensExternalPrices.ts similarity index 94% rename from apps/cow-amm-deployer/src/hooks/useStandaloneAmm/getTokesFrom.ts rename to apps/cow-amm-deployer/src/lib/getTokensExternalPrices.ts index ecde6ac50..485e9cd3b 100644 --- a/apps/cow-amm-deployer/src/hooks/useStandaloneAmm/getTokesFrom.ts +++ b/apps/cow-amm-deployer/src/lib/getTokensExternalPrices.ts @@ -1,5 +1,5 @@ +import { IToken } from "#/lib/fetchAmmData"; import { fetchTokenUsdPrice } from "#/lib/tokenUtils"; -import { IToken } from "#/lib/types"; import { ChainId } from "#/utils/chainsPublicClients"; export async function getTokensExternalPrices([_, chainId, token0, token1]: [ diff --git a/apps/cow-amm-deployer/src/lib/tokenUtils.ts b/apps/cow-amm-deployer/src/lib/tokenUtils.ts index d42428d4f..9c6e172d1 100644 --- a/apps/cow-amm-deployer/src/lib/tokenUtils.ts +++ b/apps/cow-amm-deployer/src/lib/tokenUtils.ts @@ -1,12 +1,12 @@ import { Address } from "@bleu/utils"; import { formatUnits } from "viem"; +import { IToken } from "#/lib/fetchAmmData"; import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients"; import { erc20ABI } from "./abis/erc20"; import { getCoingeckoUsdPrice } from "./coingeckoApi"; import { getCowProtocolUsdPrice } from "./getCowProtocolUsdPrice"; -import { IToken } from "./types"; /** * Fetches USD price for a given currency from coingecko or CowProtocol diff --git a/apps/cow-amm-deployer/src/lib/types.ts b/apps/cow-amm-deployer/src/lib/types.ts index 902059b69..21f684c65 100644 --- a/apps/cow-amm-deployer/src/lib/types.ts +++ b/apps/cow-amm-deployer/src/lib/types.ts @@ -8,18 +8,6 @@ export enum PRICE_ORACLES { CHAINLINK = "Chainlink", } -export interface IToken { - address: Address; - symbol: string; - decimals: number; -} - -export interface ITokenExtended extends IToken { - balance: string; - usdPrice: number; - usdValue: number; -} - export interface PriceOracleData { balancerPoolId?: `0x${string}`; uniswapV2PairAddress?: Address; @@ -30,12 +18,3 @@ export interface PriceOracleData { customPriceOracleAddress?: Address; customPriceOracleData?: `0x${string}`; } -export interface ICowAmm { - token0: ITokenExtended; - token1: ITokenExtended; - totalUsdValue: number; - minTradedToken0: number; - priceOracleData: `0x${string}`; - priceOracleAddress: Address; - disabled: boolean; -} diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 93191f735..8b86e5526 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -126,9 +126,10 @@ export function buildBlockExplorerAddressURL({ address, }: { chainId?: NetworkChainId; - address: Address; + address?: Address; }) { - if (!chainId) return undefined; + if (!chainId || !address) return undefined; + const networkUrl = networkUrls[chainId as keyof typeof networkUrls]; return { url: `${networkUrl.url}address/${address}`, @@ -171,8 +172,8 @@ const networksNamesOnBalancer = [ export const networksOnBalancer = Object.fromEntries( Object.entries(networkIdEnumMap).filter(([key]) => - networksNamesOnBalancer.includes(key), - ), + networksNamesOnBalancer.includes(key) + ) ); export function networkFor(key?: string | number) { @@ -192,7 +193,7 @@ export function networkIdFor(name?: string) { export function unsafeNetworkIdFor(name: string) { return Object.keys(networkIdEnumMap).find( - (key) => networkIdEnumMap[key as keyof typeof networkIdEnumMap] === name, + (key) => networkIdEnumMap[key as keyof typeof networkIdEnumMap] === name ); }