diff --git a/README.md b/README.md index 3e46610..b19c3b8 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 ``` diff --git a/packages/nextjs/app/v3/_components/ChooseToken.tsx b/packages/nextjs/app/v3/_components/ChooseToken.tsx index 1be7a14..570c82b 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/PoolConfiguration.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration.tsx index 2d83e31..cd34aa9 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 c6ab0a4..03ec815 100644 --- a/packages/nextjs/app/v3/_components/PoolCreationManager.tsx +++ b/packages/nextjs/app/v3/_components/PoolCreationManager.tsx @@ -11,11 +11,13 @@ import { import { useBoostableWhitelist, useCreatePool, + useCreatePoolTxHash, useInitializePool, + useInitializePoolTxHash, useMultiSwap, + useMultiSwapTxHash, usePoolCreationStore, useUserDataStore, - useWaitForTransactionReceipt, } from "~~/hooks/v3/"; import { bgBeigeGradient, bgBeigeGradientHover, bgPrimaryGradient } from "~~/utils"; import { getBlockExplorerTxLink } from "~~/utils/scaffold-eth"; @@ -24,30 +26,36 @@ 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 } = + const { step, tokenConfigs, clearPoolStore, createPoolTx, swapToBoostedTx, 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 { isFetching: isMultiSwapTxHashPending, error: multiSwapTxHashError } = useMultiSwapTxHash(); + + const { mutate: initPool, isPending: isInitPoolPending, error: initPoolError } = useInitializePool(); + const { isFetching: isInitPoolTxHashPending, error: initPoolTxHashError } = useInitializePoolTxHash(); - const poolDeploymentUrl = createPoolTxHash ? getBlockExplorerTxLink(chain?.id, createPoolTxHash) : undefined; - const poolInitializationUrl = initPoolTxHash ? getBlockExplorerTxLink(chain?.id, initPoolTxHash) : undefined; - const multiSwapUrl = swapTxHash ? getBlockExplorerTxLink(chain?.id, swapTxHash) : undefined; + 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 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) => { @@ -56,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 { @@ -71,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, }), ); @@ -102,9 +110,9 @@ export function PoolCreationManager({ setIsModalOpen }: { setIsModalOpen: (isOpe const initializeStep = createTransactionStep({ label: "Initialize Pool", - onSubmit: initializePool, - isPending: isInitializePoolPending || isLoadingTxReceipt, - error: initializePoolError, + onSubmit: initPool, + isPending: isInitPoolPending || isInitPoolTxHashPending, + error: initPoolError || initPoolTxHashError, blockExplorerUrl: poolInitializationUrl, }); diff --git a/packages/nextjs/app/v3/page.tsx b/packages/nextjs/app/v3/page.tsx index 5ee5c60..3fbddf4 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/components/Footer.tsx b/packages/nextjs/components/Footer.tsx index 199b51e..d719897 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 + + + · +
+ + v3 Contracts
diff --git a/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx b/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx index 05b137a..899ca19 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 7fa3f95..482784e 100644 --- a/packages/nextjs/hooks/v3/index.ts +++ b/packages/nextjs/hooks/v3/index.ts @@ -10,4 +10,6 @@ export * from "./useValidateHooksContract"; export * from "./useValidateRateProvider"; export * from "./useUserDataStore"; export * from "./useCheckIfV3PoolExists"; -export * from "./useWaitForTransactionReceipt"; +export * from "./useCreatePoolTxHash"; +export * from "./useMultiSwapTxHash"; +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 ce85d7b..458755a 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"; @@ -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,23 +102,11 @@ export const useCreatePool = () => { to: call.to, }), { - onTransactionHash: txHash => updatePool({ createPoolTxHash: txHash }), + // tx not considered successful until tx receipt is parsed + onTransactionHash: txHash => + updatePool({ createPoolTx: { wagmiHash: txHash, safeHash: undefined, isSuccess: false } }), }, ); - 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 0000000..eb7ebdc --- /dev/null +++ b/packages/nextjs/hooks/v3/useCreatePoolTxHash.ts @@ -0,0 +1,57 @@ +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"; + +/** + * Parses the create pool tx hash to fetch pool address and save to store + */ +export function useCreatePoolTxHash() { + const { createPoolTx, updatePool, poolType } = usePoolCreationStore(); + const { wagmiHash, safeHash } = createPoolTx; + + const publicClient = usePublicClient(); + const isSafeWallet = useIsSafeWallet(); + const { sdk } = useSafeAppsSDK(); + + return useQuery({ + 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, 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: poolFactoryAbi[poolType], + logs: txReceipt.logs, + }); + + if (logs.length > 0 && "args" in logs[0] && "pool" in logs[0].args) { + const newPoolAddress = logs[0].args.pool; + 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"); + } + } else { + throw new Error("Create pool transaction reverted"); + } + }, + enabled: Boolean(!createPoolTx.isSuccess && (safeHash || wagmiHash)), + }); +} diff --git a/packages/nextjs/hooks/v3/useInitializePool.ts b/packages/nextjs/hooks/v3/useInitializePool.ts index 652ea75..3b32021 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,17 +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), { - 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, 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 new file mode 100644 index 0000000..0c1c9c5 --- /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, 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({ initPoolTx: { 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") { + updatePool({ step: step + 1, initPoolTx: { safeHash, wagmiHash, isSuccess: true } }); + } else { + throw new Error("Init pool transaction reverted"); + } + }, + enabled: Boolean(!initPoolTx.isSuccess && (safeHash || wagmiHash)), + }); +} diff --git a/packages/nextjs/hooks/v3/useMultiSwap.ts b/packages/nextjs/hooks/v3/useMultiSwap.ts index 9cd3a53..a3e205b 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 0000000..97c401f --- /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 74b6ffa..71ac772 100644 --- a/packages/nextjs/hooks/v3/usePoolCreationStore.ts +++ b/packages/nextjs/hooks/v3/usePoolCreationStore.ts @@ -26,6 +26,11 @@ export type TokenConfig = { useBoostedVariant: boolean; }; +export interface TransactionDetails { + safeHash: `0x${string}` | undefined; + wagmiHash: `0x${string}` | undefined; + isSuccess: boolean; +} export interface PoolCreationStore { chain: ChainWithAttributes | undefined; step: number; @@ -45,10 +50,9 @@ export interface PoolCreationStore { disableUnbalancedLiquidity: boolean; enableDonation: boolean; amplificationParameter: string; - createPoolTxHash: `0x${string}` | undefined; - swapTxHash: `0x${string}` | undefined; - initPoolTxHash: `0x${string}` | undefined; - hasBeenInitialized: boolean; + createPoolTx: TransactionDetails; + initPoolTx: TransactionDetails; + swapToBoostedTx: TransactionDetails; updatePool: (updates: Partial) => void; updateTokenConfig: (index: number, updates: Partial) => void; clearPoolStore: () => void; @@ -73,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: "", @@ -86,10 +90,10 @@ export const initialPoolCreationState = { poolHooksContract: "", disableUnbalancedLiquidity: false, enableDonation: false, - createPoolTxHash: undefined, - initPoolTxHash: 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 diff --git a/packages/nextjs/hooks/v3/useWaitForTransactionReceipt.ts b/packages/nextjs/hooks/v3/useWaitForTransactionReceipt.ts deleted file mode 100644 index 3c3d89b..0000000 --- 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 93e4b40..535dd01 100644 --- a/packages/nextjs/public/logo.svg +++ b/packages/nextjs/public/logo.svg @@ -1,10 +1,4 @@ - - - - - - - - - - + + + + \ No newline at end of file