diff --git a/packages/nextjs/app/v3/_components/PoolCreation/index.tsx b/packages/nextjs/app/v3/_components/PoolCreation/index.tsx index a55522c..068cc4e 100644 --- a/packages/nextjs/app/v3/_components/PoolCreation/index.tsx +++ b/packages/nextjs/app/v3/_components/PoolCreation/index.tsx @@ -2,6 +2,7 @@ import { PoolDetails } from "../PoolDetails"; import { SupportAndResetModals } from "../SupportAndResetModals"; import { ApproveOnTokenManager } from "./ApproveOnTokenManager"; import { PoolCreatedView } from "./PoolCreatedView"; +import { useStableSurgeStep } from "./useStableSurgeStep"; import { Alert, PoolStepsDisplay, TransactionButton } from "~~/components/common"; import { useIsHyperEvm, useIsUsingBigBlocks, useToggleBlockSize } from "~~/hooks/hyperliquid"; import { @@ -142,6 +143,8 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool const showUseBigBlocksStep = isHyperEvm && !isUsingBigBlocks && step === 1; const showUseSmallBlocksStep = isHyperEvm && isUsingBigBlocks && step > 1; + const { showSetMaxSurgeFeeStep, setMaxSurgeFeeStep, showWarnDaoMustUpdateFee } = useStableSurgeStep(); + const poolCreationSteps = [ ...(showUseBigBlocksStep ? [useToggleBlockSizeStep] : []), deployStep, @@ -150,6 +153,7 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool ...swapToBoostedStep, ...approveOnBoostedVariantSteps, initializeStep, + ...(showSetMaxSurgeFeeStep ? [setMaxSurgeFeeStep] : []), ]; return ( @@ -172,6 +176,16 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool + {showWarnDaoMustUpdateFee && ( + + Since you have chosen the Balancer DAO as the swap fee manager, ask for help{" "} + + on our Discord + {" "} + to update the max surge fee for better integration with aggregators. + + )} + {step <= poolCreationSteps.length ? ( poolCreationSteps[step - 1].component ) : ( @@ -180,6 +194,7 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool setIsModalOpen(false)} /> + {step > poolCreationSteps.length && ( Your pool has been successfully initialized and will be available to view in the Balancer app shortly! @@ -203,20 +218,23 @@ interface TransactionButtonManagerProps { onSubmit: () => void; isPending: boolean; error: Error | null; + infoMsg?: string; } -function transactionButtonManager({ +export function transactionButtonManager({ label, blockExplorerUrl, onSubmit, isPending, error, + infoMsg, }: TransactionButtonManagerProps) { return { label, blockExplorerUrl, component: (
+ {infoMsg && {infoMsg}} {error && ( diff --git a/packages/nextjs/app/v3/_components/PoolCreation/useStableSurgeStep.tsx b/packages/nextjs/app/v3/_components/PoolCreation/useStableSurgeStep.tsx new file mode 100644 index 0000000..4f89906 --- /dev/null +++ b/packages/nextjs/app/v3/_components/PoolCreation/useStableSurgeStep.tsx @@ -0,0 +1,38 @@ +import { transactionButtonManager } from "./index"; +import { PoolType } from "@balancer/sdk"; +import { zeroAddress } from "viem"; +import { useAccount } from "wagmi"; +import { useSetMaxSurgeFee } from "~~/hooks/v3/"; +import { useSetMaxSurgeFeeTxHash } from "~~/hooks/v3/"; +import { usePoolCreationStore } from "~~/hooks/v3/"; +import { getBlockExplorerTxLink } from "~~/utils/scaffold-eth"; + +export function useStableSurgeStep() { + const { address: connectedWalletAddress } = useAccount(); + const { chain, setMaxSurgeFeeTx, poolType, swapFeeManager } = usePoolCreationStore(); + const { + mutate: setMaxSurgeFee, + isPending: isSetMaxSurgeFeePending, + error: setMaxSurgeFeeError, + } = useSetMaxSurgeFee(); + const { isFetching: isSetMaxSurgeFeeTxHashPending, error: setMaxSurgeFeeTxHashError } = useSetMaxSurgeFeeTxHash(); + + const setMaxSurgeFeeUrl = setMaxSurgeFeeTx.wagmiHash && getBlockExplorerTxLink(chain?.id, setMaxSurgeFeeTx.wagmiHash); + + const setMaxSurgeFeeStep = transactionButtonManager({ + label: "Set Max Fee", + onSubmit: setMaxSurgeFee, + isPending: isSetMaxSurgeFeePending || isSetMaxSurgeFeeTxHashPending, + error: setMaxSurgeFeeError || setMaxSurgeFeeTxHashError, + blockExplorerUrl: setMaxSurgeFeeUrl, + infoMsg: "For better integration with aggregators, we recommend setting this pool's max surge fee to 10%", + }); + + const isStableSurge = poolType === PoolType.StableSurge; + const connectedWalletIsSwapFeeManager = swapFeeManager === connectedWalletAddress; + const showSetMaxSurgeFeeStep = isStableSurge && connectedWalletIsSwapFeeManager; + const isDaoSwapFeeManager = swapFeeManager === "" || swapFeeManager === zeroAddress; + const showWarnDaoMustUpdateFee = isStableSurge && isDaoSwapFeeManager; + + return { setMaxSurgeFeeStep, showSetMaxSurgeFeeStep, showWarnDaoMustUpdateFee }; +} diff --git a/packages/nextjs/app/v3/_components/PoolDetails.tsx b/packages/nextjs/app/v3/_components/PoolDetails.tsx index fd31a58..21a9c2b 100644 --- a/packages/nextjs/app/v3/_components/PoolDetails.tsx +++ b/packages/nextjs/app/v3/_components/PoolDetails.tsx @@ -39,6 +39,7 @@ export function PoolDetails({ isPreview }: { isPreview?: boolean }) { eclpParams, } = usePoolCreationStore(); const { poolHooksWhitelist } = usePoolHooksWhitelist(chain?.id); + const stableSurgeHookAddress = poolHooksWhitelist.find(hook => hook.label === "StableSurge")?.value; const { isOnlyInitializingPool } = useUserDataStore(); @@ -203,7 +204,17 @@ export function PoolDetails({ isPreview }: { isPreview?: boolean }) {
Pool hooks contract
- {poolHooksContract === zeroAddress ? ( + {poolType === PoolType.StableSurge && stableSurgeHookAddress ? ( + + StableSurge + + + ) : poolHooksContract === zeroAddress ? ( "None" ) : !poolHooksContract ? ( "-" diff --git a/packages/nextjs/hooks/v3/index.ts b/packages/nextjs/hooks/v3/index.ts index e70d8ec..a1a5461 100644 --- a/packages/nextjs/hooks/v3/index.ts +++ b/packages/nextjs/hooks/v3/index.ts @@ -13,3 +13,5 @@ export * from "./useInitializePoolTxHash"; export * from "./useValidateInitializationInputs"; export * from "./useFetchTokenRate"; export * from "./usePoolHooksWhitelist"; +export * from "./useSetMaxSurgeFee"; +export * from "./useSetMaxSurgeFeeTxHash"; diff --git a/packages/nextjs/hooks/v3/usePoolCreationStore.ts b/packages/nextjs/hooks/v3/usePoolCreationStore.ts index fee49d5..607c519 100644 --- a/packages/nextjs/hooks/v3/usePoolCreationStore.ts +++ b/packages/nextjs/hooks/v3/usePoolCreationStore.ts @@ -68,6 +68,7 @@ export interface PoolCreationStore { createPoolTx: TransactionDetails; initPoolTx: TransactionDetails; swapToBoostedTx: TransactionDetails; + setMaxSurgeFeeTx: TransactionDetails; eclpParams: EclpParams; reClammParams: ReClammParams; updatePool: (updates: Partial) => void; @@ -137,6 +138,7 @@ export const initialPoolCreationState = { createPoolTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false }, initPoolTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false }, swapToBoostedTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false }, + setMaxSurgeFeeTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false }, // UX controls isChooseTokenAmountsModalOpen: false, }; diff --git a/packages/nextjs/hooks/v3/useSetMaxSurgeFee.ts b/packages/nextjs/hooks/v3/useSetMaxSurgeFee.ts new file mode 100644 index 0000000..42f38f1 --- /dev/null +++ b/packages/nextjs/hooks/v3/useSetMaxSurgeFee.ts @@ -0,0 +1,50 @@ +import { usePoolCreationStore } from "./usePoolCreationStore"; +import { usePoolHooksWhitelist } from "./usePoolHooksWhitelist"; +import { useMutation } from "@tanstack/react-query"; +import { parseAbi, parseUnits } from "viem"; +import { usePublicClient, useWalletClient } from "wagmi"; +import { useTransactor } from "~~/hooks/scaffold-eth"; + +export const useSetMaxSurgeFee = () => { + const { data: walletClient } = useWalletClient(); + const publicClient = usePublicClient(); + const writeTx = useTransactor(); // scaffold hook for tx status toast notifications + const { updatePool, setMaxSurgeFeeTx, poolAddress, chain } = usePoolCreationStore(); + const { poolHooksWhitelist } = usePoolHooksWhitelist(chain?.id); + + const stableSurgeHookAddress = poolHooksWhitelist.find(hook => hook.label === "StableSurge")?.value; + + const setMaxSurgeFee = async () => { + if (!poolAddress) throw new Error("useSetMaxSurgeFee: poolAddress is undefined"); + if (!walletClient) throw new Error("useApproveToken: wallet client is undefined"); + if (!publicClient) throw new Error("useApproveToken: public client is undefined"); + if (!stableSurgeHookAddress) throw new Error("useSetMaxSurgeFee: stableSurgeHookAddress is undefined"); + + console.log("stableSurgeHookAddress", stableSurgeHookAddress); + + const { request: setMaxFee } = await publicClient.simulateContract({ + address: stableSurgeHookAddress, // stable surge hook address + abi: parseAbi(["function setMaxSurgeFeePercentage(address pool, uint256 newMaxSurgeSurgeFeePercentage)"]), + functionName: "setMaxSurgeFeePercentage", + account: walletClient.account, + args: [poolAddress, parseUnits("10", 16)], // fixed to 10% ? + }); + + console.log("setMaxFee", setMaxFee); + + const txHash = await writeTx(() => walletClient.writeContract(setMaxFee), { + // callbacks to save tx hash's to store + onSafeTxHash: safeHash => updatePool({ setMaxSurgeFeeTx: { ...setMaxSurgeFeeTx, safeHash } }), + onWagmiTxHash: wagmiHash => updatePool({ setMaxSurgeFeeTx: { ...setMaxSurgeFeeTx, wagmiHash } }), + }); + console.log("Approved pool contract to spend token, txHash:", txHash); + return txHash; + }; + + return useMutation({ + mutationFn: () => setMaxSurgeFee(), + onError: error => { + console.error(error); + }, + }); +}; diff --git a/packages/nextjs/hooks/v3/useSetMaxSurgeFeeTxHash.ts b/packages/nextjs/hooks/v3/useSetMaxSurgeFeeTxHash.ts new file mode 100644 index 0000000..457485a --- /dev/null +++ b/packages/nextjs/hooks/v3/useSetMaxSurgeFeeTxHash.ts @@ -0,0 +1,44 @@ +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"; + +export function useSetMaxSurgeFeeTxHash() { + const { setMaxSurgeFeeTx, updatePool, poolType, step } = usePoolCreationStore(); + const { wagmiHash, safeHash } = setMaxSurgeFeeTx; + + const publicClient = usePublicClient(); + const isSafeWallet = useIsSafeWallet(); + const { sdk } = useSafeAppsSDK(); + + return useQuery({ + queryKey: ["setMaxSurgeFeeTx", wagmiHash, safeHash], + queryFn: async () => { + if (!publicClient) throw new Error("No public client for set max surge fee 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, setMaxSurgeFeeTx: { safeHash, wagmiHash, isSuccess: true } }); + return { isSuccess: true }; + } else if (txReceipt.status === "reverted") { + updatePool({ setMaxSurgeFeeTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false } }); + // other option is tx reverts at which point we want to clear state to attempt new tx to be sent + updatePool({ setMaxSurgeFeeTx: { safeHash: undefined, wagmiHash: undefined, isSuccess: false } }); + throw new Error("Set max surge fee transaction reverted"); + } + }, + enabled: Boolean(!setMaxSurgeFeeTx.isSuccess && (safeHash || wagmiHash)), + }); +}