Skip to content

Commit

Permalink
Refactor user flow
Browse files Browse the repository at this point in the history
  • Loading branch information
MattPereira committed Aug 1, 2024
1 parent bacbc94 commit a0df8f1
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 420 deletions.
296 changes: 186 additions & 110 deletions packages/nextjs/app/cow/_components/CreatePool.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,60 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import { parseEventLogs } from "viem";
import { StepsDisplay } from "./StepsDisplay";
import { Address, parseEventLogs } from "viem";
import { useAccount } from "wagmi";
import { usePublicClient } from "wagmi";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { TokenSelectModal } from "~~/components/common";
import { TransactionButton } from "~~/components/common/";
import { abis } from "~~/contracts/abis";
import { useFetchExistingPools } from "~~/hooks/cow";
import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
import { type Token, useFetchTokenList } from "~~/hooks/token";
import { useFetchExistingPools, useReadPool, useWritePool } from "~~/hooks/cow/";
import {
useScaffoldEventHistory,
useScaffoldWatchContractEvent,
useScaffoldWriteContract,
} from "~~/hooks/scaffold-eth";
import { useReadToken, useWriteToken } from "~~/hooks/token";

type TokenInput = {
rawAmount: bigint;
address: Address | undefined;
};

export const CreatePool = ({
setCurrentStep,
setUserPool,
name,
symbol,
token1,
token2,
}: {
setCurrentStep: (step: number) => void;
setUserPool: (pool: string | undefined) => void;
name: string;
symbol: string;
token1: TokenInput;
token2: TokenInput;
}) => {
const [isModalOpen1, setIsModalOpen1] = useState(false);
const [isModalOpen2, setIsModalOpen2] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
const [userPoolAddress, setUserPoolAddress] = useState<string>();

const [isCreatingPool, setIsCreatingPool] = useState(false);
const [token1, setToken1] = useState<Token>();
const [token2, setToken2] = useState<Token>();
const [isApproving, setIsApproving] = useState(false);
const [isBinding, setIsBinding] = useState(false);
const [isSettingFee, setIsSettingFee] = useState(false);
const [isFinalizing, setIsFinalizing] = useState(false);

const { address } = useAccount();
const publicClient = usePublicClient();
const { writeContractAsync: bCoWFactory } = useScaffoldWriteContract("BCoWFactory");

const { data: tokenList } = useFetchTokenList();
const { data: pool, refetch: refetchPool } = useReadPool(userPoolAddress);
const { data: existingPools } = useFetchExistingPools();

const { allowance: allowance1, refetchAllowance: refetchAllowance1 } = useReadToken(token1?.address, pool?.address);
const { allowance: allowance2, refetchAllowance: refetchAllowance2 } = useReadToken(token2?.address, pool?.address);
const { writeContractAsync: bCoWFactory } = useScaffoldWriteContract("BCoWFactory");
const { approve: approve1 } = useWriteToken(token1?.address, pool?.address);
const { approve: approve2 } = useWriteToken(token2?.address, pool?.address);
const { bind, setSwapFee, finalize } = useWritePool(pool?.address);

const createPool = async () => {
console.log("name", name);
console.log("symbol", symbol);
try {
setIsCreatingPool(true);
const hash = await bCoWFactory({
Expand All @@ -44,7 +68,8 @@ export const CreatePool = ({
logs: txReceipt.logs,
});
const newPool = (logs[0].args as { caller: string; bPool: string }).bPool;
setUserPool(newPool);
console.log("New pool address from txReceipt logs:", newPool);
setUserPoolAddress(newPool);
}
setIsCreatingPool(false);
} catch (e) {
Expand All @@ -53,37 +78,112 @@ export const CreatePool = ({
}
};

// Load tokens from local storage on mount
useEffect(() => {
const savedToken1 = localStorage.getItem("token1");
const savedToken2 = localStorage.getItem("token2");
const handleApprovals = 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);
};

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);
};

if (savedToken1) {
setToken1(JSON.parse(savedToken1));
const handleSetSwapFee = async () => {
if (!pool) throw new Error("Cannot set swap fee without a pool");
try {
setIsSettingFee(true);
await setSwapFee(pool.MAX_FEE);
setIsSettingFee(false);
refetchPool();
} catch (e) {
console.error("Error setting swap fee", e);
setIsSettingFee(false);
}
if (savedToken2) {
setToken2(JSON.parse(savedToken2));
};

const handleFinalize = async () => {
try {
setIsFinalizing(true);
await finalize();
setIsFinalizing(false);
refetchPool();
} catch (e) {
console.error("Error finalizing pool", e);
setIsFinalizing(false);
}
}, []);
};

const { data: events, isLoading: isLoadingEvents } = useScaffoldEventHistory({
contractName: "BCoWFactory",
eventName: "LOG_NEW_POOL",
fromBlock: 6381641n,
watch: true,
filters: { caller: address },
});

useScaffoldWatchContractEvent({
contractName: "BCoWFactory",
eventName: "LOG_NEW_POOL",
onLogs: logs => {
logs.forEach(log => {
const { bPool, caller } = log.args;
if (bPool && caller == address) {
console.log("useScaffoldWatchContractEvent: LOG_NEW_POOL", { bPool, caller });
setUserPoolAddress(bPool);
}
});
},
});

// Save tokens to local storage when they change
useEffect(() => {
if (token1) {
localStorage.setItem("token1", JSON.stringify(token1));
if (!isLoadingEvents && events) {
const pools = events.map(e => e.args.bPool).filter((pool): pool is string => pool !== undefined);
const mostRecentlyCreated = pools[0];
setUserPoolAddress(mostRecentlyCreated);
}
if (token2) {
localStorage.setItem("token2", JSON.stringify(token2));
}, [isLoadingEvents, events]);

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 it is not finalized and the tokens are not binded
if (pool !== undefined && !pool.isFinalized && pool.getNumTokens < 2n) {
setCurrentStep(2);
}
// 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 (pool.getSwapFee !== pool.MAX_FEE) {
setCurrentStep(3);
} else {
setCurrentStep(4);
}
}
}, [token1, token2]);
}, [pool, address, events, isLoadingEvents, userPoolAddress, pool?.isFinalized, pool?.getNumTokens]);

