Skip to content

Commit

Permalink
[CoW AMM Deployer] Integrate chainlink contract into the application (#…
Browse files Browse the repository at this point in the history
…677)

* add custom oracle feature

* chore: add bleu ui

* remove unused context

* chore: use bleu ui toaster

* refactor: separete price oracle inputs into new files

* chore: add npm token on github pnpm setup action

* wip: move .npmrc to repo root

* add sushi gql

* chore: remove rainbowkit colors from milkman project

* chore: add missing dependencies on balancer tools

* add npm_token input on all workflows

* add input into setup-pnpm workflow

* chore: run formatter

* add sushiswap pricechecker component

* run lint

* chore: rename @bleu-fi/ui to @bleu/ui due to org name change

* remove bleu-ui from tsconfig

* chore: use try and catch patern and change tooltip texts

* add chainlink price oracle and rename other price oracle components

* add chainlink information on manager page

* fix: load current parameters on cow amm edit

* fix: error message overload
  • Loading branch information
yvesfracari authored May 22, 2024
1 parent 4640988 commit 968becb
Show file tree
Hide file tree
Showing 17 changed files with 1,117 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Address } from "viem";
import { gnosis, sepolia } from "viem/chains";

import { priceFeedAbi } from "#/lib/abis/priceFeed";
import { ICowAmm } from "#/lib/types";
import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients";

