diff --git a/packages/nextjs/app/cow/_components/PoolConfiguration.tsx b/packages/nextjs/app/cow/_components/PoolConfiguration.tsx index a4bb70b..4845020 100644 --- a/packages/nextjs/app/cow/_components/PoolConfiguration.tsx +++ b/packages/nextjs/app/cow/_components/PoolConfiguration.tsx @@ -182,6 +182,7 @@ export const PoolConfiguration = () => { token2Amount, poolName, poolSymbol, + step: 1, }); }} /> diff --git a/packages/nextjs/app/cow/_components/PoolCreation.tsx b/packages/nextjs/app/cow/_components/PoolCreation.tsx index ce67ab8..6a40b40 100644 --- a/packages/nextjs/app/cow/_components/PoolCreation.tsx +++ b/packages/nextjs/app/cow/_components/PoolCreation.tsx @@ -1,11 +1,19 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { StepsDisplay } from "./StepsDisplay"; import { Address, parseUnits } from "viem"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { Alert, ExternalLinkButton, TextField, TokenField, TransactionButton } from "~~/components/common/"; -import { useBindPool, useCreatePool, useFinalizePool, useReadPool, useSetSwapFee } from "~~/hooks/cow/"; +import { + useBindPool, + useCreatePool, + useFinalizePool, + useNewPoolEvents, + useReadPool, + useSetSwapFee, +} from "~~/hooks/cow/"; import { getPoolUrl } from "~~/hooks/cow/getPoolUrl"; import { PoolCreationState } from "~~/hooks/cow/usePoolCreationState"; +import { usePoolCreationPersistedState } from "~~/hooks/cow/usePoolCreationState"; import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; import { useApproveToken, useReadToken } from "~~/hooks/token"; import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth"; @@ -18,9 +26,10 @@ interface ManagePoolCreationProps { export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) => { const token1RawAmount = parseUnits(state.token1Amount, state.token1.decimals); const token2RawAmount = parseUnits(state.token2Amount, state.token2.decimals); - const [currentStep, setCurrentStep] = useState(1); + const [userPoolAddress, setUserPoolAddress] = useState
(); + useNewPoolEvents(setUserPoolAddress); const { targetNetwork } = useTargetNetwork(); const isWrongNetwork = targetNetwork.id !== state.chainId; const { data: pool, refetch: refetchPool } = useReadPool(userPoolAddress); @@ -44,24 +53,27 @@ export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) => const { mutate: finalizePool, isPending: isFinalizePending, error: finalizeError } = useFinalizePool(); const txError = createPoolError || approveError || bindError || setSwapFeeError || finalizeError; + const setPersistedState = usePoolCreationPersistedState(state => state.setPersistedState); + const handleCreatePool = () => { - const payload = { name: state.poolName, symbol: state.poolSymbol }; - createPool(payload, { - onSuccess: newPoolAddress => { - setUserPoolAddress(newPoolAddress); - setCurrentStep(2); + createPool( + { name: state.poolName, symbol: state.poolSymbol }, + { + onSuccess: newPoolAddress => { + setUserPoolAddress(newPoolAddress); + setPersistedState({ ...state, step: 2 }); + }, }, - }); + ); }; const handleApproveTokens = async () => { - if (!pool) throw new Error("Pool address is required to approve tokens"); const txs = []; if (token1RawAmount > allowance1) { txs.push( approve({ token: state.token1.address, - spender: pool.address, + spender: pool?.address, rawAmount: token1RawAmount, }), ); @@ -70,53 +82,73 @@ export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) => txs.push( approve({ token: state.token2.address, - spender: pool.address, + spender: pool?.address, rawAmount: token2RawAmount, }), ); const results = await Promise.all(txs); - if (results.every(result => result === "success")) setCurrentStep(3); + if (results.every(result => result === "success")) setPersistedState({ ...state, step: 3 }); }; const handleBindTokens = async () => { - if (!pool) throw new Error("Required value is undefined in handleBindTokens"); - const poolTokens = pool.getCurrentTokens.map(token => token.toLowerCase()); - // If not already bound, bind the token const txs = []; - if (!poolTokens.includes(state.token1.address.toLowerCase())) { + // If not already bound, bind the token + const poolTokens = pool?.currentTokens.map(token => token.toLowerCase()); + if (!poolTokens?.includes(state.token1.address.toLowerCase())) { txs.push( bind({ - pool: pool.address, + pool: pool?.address, token: state.token1.address, rawAmount: token1RawAmount, }), ); } - if (!poolTokens.includes(state.token2.address.toLowerCase())) { + if (!poolTokens?.includes(state.token2.address.toLowerCase())) { txs.push( bind({ - pool: pool.address, + pool: pool?.address, token: state.token2.address, rawAmount: token2RawAmount, }), ); } const results = await Promise.all(txs); - if (results.every(result => result === "success")) setCurrentStep(4); + if (results.every(result => result === "success")) setPersistedState({ ...state, step: 4 }); }; const handleSetSwapFee = async () => { if (!pool) throw new Error("Pool is undefined in handleSetSwapFee"); - setSwapFee({ pool: pool.address, rawAmount: pool.MAX_FEE }, { onSuccess: () => setCurrentStep(5) }); + setSwapFee( + { pool: pool.address, rawAmount: pool.MAX_FEE }, + { onSuccess: () => setPersistedState({ ...state, step: 5 }) }, + ); }; const handleFinalize = async () => { - if (!pool) throw new Error("Pool is undefined in handleFinalize"); - finalizePool(pool.address, { - onSuccess: () => setCurrentStep(6), + finalizePool(pool?.address, { + onSuccess: () => setPersistedState({ ...state, step: 6 }), }); }; + useEffect(() => { + if (pool && pool.numTokens < 2n) { + if (allowance1 < token1RawAmount || allowance2 < token2RawAmount) { + setPersistedState({ ...state, step: 2 }); + } else { + setPersistedState({ ...state, step: 3 }); + } + } + if (pool && pool.numTokens === 2n && !pool.isFinalized) { + if (pool.swapFee !== pool.MAX_FEE) { + setPersistedState({ ...state, step: 4 }); + } else { + setPersistedState({ ...state, step: 5 }); + } + } + if (pool && pool.isFinalized && state.step !== 1) setPersistedState({ ...state, step: 6 }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pool, allowance1, allowance2, token1RawAmount, token2RawAmount]); + return ( <>
@@ -133,15 +165,18 @@ export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) =>
- {currentStep < 6 && } + {state.step < 6 && } - {pool && currentStep === 6 && ( + {pool && state.step === 6 && ( <> -
- {pool.address} +
+
+ {pool.address} +
+ - You CoW AMM pool was successfully created! Because of caching, it may take a few minutes for the pool to + Your CoW AMM pool was successfully created! Because of caching, it may take a few minutes for the pool to appear in the Balancer app @@ -158,15 +193,20 @@ export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) => {isWrongNetwork && You're connected to the wrong network} {(() => { - switch (currentStep) { + switch (state.step) { case 1: return ( - + <> + +
+ Back to Configure +
+ ); case 2: return ( diff --git a/packages/nextjs/app/cow/page.tsx b/packages/nextjs/app/cow/page.tsx index 68b2dec..c0a2d73 100644 --- a/packages/nextjs/app/cow/page.tsx +++ b/packages/nextjs/app/cow/page.tsx @@ -1,22 +1,34 @@ "use client"; +import { useEffect, useState } from "react"; import { PoolCreation } from "./_components"; import type { NextPage } from "next"; import { PoolConfiguration } from "~~/app/cow/_components/PoolConfiguration"; import { usePoolCreationPersistedState } from "~~/hooks/cow/usePoolCreationState"; const CowAmm: NextPage = () => { + const [isMounted, setIsMounted] = useState(false); + const persistedState = usePoolCreationPersistedState(state => state.state); const clearPersistedState = usePoolCreationPersistedState(state => state.clearPersistedState); + useEffect(() => { + setIsMounted(true); + }, []); + return (

Create a CoW AMM Pool

- {!persistedState && } - {persistedState && } + {!isMounted ? ( + + ) : !persistedState ? ( + + ) : ( + persistedState && + )}
@@ -25,3 +37,16 @@ const CowAmm: NextPage = () => { }; export default CowAmm; + +const CowLoadingSkeleton = () => { + return ( + <> +
+
+
+
+
+
+ + ); +}; diff --git a/packages/nextjs/components/common/TextField.tsx b/packages/nextjs/components/common/TextField.tsx index 4c3fb5b..ad142c0 100644 --- a/packages/nextjs/components/common/TextField.tsx +++ b/packages/nextjs/components/common/TextField.tsx @@ -18,7 +18,7 @@ export const TextField: React.FC = ({ label, placeholder, value, value={value} onChange={onChange} disabled={isDisabled} - className="shadow-md rounded-xl w-full input bg-base-300 disabled:text-base-content disabled:bg-base-300 px-5 h-[55px] text-lg" + className="shadow-md border-0 rounded-xl w-full input bg-base-300 disabled:text-base-content disabled:bg-base-300 px-5 h-[55px] text-lg" />
); diff --git a/packages/nextjs/components/common/TokenField.tsx b/packages/nextjs/components/common/TokenField.tsx index 2a9fbcd..48fe329 100644 --- a/packages/nextjs/components/common/TokenField.tsx +++ b/packages/nextjs/components/common/TokenField.tsx @@ -51,7 +51,7 @@ export const TokenField: React.FC = ({ min="0" placeholder="0.0" value={value} - className={`${sufficientAmount !== undefined && (amountGreaterThanBalance || !sufficientAmount) && "ring-1 ring-red-400"} h-[77px] pb-5 text-right text-2xl w-full input rounded-xl bg-base-300 disabled:bg-base-300 disabled:text-base-content`} + className={`${sufficientAmount !== undefined && (amountGreaterThanBalance || !sufficientAmount) && "ring-1 ring-red-400"} border-0 h-[77px] pb-5 text-right text-2xl w-full input rounded-xl bg-base-300 disabled:bg-base-300 disabled:text-base-content`} />
diff --git a/packages/nextjs/hooks/common/index.ts b/packages/nextjs/hooks/common/index.ts deleted file mode 100644 index 709aadd..0000000 --- a/packages/nextjs/hooks/common/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useLocalStorage"; diff --git a/packages/nextjs/hooks/common/useLocalStorage.ts b/packages/nextjs/hooks/common/useLocalStorage.ts deleted file mode 100644 index 77d3a29..0000000 --- a/packages/nextjs/hooks/common/useLocalStorage.ts +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -export const useLocalStorage = (key: string, defaultValue: T) => { - const [state, setState] = useState(defaultValue); - const [hasMounted, setHasMounted] = useState(false); - - useEffect(() => { - setHasMounted(true); - }, []); - - useEffect(() => { - if (hasMounted) { - const savedValue = window.localStorage.getItem(key); - if (savedValue) { - try { - setState(JSON.parse(savedValue)); - }catch {} - } - } - }, [hasMounted, key]); - - useEffect(() => { - if (hasMounted) { - if (state) { - window.localStorage.setItem(key, JSON.stringify(state)); - } - } - }, [key, state, hasMounted]); - - return [state, setState] as const; -}; diff --git a/packages/nextjs/hooks/cow/types.ts b/packages/nextjs/hooks/cow/types.ts index 8ec2822..a6f453c 100644 --- a/packages/nextjs/hooks/cow/types.ts +++ b/packages/nextjs/hooks/cow/types.ts @@ -3,9 +3,9 @@ import { type Address } from "viem"; export type BCowPool = { address: Address; isFinalized: boolean; - getNumTokens: bigint; - getCurrentTokens: Address[]; - getSwapFee: bigint; + numTokens: bigint; + currentTokens: Address[]; + swapFee: bigint; MAX_FEE: bigint; }; diff --git a/packages/nextjs/hooks/cow/useBindPool.ts b/packages/nextjs/hooks/cow/useBindPool.ts index 88489de..41f6089 100644 --- a/packages/nextjs/hooks/cow/useBindPool.ts +++ b/packages/nextjs/hooks/cow/useBindPool.ts @@ -5,7 +5,7 @@ import { abis } from "~~/contracts/abis"; import { useTransactor } from "~~/hooks/scaffold-eth"; type BindPayload = { - pool: Address; + pool: Address | undefined; token: Address; rawAmount: bigint; }; diff --git a/packages/nextjs/hooks/cow/useFinalizePool.ts b/packages/nextjs/hooks/cow/useFinalizePool.ts index 7f6e3b0..b2fa36c 100644 --- a/packages/nextjs/hooks/cow/useFinalizePool.ts +++ b/packages/nextjs/hooks/cow/useFinalizePool.ts @@ -9,9 +9,10 @@ export const useFinalizePool = () => { const publicClient = usePublicClient(); const writeTx = useTransactor(); // scaffold hook for tx status toast notifications - const finalize = async (pool: Address) => { + const finalize = async (pool: Address | undefined) => { if (!publicClient) throw new Error("No public client found!"); if (!walletClient) throw new Error("No wallet client found!"); + if (!pool) throw new Error("No pool address found!"); const { request: finalizePool } = await publicClient.simulateContract({ abi: abis.CoW.BCoWPool, @@ -28,5 +29,5 @@ export const useFinalizePool = () => { }); }; - return useMutation({ mutationFn: (pool: Address) => finalize(pool) }); + return useMutation({ mutationFn: (pool: Address | undefined) => finalize(pool) }); }; diff --git a/packages/nextjs/hooks/cow/usePoolCreationState.ts b/packages/nextjs/hooks/cow/usePoolCreationState.ts index c6165af..306a694 100644 --- a/packages/nextjs/hooks/cow/usePoolCreationState.ts +++ b/packages/nextjs/hooks/cow/usePoolCreationState.ts @@ -1,4 +1,5 @@ import { create } from "zustand"; +import { persist } from "zustand/middleware"; import { Token } from "~~/hooks/token"; export interface PoolCreationState { @@ -9,14 +10,23 @@ export interface PoolCreationState { token2Amount: string; poolName: string; poolSymbol: string; + step: number; } -export const usePoolCreationPersistedState = create<{ - state: PoolCreationState | null; - setPersistedState: (state: PoolCreationState) => void; - clearPersistedState: () => void; -}>(set => ({ - state: null, - setPersistedState: (state: PoolCreationState) => set({ state }), - clearPersistedState: () => set({ state: null }), -})); +export const usePoolCreationPersistedState = create( + persist<{ + state: PoolCreationState | null; + setPersistedState: (state: PoolCreationState) => void; + clearPersistedState: () => void; + }>( + set => ({ + state: null, + setPersistedState: (state: PoolCreationState) => set({ state }), + clearPersistedState: () => set({ state: null }), + }), + { + name: "cow-pool-creation-state", + getStorage: () => localStorage, + }, + ), +); diff --git a/packages/nextjs/hooks/cow/useReadPool.ts b/packages/nextjs/hooks/cow/useReadPool.ts index a82b724..62a6043 100644 --- a/packages/nextjs/hooks/cow/useReadPool.ts +++ b/packages/nextjs/hooks/cow/useReadPool.ts @@ -15,7 +15,7 @@ export const useReadPool = (address: Address | undefined) => { if (!client) throw new Error("Wagmi public client is undefined"); if (!address) throw new Error("Pool address is undefined"); - const [isFinalized, getNumTokens, getCurrentTokens, getSwapFee, MAX_FEE] = await Promise.all([ + const [isFinalized, numTokens, currentTokens, swapFee, MAX_FEE] = await Promise.all([ client.readContract({ abi, address, @@ -43,7 +43,7 @@ export const useReadPool = (address: Address | undefined) => { }), ]); - return { address, isFinalized, getNumTokens, getCurrentTokens, getSwapFee, MAX_FEE }; + return { address, isFinalized, numTokens, currentTokens, swapFee, MAX_FEE }; }, enabled: !!address, }); diff --git a/packages/nextjs/hooks/token/useApproveToken.ts b/packages/nextjs/hooks/token/useApproveToken.ts index 66e5fdf..0db62ad 100644 --- a/packages/nextjs/hooks/token/useApproveToken.ts +++ b/packages/nextjs/hooks/token/useApproveToken.ts @@ -17,7 +17,7 @@ export const useApproveToken = (refetchAllowances: () => void) => { const approve = async ({ token, spender, rawAmount }: ApprovePayload) => { if (!token) throw new Error("Cannot approve token without token address"); - if (!spender) throw new Error("Cannot approve token without spender address"); + if (!spender) throw new Error("Cannot approve token without spender address (the pool)"); if (!walletClient) throw new Error("No wallet client found"); if (!publicClient) throw new Error("No public client found");