Skip to content

Commit

Permalink
[CoW Amm Deployer] Calculate min amount out (#608)
Browse files Browse the repository at this point in the history
* chore: replace module factory to single module design

* fetch AMM info and show it to user

* enable stop amm button and create tx

* chore: add advanced options with min traded token 0 field

* check if module is enable or not

* run formatter
  • Loading branch information
yvesfracari authored Feb 28, 2024
1 parent 3118a5c commit cef6e71
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 31 deletions.
64 changes: 39 additions & 25 deletions apps/cow-amm-deployer/src/app/amm/(components)/CreateAmmForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk";
import { TokenBalance } from "@gnosis.pm/safe-apps-sdk";
import { zodResolver } from "@hookform/resolvers/zod";
import { slateDarkA } from "@radix-ui/colors";
import { InfoCircledIcon } from "@radix-ui/react-icons";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm, UseFormReturn } from "react-hook-form";

Expand All @@ -10,44 +12,45 @@ import { Input } from "#/components/Input";
import { Select, SelectItem } from "#/components/Select";
import { Spinner } from "#/components/Spinner";
import { Tooltip } from "#/components/Tooltip";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "#/components/ui/accordion";
import { Form, FormMessage } from "#/components/ui/form";
import { Label } from "#/components/ui/label";
import { useFallbackState } from "#/hooks/useFallbackState";
import { useRawTxData } from "#/hooks/useRawTxData";
import { useSafeBalances } from "#/hooks/useSafeBalances";
import { createAmmSchema } from "#/lib/schema";
import { createAMMArgs } from "#/lib/transactionFactory";
import { FALLBACK_STATES, IToken, PRICE_ORACLES } from "#/lib/types";
import { ChainId } from "#/utils/chainsPublicClients";
import { cowTokenList } from "#/utils/cowTokenList";

import { FallbackAndDomainWarning } from "./FallbackAndDomainWarning";
import { TokenSelect } from "./TokenSelect";

