diff --git a/README.md b/README.md index 54092dfc..8e4212f8 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,26 @@ A frontend tool for creating and initializing various pool types on Balancer -## CoW AMMs - -### Creation Lifecycle - -1. Create a pool by calling `newBPool()` at the factory contract -2. Approve each token to be spent by the pool -3. Bind each token setting the denormalized weight to be 1e18 so that the pool is 50/50 -4. Set the swap fee to be the maximum with `pool.setSwapFee(pool.MAX_FEE)` -5. Enable normal liquidity operations by calling `pool.finalize` which mints bpt to sender - -### Resources - -- [Pool Creation Script](https://github.com/balancer/cow-amm/blob/main/script/Script.s.sol#L37) -- [Factory Addresses](https://balancerecosystem.slack.com/archives/C070C8VLSNM/p1722012869691689) -- [ABIs](https://github.com/balancer/cow-amm-subgraph/tree/main/abis) -- [Create Pool Tx](https://sepolia.etherscan.io/tx/0x2ae8e9cf4a8e5d9df26140fc265d8c7679386239de3cdaf549be5ab6108b5035) -- [Init Pool Txs](https://sepolia.etherscan.io/address/0x60048091401F27117C3DFb8136c1ec550D949B12) -- [Wonderland source code](https://github.com/defi-wonderland/balancer-v1-amm/) -- [Balancer SDK addLiquidity test](https://github.com/balancer/b-sdk/blob/7fc1a5d13b1d5408d23a8c4e856d671f40549c11/test/cowAmm/addLiquidity.integration.test.ts) -- [Balancer v2 Pool Creator demo](https://www.youtube.com/watch?v=eCjQIMHWMNs) +## Requirements + +To run the code locally, the following tools are required: + +- [Node (>= v18.17)](https://nodejs.org/en/download/) +- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install)) +- [Git](https://git-scm.com/downloads) + +## Quickstart + +1. Clone this repo & install dependencies + +``` +git clone https://github.com/balancer/pool-creator.git +cd pool-creator +yarn install +``` + +2. Start the frontend + +``` +yarn start +``` diff --git a/packages/nextjs/app/cow/_components/Alert.tsx b/packages/nextjs/app/cow/_components/Alert.tsx new file mode 100644 index 00000000..f5b82d8a --- /dev/null +++ b/packages/nextjs/app/cow/_components/Alert.tsx @@ -0,0 +1,13 @@ +interface AlertProps { + bgColor: string; + borderColor: string; + children?: React.ReactNode; // `children` can be optional +} + +export const Alert: React.FC = ({ children, bgColor, borderColor }) => { + return ( +
+
{children}
+
+ ); +}; diff --git a/packages/nextjs/app/cow/_components/CreatePool.tsx b/packages/nextjs/app/cow/_components/CreatePool.tsx index d0adc281..dabd3041 100644 --- a/packages/nextjs/app/cow/_components/CreatePool.tsx +++ b/packages/nextjs/app/cow/_components/CreatePool.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import Link from "next/link"; +import { Alert } from "./Alert"; import { StepsDisplay } from "./StepsDisplay"; import { Address } from "viem"; import { useAccount } from "wagmi"; @@ -22,6 +23,7 @@ interface CreatePoolProps { export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) => { const [currentStep, setCurrentStep] = useState(1); + const [hasAgreedToWarning, setHasAgreedToWarning] = useState(false); const [userPoolAddress, setUserPoolAddress] = useState(); // TODO: refactor to using tanstack query @@ -45,45 +47,70 @@ export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) => const handleCreatePool = async () => { console.log("name", name); console.log("symbol", symbol); - setIsCreatingPool(true); - const newPool = await createPool(); - setUserPoolAddress(newPool); - setCurrentStep(2); - setIsCreatingPool(false); + try { + setIsCreatingPool(true); + const newPool = await createPool(); + setUserPoolAddress(newPool); + setCurrentStep(2); + } catch (e) { + console.error("Error creating pool", e); + } finally { + setIsCreatingPool(false); + } }; const handleApproveTokens = async () => { - setIsApproving(true); - const txs = []; - if (token1.rawAmount > allowance1) txs.push(approve1(token1.rawAmount)); - if (token2.rawAmount > allowance2) txs.push(approve2(token2.rawAmount)); - await Promise.all(txs); - refetchAllowance1(); - refetchAllowance2(); - setIsApproving(false); + try { + setIsApproving(true); + const txs = []; + if (token1.rawAmount > allowance1) txs.push(approve1(token1.rawAmount)); + if (token2.rawAmount > allowance2) txs.push(approve2(token2.rawAmount)); + await Promise.all(txs); + refetchAllowance1(); + refetchAllowance2(); + } catch (e) { + console.error("Error approving tokens", e); + } finally { + setIsApproving(false); + } }; const handleBindTokens = async () => { if (!token1.address || !token2.address) throw new Error("Must select tokens before binding"); - setIsBinding(true); - await Promise.all([bind(token1.address, token1.rawAmount), bind(token2.address, token2.rawAmount)]); - refetchPool(); - setIsBinding(false); + try { + setIsBinding(true); + await Promise.all([bind(token1.address, token1.rawAmount), bind(token2.address, token2.rawAmount)]); + refetchPool(); + } catch (e) { + console.error("Error approving tokens", e); + } finally { + setIsBinding(false); + } }; const handleSetSwapFee = async () => { if (!pool) throw new Error("Cannot set swap fee without a pool"); - setIsSettingSwapFee(true); - await setSwapFee(pool.MAX_FEE); - refetchPool(); - setIsSettingSwapFee(false); + try { + setIsSettingSwapFee(true); + await setSwapFee(pool.MAX_FEE); + refetchPool(); + } catch (e) { + console.error("Error setting swap fee", e); + } finally { + setIsSettingSwapFee(false); + } }; const handleFinalize = async () => { - setIsFinalizing(true); - await finalize(); - refetchPool(); - setIsFinalizing(false); + try { + setIsFinalizing(true); + await finalize(); + refetchPool(); + } catch (e) { + console.error("Error finalizing pool", e); + } finally { + setIsFinalizing(false); + } }; const { data: events, isLoading: isLoadingEvents } = useScaffoldEventHistory({ @@ -116,6 +143,10 @@ export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) => } }, [isLoadingEvents, events]); + const validTokenAmounts = token1.rawAmount > 0n && token2.rawAmount > 0n; + // Determine if token allowances are sufficient + const isSufficientAllowance = allowance1 >= token1.rawAmount && allowance2 >= token2.rawAmount && validTokenAmounts; + useEffect(() => { // If the user has no pools or their most recent pool is finalized if (userPoolAddress || pool?.isFinalized) { @@ -123,7 +154,7 @@ export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) => } // If the user has created a pool, but not finalized and tokens not binded if (pool !== undefined && !pool.isFinalized && pool.getNumTokens < 2n) { - if (allowance1 < token1.rawAmount || allowance2 < token2.rawAmount) { + if (!isSufficientAllowance) { setCurrentStep(2); } else { setCurrentStep(3); @@ -131,6 +162,7 @@ export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) => } // If the user has a pool with 2 tokens binded, but it has not been finalized if (pool !== undefined && !pool.isFinalized && pool.getNumTokens === 2n) { + // If the pool swap fee has not been set to the maximum if (pool.getSwapFee !== pool.MAX_FEE) { setCurrentStep(4); } else { @@ -151,12 +183,8 @@ export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) => token2.rawAmount, ]); - // Must choose tokens and set amounts approve button is enabled - const isApproveDisabled = - token1.rawAmount === 0n || token1.address === undefined || token2.rawAmount === 0n || token2.address === undefined; - // Determine if token allowances are sufficient - const isSufficientAllowance = - allowance1 >= token1.rawAmount && allowance2 >= token2.rawAmount && token1.rawAmount > 0n && token2.rawAmount > 0n; + const isApproveDisabled = // If user has not selected tokens or entered amounts + token1.rawAmount === 0n || token2.rawAmount === 0n || token1.address === undefined || token2.address === undefined; const existingPool = existingPools?.find(pool => { if (!token1.address || !token2.address) return false; @@ -173,57 +201,103 @@ export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) => return ( <> + {existingPool ? ( + + A CoW AMM pool with selected tokens already exists. To add liquidity, go to the{" "} + + Balancer v3 frontend. + + + ) : ( + +
+
+
+ +
+
+
+
+ )} +
- {existingPool ? ( -
- A CoW AMM with selected tokens{" "} - - already exists! - -
- ) : !userPoolAddress || pool?.isFinalized ? ( - - ) : !isSufficientAllowance ? ( - - ) : (pool?.getNumTokens || 0) < 2 ? ( - - ) : pool?.MAX_FEE !== pool?.getSwapFee ? ( - - ) : ( - - )} + {(() => { + switch (currentStep) { + case 1: + return ( + + ); + case 2: + return ( + + ); + case 3: + return ( + + ); + case 4: + return ( + + ); + case 5: + return ( + + ); + default: + return null; + } + })()}
); diff --git a/packages/nextjs/app/cow/_components/StepsDisplay.tsx b/packages/nextjs/app/cow/_components/StepsDisplay.tsx index c4e1bfdb..e664e34d 100644 --- a/packages/nextjs/app/cow/_components/StepsDisplay.tsx +++ b/packages/nextjs/app/cow/_components/StepsDisplay.tsx @@ -3,7 +3,7 @@ */ export const StepsDisplay = ({ currentStep }: { currentStep: number }) => { return ( -
    +
    • Create
    • 1 && "step-accent"}`}>Approve
    • 2 && "step-accent"}`}>Bind
    • diff --git a/packages/nextjs/app/cow/page.tsx b/packages/nextjs/app/cow/page.tsx index 0b147d7d..b16d0a3f 100644 --- a/packages/nextjs/app/cow/page.tsx +++ b/packages/nextjs/app/cow/page.tsx @@ -29,13 +29,11 @@ const CoW: NextPage = () => {
      -

      Create a CoW AMM Pool

      +

      Create a CoW AMM Pool

      -
      +
      -
      -
      Configure your pool
      -
      +
      Configure your pool
      Select pool tokens:
      diff --git a/packages/nextjs/public/thumbnail.jpg b/packages/nextjs/public/thumbnail.jpg index 2551ead3..047b8100 100644 Binary files a/packages/nextjs/public/thumbnail.jpg and b/packages/nextjs/public/thumbnail.jpg differ