Skip to content

Commit

Permalink
Fix indefinite button pending state if user rejects transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
MattPereira committed Aug 2, 2024
1 parent 20e5b1c commit 827e227
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 106 deletions.
43 changes: 23 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
13 changes: 13 additions & 0 deletions packages/nextjs/app/cow/_components/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface AlertProps {
bgColor: string;
borderColor: string;
children?: React.ReactNode; // `children` can be optional
}

export const Alert: React.FC<AlertProps> = ({ children, bgColor, borderColor }) => {
return (
<div className={`${bgColor} border ${borderColor} rounded-lg p-3 overflow-auto w-full sm:w-[555px] `}>
<div>{children}</div>
</div>
);
};
234 changes: 154 additions & 80 deletions packages/nextjs/app/cow/_components/CreatePool.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string>();

// TODO: refactor to using tanstack query
Expand All @@ -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({
Expand Down Expand Up @@ -116,21 +143,26 @@ 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) {
setCurrentStep(1);
}
// 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);
}
}
// 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 {
Expand All @@ -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;
Expand All @@ -173,57 +201,103 @@ export const CreatePool = ({ name, symbol, token1, token2 }: CreatePoolProps) =>

return (
<>
{existingPool ? (
<Alert bgColor="bg-[#d64e4e2b]" borderColor="border-red-500">
A CoW AMM pool with selected tokens already exists. To add liquidity, go to the{" "}
<Link
className="link"
rel="noopener noreferrer"
target="_blank"
href={`https://balancer.fi/pools/${existingPool.chain.toLowerCase()}/cow/${existingPool.address}`}
>
Balancer v3 frontend.
</Link>
</Alert>
) : (
<Alert bgColor="bg-[#fb923c40]" borderColor="border-orange-400">
<div className="flex gap-2">
<div>
<div className="form-control">
<label className="label cursor-pointer flex gap-4 m-0 p-0">
<input
type="checkbox"
className="checkbox rounded-lg"
onChange={() => setHasAgreedToWarning(!hasAgreedToWarning)}
checked={hasAgreedToWarning}
/>
<span className="">
I understand that assets must be added proportionally, or I risk loss of funds via arbitrage.
</span>
</label>
</div>
</div>
</div>
</Alert>
)}

<StepsDisplay currentStep={currentStep} />

<div className="min-w-96">
{existingPool ? (
<div className="text-lg text-red-400">
A CoW AMM with selected tokens{" "}
<Link
className="link"
rel="noopener noreferrer"
target="_blank"
href={`https://balancer.fi/pools/${existingPool.chain.toLowerCase()}/cow/${existingPool.address}`}
>
already exists!
</Link>
</div>
) : !userPoolAddress || pool?.isFinalized ? (
<TransactionButton
title="Create Pool"
isPending={isCreatingPool}
isDisabled={isCreatingPool || !token1.address || !token2.address || existingPool !== undefined}
onClick={handleCreatePool}
/>
) : !isSufficientAllowance ? (
<TransactionButton
title="Approve"
isPending={isApproving}
isDisabled={isApproveDisabled || isApproving}
onClick={handleApproveTokens}
/>
) : (pool?.getNumTokens || 0) < 2 ? (
<TransactionButton
title="Add Liquidity"
isPending={isBinding}
isDisabled={isBinding}
onClick={handleBindTokens}
/>
) : pool?.MAX_FEE !== pool?.getSwapFee ? (
<TransactionButton
title="Set Swap Fee"
onClick={handleSetSwapFee}
isPending={isSettingFee}
isDisabled={isSettingFee}
/>
) : (
<TransactionButton
title="Finalize"
onClick={handleFinalize}
isPending={isFinalizing}
isDisabled={isFinalizing}
/>
)}
{(() => {
switch (currentStep) {
case 1:
return (
<TransactionButton
title="Create Pool"
isPending={isCreatingPool}
isDisabled={
isCreatingPool ||
!token1.address ||
!token2.address ||
!validTokenAmounts ||
!hasAgreedToWarning ||
existingPool !== undefined ||
name === "" ||
symbol === ""
}
onClick={handleCreatePool}
/>
);
case 2:
return (
<TransactionButton
title="Approve"
isPending={isApproving}
isDisabled={isApproveDisabled || isApproving}
onClick={handleApproveTokens}
/>
);
case 3:
return (
<TransactionButton
title="Add Liquidity"
isPending={isBinding}
isDisabled={isBinding}
onClick={handleBindTokens}
/>
);
case 4:
return (
<TransactionButton
title="Set Swap Fee"
onClick={handleSetSwapFee}
isPending={isSettingFee}
isDisabled={isSettingFee}
/>
);
case 5:
return (
<TransactionButton
title="Finalize"
onClick={handleFinalize}
isPending={isFinalizing}
isDisabled={isFinalizing}
/>
);
default:
return null;
}
})()}
</div>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/cow/_components/StepsDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
export const StepsDisplay = ({ currentStep }: { currentStep: number }) => {
return (
<ul className="steps steps-vertical md:steps-horizontal w-[555px] bg-base-200 py-4 rounded-xl">
<ul className="steps steps-vertical md:steps-horizontal w-full sm:w-[555px] bg-base-200 py-4 rounded-xl">
<li className="step step-accent">Create </li>
<li className={`step ${currentStep > 1 && "step-accent"}`}>Approve </li>
<li className={`step ${currentStep > 2 && "step-accent"}`}>Bind</li>
Expand Down
8 changes: 3 additions & 5 deletions packages/nextjs/app/cow/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@ const CoW: NextPage = () => {
<div className="flex-grow bg-base-300">
<div className="max-w-screen-2xl mx-auto">
<div className="flex items-center flex-col flex-grow py-10 px-5 lg:px-10 gap-7">
<h1 className="text-4xl font-bold">Create a CoW AMM Pool</h1>
<h1 className="text-2xl md:text-4xl font-bold">Create a CoW AMM Pool</h1>

<div className="bg-base-200 p-7 rounded-xl w-[555px] flex flex-grow">
<div className="bg-base-200 p-7 rounded-xl w-full sm:w-[555px] flex flex-grow">
<div className="flex flex-col items-center gap-4 w-full">
<div>
<h5 className="text-2xl font-bold">Configure your pool</h5>
</div>
<h5 className="text-xl md:text-2xl font-bold">Configure your pool</h5>

<div className="w-full">
<div className="ml-1 mb-1">Select pool tokens:</div>
Expand Down
Binary file modified packages/nextjs/public/thumbnail.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 827e227

Please sign in to comment.