const getDefaultData = (chainId: ChainId) => {
const token0 = cowTokenList.find(
(token) => token.chainId === chainId && token.symbol === "COW",
const getNewMinTradeToken0 = (newToken0: IToken, assets: TokenBalance[]) => {
const asset0 = assets.find(
(asset) =>
asset.tokenInfo.address.toLowerCase() === newToken0.address.toLowerCase(),
);
const token1 = cowTokenList.find(
(token) => token.chainId === chainId && token.symbol === "wstETH",
);
return {
token0,
token1,
chainId,
minTradedToken0: 0.2,
priceOracle: PRICE_ORACLES.BALANCER,
balancerPoolId:
"0x4cdabe9e07ca393943acfb9286bbbd0d0a310ff600020000000000000000005c",
};
return 10 / Number(asset0?.fiatConversion);
};

export function CreateAmmForm() {
const {
safe: { chainId, safeAddress },
safe: { safeAddress, chainId },
} = useSafeAppsSDK();
const router = useRouter();
const { assets } = useSafeBalances();

const form = useForm<typeof createAmmSchema._type>({
resolver: zodResolver(createAmmSchema),
defaultValues: getDefaultData(chainId as ChainId),
defaultValues: {
chainId,
safeAddress: safeAddress,
},
});

const {
Expand All @@ -66,6 +69,7 @@ export function CreateAmmForm() {
await createAMMArgs(data).then((txArgs) => {
sendTransactions(txArgs);
});
router.push("/amm");
};

useEffect(() => {
Expand Down Expand Up @@ -94,6 +98,7 @@ export function CreateAmmForm() {
address: token.address,
symbol: token.symbol,
});
setValue("minTradedToken0", getNewMinTradeToken0(token, assets));
}}
label="First Token"
selectedToken={token0 ?? undefined}
Expand Down Expand Up @@ -123,13 +128,22 @@ export function CreateAmmForm() {
)}
</div>
</div>
<Input
label="Minimum amount of the first token to be traded on each order"
type="number"
step={10 ** -token0.decimals}
name="minTradedToken0"
/>
<PriceOracleFields form={form} />
<Accordion className="w-full" type="single" collapsible>
<AccordionItem value="advancedOptions" key="advancedOption">
<AccordionTrigger>Advanced Options</AccordionTrigger>
<AccordionContent>
<Input
label="Minimum first token amount on each order"
type="number"
step={10 ** (-token0?.decimals || 18)}
name="minTradedToken0"
tooltipText="This parameter is used to not overload the CoW Orderbook with small orders. By default, 10 dollars worth of the first token will be the minimum amount for each order."
/>
</AccordionContent>
</AccordionItem>
</Accordion>

{fallbackState !== FALLBACK_STATES.HAS_DOMAIN_VERIFIER && (
<FallbackAndDomainWarning
confirmedFallbackSetup={confirmedFallbackSetup}
Expand Down
149 changes: 149 additions & 0 deletions apps/cow-amm-deployer/src/app/amm/HomePageWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk";
import {
ArrowTopRightIcon,
Pencil2Icon,
PlusIcon,
StopIcon,
} from "@radix-ui/react-icons";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { gnosis, mainnet } from "viem/chains";

import { Button } from "#/components";
import { LinkComponent } from "#/components/Link";
import { Spinner } from "#/components/Spinner";
import WalletNotConnected from "#/components/WalletNotConnected";
import { useRawTxData } from "#/hooks/useRawTxData";
import { useRunningAMM } from "#/hooks/useRunningAmmInfo";
import { TRANSACTION_TYPES } from "#/lib/transactionFactory";
import { PRICE_ORACLES } from "#/lib/types";
import { getBalancerPoolUrl } from "#/utils/balancerPoolUrl";
import { ChainId } from "#/utils/chainsPublicClients";
import { getUniV2PairUrl } from "#/utils/univ2pairUrl";

import { PoolCompositionTable } from "./(components)/PoolCompositionTable";

export function HomePageWrapper() {
const router = useRouter();
const { sendTransactions } = useRawTxData();
const { safe, connected } = useSafeAppsSDK();
const { loaded, isAmmRunning, cowAmm } = useRunningAMM();

if (!connected) {
return <WalletNotConnected />;
}

if (!loaded) {
return <Spinner />;
}

if (safe.chainId !== mainnet.id && safe.chainId !== gnosis.id) {
return (
<div className="flex h-full w-full flex-col items-center rounded-3xl px-12 py-16 md:py-20">
<div className="text-center text-3xl text-amber9">
This app isn't available on this network
</div>
<div className="text-xl text-white">
Please change to {gnosis.name} or {mainnet.name}
</div>
</div>
);
}

if (!isAmmRunning || !cowAmm) {
return (
<div className="flex w-full justify-center">
<div className="my-10 flex w-9/12 flex-col gap-y-5 justify center">
<div className="flex items-center justify-between gap-x-8">
<div className="flex flex-col gap-1">
<h1 className="text-3xl text-slate12">
CoW AMM (Automatic Market Maker)
</h1>
<span>
There isn't any AMM running. Create it to provide liquidity
without suffering with LVR.
</span>
</div>
<div className="flex gap-4">
<LinkComponent
loaderColor="amber"
href={`/amm/new`}
content={
<Button
className="flex items-center gap-1 py-3 px-6"
title="New order"
>
<PlusIcon />
Create AMM
</Button>
}
/>
</div>
</div>
</div>
</div>
);
}

const priceOracleLink =
cowAmm.priceOracle === PRICE_ORACLES.BALANCER
? getBalancerPoolUrl(
safe.chainId as ChainId,
cowAmm.priceOracleData?.balancerPoolId,
)
: getUniV2PairUrl(
safe.chainId as ChainId,
cowAmm.priceOracleData?.uniswapV2PairAddress,
);

return (
<div className="flex w-full justify-center">
<div className="my-10 flex w-9/12 flex-col gap-y-5 justify center">
<div className="flex items-center justify-between gap-x-8">
<div className="flex flex-col gap-1">
<h1 className="text-3xl text-slate12">
CoW AMM (Automatic Market Maker)
</h1>
<div className="flex flex-row gap-x-1 items-center">
<span>Using price information from {cowAmm.priceOracle}</span>
{priceOracleLink && (
<Link href={priceOracleLink} target="_blank">
<ArrowTopRightIcon className="hover:text-slate11" />
</Link>
)}
</div>
</div>
<div className="flex gap-4">
<Button
className="flex items-center gap-1 py-3 px-6 "
color="tomato"
onClick={async () => {
await sendTransactions([
{
type: TRANSACTION_TYPES.STOP_COW_AMM,
},
]);
router.refresh();
}}
>
<StopIcon />
Stop
</Button>
<Button className="flex items-center gap-1 py-3 px-6" disabled>
<Pencil2Icon />
Edit
</Button>
</div>
</div>
<div className="flex flex-col">
<span className="text-xl my-2 border-b-2 border-blue7">
AMM Composition
</span>
</div>
<PoolCompositionTable cowAmm={cowAmm} />
</div>
</div>
);
}
57 changes: 57 additions & 0 deletions apps/cow-amm-deployer/src/components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import * as React from "react";

import { cn } from "#/lib/utils";

const Accordion = AccordionPrimitive.Root;

const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";

const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;

const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;

export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
2 changes: 1 addition & 1 deletion apps/cow-amm-deployer/src/lib/gql/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GraphQLClient } from "graphql-request";

import { getSdk } from "#/lib/gql/generated";

export const ENDPOINT = "http://localhost:42069";
export const ENDPOINT = "https://composable-cow-api.up.railway.app";

const client = new GraphQLClient(ENDPOINT);

Expand Down
24 changes: 19 additions & 5 deletions apps/cow-amm-deployer/src/lib/transactionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BaseTransaction } from "@gnosis.pm/safe-apps-sdk";
import { Address, encodeFunctionData, parseUnits } from "viem";

import { FALLBACK_STATES, PRICE_ORACLES } from "#/lib/types";
import { ChainId } from "#/utils/chainsPublicClients";
import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients";

import { cowAmmModuleAbi } from "./abis/cowAmmModule";
import { gnosisSafeV12 } from "./abis/gnosisSafeV12";
Expand Down Expand Up @@ -212,6 +212,23 @@ export async function createAMMArgs(data: typeof createAmmSchema._type) {
}
})();

const publicClient = publicClientsFromIds[data.chainId as ChainId];

const isCoWAmmModuleEnabled = await publicClient.readContract({
address: data.safeAddress as Address,
abi: gnosisSafeV12,
functionName: "isModuleEnabled",
args: [COW_AMM_MODULE_ADDRESS],
});
const enableCoWAmmTxs = isCoWAmmModuleEnabled
? []
: [
{
type: TRANSACTION_TYPES.ENABLE_COW_AMM_MODULE,
safeAddress: data.safeAddress,
} as enableCowAmmModuleArgs,
];

const metadataApi = new MetadataApi();

const appDataDoc = await metadataApi.generateAppDataDoc({
Expand All @@ -228,10 +245,7 @@ export async function createAMMArgs(data: typeof createAmmSchema._type) {

return [
...fallbackTxs,
{
type: TRANSACTION_TYPES.ENABLE_COW_AMM_MODULE,
safeAddress: data.safeAddress,
} as enableCowAmmModuleArgs,
...enableCoWAmmTxs,
{
type: TRANSACTION_TYPES.CREATE_COW_AMM,
token0: data.token0.address,
Expand Down

0 comments on commit cef6e71

Please sign in to comment.