From 19cf9be1781074913bf55a0bd8805f5b7731b4b6 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Mon, 13 Jan 2025 16:26:24 -0800 Subject: [PATCH 1/4] refactor v3 pool creation to support safe tx hashes --- .../app/v3/_components/PoolConfiguration.tsx | 8 +- .../v3/_components/PoolCreationManager.tsx | 28 ++- packages/nextjs/app/v3/page.tsx | 234 +++++++++--------- packages/nextjs/hooks/v3/index.ts | 3 +- .../{useCreatePool.tsx => useCreatePool.ts} | 19 +- .../nextjs/hooks/v3/useCreatePoolTxHash.ts | 55 ++++ packages/nextjs/hooks/v3/useInitializePool.ts | 3 +- .../hooks/v3/useInitializePoolTxHash.ts | 43 ++++ .../nextjs/hooks/v3/usePoolCreationStore.ts | 12 +- .../hooks/v3/useWaitForTransactionReceipt.ts | 67 ----- packages/nextjs/public/logo.svg | 14 +- 11 files changed, 255 insertions(+), 231 deletions(-) rename packages/nextjs/hooks/v3/{useCreatePool.tsx => useCreatePool.ts} (83%) create mode 100644 packages/nextjs/hooks/v3/useCreatePoolTxHash.ts create mode 100644 packages/nextjs/hooks/v3/useInitializePoolTxHash.ts delete mode 100644 packages/nextjs/hooks/v3/useWaitForTransactionReceipt.ts diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration.tsx index 2d83e314..cd34aa95 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration.tsx +++ b/packages/nextjs/app/v3/_components/PoolConfiguration.tsx @@ -9,7 +9,7 @@ import { TABS, type TabType, usePoolCreationStore, useValidatePoolCreationInput import { bgBeigeGradient } from "~~/utils"; export function PoolConfiguration() { - const { selectedTab, updatePool, createPoolTxHash } = usePoolCreationStore(); + const { selectedTab, updatePool, createPoolTx } = usePoolCreationStore(); const { targetNetwork } = useTargetNetwork(); const [isPoolCreationModalOpen, setIsPoolCreationModalOpen] = useState(false); const { prev, next } = getAdjacentTabs(selectedTab); @@ -44,10 +44,10 @@ export function PoolConfiguration() { }; } - // Force modal to open if user has already sent "Deploy Pool" transaction (always step 1) + // Force modal to open if user has already sent "Deploy Pool" transaction (which is step 1) useEffect(() => { - if (createPoolTxHash) setIsPoolCreationModalOpen(true); - }, [createPoolTxHash]); + if (createPoolTx.wagmiHash || createPoolTx.safeHash) setIsPoolCreationModalOpen(true); + }, [createPoolTx.wagmiHash, createPoolTx.safeHash]); return ( <> diff --git a/packages/nextjs/app/v3/_components/PoolCreationManager.tsx b/packages/nextjs/app/v3/_components/PoolCreationManager.tsx index c6ab0a4c..fe35f2ff 100644 --- a/packages/nextjs/app/v3/_components/PoolCreationManager.tsx +++ b/packages/nextjs/app/v3/_components/PoolCreationManager.tsx @@ -11,11 +11,12 @@ import { import { useBoostableWhitelist, useCreatePool, + useCreatePoolTxHash, useInitializePool, + useInitializePoolTxHash, useMultiSwap, usePoolCreationStore, useUserDataStore, - useWaitForTransactionReceipt, } from "~~/hooks/v3/"; import { bgBeigeGradient, bgBeigeGradientHover, bgPrimaryGradient } from "~~/utils"; import { getBlockExplorerTxLink } from "~~/utils/scaffold-eth"; @@ -24,30 +25,35 @@ import { getBlockExplorerTxLink } from "~~/utils/scaffold-eth"; * Manages the pool creation process using a modal that cannot be closed after execution of the first step */ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpen: boolean) => void }) { - const { step, tokenConfigs, clearPoolStore, createPoolTxHash, swapTxHash, initPoolTxHash, chain } = - usePoolCreationStore(); + const { step, tokenConfigs, clearPoolStore, createPoolTx, swapTxHash, initPoolTx, chain } = usePoolCreationStore(); const { clearUserData } = useUserDataStore(); const { data: boostableWhitelist } = useBoostableWhitelist(); const { mutate: createPool, isPending: isCreatePoolPending, error: createPoolError } = useCreatePool(); + const { isFetching: isFetchPoolAddressPending, error: fetchPoolAddressError } = useCreatePoolTxHash(); + const { mutate: multiSwap, isPending: isMultiSwapPending, error: multiSwapError } = useMultiSwap(); const { mutate: initializePool, isPending: isInitializePoolPending, error: initializePoolError, } = useInitializePool(); - const { isLoadingTxReceipt } = useWaitForTransactionReceipt(); // Fetches tx from tx hash if user disconnects during pending tx state - - const poolDeploymentUrl = createPoolTxHash ? getBlockExplorerTxLink(chain?.id, createPoolTxHash) : undefined; - const poolInitializationUrl = initPoolTxHash ? getBlockExplorerTxLink(chain?.id, initPoolTxHash) : undefined; + const { isFetching: isInitPoolTxHashPending, error: initPoolTxHashError } = useInitializePoolTxHash(); + + const poolDeploymentUrl = createPoolTx.wagmiHash + ? getBlockExplorerTxLink(chain?.id, createPoolTx.wagmiHash) + : undefined; + const poolInitializationUrl = initPoolTx.wagmiHash + ? getBlockExplorerTxLink(chain?.id, initPoolTx.wagmiHash) + : undefined; const multiSwapUrl = swapTxHash ? getBlockExplorerTxLink(chain?.id, swapTxHash) : undefined; const deployStep = createTransactionStep({ label: "Deploy Pool", blockExplorerUrl: poolDeploymentUrl, onSubmit: createPool, - isPending: isCreatePoolPending || isLoadingTxReceipt, - error: createPoolError, + isPending: isCreatePoolPending || isFetchPoolAddressPending, + error: createPoolError || fetchPoolAddressError, }); const approveOnTokenSteps = tokenConfigs.map((token, idx) => { @@ -103,8 +109,8 @@ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpe const initializeStep = createTransactionStep({ label: "Initialize Pool", onSubmit: initializePool, - isPending: isInitializePoolPending || isLoadingTxReceipt, - error: initializePoolError, + isPending: isInitializePoolPending || isInitPoolTxHashPending, + error: initializePoolError || initPoolTxHashError, blockExplorerUrl: poolInitializationUrl, }); diff --git a/packages/nextjs/app/v3/page.tsx b/packages/nextjs/app/v3/page.tsx index 5ee5c601..3fbddf4d 100644 --- a/packages/nextjs/app/v3/page.tsx +++ b/packages/nextjs/app/v3/page.tsx @@ -6,19 +6,13 @@ import { PoolConfiguration, PoolDetails } from "./_components"; import type { NextPage } from "next"; import { ArrowUpRightIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { BalancerLogo } from "~~/components/assets/BalancerLogo"; -import { Alert, ContactSupportModal, PoolStateResetModal, SafeWalletAlert } from "~~/components/common"; -import { useIsSafeWallet } from "~~/hooks/safe"; +import { Alert, ContactSupportModal, PoolStateResetModal } from "~~/components/common"; import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; import { usePoolCreationStore, useUserDataStore, useValidateNetwork } from "~~/hooks/v3"; const BalancerV3: NextPage = () => { - const { isWrongNetwork, isWalletConnected } = useValidateNetwork(); const { clearPoolStore, chain } = usePoolCreationStore(); const { clearUserData } = useUserDataStore(); - const { targetNetwork: selectedNetwork } = useTargetNetwork(); - const [isInfoAlertVisible, setIsInfoAlertVisible] = useState(true); - - const isSafeWallet = useIsSafeWallet(); return (
@@ -29,121 +23,44 @@ const BalancerV3: NextPage = () => {

Balancer v3

- {!isWalletConnected ? ( -
-
- -
- Please connect a wallet and switch to the network you wish to create a pool - -
-
-
-
- ) : isSafeWallet ? ( -
- -
- ) : isWrongNetwork ? ( -
-
- -
- You are connected to an unsupported network. To continue, please switch to Sepolia, Ethereum, or - Gnosis - -
-
-
-
- ) : chain && selectedNetwork.id !== chain.id ? ( -
-
- -
- You have already begun the pool configuration process. Please switch back to {chain.name} - -
-
-
-
- ) : ( - <> - {!chain && ( -
-
- -
- Make sure you switch to your desired network before beginning pool creation. You cannot switch - after selecting pool type unless you reset progress - -
-
-
-
- )} - {isInfoAlertVisible && ( -
-
- -
- For tips and guidance on pool configuration and creation, check out our - - partner onboarding documentation - -
-
- -
-
- )} + -
- + <> +
+ -
-
-
Pool Preview
- {chain && ( -
- {chain?.name} -
- )} -
- -
- -
·
- { - clearPoolStore(); - clearUserData(); - }} - /> -
+
+
+
Pool Preview
+ {chain && ( +
+ {chain?.name} +
+ )} +
+ +
+ +
·
+ { + clearPoolStore(); + clearUserData(); + }} + />
+
-
- -
Pool creation not available on mobile
-
-
- - )} +
+ +
Pool creation not available on mobile
+
+
+
@@ -151,3 +68,86 @@ const BalancerV3: NextPage = () => { }; export default BalancerV3; + +function UserExperienceAlerts() { + const { targetNetwork: selectedNetwork } = useTargetNetwork(); + const [isInfoAlertVisible, setIsInfoAlertVisible] = useState(true); + const { isWrongNetwork, isWalletConnected } = useValidateNetwork(); + const { chain } = usePoolCreationStore(); + + return !isWalletConnected ? ( +
+
+ +
+ Please connect a wallet and switch to the network you wish to create a pool + +
+
+
+
+ ) : isWrongNetwork ? ( +
+
+ +
+ You are connected to an unsupported network. To continue, please switch to Sepolia, Ethereum, or Gnosis + +
+
+
+
+ ) : chain && selectedNetwork.id !== chain.id ? ( +
+
+ +
+ You have already begun the pool configuration process. Please switch back to {chain.name} + +
+
+
+
+ ) : ( + <> + {!chain && ( +
+
+ +
+ Make sure you switch to your desired network before beginning pool creation. You cannot switch after + selecting pool type unless you reset progress + +
+
+
+
+ )} + {isInfoAlertVisible && ( +
+
+ +
+ For tips and guidance on pool configuration and creation, check out our + + partner onboarding documentation + +
+
+ +
+
+ )} + + ); +} diff --git a/packages/nextjs/hooks/v3/index.ts b/packages/nextjs/hooks/v3/index.ts index 7fa3f951..c1dbaea2 100644 --- a/packages/nextjs/hooks/v3/index.ts +++ b/packages/nextjs/hooks/v3/index.ts @@ -10,4 +10,5 @@ export * from "./useValidateHooksContract"; export * from "./useValidateRateProvider"; export * from "./useUserDataStore"; export * from "./useCheckIfV3PoolExists"; -export * from "./useWaitForTransactionReceipt"; +export * from "./useCreatePoolTxHash"; +export * from "./useInitializePoolTxHash"; diff --git a/packages/nextjs/hooks/v3/useCreatePool.tsx b/packages/nextjs/hooks/v3/useCreatePool.ts similarity index 83% rename from packages/nextjs/hooks/v3/useCreatePool.tsx rename to packages/nextjs/hooks/v3/useCreatePool.ts index ce85d7bb..ae77c418 100644 --- a/packages/nextjs/hooks/v3/useCreatePool.tsx +++ b/packages/nextjs/hooks/v3/useCreatePool.ts @@ -8,7 +8,7 @@ import { weightedPoolFactoryAbi_V3, } from "@balancer/sdk"; import { useMutation } from "@tanstack/react-query"; -import { parseEventLogs, parseUnits, zeroAddress } from "viem"; +import { parseUnits, zeroAddress } from "viem"; import { usePublicClient, useWalletClient } from "wagmi"; import { useTransactor } from "~~/hooks/scaffold-eth"; import { useBoostableWhitelist, usePoolCreationStore } from "~~/hooks/v3"; @@ -99,23 +99,10 @@ export const useCreatePool = () => { to: call.to, }), { - onTransactionHash: txHash => updatePool({ createPoolTxHash: txHash }), + // I think it's safe to make safeHash undefined once wagmiHash is available + onTransactionHash: txHash => updatePool({ createPoolTx: { wagmiHash: txHash, safeHash: undefined } }), }, ); - if (!hash) throw new Error("Failed to generate pool creation transaction hash"); - - const txReceipt = await publicClient.waitForTransactionReceipt({ hash }); - const logs = parseEventLogs({ - abi: poolFactoryAbi[poolType], - logs: txReceipt.logs, - }); - - if (logs.length > 0 && "args" in logs[0] && "pool" in logs[0].args) { - const newPool = logs[0].args.pool; - updatePool({ poolAddress: newPool, step: 2 }); - } else { - throw new Error("Expected pool address not found in event logs"); - } return hash; } diff --git a/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts b/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts new file mode 100644 index 00000000..1136df74 --- /dev/null +++ b/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts @@ -0,0 +1,55 @@ +import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; +import { useQuery } from "@tanstack/react-query"; +import { parseEventLogs } from "viem"; +import { usePublicClient } from "wagmi"; +import { useIsSafeWallet } from "~~/hooks/safe/useIsSafeWallet"; +import { poolFactoryAbi, usePoolCreationStore } from "~~/hooks/v3/"; +import { pollSafeTxStatus } from "~~/utils/safe"; + +/** + * Use create pool tx hash to fetch pool address and save to store + */ +export function useCreatePoolTxHash() { + const { createPoolTx, updatePool, poolType, poolAddress } = usePoolCreationStore(); + const { wagmiHash, safeHash } = createPoolTx; + + const publicClient = usePublicClient(); + const isSafeWallet = useIsSafeWallet(); + const { sdk } = useSafeAppsSDK(); + + return useQuery({ + queryKey: ["v3PoolAddress", wagmiHash, safeHash], + queryFn: async () => { + if (!publicClient) throw new Error("No public client for fetching pool address"); + if (poolType === undefined) throw new Error("Pool type is undefined"); + + if (isSafeWallet && safeHash && !wagmiHash) { + const wagmiHash = await pollSafeTxStatus(sdk, safeHash); + updatePool({ createPoolTx: { safeHash, wagmiHash } }); + return null; // Trigger a re-query with the new wagmiHash + } + + if (!wagmiHash) return null; + + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: wagmiHash }); + + if (txReceipt.status === "success") { + const logs = parseEventLogs({ + abi: poolFactoryAbi[poolType], + logs: txReceipt.logs, + }); + + if (logs.length > 0 && "args" in logs[0] && "pool" in logs[0].args) { + const newPoolAddress = logs[0].args.pool; + updatePool({ poolAddress: newPoolAddress, step: 2 }); + return newPoolAddress; + } else { + throw new Error("Pool address not found in PoolCreated event logs"); + } + } else { + throw new Error("Create pool transaction reverted"); + } + }, + enabled: Boolean(!poolAddress && (safeHash || wagmiHash)), + }); +} diff --git a/packages/nextjs/hooks/v3/useInitializePool.ts b/packages/nextjs/hooks/v3/useInitializePool.ts index 652ea759..9669d7f5 100644 --- a/packages/nextjs/hooks/v3/useInitializePool.ts +++ b/packages/nextjs/hooks/v3/useInitializePool.ts @@ -79,7 +79,8 @@ export const useInitializePool = () => { // Execute the transaction const hash = await writeTx(() => router.write.permitBatchAndCall(args), { - onTransactionHash: txHash => updatePool({ initPoolTxHash: txHash }), + // Should be okay to set off-chain safeHash undefined once we have on chain wagmiHash + onTransactionHash: txHash => updatePool({ initPoolTx: { wagmiHash: txHash, safeHash: undefined } }), }); if (!hash) throw new Error("No pool initialization transaction hash"); diff --git a/packages/nextjs/hooks/v3/useInitializePoolTxHash.ts b/packages/nextjs/hooks/v3/useInitializePoolTxHash.ts new file mode 100644 index 00000000..7c87c7c3 --- /dev/null +++ b/packages/nextjs/hooks/v3/useInitializePoolTxHash.ts @@ -0,0 +1,43 @@ +import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; +import { useQuery } from "@tanstack/react-query"; +import { usePublicClient } from "wagmi"; +import { useIsSafeWallet } from "~~/hooks/safe/useIsSafeWallet"; +import { usePoolCreationStore } from "~~/hooks/v3/"; +import { pollSafeTxStatus } from "~~/utils/safe"; + +/** + * Use init pool tx hash to move step progression forward + */ +export function useInitializePoolTxHash() { + const { initPoolTx, updatePool, poolType, poolAddress, step } = usePoolCreationStore(); + const { wagmiHash, safeHash } = initPoolTx; + + const publicClient = usePublicClient(); + const isSafeWallet = useIsSafeWallet(); + const { sdk } = useSafeAppsSDK(); + + return useQuery({ + queryKey: ["initPoolTx", wagmiHash, safeHash], + queryFn: async () => { + if (!publicClient) throw new Error("No public client for init pool tx hash"); + if (poolType === undefined) throw new Error("Pool type is undefined"); + + if (isSafeWallet && safeHash && !wagmiHash) { + const wagmiHash = await pollSafeTxStatus(sdk, safeHash); + updatePool({ createPoolTx: { safeHash, wagmiHash } }); + return null; // Trigger a re-query with the new wagmiHash + } + + if (!wagmiHash) return null; + + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: wagmiHash }); + + if (txReceipt.status === "success") { + updatePool({ step: step + 1, hasBeenInitialized: true }); + } else { + throw new Error("Init pool transaction reverted"); + } + }, + enabled: Boolean(!poolAddress && (safeHash || wagmiHash)), + }); +} diff --git a/packages/nextjs/hooks/v3/usePoolCreationStore.ts b/packages/nextjs/hooks/v3/usePoolCreationStore.ts index 74b6ffa4..8e04e461 100644 --- a/packages/nextjs/hooks/v3/usePoolCreationStore.ts +++ b/packages/nextjs/hooks/v3/usePoolCreationStore.ts @@ -26,6 +26,10 @@ export type TokenConfig = { useBoostedVariant: boolean; }; +export interface TransactionHashes { + safeHash: `0x${string}` | undefined; + wagmiHash: `0x${string}` | undefined; +} export interface PoolCreationStore { chain: ChainWithAttributes | undefined; step: number; @@ -45,9 +49,9 @@ export interface PoolCreationStore { disableUnbalancedLiquidity: boolean; enableDonation: boolean; amplificationParameter: string; - createPoolTxHash: `0x${string}` | undefined; + createPoolTx: TransactionHashes; + initPoolTx: TransactionHashes; swapTxHash: `0x${string}` | undefined; - initPoolTxHash: `0x${string}` | undefined; hasBeenInitialized: boolean; updatePool: (updates: Partial) => void; updateTokenConfig: (index: number, updates: Partial) => void; @@ -86,8 +90,8 @@ export const initialPoolCreationState = { poolHooksContract: "", disableUnbalancedLiquidity: false, enableDonation: false, - createPoolTxHash: undefined, - initPoolTxHash: undefined, + createPoolTx: { safeHash: undefined, wagmiHash: undefined }, + initPoolTx: { safeHash: undefined, wagmiHash: undefined }, swapTxHash: undefined, hasBeenInitialized: false, }; diff --git a/packages/nextjs/hooks/v3/useWaitForTransactionReceipt.ts b/packages/nextjs/hooks/v3/useWaitForTransactionReceipt.ts deleted file mode 100644 index 3c3d89b0..00000000 --- a/packages/nextjs/hooks/v3/useWaitForTransactionReceipt.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect, useState } from "react"; -import { parseEventLogs } from "viem"; -import { usePublicClient } from "wagmi"; -import { poolFactoryAbi, usePoolCreationStore } from "~~/hooks/v3/"; - -/** - * If user disconnects during pending tx state, these useEffect hooks will attempt to get tx receipt based on tx hash already saved in local storage - * @returns Flag to indicate if tx receipt is being fetched - */ -export const useWaitForTransactionReceipt = () => { - const [isLoadingTxReceipt, setIsLoadingTxReceipt] = useState(false); - - const publicClient = usePublicClient(); - const { createPoolTxHash, initPoolTxHash, poolType, step, updatePool, hasBeenInitialized } = usePoolCreationStore(); - - // Handle edge case where user disconnects while create pool tx is pending - useEffect(() => { - async function getPoolCreationTxReceipt() { - if (!publicClient || !createPoolTxHash || poolType === undefined || step !== 1) return; - console.log("Fetching tx receipt from create pool tx hash..."); - setIsLoadingTxReceipt(true); - try { - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: createPoolTxHash }); - if (txReceipt.status === "success") { - // Parse "PoolCreated" event logs to get pool address - const logs = parseEventLogs({ - abi: poolFactoryAbi[poolType], - logs: txReceipt.logs, - }); - if (logs.length > 0 && "args" in logs[0] && "pool" in logs[0].args) { - const newPool = logs[0].args.pool; - updatePool({ poolAddress: newPool, step: 2 }); - } else { - throw new Error("Pool address not found in PoolCreated event logs"); - } - } else { - throw new Error("Create pool transaction reverted"); - } - } catch (error) { - console.error("Error getting create pool transaction receipt:", error); - } finally { - setIsLoadingTxReceipt(false); - } - } - getPoolCreationTxReceipt(); - }, [createPoolTxHash, publicClient, poolType, updatePool, step]); - - // Handle edge case where user disconnects while pool init tx is pending - useEffect(() => { - async function getPoolInitTxReceipt() { - if (!publicClient || !initPoolTxHash || hasBeenInitialized) return; - console.log("Fetching tx receipt from init pool tx hash..."); - try { - setIsLoadingTxReceipt(true); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: initPoolTxHash }); - if (txReceipt.status === "success") updatePool({ step: step + 1, hasBeenInitialized: true }); - } catch (error) { - console.error("Error getting init pool transaction receipt:", error); - } finally { - setIsLoadingTxReceipt(false); - } - } - getPoolInitTxReceipt(); - }, [initPoolTxHash, publicClient, updatePool, step, hasBeenInitialized]); - - return { isLoadingTxReceipt }; -}; diff --git a/packages/nextjs/public/logo.svg b/packages/nextjs/public/logo.svg index 93e4b40a..535dd013 100644 --- a/packages/nextjs/public/logo.svg +++ b/packages/nextjs/public/logo.svg @@ -1,10 +1,4 @@ - - - - - - - - - - + + + + \ No newline at end of file From 9e5063f103bf329102937f1176b554be60bcecc9 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Tue, 14 Jan 2025 14:26:35 -0800 Subject: [PATCH 2/4] handle disconnect while safe tx pending --- .../nextjs/app/v3/_components/ChooseToken.tsx | 4 +- .../v3/_components/PoolCreationManager.tsx | 28 +++---- .../hooks/scaffold-eth/useTransactor.tsx | 2 +- packages/nextjs/hooks/v3/index.ts | 1 + packages/nextjs/hooks/v3/useCreatePool.ts | 8 +- .../nextjs/hooks/v3/useCreatePoolTxHash.ts | 14 ++-- packages/nextjs/hooks/v3/useInitializePool.ts | 17 ++--- .../hooks/v3/useInitializePoolTxHash.ts | 8 +- packages/nextjs/hooks/v3/useMultiSwap.ts | 49 +++--------- .../nextjs/hooks/v3/useMultiSwapTxHash.ts | 75 +++++++++++++++++++ .../nextjs/hooks/v3/usePoolCreationStore.ts | 20 ++--- 11 files changed, 141 insertions(+), 85 deletions(-) create mode 100644 packages/nextjs/hooks/v3/useMultiSwapTxHash.ts diff --git a/packages/nextjs/app/v3/_components/ChooseToken.tsx b/packages/nextjs/app/v3/_components/ChooseToken.tsx index 1be7a14e..570c82b8 100644 --- a/packages/nextjs/app/v3/_components/ChooseToken.tsx +++ b/packages/nextjs/app/v3/_components/ChooseToken.tsx @@ -341,8 +341,8 @@ const BoostOpportunityModal = ({

{boostedVariant.name}

Boosted tokens provide your liquidity pool with a layer of sustainable yield. If you select{" "} - {boostedVariant.symbol}, all {standardVariant.symbol} in this pool will be supplied to - Aave's lending market to earn additional yield. + {boostedVariant.symbol}, all {standardVariant.symbol} in this pool will be supplied to earn + additional yield.
Note that if you choose the boosted variant, the necessary rate provider address will be auto-filled for you
diff --git a/packages/nextjs/app/v3/_components/PoolCreationManager.tsx b/packages/nextjs/app/v3/_components/PoolCreationManager.tsx index fe35f2ff..03ec815c 100644 --- a/packages/nextjs/app/v3/_components/PoolCreationManager.tsx +++ b/packages/nextjs/app/v3/_components/PoolCreationManager.tsx @@ -15,6 +15,7 @@ import { useInitializePool, useInitializePoolTxHash, useMultiSwap, + useMultiSwapTxHash, usePoolCreationStore, useUserDataStore, } from "~~/hooks/v3/"; @@ -25,7 +26,8 @@ import { getBlockExplorerTxLink } from "~~/utils/scaffold-eth"; * Manages the pool creation process using a modal that cannot be closed after execution of the first step */ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpen: boolean) => void }) { - const { step, tokenConfigs, clearPoolStore, createPoolTx, swapTxHash, initPoolTx, chain } = usePoolCreationStore(); + const { step, tokenConfigs, clearPoolStore, createPoolTx, swapToBoostedTx, initPoolTx, chain } = + usePoolCreationStore(); const { clearUserData } = useUserDataStore(); const { data: boostableWhitelist } = useBoostableWhitelist(); @@ -33,20 +35,20 @@ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpe const { isFetching: isFetchPoolAddressPending, error: fetchPoolAddressError } = useCreatePoolTxHash(); const { mutate: multiSwap, isPending: isMultiSwapPending, error: multiSwapError } = useMultiSwap(); - const { - mutate: initializePool, - isPending: isInitializePoolPending, - error: initializePoolError, - } = useInitializePool(); + const { isFetching: isMultiSwapTxHashPending, error: multiSwapTxHashError } = useMultiSwapTxHash(); + + const { mutate: initPool, isPending: isInitPoolPending, error: initPoolError } = useInitializePool(); const { isFetching: isInitPoolTxHashPending, error: initPoolTxHashError } = useInitializePoolTxHash(); const poolDeploymentUrl = createPoolTx.wagmiHash ? getBlockExplorerTxLink(chain?.id, createPoolTx.wagmiHash) : undefined; + const multiSwapUrl = swapToBoostedTx.wagmiHash + ? getBlockExplorerTxLink(chain?.id, swapToBoostedTx.wagmiHash) + : undefined; const poolInitializationUrl = initPoolTx.wagmiHash ? getBlockExplorerTxLink(chain?.id, initPoolTx.wagmiHash) : undefined; - const multiSwapUrl = swapTxHash ? getBlockExplorerTxLink(chain?.id, swapTxHash) : undefined; const deployStep = createTransactionStep({ label: "Deploy Pool", @@ -62,7 +64,7 @@ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpe if (!symbol || !decimals) return { label: "Token Approval", - component: Invalid token configuration. Missing symbol or decimals., + component: Missing token info!, }; return { @@ -77,8 +79,8 @@ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpe createTransactionStep({ label: "Swap to Boosted", onSubmit: multiSwap, - isPending: isMultiSwapPending, - error: multiSwapError, + isPending: isMultiSwapPending || isMultiSwapTxHashPending, + error: multiSwapError || multiSwapTxHashError, blockExplorerUrl: multiSwapUrl, }), ); @@ -108,9 +110,9 @@ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpe const initializeStep = createTransactionStep({ label: "Initialize Pool", - onSubmit: initializePool, - isPending: isInitializePoolPending || isInitPoolTxHashPending, - error: initializePoolError || initPoolTxHashError, + onSubmit: initPool, + isPending: isInitPoolPending || isInitPoolTxHashPending, + error: initPoolError || initPoolTxHashError, blockExplorerUrl: poolInitializationUrl, }); diff --git a/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx b/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx index 05b137a2..899ca19e 100644 --- a/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx +++ b/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx @@ -72,7 +72,7 @@ export const useTransactor = (_walletClient?: WalletClient): TransactionFunc => } notification.remove(notificationId); - notificationId = notification.loading(); + notificationId = notification.loading(); // if safe wallet, the transaction hash will be off-chain for safe infra, not on chain if (isSafeWallet) { diff --git a/packages/nextjs/hooks/v3/index.ts b/packages/nextjs/hooks/v3/index.ts index c1dbaea2..482784e8 100644 --- a/packages/nextjs/hooks/v3/index.ts +++ b/packages/nextjs/hooks/v3/index.ts @@ -11,4 +11,5 @@ export * from "./useValidateRateProvider"; export * from "./useUserDataStore"; export * from "./useCheckIfV3PoolExists"; export * from "./useCreatePoolTxHash"; +export * from "./useMultiSwapTxHash"; export * from "./useInitializePoolTxHash"; diff --git a/packages/nextjs/hooks/v3/useCreatePool.ts b/packages/nextjs/hooks/v3/useCreatePool.ts index ae77c418..458755ac 100644 --- a/packages/nextjs/hooks/v3/useCreatePool.ts +++ b/packages/nextjs/hooks/v3/useCreatePool.ts @@ -21,6 +21,9 @@ export const poolFactoryAbi = { const SWAP_FEE_PERCENTAGE_DECIMALS = 16; const TOKEN_WEIGHT_DECIMALS = 16; +/** + * Handles sending the create pool transaction + */ export const useCreatePool = () => { const { data: walletClient } = useWalletClient(); const publicClient = usePublicClient(); @@ -99,8 +102,9 @@ export const useCreatePool = () => { to: call.to, }), { - // I think it's safe to make safeHash undefined once wagmiHash is available - onTransactionHash: txHash => updatePool({ createPoolTx: { wagmiHash: txHash, safeHash: undefined } }), + // tx not considered successful until tx receipt is parsed + onTransactionHash: txHash => + updatePool({ createPoolTx: { wagmiHash: txHash, safeHash: undefined, isSuccess: false } }), }, ); diff --git a/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts b/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts index 1136df74..eb7ebdcd 100644 --- a/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts +++ b/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts @@ -7,10 +7,10 @@ import { poolFactoryAbi, usePoolCreationStore } from "~~/hooks/v3/"; import { pollSafeTxStatus } from "~~/utils/safe"; /** - * Use create pool tx hash to fetch pool address and save to store + * Parses the create pool tx hash to fetch pool address and save to store */ export function useCreatePoolTxHash() { - const { createPoolTx, updatePool, poolType, poolAddress } = usePoolCreationStore(); + const { createPoolTx, updatePool, poolType } = usePoolCreationStore(); const { wagmiHash, safeHash } = createPoolTx; const publicClient = usePublicClient(); @@ -18,14 +18,14 @@ export function useCreatePoolTxHash() { const { sdk } = useSafeAppsSDK(); return useQuery({ - queryKey: ["v3PoolAddress", wagmiHash, safeHash], + queryKey: ["createPoolTx", wagmiHash, safeHash], queryFn: async () => { if (!publicClient) throw new Error("No public client for fetching pool address"); if (poolType === undefined) throw new Error("Pool type is undefined"); if (isSafeWallet && safeHash && !wagmiHash) { const wagmiHash = await pollSafeTxStatus(sdk, safeHash); - updatePool({ createPoolTx: { safeHash, wagmiHash } }); + updatePool({ createPoolTx: { safeHash, wagmiHash, isSuccess: false } }); return null; // Trigger a re-query with the new wagmiHash } @@ -41,7 +41,9 @@ export function useCreatePoolTxHash() { if (logs.length > 0 && "args" in logs[0] && "pool" in logs[0].args) { const newPoolAddress = logs[0].args.pool; - updatePool({ poolAddress: newPoolAddress, step: 2 }); + if (!newPoolAddress) throw new Error("Pool address not found in PoolCreated event logs"); + + updatePool({ poolAddress: newPoolAddress, step: 2, createPoolTx: { safeHash, wagmiHash, isSuccess: true } }); return newPoolAddress; } else { throw new Error("Pool address not found in PoolCreated event logs"); @@ -50,6 +52,6 @@ export function useCreatePoolTxHash() { throw new Error("Create pool transaction reverted"); } }, - enabled: Boolean(!poolAddress && (safeHash || wagmiHash)), + enabled: Boolean(!createPoolTx.isSuccess && (safeHash || wagmiHash)), }); } diff --git a/packages/nextjs/hooks/v3/useInitializePool.ts b/packages/nextjs/hooks/v3/useInitializePool.ts index 9669d7f5..3b32021e 100644 --- a/packages/nextjs/hooks/v3/useInitializePool.ts +++ b/packages/nextjs/hooks/v3/useInitializePool.ts @@ -6,6 +6,9 @@ import { useTransactor } from "~~/hooks/scaffold-eth"; import { useBoostableWhitelist, usePoolCreationStore } from "~~/hooks/v3"; import { createPermit2 } from "~~/utils/permit2Helper"; +/** + * Handles sending the init pool transaction + */ export const useInitializePool = () => { const { data: walletClient } = useWalletClient(); const publicClient = usePublicClient(); @@ -13,7 +16,7 @@ export const useInitializePool = () => { const chainId = publicClient?.chain.id; const rpcUrl = publicClient?.transport.transports[0].value.url; const protocolVersion = 3; - const { poolAddress, poolType, tokenConfigs, updatePool, step } = usePoolCreationStore(); + const { poolAddress, poolType, tokenConfigs, updatePool } = usePoolCreationStore(); const { data: boostableWhitelist } = useBoostableWhitelist(); async function initializePool() { @@ -77,18 +80,12 @@ export const useInitializePool = () => { const args = [[], [], batch, signature, [encodedInitData]] as const; console.log("router.permitBatchAndCall args for initialize pool", args); - // Execute the transaction const hash = await writeTx(() => router.write.permitBatchAndCall(args), { // Should be okay to set off-chain safeHash undefined once we have on chain wagmiHash - onTransactionHash: txHash => updatePool({ initPoolTx: { wagmiHash: txHash, safeHash: undefined } }), + onTransactionHash: txHash => + updatePool({ initPoolTx: { wagmiHash: txHash, safeHash: undefined, isSuccess: false } }), }); - if (!hash) throw new Error("No pool initialization transaction hash"); - - // Move the step forward if the transaction is successful - const txReceipt = await publicClient.waitForTransactionReceipt({ hash }); - if (txReceipt.status === "success") { - updatePool({ step: step + 1, hasBeenInitialized: true }); - } + if (!hash) throw new Error("Missing init pool transaction hash"); return hash; } diff --git a/packages/nextjs/hooks/v3/useInitializePoolTxHash.ts b/packages/nextjs/hooks/v3/useInitializePoolTxHash.ts index 7c87c7c3..0c1c9c50 100644 --- a/packages/nextjs/hooks/v3/useInitializePoolTxHash.ts +++ b/packages/nextjs/hooks/v3/useInitializePoolTxHash.ts @@ -9,7 +9,7 @@ import { pollSafeTxStatus } from "~~/utils/safe"; * Use init pool tx hash to move step progression forward */ export function useInitializePoolTxHash() { - const { initPoolTx, updatePool, poolType, poolAddress, step } = usePoolCreationStore(); + const { initPoolTx, updatePool, poolType, step } = usePoolCreationStore(); const { wagmiHash, safeHash } = initPoolTx; const publicClient = usePublicClient(); @@ -24,7 +24,7 @@ export function useInitializePoolTxHash() { if (isSafeWallet && safeHash && !wagmiHash) { const wagmiHash = await pollSafeTxStatus(sdk, safeHash); - updatePool({ createPoolTx: { safeHash, wagmiHash } }); + updatePool({ initPoolTx: { safeHash, wagmiHash, isSuccess: false } }); return null; // Trigger a re-query with the new wagmiHash } @@ -33,11 +33,11 @@ export function useInitializePoolTxHash() { const txReceipt = await publicClient.waitForTransactionReceipt({ hash: wagmiHash }); if (txReceipt.status === "success") { - updatePool({ step: step + 1, hasBeenInitialized: true }); + updatePool({ step: step + 1, initPoolTx: { safeHash, wagmiHash, isSuccess: true } }); } else { throw new Error("Init pool transaction reverted"); } }, - enabled: Boolean(!poolAddress && (safeHash || wagmiHash)), + enabled: Boolean(!initPoolTx.isSuccess && (safeHash || wagmiHash)), }); } diff --git a/packages/nextjs/hooks/v3/useMultiSwap.ts b/packages/nextjs/hooks/v3/useMultiSwap.ts index 9cd3a536..a3e205b1 100644 --- a/packages/nextjs/hooks/v3/useMultiSwap.ts +++ b/packages/nextjs/hooks/v3/useMultiSwap.ts @@ -8,19 +8,21 @@ import { vaultV3Abi, } from "@balancer/sdk"; import { useMutation } from "@tanstack/react-query"; -import { encodeFunctionData, formatUnits, getContract, parseEventLogs, parseUnits, zeroAddress } from "viem"; +import { encodeFunctionData, getContract, parseUnits, zeroAddress } from "viem"; import { usePublicClient, useWalletClient } from "wagmi"; import { useTransactor } from "~~/hooks/scaffold-eth"; import { useBoostableWhitelist, usePoolCreationStore } from "~~/hooks/v3"; import { createPermit2 } from "~~/utils/permit2Helper"; -// This hook only used if creating boosted pool using standard tokens +/** + * For creating boosted pool using standard tokens + */ export const useMultiSwap = () => { const { data: walletClient } = useWalletClient(); const publicClient = usePublicClient(); const writeTx = useTransactor(); const chainId = publicClient?.chain.id; - const { tokenConfigs, updatePool, step, updateTokenConfig } = usePoolCreationStore(); + const { tokenConfigs, updatePool } = usePoolCreationStore(); const { data: boostableWhitelist } = useBoostableWhitelist(); const userData = "0x"; @@ -101,48 +103,21 @@ export const useMultiSwap = () => { console.log("batchRouter.permitBatchAndCall args", args); const hash = await writeTx(() => batchRouterContract.write.permitBatchAndCall(args), { - blockConfirmations: 1, - onBlockConfirmation: () => { - console.log("Successfully multi swapped bitches!"); - }, + // tx not considered successful until tx receipt is parsed + onTransactionHash: txHash => + updatePool({ swapToBoostedTx: { wagmiHash: txHash, safeHash: undefined, isSuccess: false } }), }); if (!hash) throw new Error("No multi swap transaction hash"); - - const txReceipt = await publicClient.getTransactionReceipt({ hash }); - const logs = parseEventLogs({ - abi: vaultV3Abi, - eventName: "Wrap", - logs: txReceipt.logs, - }); - - console.log("logs", logs); - - logs.forEach(log => { - // mintedShares is the amount, underlyingToken is an address - const { mintedShares, wrappedToken } = log.args; - console.log("wrappedToken", wrappedToken); - const boostedToken = Object.values(boostableWhitelist ?? {}).find( - token => token.address.toLowerCase() === wrappedToken.toLowerCase(), - ); - if (!boostedToken) throw new Error("Boosted token not found"); - console.log("boostedToken", boostedToken); - const amount = formatUnits(mintedShares, boostedToken?.decimals); - // find corresponding token index for tokenConfigs array - const tokenIndex = tokenConfigs.findIndex( - token => token.address.toLowerCase() === boostedToken?.underlyingTokenAddress?.toLowerCase(), - ); - console.log("amount", amount, "tokenIndex", tokenIndex); - updateTokenConfig(tokenIndex, { amount }); - }); - - updatePool({ step: step + 1, swapTxHash: hash }); + return hash; } return useMutation({ mutationFn: () => multiSwap() }); }; -// ATTEMPT USING SDK BLOCKED BY SDK NOT ALLOWING MULTI SWAP PATHS +/** + * ATTEMPT USING SDK BLOCKED BY SDK NOT ALLOWING MULTI SWAP PATHS + */ // const paths = tokenConfigs.map(token => { // const boostedToken = standardToBoosted[token.address]; diff --git a/packages/nextjs/hooks/v3/useMultiSwapTxHash.ts b/packages/nextjs/hooks/v3/useMultiSwapTxHash.ts new file mode 100644 index 00000000..97c401fc --- /dev/null +++ b/packages/nextjs/hooks/v3/useMultiSwapTxHash.ts @@ -0,0 +1,75 @@ +import { vaultV3Abi } from "@balancer/sdk"; +import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; +import { useQuery } from "@tanstack/react-query"; +import { formatUnits, parseEventLogs } from "viem"; +import { usePublicClient } from "wagmi"; +import { useIsSafeWallet } from "~~/hooks/safe/useIsSafeWallet"; +import { useBoostableWhitelist, usePoolCreationStore } from "~~/hooks/v3/"; +import { pollSafeTxStatus } from "~~/utils/safe"; + +/** + * Use multi swap tx hash to parse amounts after underlying tokens are swapped to boosted variants + * Also moves step progression forward + */ +export function useMultiSwapTxHash() { + const publicClient = usePublicClient(); + const isSafeWallet = useIsSafeWallet(); + const { sdk } = useSafeAppsSDK(); + + const { swapToBoostedTx, updatePool, poolType, tokenConfigs, updateTokenConfig, step } = usePoolCreationStore(); + const { wagmiHash, safeHash } = swapToBoostedTx; + + const { data: boostableWhitelist } = useBoostableWhitelist(); + + return useQuery({ + queryKey: ["multiSwapTx", wagmiHash, safeHash], + queryFn: async () => { + if (!publicClient) throw new Error("No public client for fetching pool address"); + if (poolType === undefined) throw new Error("Pool type is undefined"); + + if (isSafeWallet && safeHash && !wagmiHash) { + console.log("beginning poll for swap to boosted safe hash:", safeHash); + const wagmiHash = await pollSafeTxStatus(sdk, safeHash); + updatePool({ swapToBoostedTx: { safeHash, wagmiHash, isSuccess: false } }); + return null; // Trigger a re-query with the new wagmiHash + } + + if (!wagmiHash) return null; + + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: wagmiHash }); + + if (txReceipt.status === "success") { + const logs = parseEventLogs({ + abi: vaultV3Abi, + eventName: "Wrap", + logs: txReceipt.logs, + }); + + logs.forEach(log => { + // mintedShares is the amount, underlyingToken is an address + const { mintedShares, wrappedToken } = log.args; + console.log("wrappedToken", wrappedToken); + const boostedToken = Object.values(boostableWhitelist ?? {}).find( + token => token.address.toLowerCase() === wrappedToken.toLowerCase(), + ); + if (!boostedToken) throw new Error("Boosted token not found"); + console.log("boostedToken", boostedToken); + const amount = formatUnits(mintedShares, boostedToken?.decimals); + // find corresponding token index for tokenConfigs array + const tokenIndex = tokenConfigs.findIndex( + token => token.address.toLowerCase() === boostedToken?.underlyingTokenAddress?.toLowerCase(), + ); + if (tokenIndex === -1) throw new Error("Token index not matched in swap to boosted tx hash"); + + console.log("amount", amount, "tokenIndex", tokenIndex); + updateTokenConfig(tokenIndex, { amount }); + }); + + updatePool({ step: step + 1, swapToBoostedTx: { wagmiHash, safeHash, isSuccess: true } }); + } else { + throw new Error("Swap to boosted variant transaction reverted"); + } + }, + enabled: Boolean(!swapToBoostedTx.isSuccess && (safeHash || wagmiHash)), + }); +} diff --git a/packages/nextjs/hooks/v3/usePoolCreationStore.ts b/packages/nextjs/hooks/v3/usePoolCreationStore.ts index 8e04e461..71ac7728 100644 --- a/packages/nextjs/hooks/v3/usePoolCreationStore.ts +++ b/packages/nextjs/hooks/v3/usePoolCreationStore.ts @@ -26,9 +26,10 @@ export type TokenConfig = { useBoostedVariant: boolean; }; -export interface TransactionHashes { +export interface TransactionDetails { safeHash: `0x${string}` | undefined; wagmiHash: `0x${string}` | undefined; + isSuccess: boolean; } export interface PoolCreationStore { chain: ChainWithAttributes | undefined; @@ -49,10 +50,9 @@ export interface PoolCreationStore { disableUnbalancedLiquidity: boolean; enableDonation: boolean; amplificationParameter: string; - createPoolTx: TransactionHashes; - initPoolTx: TransactionHashes; - swapTxHash: `0x${string}` | undefined; - hasBeenInitialized: boolean; + createPoolTx: TransactionDetails; + initPoolTx: TransactionDetails; + swapToBoostedTx: TransactionDetails; updatePool: (updates: Partial) => void; updateTokenConfig: (index: number, updates: Partial) => void; clearPoolStore: () => void; @@ -77,7 +77,7 @@ export const initialPoolCreationState = { isDelegatingPauseManagement: true, isDelegatingSwapFeeManagement: true, isUsingHooks: false, - poolAddress: undefined, // set after pool deployment + poolAddress: undefined, // set after pool deployment by parsing the tx hash selectedTab: TABS[0], name: "", symbol: "", @@ -90,10 +90,10 @@ export const initialPoolCreationState = { poolHooksContract: "", disableUnbalancedLiquidity: false, enableDonation: false, - createPoolTx: { safeHash: undefined, wagmiHash: undefined }, - initPoolTx: { safeHash: undefined, wagmiHash: undefined }, - swapTxHash: undefined, - hasBeenInitialized: false, + // isSuccess is only flipped to true after parsing tx receipt for status + createPoolTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false }, + initPoolTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false }, + swapToBoostedTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false }, }; // Stores all the data that will be used for pool creation From 150095f8c13cacebd97e1fd7088816099bf25c80 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Tue, 14 Jan 2025 14:33:07 -0800 Subject: [PATCH 3/4] fix footer --- packages/nextjs/components/Footer.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/components/Footer.tsx b/packages/nextjs/components/Footer.tsx index 199b51e5..d719897a 100644 --- a/packages/nextjs/components/Footer.tsx +++ b/packages/nextjs/components/Footer.tsx @@ -72,7 +72,7 @@ export const Footer = () => { rel="noreferrer" className="link no-underline hover:underline" > - Github + Pool Creator Github
· @@ -83,7 +83,18 @@ export const Footer = () => { rel="noreferrer" className="link no-underline hover:underline" > - Contracts + CoW Contracts + + + · + From 2524560964813a801b752de080675706f993ddd4 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Tue, 14 Jan 2025 14:38:05 -0800 Subject: [PATCH 4/4] notice about fork dev --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3e466101..b19c3b81 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ yarn start ## Run on Fork +> ⚠️ **Note:** Fork development is only supported for CoW AMMs + 1. Add the following ENV vars to a `.env` file located in the root directory ```