// Filter out tokens that are already selected
const selectableTokens = tokenList?.filter(token => token !== token1 && token !== token2);
// 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 existingPool = existingPools?.find(pool => {
if (!token1.address || !token2.address) return false;
const poolTokenAddresses = pool.allTokens.map(token => token.address);
const hasOnlyTwoTokens = poolTokenAddresses.length === 2;
const selectedToken1 = token1?.address.toLowerCase() ?? "";
const selectedToken2 = token2?.address.toLowerCase() ?? "";
const selectedToken1 = token1.address.toLowerCase() ?? "";
const selectedToken2 = token2.address.toLowerCase() ?? "";
const includesToken1 = poolTokenAddresses.includes(selectedToken1);
const includesToken2 = poolTokenAddresses.includes(selectedToken2);
const has5050Weight = pool.allTokens.every(token => token.weight === "0.5");
Expand All @@ -93,82 +193,58 @@ export const CreatePool = ({

return (
<>
<div className="flex flex-col justify-between items-center gap-5 w-full">
<div>
<h5 className="text-2xl font-bold text-center mb-5">Create a Pool</h5>

{existingPool ? (
<div className="text-xl 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>
) : (
<div className="text-xl">Choose two tokens for the pool</div>
)}
</div>

<div className="flex flex-col gap-4">
<ChooseTokenButton
selectedToken={token1}
setToken={setToken1}
isModalOpen={isModalOpen1}
tokenOptions={selectableTokens}
setIsModalOpen={setIsModalOpen1}
<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={createPool}
/>
<ChooseTokenButton
selectedToken={token2}
setToken={setToken2}
isModalOpen={isModalOpen2}
tokenOptions={selectableTokens}
setIsModalOpen={setIsModalOpen2}
) : !isSufficientAllowance ? (
<TransactionButton
title="Approve"
isPending={isApproving}
isDisabled={isApproveDisabled || isApproving}
onClick={handleApprovals}
/>
</div>

<TransactionButton
title="Create Pool"
isPending={isCreatingPool}
isDisabled={isCreatingPool || !token1 || !token2 || existingPool !== undefined}
onClick={createPool}
/>
</div>
</>
);
};

const ChooseTokenButton = ({
isModalOpen,
tokenOptions,
setToken,
setIsModalOpen,
selectedToken,
}: {
isModalOpen: boolean;
tokenOptions: Token[] | undefined;
setToken: Dispatch<SetStateAction<Token | undefined>>;
setIsModalOpen: Dispatch<SetStateAction<boolean>>;
selectedToken: Token | undefined;
}) => {
return (
<>
<div>
<button
onClick={() => setIsModalOpen(true)}
className="p-3 font-bold bg-base-100 rounded-lg flex justify-between items-center gap-3 text-lg min-w-52"
>
{selectedToken ? selectedToken.symbol : "Select Token"} <ChevronDownIcon className="w-4 h-4 mt-0.5" />
</button>
) : (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}
/>
)}
</div>

{isModalOpen && tokenOptions && (
<TokenSelectModal tokenOptions={tokenOptions} setToken={setToken} setIsModalOpen={setIsModalOpen} />
)}
</>
);
};
Loading

0 comments on commit a0df8f1

Please sign in to comment.