From 778430e01a81bd66a857ea64aa8f4f5bfaee12d6 Mon Sep 17 00:00:00 2001 From: Matt Pereira Date: Thu, 25 Sep 2025 16:45:18 -0700 Subject: [PATCH 1/2] add set max fee step for stable surge pools --- .../app/v3/_components/PoolCreation/index.tsx | 36 ++++++++++++- .../nextjs/app/v3/_components/PoolDetails.tsx | 13 ++++- packages/nextjs/hooks/v3/index.ts | 2 + .../nextjs/hooks/v3/usePoolCreationStore.ts | 2 + packages/nextjs/hooks/v3/useSetMaxSurgeFee.ts | 50 +++++++++++++++++++ .../hooks/v3/useSetMaxSurgeFeeTxHash.ts | 44 ++++++++++++++++ 6 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 packages/nextjs/hooks/v3/useSetMaxSurgeFee.ts create mode 100644 packages/nextjs/hooks/v3/useSetMaxSurgeFeeTxHash.ts diff --git a/packages/nextjs/app/v3/_components/PoolCreation/index.tsx b/packages/nextjs/app/v3/_components/PoolCreation/index.tsx index a55522c..7a9a95d 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 { PoolType } from "@balancer/sdk"; import { Alert, PoolStepsDisplay, TransactionButton } from "~~/components/common"; import { useIsHyperEvm, useIsUsingBigBlocks, useToggleBlockSize } from "~~/hooks/hyperliquid"; import { @@ -13,6 +14,8 @@ import { useMultiSwap, useMultiSwapTxHash, usePoolCreationStore, + useSetMaxSurgeFee, + useSetMaxSurgeFeeTxHash, } from "~~/hooks/v3/"; import { getBlockExplorerTxLink } from "~~/utils/scaffold-eth"; @@ -20,7 +23,17 @@ 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 PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: boolean) => void }) { - const { step, tokenConfigs, createPoolTx, swapToBoostedTx, initPoolTx, chain, poolAddress } = usePoolCreationStore(); + const { + step, + tokenConfigs, + createPoolTx, + swapToBoostedTx, + initPoolTx, + chain, + poolAddress, + poolType, + setMaxSurgeFeeTx, + } = usePoolCreationStore(); const { data: boostableWhitelist } = useBoostableWhitelist(); const { mutate: createPool, isPending: isCreatePoolPending, error: createPoolError } = useCreatePool(); @@ -32,9 +45,17 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool const { mutate: initPool, isPending: isInitPoolPending, error: initPoolError } = useInitializePool(); const { isFetching: isInitPoolTxHashPending, error: initPoolTxHashError } = useInitializePoolTxHash(); + const { + mutate: setMaxSurgeFee, + isPending: isSetMaxSurgeFeePending, + error: setMaxSurgeFeeError, + } = useSetMaxSurgeFee(); + const { isFetching: isSetMaxSurgeFeeTxHashPending, error: setMaxSurgeFeeTxHashError } = useSetMaxSurgeFeeTxHash(); + const poolDeploymentUrl = createPoolTx.wagmiHash && getBlockExplorerTxLink(chain?.id, createPoolTx.wagmiHash); const multiSwapUrl = swapToBoostedTx.wagmiHash && getBlockExplorerTxLink(chain?.id, swapToBoostedTx.wagmiHash); const poolInitializationUrl = initPoolTx.wagmiHash && getBlockExplorerTxLink(chain?.id, initPoolTx.wagmiHash); + const setMaxSurgeFeeUrl = setMaxSurgeFeeTx.wagmiHash && getBlockExplorerTxLink(chain?.id, setMaxSurgeFeeTx.wagmiHash); const deployStep = transactionButtonManager({ label: "Deploy Pool", @@ -142,6 +163,15 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool const showUseBigBlocksStep = isHyperEvm && !isUsingBigBlocks && step === 1; const showUseSmallBlocksStep = isHyperEvm && isUsingBigBlocks && step > 1; + const maxSurgeFeeStep = transactionButtonManager({ + label: "Set Max Fee", + onSubmit: setMaxSurgeFee, + isPending: isSetMaxSurgeFeePending || isSetMaxSurgeFeeTxHashPending, + error: setMaxSurgeFeeError || setMaxSurgeFeeTxHashError, + blockExplorerUrl: setMaxSurgeFeeUrl, + infoMsg: "You must set the max stable surge fee to 10% for aggregators to route through this pool", + }); + const poolCreationSteps = [ ...(showUseBigBlocksStep ? [useToggleBlockSizeStep] : []), deployStep, @@ -150,6 +180,7 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool ...swapToBoostedStep, ...approveOnBoostedVariantSteps, initializeStep, + ...(poolType === PoolType.StableSurge ? [maxSurgeFeeStep] : []), ]; return ( @@ -203,6 +234,7 @@ interface TransactionButtonManagerProps { onSubmit: () => void; isPending: boolean; error: Error | null; + infoMsg?: string; } function transactionButtonManager({ @@ -211,12 +243,14 @@ function transactionButtonManager({ onSubmit, isPending, error, + infoMsg, }: TransactionButtonManagerProps) { return { label, blockExplorerUrl, component: (
+ {infoMsg && {infoMsg}} {error && ( 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)), + }); +} From dcc90cfaee465fa958cdf1f5a3d1a5356f24a4e9 Mon Sep 17 00:00:00 2001 From: Matt Pereira Date: Mon, 29 Sep 2025 15:35:01 -0700 Subject: [PATCH 2/2] handle stable surge max fee advice --- .../app/v3/_components/PoolCreation/index.tsx | 48 +++++++------------ .../PoolCreation/useStableSurgeStep.tsx | 38 +++++++++++++++ 2 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 packages/nextjs/app/v3/_components/PoolCreation/useStableSurgeStep.tsx diff --git a/packages/nextjs/app/v3/_components/PoolCreation/index.tsx b/packages/nextjs/app/v3/_components/PoolCreation/index.tsx index 7a9a95d..068cc4e 100644 --- a/packages/nextjs/app/v3/_components/PoolCreation/index.tsx +++ b/packages/nextjs/app/v3/_components/PoolCreation/index.tsx @@ -2,7 +2,7 @@ import { PoolDetails } from "../PoolDetails"; import { SupportAndResetModals } from "../SupportAndResetModals"; import { ApproveOnTokenManager } from "./ApproveOnTokenManager"; import { PoolCreatedView } from "./PoolCreatedView"; -import { PoolType } from "@balancer/sdk"; +import { useStableSurgeStep } from "./useStableSurgeStep"; import { Alert, PoolStepsDisplay, TransactionButton } from "~~/components/common"; import { useIsHyperEvm, useIsUsingBigBlocks, useToggleBlockSize } from "~~/hooks/hyperliquid"; import { @@ -14,8 +14,6 @@ import { useMultiSwap, useMultiSwapTxHash, usePoolCreationStore, - useSetMaxSurgeFee, - useSetMaxSurgeFeeTxHash, } from "~~/hooks/v3/"; import { getBlockExplorerTxLink } from "~~/utils/scaffold-eth"; @@ -23,17 +21,7 @@ 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 PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: boolean) => void }) { - const { - step, - tokenConfigs, - createPoolTx, - swapToBoostedTx, - initPoolTx, - chain, - poolAddress, - poolType, - setMaxSurgeFeeTx, - } = usePoolCreationStore(); + const { step, tokenConfigs, createPoolTx, swapToBoostedTx, initPoolTx, chain, poolAddress } = usePoolCreationStore(); const { data: boostableWhitelist } = useBoostableWhitelist(); const { mutate: createPool, isPending: isCreatePoolPending, error: createPoolError } = useCreatePool(); @@ -45,17 +33,9 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool const { mutate: initPool, isPending: isInitPoolPending, error: initPoolError } = useInitializePool(); const { isFetching: isInitPoolTxHashPending, error: initPoolTxHashError } = useInitializePoolTxHash(); - const { - mutate: setMaxSurgeFee, - isPending: isSetMaxSurgeFeePending, - error: setMaxSurgeFeeError, - } = useSetMaxSurgeFee(); - const { isFetching: isSetMaxSurgeFeeTxHashPending, error: setMaxSurgeFeeTxHashError } = useSetMaxSurgeFeeTxHash(); - const poolDeploymentUrl = createPoolTx.wagmiHash && getBlockExplorerTxLink(chain?.id, createPoolTx.wagmiHash); const multiSwapUrl = swapToBoostedTx.wagmiHash && getBlockExplorerTxLink(chain?.id, swapToBoostedTx.wagmiHash); const poolInitializationUrl = initPoolTx.wagmiHash && getBlockExplorerTxLink(chain?.id, initPoolTx.wagmiHash); - const setMaxSurgeFeeUrl = setMaxSurgeFeeTx.wagmiHash && getBlockExplorerTxLink(chain?.id, setMaxSurgeFeeTx.wagmiHash); const deployStep = transactionButtonManager({ label: "Deploy Pool", @@ -163,14 +143,7 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool const showUseBigBlocksStep = isHyperEvm && !isUsingBigBlocks && step === 1; const showUseSmallBlocksStep = isHyperEvm && isUsingBigBlocks && step > 1; - const maxSurgeFeeStep = transactionButtonManager({ - label: "Set Max Fee", - onSubmit: setMaxSurgeFee, - isPending: isSetMaxSurgeFeePending || isSetMaxSurgeFeeTxHashPending, - error: setMaxSurgeFeeError || setMaxSurgeFeeTxHashError, - blockExplorerUrl: setMaxSurgeFeeUrl, - infoMsg: "You must set the max stable surge fee to 10% for aggregators to route through this pool", - }); + const { showSetMaxSurgeFeeStep, setMaxSurgeFeeStep, showWarnDaoMustUpdateFee } = useStableSurgeStep(); const poolCreationSteps = [ ...(showUseBigBlocksStep ? [useToggleBlockSizeStep] : []), @@ -180,7 +153,7 @@ export function PoolCreation({ setIsModalOpen }: { setIsModalOpen: (isOpen: bool ...swapToBoostedStep, ...approveOnBoostedVariantSteps, initializeStep, - ...(poolType === PoolType.StableSurge ? [maxSurgeFeeStep] : []), + ...(showSetMaxSurgeFeeStep ? [setMaxSurgeFeeStep] : []), ]; return ( @@ -203,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 ) : ( @@ -211,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! @@ -237,7 +221,7 @@ interface TransactionButtonManagerProps { infoMsg?: string; } -function transactionButtonManager({ +export function transactionButtonManager({ label, blockExplorerUrl, onSubmit, 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 }; +}