export function ChainlinkPriceInformation({ cowAmm }: { cowAmm: ICowAmm }) {
const { safe } = useSafeAppsSDK();
const [priceFeed0Link, setPriceFeed0Link] = useState<string>();
const [priceFeed1Link, setPriceFeed1Link] = useState<string>();

const fetchPriceFeedLinks = async () => {
const [priceFeed0Link, priceFeed1Link] = await Promise.all([
getPriceFeedLink(
safe.chainId as ChainId,
cowAmm.priceOracleData.chainlinkPriceFeed0,
),
getPriceFeedLink(
safe.chainId as ChainId,
cowAmm.priceOracleData.chainlinkPriceFeed1,
),
]);
setPriceFeed0Link(priceFeed0Link);
setPriceFeed1Link(priceFeed1Link);
};

useEffect(() => {
fetchPriceFeedLinks();
}, []);
return (
<div className="flex flex-row gap-x-1 items-start hover:text-foreground/90">
<span>Using price information from Chainlink</span>
{priceFeed0Link && (
<Link
href={priceFeed0Link}
target="_blank"
className="text-primary hover:text-primary/80 text-xs"
>
1
</Link>
)}
{priceFeed1Link && (
<Link
href={priceFeed1Link}
target="_blank"
className="text-primary hover:text-primary/80 text-xs"
>
2
</Link>
)}
</div>
);
}

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}`;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ICowAmm, PRICE_ORACLES } from "#/lib/types";

import { BalancerPriceInformation } from "./BalancerPriceInformation";
import { ChainlinkPriceInformation } from "./ChainlinkPriceInformation";
import { CustomPriceInformation } from "./CustomPriceInformation";
import { SushiV2PriceInformation } from "./SushiV2PriceInformation";
import { UniswapV2PriceInformation } from "./UniswapV2PriceInformation";
Expand All @@ -13,6 +14,8 @@ export function PriceInformation({ cowAmm }: { cowAmm: ICowAmm }) {
return <BalancerPriceInformation cowAmm={cowAmm} />;
case PRICE_ORACLES.SUSHI:
return <SushiV2PriceInformation cowAmm={cowAmm} />;
case PRICE_ORACLES.CHAINLINK:
return <ChainlinkPriceInformation cowAmm={cowAmm} />;
default:
return <CustomPriceInformation cowAmm={cowAmm} />;
}
Expand Down
26 changes: 11 additions & 15 deletions apps/cow-amm-deployer/src/app/new/(components)/AmmForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ import { FALLBACK_STATES, IToken, PRICE_ORACLES } from "#/lib/types";
import { cn } from "#/lib/utils";
import { ChainId } from "#/utils/chainsPublicClients";

import { BalancerWeightedPriceCheckerForm } from "./BalancerWeightedPriceCheckerForm";
import { CustomPriceCheckerForm } from "./CustomPriceCheckerForm";
import { BalancerWeightedForm } from "./BalancerWeightedForm";
import { ChainlinkForm } from "./ChainlinkForm";
import { CustomOracleForm } from "./CustomOracleForm";
import { FallbackAndDomainWarning } from "./FallbackAndDomainWarning";
import { SushiV2PriceChecker } from "./SushiPriceChecker";
import { UniswapV2PriceChecker } from "./UniswapV2PriceChecker";
import { SushiForm } from "./SushiForm";
import { UniswapV2Form } from "./UniswapV2Form";

const getNewMinTradeToken0 = async (newToken0: IToken, chainId: ChainId) => {
return fetchTokenUsdPrice({
Expand Down Expand Up @@ -265,7 +266,7 @@ function PriceOracleFields({
placeholder={priceOracle}
/>
{errors.priceOracle && (
<FormMessage className="h-6 text-sm text-destructive w-full">
<FormMessage className="text-sm text-destructive w-full">
<p className="text-wrap">
{errors.priceOracle.message as string}
</p>
Expand All @@ -275,17 +276,12 @@ function PriceOracleFields({
</div>

{priceOracle === PRICE_ORACLES.BALANCER && (
<BalancerWeightedPriceCheckerForm form={form} />
)}
{priceOracle === PRICE_ORACLES.UNI && (
<UniswapV2PriceChecker form={form} />
)}
{priceOracle === PRICE_ORACLES.CUSTOM && (
<CustomPriceCheckerForm form={form} />
)}
{priceOracle === PRICE_ORACLES.SUSHI && (
<SushiV2PriceChecker form={form} />
<BalancerWeightedForm form={form} />
)}
{priceOracle === PRICE_ORACLES.UNI && <UniswapV2Form form={form} />}
{priceOracle === PRICE_ORACLES.CUSTOM && <CustomOracleForm form={form} />}
{priceOracle === PRICE_ORACLES.SUSHI && <SushiForm form={form} />}
{priceOracle === PRICE_ORACLES.CHAINLINK && <ChainlinkForm form={form} />}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { pools } from "#/lib/gqlBalancer";
import { ammFormSchema } from "#/lib/schema";
import { loadDEXPriceCheckerErrorText } from "#/lib/utils";

export function BalancerWeightedPriceCheckerForm({
export function BalancerWeightedForm({
form,
}: {
form: UseFormReturn<typeof ammFormSchema._type>;
Expand Down
87 changes: 87 additions & 0 deletions apps/cow-amm-deployer/src/app/new/(components)/ChainlinkForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";

import { toast } from "@bleu/ui";
import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk";
import { UseFormReturn } from "react-hook-form";

import { Input } from "#/components/Input";
import { CHAINS_ORACLE_ROUTER_FACTORY } from "#/lib/chainlinkPriceFeedRouter";
import { ammFormSchema } from "#/lib/schema";
import { ChainId } from "#/utils/chainsPublicClients";

const TOOLTIP_PRICE_FEED_TEXT =
"The address of the Chainlink price feed that will be used as the price oracle for the second token. Both price feeds have to have the same token as base. Click on the load button to try to find a valid Chainlink price feed pair.";

const TOOLTIP_PRICE_FEED_LINK = "https://data.chain.link/feeds";

export function ChainlinkForm({
form,
}: {
form: UseFormReturn<typeof ammFormSchema._type>;
}) {
const { register, setValue, watch } = form;
const {
safe: { chainId },
} = useSafeAppsSDK();

const token0 = watch("token0");
const token1 = watch("token1");
return (
<div className="flex flex-col gap-y-1">
<div className="flex h-fit justify-between gap-x-7">
<div className="w-full">
<Input
{...register("chainlinkPriceFeed0")}
label="First Token Price Feed"
tooltipText={TOOLTIP_PRICE_FEED_TEXT}
tooltipLink={TOOLTIP_PRICE_FEED_LINK}
/>
</div>
<div className="w-full">
<Input
{...register("chainlinkPriceFeed1")}
label="Second Token Price Feed"
tooltipText={TOOLTIP_PRICE_FEED_TEXT}
tooltipLink={TOOLTIP_PRICE_FEED_LINK}
/>
</div>
</div>
<button
type="button"
className="flex flex-row outline-none hover:text-highlight text-xs my-1"
onClick={async () => {
try {
const oracleRouterFactory =
CHAINS_ORACLE_ROUTER_FACTORY[chainId as ChainId];
const oracleRouter = new oracleRouterFactory({
chainId: chainId as ChainId,
token0,
token1,
});

const { priceFeedToken0, priceFeedToken1 } =
await oracleRouter.findRoute();
setValue("chainlinkPriceFeed0", priceFeedToken0);
setValue("chainlinkPriceFeed1", priceFeedToken1);
} catch (e) {
toast({
title: "Price feed not found",
description:
"No Chainlink price feed pair found for these tokens",
variant: "destructive",
});
}
}}
>
Load Chainlink Price Feeds
</button>
<Input
{...register("chainlinkTimeThresholdInHours")}
type="number"
label="Maximum time since last price feed update (hours)"
tooltipText="If the price feed is older than this value, the order will be rejected"
defaultValue={24}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AlertCard } from "#/components/AlertCard";
import { Input } from "#/components/Input";
import { ammFormSchema } from "#/lib/schema";

export function CustomPriceCheckerForm({
export function CustomOracleForm({
form,
}: {
form: UseFormReturn<typeof ammFormSchema._type>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { pairs } from "#/lib/gqlSushi";
import { ammFormSchema } from "#/lib/schema";
import { loadDEXPriceCheckerErrorText } from "#/lib/utils";

export function SushiV2PriceChecker({
export function SushiForm({
form,
}: {
form: UseFormReturn<typeof ammFormSchema._type>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { pairs } from "#/lib/gqlUniswapV2";
import { ammFormSchema } from "#/lib/schema";
import { loadDEXPriceCheckerErrorText } from "#/lib/utils";

export function UniswapV2PriceChecker({
export function UniswapV2Form({
form,
}: {
form: UseFormReturn<typeof ammFormSchema._type>;
Expand Down
7 changes: 7 additions & 0 deletions apps/cow-amm-deployer/src/app/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ function cowAmmToFormValues(cowAmm: ICowAmm): FieldValues {
priceOracle: cowAmm.priceOracle,
balancerPoolId: cowAmm.priceOracleData.balancerPoolId,
uniswapV2Pair: cowAmm.priceOracleData.uniswapV2PairAddress,
sushiSwapPair: cowAmm.priceOracleData.sushiSwapPairAddress,
chainlinkPriceFeed0: cowAmm.priceOracleData.chainlinkPriceFeed0,
chainlinkPriceFeed1: cowAmm.priceOracleData.chainlinkPriceFeed1,
chainlinkTimeThresholdInHours:
cowAmm.priceOracleData.chainlinkTimeThresholdInHours,
customPriceOracleAddress: cowAmm.priceOracleData.customPriceOracleAddress,
customPriceOracleData: cowAmm.priceOracleData.customPriceOracleData,
};
}

Expand Down
2 changes: 1 addition & 1 deletion apps/cow-amm-deployer/src/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const Input = React.forwardRef<HTMLInputElement, IInput>(
/>
</FormControl>
{errorMessage && (
<FormMessage className="mt-1 h-6 text-sm text-destructive">
<FormMessage className="mt-1 text-sm text-destructive">
<span>{errorMessage}</span>
</FormMessage>
)}
Expand Down
80 changes: 80 additions & 0 deletions apps/cow-amm-deployer/src/lib/abis/priceFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export const priceFeedAbi = [
{
inputs: [
{ internalType: "uint8", name: "_decimals", type: "uint8" },
{ internalType: "string", name: "_description", type: "string" },
{ internalType: "uint256", name: "_version", type: "uint256" },
{ internalType: "int256", name: "_price", type: "int256" },
],
stateMutability: "nonpayable",
type: "constructor",
},
{
inputs: [],
name: "decimals",
outputs: [{ internalType: "uint8", name: "", type: "uint8" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "description",
outputs: [{ internalType: "string", name: "", type: "string" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "uint80", name: "", type: "uint80" }],
name: "getRoundData",
outputs: [
{ internalType: "uint80", name: "roundId", type: "uint80" },
{ internalType: "int256", name: "currentPrice", type: "int256" },
{ internalType: "uint256", name: "startedAt", type: "uint256" },
{ internalType: "uint256", name: "currentUpdatedAt", type: "uint256" },
{ internalType: "uint80", name: "answeredInRound", type: "uint80" },
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "latestRoundData",
outputs: [
{ internalType: "uint80", name: "roundId", type: "uint80" },
{ internalType: "int256", name: "answer", type: "int256" },
{ internalType: "uint256", name: "startedAt", type: "uint256" },
{ internalType: "uint256", name: "currentUpdatedAt", type: "uint256" },
{ internalType: "uint80", name: "answeredInRound", type: "uint80" },
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "price",
outputs: [{ internalType: "int256", name: "", type: "int256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "int256", name: "_price", type: "int256" }],
name: "setPrice",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "updatedAt",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "version",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
];
Loading

0 comments on commit 968becb

Please sign in to comment.