From 39a9d46451e11fe52112d78349c6e8e6c23d87b8 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 15 Dec 2025 12:09:23 +0800 Subject: [PATCH 1/8] feat: ExecuteTransactionButton --- .../components/deployment/DeploymentModal.tsx | 17 +- app/positions/components/RebalanceModal.tsx | 36 +--- .../Borrow/AddCollateralAndBorrow.tsx | 80 ++------ .../Borrow/WithdrawCollateralAndRepay.tsx | 75 ++------ src/components/SupplyModalContent.tsx | 178 +++++++----------- src/components/WithdrawModalContent.tsx | 91 ++++----- .../ui/ExecuteTransactionButton.tsx | 149 +++++++++++++++ 7 files changed, 308 insertions(+), 318 deletions(-) create mode 100644 src/components/ui/ExecuteTransactionButton.tsx diff --git a/app/autovault/components/deployment/DeploymentModal.tsx b/app/autovault/components/deployment/DeploymentModal.tsx index 69ae6494..d1bfef34 100644 --- a/app/autovault/components/deployment/DeploymentModal.tsx +++ b/app/autovault/components/deployment/DeploymentModal.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react'; import { Checkbox } from '@heroui/react'; import { FaCube } from 'react-icons/fa'; -import { Button } from '@/components/ui/button'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { Spinner } from '@/components/common/Spinner'; import { useMarkets } from '@/contexts/MarketsContext'; @@ -23,7 +23,7 @@ type DeploymentModalContentProps = { }; function DeploymentModalContent({ isOpen, onOpenChange, existingVaults }: DeploymentModalContentProps) { - const { selectedTokenAndNetwork, needSwitchChain, switchToNetwork, createVault, isDeploying } = useDeployment(); + const { selectedTokenAndNetwork, createVault, isDeploying } = useDeployment(); // Load balances and tokens at modal level const { balances, loading: balancesLoading } = useUserBalances({ @@ -105,16 +105,17 @@ function DeploymentModalContent({ isOpen, onOpenChange, existingVaults }: Deploy )}
-
) : balancesLoading || marketsLoading ? ( 'Loading...' - ) : needSwitchChain && selectedTokenAndNetwork ? ( - `Switch to ${getNetworkName(selectedTokenAndNetwork.networkId)}` ) : selectedTokenAndNetwork ? ( 'Deploy Vault' ) : ( 'Select Asset & Network' )} - + diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index 028f9ef3..d9d887f1 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -6,8 +6,8 @@ import { MarketSelectionModal } from '@/components/common/MarketSelectionModal'; import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { Spinner } from '@/components/common/Spinner'; import { TokenIcon } from '@/components/TokenIcon'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useMarkets } from '@/hooks/useMarkets'; import { useRebalance } from '@/hooks/useRebalance'; import { useStyledToast } from '@/hooks/useStyledToast'; @@ -110,7 +110,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, return false; } return true; - }, [selectedFromMarketUniqueKey, amount, groupedPosition.loanAssetDecimals, getPendingDelta, toast]); + }, [selectedFromMarketUniqueKey, amount, groupedPosition.loanAssetDecimals, getPendingDelta, toast, groupedPosition.markets]); const createAction = useCallback((fromMarket: Market, toMarket: Market, actionAmount: bigint, isMax: boolean): RebalanceAction => { return { @@ -194,30 +194,13 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, resetSelections, ]); - // Use the market network hook for chain switching with direct chainId - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: groupedPosition.chainId, - }); - const handleExecuteRebalance = useCallback(async () => { - if (needSwitchChain) { - try { - // Call our switchToNetwork function - switchToNetwork(); - // Wait a bit for the network switch to complete - await new Promise((resolve) => setTimeout(resolve, 1000)); - } catch (_error) { - toast.error('Something went wrong', 'Failed to switch network. Please try again'); - return; - } - } - setShowProcessModal(true); try { const result = await executeRebalance(); // Explicitly refetch AFTER successful execution - if (result == true) { + if (result === true) { refetch(() => { toast.info('Data refreshed', 'Position data updated after rebalance.'); }); @@ -227,7 +210,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, } finally { setShowProcessModal(false); } - }, [executeRebalance, needSwitchChain, switchToNetwork, toast, refetch]); + }, [executeRebalance, toast, refetch]); const handleManualRefresh = () => { refetch(() => { @@ -319,15 +302,16 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, > Cancel - + Execute Rebalance + {showProcessModal && ( diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index a686a347..616d80b0 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -1,21 +1,18 @@ import { useState, useEffect, useCallback } from 'react'; import { Switch } from '@heroui/react'; import { ReloadIcon } from '@radix-ui/react-icons'; -import { useConnection } from 'wagmi'; -import { Button } from '@/components/ui/button'; import { LTVWarning } from '@/components/common/LTVWarning'; import { MarketDetailsBlock } from '@/components/common/MarketDetailsBlock'; import Input from '@/components/Input/Input'; -import AccountConnect from '@/components/layout/header/AccountConnect'; import { useBorrowTransaction } from '@/hooks/useBorrowTransaction'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { formatBalance, formatReadable } from '@/utils/balance'; import { getNativeTokenSymbol } from '@/utils/networks'; import { isWrappedNativeToken } from '@/utils/tokens'; import type { Market, MarketPosition } from '@/utils/types'; import { BorrowProcessModal } from '../BorrowProcessModal'; import { TokenIcon } from '../TokenIcon'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { getLTVColor, getLTVProgressColor } from './helpers'; type BorrowLogicProps = { @@ -44,8 +41,6 @@ export function AddCollateralAndBorrow({ const [borrowInputError, setBorrowInputError] = useState(null); const [usePermit2Setting] = useLocalStorage('usePermit2', true); - const { isConnected } = useConnection(); - // lltv with 18 decimals const lltv = BigInt(market.lltv); @@ -53,11 +48,6 @@ export function AddCollateralAndBorrow({ const [currentLTV, setCurrentLTV] = useState(BigInt(0)); const [newLTV, setNewLTV] = useState(BigInt(0)); - // Use the market network hook for chain switching - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: market.morphoBlue.chain.id, - }); - // Use the new hook for borrow transaction logic const { currentStep, @@ -238,8 +228,7 @@ export function AddCollateralAndBorrow({ /> - {isConnected && ( -
+
{/* Collateral Input Section */}
@@ -307,60 +296,25 @@ export function AddCollateralAndBorrow({
- )} {/* Action Button */}
- {isConnected ? ( - needSwitchChain ? ( - - ) : (!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? ( - - ) : ( - - ) - ) : ( -
- -
- )} + void ((!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? approveAndBorrow() : signAndBorrow())} + isLoading={isLoadingPermit2 || borrowPending} + disabled={ + collateralInputError !== null || + borrowInputError !== null || + (collateralAmount === BigInt(0) && borrowAmount === BigInt(0)) || + newLTV >= lltv + } + variant="primary" + className="min-w-32" + > + {collateralAmount > 0n && borrowAmount === 0n ? 'Add Collateral' : 'Borrow'} +
{(borrowAmount > 0n || collateralAmount > 0n) && ( <> diff --git a/src/components/Borrow/WithdrawCollateralAndRepay.tsx b/src/components/Borrow/WithdrawCollateralAndRepay.tsx index aa45ea37..e2989fa8 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -1,17 +1,14 @@ import { useMemo, useState, useEffect, useCallback } from 'react'; import { ReloadIcon } from '@radix-ui/react-icons'; -import { useConnection } from 'wagmi'; -import { Button } from '@/components/ui/button'; import { LTVWarning } from '@/components/common/LTVWarning'; import Input from '@/components/Input/Input'; -import AccountConnect from '@/components/layout/header/AccountConnect'; import { RepayProcessModal } from '@/components/RepayProcessModal'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useRepayTransaction } from '@/hooks/useRepayTransaction'; import { formatBalance } from '@/utils/balance'; import type { Market, MarketPosition } from '@/utils/types'; import { MarketDetailsBlock } from '../common/MarketDetailsBlock'; import { TokenIcon } from '../TokenIcon'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { getLTVColor, getLTVProgressColor } from './helpers'; type WithdrawCollateralAndRepayProps = { @@ -44,8 +41,6 @@ export function WithdrawCollateralAndRepay({ const [withdrawInputError, setWithdrawInputError] = useState(null); const [repayInputError, setRepayInputError] = useState(null); - const { isConnected } = useConnection(); - // lltv with 18 decimals const lltv = BigInt(market.lltv); @@ -53,11 +48,6 @@ export function WithdrawCollateralAndRepay({ const [currentLTV, setCurrentLTV] = useState(BigInt(0)); const [newLTV, setNewLTV] = useState(BigInt(0)); - // Use the market network hook for chain switching - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: market.morphoBlue.chain.id, - }); - // Use the repay transaction hook const { currentStep, @@ -242,8 +232,7 @@ export function WithdrawCollateralAndRepay({ />
- {isConnected && ( -
+
{/* Withdraw Input Section */}
@@ -293,7 +282,6 @@ export function WithdrawCollateralAndRepay({
- )} {/* Action Button */}
@@ -301,50 +289,21 @@ export function WithdrawCollateralAndRepay({ className="flex justify-end" style={{ zIndex: 1 }} > - {isConnected ? ( - needSwitchChain ? ( - - ) : ( - - ) - ) : ( -
- -
- )} + void (!isApproved && !permit2Authorized ? approveAndRepay() : signAndRepay())} + isLoading={repayPending || isLoadingPermit2} + disabled={ + withdrawInputError !== null || + repayInputError !== null || + (withdrawAmount === BigInt(0) && repayAssets === BigInt(0)) || + newLTV >= lltv + } + variant="primary" + className="min-w-32" + > + {!isApproved && !permit2Authorized ? 'Approve & Repay' : withdrawAmount > 0 ? 'Withdraw & Repay' : 'Repay'} +
{(withdrawAmount > 0n || repayAssets > 0n) && ( <> diff --git a/src/components/SupplyModalContent.tsx b/src/components/SupplyModalContent.tsx index bfc9e823..5d319407 100644 --- a/src/components/SupplyModalContent.tsx +++ b/src/components/SupplyModalContent.tsx @@ -1,17 +1,14 @@ import { useCallback } from 'react'; import { Switch } from '@heroui/react'; import { ReloadIcon } from '@radix-ui/react-icons'; -import { useConnection } from 'wagmi'; import Input from '@/components/Input/Input'; -import AccountConnect from '@/components/layout/header/AccountConnect'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useSupplyMarket } from '@/hooks/useSupplyMarket'; import { formatBalance } from '@/utils/balance'; import { getNativeTokenSymbol } from '@/utils/networks'; import { isWrappedNativeToken } from '@/utils/tokens'; import type { Market } from '@/utils/types'; -import { Button } from '@/components/ui/button'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { SupplyProcessModal } from './SupplyProcessModal'; type SupplyModalContentProps = { @@ -23,7 +20,6 @@ type SupplyModalContentProps = { export function SupplyModalContent({ onClose, market, refetch, onAmountChange }: SupplyModalContentProps): JSX.Element { const [usePermit2Setting] = useLocalStorage('usePermit2', true); - const { isConnected } = useConnection(); const onSuccess = useCallback(() => { onClose(); @@ -61,11 +57,6 @@ export function SupplyModalContent({ onClose, market, refetch, onAmountChange }: [setSupplyAmount, onAmountChange], ); - // Use the market network hook to handle network switching - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: market.morphoBlue.chain.id, - }); - return ( <> {showProcessModal && ( @@ -80,107 +71,82 @@ export function SupplyModalContent({ onClose, market, refetch, onAmountChange }: )} {!showProcessModal && (
- {isConnected ? ( - <> - {/* Supply Input Section */} -
- {isWrappedNativeToken(market.loanAsset.address, market.morphoBlue.chain.id) && ( -
-
Use {getNativeTokenSymbol(market.morphoBlue.chain.id)} instead
- -
- )} - -
-
- Supply amount -
-

- Balance:{' '} - {useEth - ? formatBalance(ethBalance ?? BigInt(0), 18) - : formatBalance(tokenBalance ?? BigInt(0), market.loanAsset.decimals)}{' '} - {useEth ? getNativeTokenSymbol(market.morphoBlue.chain.id) : market.loanAsset.symbol} -

- -
-
+ {/* Supply Input Section */} +
+ {isWrappedNativeToken(market.loanAsset.address, market.morphoBlue.chain.id) && ( +
+
Use {getNativeTokenSymbol(market.morphoBlue.chain.id)} instead
+ +
+ )} -
-
- { - if (typeof error === 'string' && !error.includes("You don't have any supplied assets")) { - setInputError(error); - } else { - setInputError(null); - } - }} - exceedMaxErrMessage={ - supplyAmount && supplyAmount > (useEth ? (ethBalance ?? BigInt(0)) : (tokenBalance ?? BigInt(0))) - ? 'Insufficient Balance' - : undefined - } - /> - {inputError && !inputError.includes("You don't have any supplied assets") && ( -

{inputError}

- )} -
+
+
+ Supply amount +
+

+ Balance:{' '} + {useEth + ? formatBalance(ethBalance ?? BigInt(0), 18) + : formatBalance(tokenBalance ?? BigInt(0), market.loanAsset.decimals)}{' '} + {useEth ? getNativeTokenSymbol(market.morphoBlue.chain.id) : market.loanAsset.symbol} +

+ +
+
- {needSwitchChain ? ( - - ) : (!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? ( - - ) : ( - - )} -
+
+
+ { + if (typeof error === 'string' && !error.includes("You don't have any supplied assets")) { + setInputError(error); + } else { + setInputError(null); + } + }} + exceedMaxErrMessage={ + supplyAmount && supplyAmount > (useEth ? (ethBalance ?? BigInt(0)) : (tokenBalance ?? BigInt(0))) + ? 'Insufficient Balance' + : undefined + } + /> + {inputError && !inputError.includes("You don't have any supplied assets") && ( +

{inputError}

+ )}
+ + void ((!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? approveAndSupply() : signAndSupply())} + isLoading={isLoadingPermit2 || supplyPending} + disabled={inputError !== null || !supplyAmount} + variant="primary" + className="ml-2 min-w-32" + > + Supply +
- - ) : ( -
-
- )} +
)} diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index f0d38957..c8a49f29 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -4,15 +4,13 @@ import { type Address, encodeFunctionData } from 'viem'; import { useConnection } from 'wagmi'; import morphoAbi from '@/abis/morpho'; import Input from '@/components/Input/Input'; -import AccountConnect from '@/components/layout/header/AccountConnect'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatBalance, formatReadable, min } from '@/utils/balance'; import { getMorphoAddress } from '@/utils/morpho'; import type { SupportedNetworks } from '@/utils/networks'; import type { Market, MarketPosition } from '@/utils/types'; -import { Button } from '@/components/ui/button'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; type WithdrawModalContentProps = { position?: MarketPosition | null; @@ -40,10 +38,6 @@ export function WithdrawModalContent({ position, market, onClose, refetch, onAmo // Prefer the market prop (which has fresh state) over position.market const activeMarket = market ?? position?.market; - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: activeMarket?.morphoBlue.chain.id ?? 0, - }); - const { isConfirming, sendTransaction } = useTransactionWithToast({ toastId: 'withdraw', pendingText: activeMarket @@ -119,59 +113,44 @@ export function WithdrawModalContent({ position, market, onClose, refetch, onAmo return (
- {isConnected ? ( - <> - {/* Withdraw Input Section */} -
-
-
- Withdraw amount -
-

- Available: {formatReadable(formatBalance(position?.state.supplyAssets ?? BigInt(0), activeMarket.loanAsset.decimals))}{' '} - {activeMarket.loanAsset.symbol} -

-
-
+ {/* Withdraw Input Section */} +
+
+
+ Withdraw amount +
+

+ Available: {formatReadable(formatBalance(position?.state.supplyAssets ?? BigInt(0), activeMarket.loanAsset.decimals))}{' '} + {activeMarket.loanAsset.symbol} +

+
+
-
-
- -
- {needSwitchChain ? ( - - ) : ( - - )} -
+
+
+
+ + void withdraw()} + isLoading={isConfirming} + disabled={!withdrawAmount} + variant="primary" + className="ml-2 min-w-32" + > + Withdraw +
- - ) : ( -
-
- )} +
); } diff --git a/src/components/ui/ExecuteTransactionButton.tsx b/src/components/ui/ExecuteTransactionButton.tsx new file mode 100644 index 00000000..12dfbbaa --- /dev/null +++ b/src/components/ui/ExecuteTransactionButton.tsx @@ -0,0 +1,149 @@ +import { useCallback, useState } from 'react'; +import { useAppKit } from '@reown/appkit/react'; +import { useConnection } from 'wagmi'; +import { Button, type ButtonProps } from '@/components/ui/button'; +import { Spinner } from '@/components/common/Spinner'; +import { useMarketNetwork } from '@/hooks/useMarketNetwork'; +import { getNetworkName } from '@/utils/networks'; + +type ExecuteTransactionButtonProps = Omit & { + /** + * The target chain ID that the transaction needs to execute on + */ + targetChainId: number; + + /** + * The transaction execution function to call when ready + */ + onClick: () => void; + + /** + * Whether the transaction is currently loading/pending + */ + isLoading?: boolean; + + /** + * The text to display when the button is ready to execute the transaction + */ + children: React.ReactNode; + + /** + * Optional custom text for the connect wallet state + * @default "Connect Wallet" + */ + connectText?: string; + + /** + * Optional custom text for the switch chain state + * @default "Switch to [Network Name]" + */ + switchChainText?: string; +}; + +/** + * A smart transaction button that handles the complete flow: + * 1. Connect wallet if not connected + * 2. Switch to correct chain if needed + * 3. Execute the transaction + * + * This eliminates the need for repetitive wallet connection and chain switching + * checks across transaction components. + * + * @example + * ```tsx + * void approveAndSupply()} + * isLoading={supplyPending} + * disabled={!supplyAmount || inputError !== null} + * variant="primary" + * > + * Supply + * + * ``` + */ +export function ExecuteTransactionButton({ + targetChainId, + onClick, + isLoading = false, + children, + connectText = 'Connect Wallet', + switchChainText, + disabled, + variant = 'primary', + ...buttonProps +}: ExecuteTransactionButtonProps): JSX.Element { + const { open } = useAppKit(); + const { isConnected } = useConnection(); + const [isSwitching, setIsSwitching] = useState(false); + + // Use the market network hook for chain validation and switching + const { needSwitchChain, switchToNetwork } = useMarketNetwork({ + targetChainId, + }); + + // Handle wallet connection + const handleConnect = useCallback(() => { + open(); + }, [open]); + + // Handle chain switching with loading state + const handleSwitchChain = useCallback(async () => { + setIsSwitching(true); + try { + switchToNetwork(); + // Wait a bit for the switch to complete + await new Promise((resolve) => setTimeout(resolve, 500)); + } finally { + setIsSwitching(false); + } + }, [switchToNetwork]); + + // Determine button state and content + if (!isConnected) { + return ( + + ); + } + + if (needSwitchChain) { + const defaultSwitchText = `Switch to ${getNetworkName(targetChainId)}`; + return ( + + ); + } + + // Ready to execute transaction + return ( + + ); +} From 0e52f828cec25cbca0510e80a074250a2d15cd4d Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 15 Dec 2025 12:33:23 +0800 Subject: [PATCH 2/8] refactor: execution button usages --- .../components/DepositToVaultModal.tsx | 89 ++++++------- .../components/deployment/DeploymentModal.tsx | 15 +-- app/positions/components/RebalanceModal.tsx | 34 ++--- .../components/onboarding/SetupPositions.tsx | 54 +++----- app/rewards/components/RewardTable.tsx | 67 ++++------ .../Borrow/AddCollateralAndBorrow.tsx | 126 ++++++++++-------- .../Borrow/WithdrawCollateralAndRepay.tsx | 91 +++++++------ src/components/SupplyModalContent.tsx | 11 +- src/components/WithdrawModalContent.tsx | 8 +- 9 files changed, 245 insertions(+), 250 deletions(-) diff --git a/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx b/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx index 2550a307..79f8bf75 100644 --- a/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx @@ -1,11 +1,10 @@ 'use client'; +import { useCallback } from 'react'; import type { Address } from 'viem'; -import { useConnection } from 'wagmi'; -import { Button } from '@/components/ui/button'; import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import Input from '@/components/Input/Input'; -import AccountConnect from '@/components/layout/header/AccountConnect'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { TokenIcon } from '@/components/TokenIcon'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useVaultV2Deposit } from '@/hooks/useVaultV2Deposit'; @@ -33,7 +32,6 @@ export function DepositToVaultModal({ onClose, onSuccess, }: DepositToVaultModalProps): JSX.Element { - const { isConnected } = useConnection(); const [usePermit2Setting] = useLocalStorage('usePermit2', true); const { @@ -61,6 +59,14 @@ export function DepositToVaultModal({ onSuccess, }); + const handleDeposit = useCallback(() => { + if (!permit2Authorized || (!usePermit2Setting && !isApproved)) { + void approveAndDeposit(); + } else { + void signAndDeposit(); + } + }, [permit2Authorized, usePermit2Setting, isApproved, approveAndDeposit, signAndDeposit]); + return ( <> - {isConnected ? ( -
-
-
- Deposit amount -

- Balance: {formatBalance(tokenBalance ?? BigInt(0), assetDecimals)} {assetSymbol} -

-
- -
-
- - {inputError &&

{inputError}

} -
+
+
+
+ Deposit amount +

+ Balance: {formatBalance(tokenBalance ?? BigInt(0), assetDecimals)} {assetSymbol} +

+
- {!permit2Authorized || (!usePermit2Setting && !isApproved) ? ( - - ) : ( - - )} +
+
+ + {inputError &&

{inputError}

}
+ + + {!permit2Authorized || (!usePermit2Setting && !isApproved) ? 'Approve' : 'Deposit'} +
- ) : ( -
- -
- )} +
diff --git a/app/autovault/components/deployment/DeploymentModal.tsx b/app/autovault/components/deployment/DeploymentModal.tsx index d1bfef34..a460b763 100644 --- a/app/autovault/components/deployment/DeploymentModal.tsx +++ b/app/autovault/components/deployment/DeploymentModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Checkbox } from '@heroui/react'; import { FaCube } from 'react-icons/fa'; import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; @@ -53,6 +53,10 @@ function DeploymentModalContent({ isOpen, onOpenChange, existingVaults }: Deploy } }, [isOpen]); + const handleCreateVault = useCallback(() => { + void createVault(); + }, [createVault]); + return ( void createVault()} - disabled={ - !selectedTokenAndNetwork || - balancesLoading || - marketsLoading || - (userAlreadyHasVault && !ackExistingVault) - } + onClick={handleCreateVault} + disabled={!selectedTokenAndNetwork || balancesLoading || marketsLoading || (userAlreadyHasVault && !ackExistingVault)} isLoading={isDeploying} variant="primary" className="min-w-[140px]" diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index d9d887f1..add6b8eb 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -194,22 +194,24 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, resetSelections, ]); - const handleExecuteRebalance = useCallback(async () => { - setShowProcessModal(true); - try { - const result = await executeRebalance(); - // Explicitly refetch AFTER successful execution - - if (result === true) { - refetch(() => { - toast.info('Data refreshed', 'Position data updated after rebalance.'); - }); + const handleExecuteRebalance = useCallback(() => { + void (async () => { + setShowProcessModal(true); + try { + const result = await executeRebalance(); + // Explicitly refetch AFTER successful execution + + if (result === true) { + refetch(() => { + toast.info('Data refreshed', 'Position data updated after rebalance.'); + }); + } + } catch (error) { + console.error('Error during rebalance:', error); + } finally { + setShowProcessModal(false); } - } catch (error) { - console.error('Error during rebalance:', error); - } finally { - setShowProcessModal(false); - } + })(); }, [executeRebalance, toast, refetch]); const handleManualRefresh = () => { @@ -304,7 +306,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, void handleExecuteRebalance()} + onClick={handleExecuteRebalance} disabled={rebalanceActions.length === 0} isLoading={isProcessing} variant="primary" diff --git a/app/positions/components/onboarding/SetupPositions.tsx b/app/positions/components/onboarding/SetupPositions.tsx index 32fb7f61..1dc8f7c1 100644 --- a/app/positions/components/onboarding/SetupPositions.tsx +++ b/app/positions/components/onboarding/SetupPositions.tsx @@ -4,11 +4,11 @@ import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'; import Image from 'next/image'; import { formatUnits, parseUnits } from 'viem'; import { Button } from '@/components/ui/button'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import Input from '@/components/Input/Input'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/components/MarketIdentity'; import { SupplyProcessModal } from '@/components/SupplyProcessModal'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useMultiMarketSupply } from '@/hooks/useMultiMarketSupply'; import { useRateLabel } from '@/hooks/useRateLabel'; import { useStyledToast } from '@/hooks/useStyledToast'; @@ -31,11 +31,6 @@ export function SetupPositions() { const [error, setError] = useState(null); const [isSupplying, setIsSupplying] = useState(false); - // Use our custom hook for network switching - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: selectedToken?.network ?? SupportedNetworks.Base, - }); - // Compute token balance and decimals const tokenBalance = useMemo(() => { if (!selectedToken) return 0n; @@ -190,32 +185,24 @@ export function SetupPositions() { goToNextStep, ); - const handleSupply = async () => { - if (isSupplying) { - toast.info('Loading', 'Supplying in progress'); - return; - } + const handleSupply = useCallback(() => { + void (async () => { + if (isSupplying) { + toast.info('Loading', 'Supplying in progress'); + return; + } + + setIsSupplying(true); - if (needSwitchChain && selectedToken) { try { - switchToNetwork(); - // Wait for network switch to complete - await new Promise((resolve) => setTimeout(resolve, 1000)); - } catch (_switchError) { - toast.error('Failed to switch network', 'Please try again'); - return; + // trigger the tx. goToNextStep() be called as a `onSuccess` callback + await approveAndSupply(); + } catch (_supplyError) { + } finally { + setIsSupplying(false); } - } - setIsSupplying(true); - - try { - // trigger the tx. goToNextStep() be called as a `onSuccess` callback - await approveAndSupply(); - } catch (_supplyError) { - } finally { - setIsSupplying(false); - } - }; + })(); + }, [isSupplying, toast, approveAndSupply]); if (!selectedToken || !selectedMarkets || selectedMarkets.length === 0) { return null; @@ -379,15 +366,16 @@ export function SetupPositions() { > Back - +
); diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx index da812e5c..11ec1421 100644 --- a/app/rewards/components/RewardTable.tsx +++ b/app/rewards/components/RewardTable.tsx @@ -1,20 +1,20 @@ 'use client'; -import { useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@heroui/react'; import Image from 'next/image'; import Link from 'next/link'; import type { Address } from 'viem'; import { useConnection } from 'wagmi'; import { Button } from '@/components/ui/button'; +import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { TokenIcon } from '@/components/TokenIcon'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import type { DistributionResponseType } from '@/hooks/useRewards'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatBalance, formatSimple } from '@/utils/balance'; import { getAssetURL } from '@/utils/external'; -import { getNetworkImg, SupportedNetworks } from '@/utils/networks'; +import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; import type { AggregatedRewardType } from '@/utils/types'; @@ -28,15 +28,6 @@ type RewardTableProps = { export default function RewardTable({ rewards, distributions, account, showClaimed }: RewardTableProps) { const { chainId } = useConnection(); const toast = useStyledToast(); - const [targetChainId, setTargetChainId] = useState(chainId ?? SupportedNetworks.Mainnet); - - // Use our custom hook for network switching - const { switchToNetwork } = useMarketNetwork({ - targetChainId, - onNetworkSwitched: () => { - // Additional actions after network switch if needed - }, - }); const { sendTransaction } = useTransactionWithToast({ toastId: 'claim', @@ -61,6 +52,26 @@ export default function RewardTable({ rewards, distributions, account, showClaim [rewards, showClaimed], ); + const handleClaim = useCallback( + (distribution: DistributionResponseType | undefined) => { + if (!account) { + toast.error('No account connected', 'Please connect your wallet to continue.'); + return; + } + if (!distribution) { + toast.error('No claim data', 'No claim data found for this reward please try again later.'); + return; + } + sendTransaction({ + account: account as Address, + to: distribution.distributor.address as Address, + data: distribution.tx_data as `0x${string}`, + chainId: distribution.distributor.chain_id, + }); + }, + [account, toast, sendTransaction], + ); + return (
@@ -202,39 +213,15 @@ export default function RewardTable({ rewards, distributions, account, showClaim ) : ( - + )}
diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index 616d80b0..d41bd88a 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -68,6 +68,14 @@ export function AddCollateralAndBorrow({ onSuccess, }); + const handleBorrow = useCallback(() => { + if ((!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved)) { + void approveAndBorrow(); + } else { + void signAndBorrow(); + } + }, [permit2Authorized, useEth, usePermit2Setting, isApproved, approveAndBorrow, signAndBorrow]); + // Calculate current and new LTV whenever relevant values change useEffect(() => { if (currentPosition) { @@ -229,80 +237,80 @@ export function AddCollateralAndBorrow({
- {/* Collateral Input Section */} -
-
-

Add Collateral

-

- Balance:{' '} - {useEth - ? formatBalance(ethBalance ? ethBalance : '0', 18) - : formatBalance(collateralTokenBalance ? collateralTokenBalance : '0', market.collateralAsset.decimals)}{' '} - {useEth ? getNativeTokenSymbol(market.morphoBlue.chain.id) : market.collateralAsset.symbol} -

-
+ {/* Collateral Input Section */} +
+
+

Add Collateral

+

+ Balance:{' '} + {useEth + ? formatBalance(ethBalance ? ethBalance : '0', 18) + : formatBalance(collateralTokenBalance ? collateralTokenBalance : '0', market.collateralAsset.decimals)}{' '} + {useEth ? getNativeTokenSymbol(market.morphoBlue.chain.id) : market.collateralAsset.symbol} +

+
- {isWrappedNativeToken(market.collateralAsset.address, market.morphoBlue.chain.id) && ( -
-
Use {getNativeTokenSymbol(market.morphoBlue.chain.id)} instead
- -
- )} + {isWrappedNativeToken(market.collateralAsset.address, market.morphoBlue.chain.id) && ( +
+
Use {getNativeTokenSymbol(market.morphoBlue.chain.id)} instead
+ +
+ )} -
-
- - {collateralInputError &&

{collateralInputError}

} -
+
+
+ + {collateralInputError &&

{collateralInputError}

}
+
- {/* Borrow Input Section */} -
-
-

Borrow

-

- Available: {formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))}{' '} - {market.loanAsset.symbol} -

-
+ {/* Borrow Input Section */} +
+
+

Borrow

+

+ Available: {formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))}{' '} + {market.loanAsset.symbol} +

+
-
-
- - {borrowInputError &&

{borrowInputError}

} -
+
+
+ + {borrowInputError &&

{borrowInputError}

}
+
{/* Action Button */}
void ((!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? approveAndBorrow() : signAndBorrow())} + onClick={handleBorrow} isLoading={isLoadingPermit2 || borrowPending} disabled={ collateralInputError !== null || diff --git a/src/components/Borrow/WithdrawCollateralAndRepay.tsx b/src/components/Borrow/WithdrawCollateralAndRepay.tsx index e2989fa8..1e265496 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -68,6 +68,14 @@ export function WithdrawCollateralAndRepay({ onSuccess, }); + const handleRepay = useCallback(() => { + if (!isApproved && !permit2Authorized) { + void approveAndRepay(); + } else { + void signAndRepay(); + } + }, [isApproved, permit2Authorized, approveAndRepay, signAndRepay]); + // if max is clicked, set the repayShares to max shares const setShareToMax = useCallback(() => { if (currentPosition) { @@ -233,55 +241,54 @@ export function WithdrawCollateralAndRepay({
- {/* Withdraw Input Section */} -
-
-

Withdraw Collateral

-

- Available: {formatBalance(BigInt(currentPosition?.state.collateral ?? 0), market.collateralAsset.decimals)}{' '} - {market.collateralAsset.symbol} -

-
+ {/* Withdraw Input Section */} +
+
+

Withdraw Collateral

+

+ Available: {formatBalance(BigInt(currentPosition?.state.collateral ?? 0), market.collateralAsset.decimals)}{' '} + {market.collateralAsset.symbol} +

+
-
-
- - {withdrawInputError &&

{withdrawInputError}

} -
+
+
+ + {withdrawInputError &&

{withdrawInputError}

}
+
- {/* Repay Input Section */} -
-
-

Repay Loan

-

- Debt: {formatBalance(BigInt(currentPosition?.state.borrowAssets ?? 0), market.loanAsset.decimals)}{' '} - {market.loanAsset.symbol} -

-
+ {/* Repay Input Section */} +
+
+

Repay Loan

+

+ Debt: {formatBalance(BigInt(currentPosition?.state.borrowAssets ?? 0), market.loanAsset.decimals)} {market.loanAsset.symbol} +

+
-
-
- - {repayInputError &&

{repayInputError}

} -
+
+
+ + {repayInputError &&

{repayInputError}

}
+
{/* Action Button */}
@@ -291,7 +298,7 @@ export function WithdrawCollateralAndRepay({ > void (!isApproved && !permit2Authorized ? approveAndRepay() : signAndRepay())} + onClick={handleRepay} isLoading={repayPending || isLoadingPermit2} disabled={ withdrawInputError !== null || diff --git a/src/components/SupplyModalContent.tsx b/src/components/SupplyModalContent.tsx index 5d319407..f989f78e 100644 --- a/src/components/SupplyModalContent.tsx +++ b/src/components/SupplyModalContent.tsx @@ -57,6 +57,15 @@ export function SupplyModalContent({ onClose, market, refetch, onAmountChange }: [setSupplyAmount, onAmountChange], ); + // Handle supply execution + const handleSupply = useCallback(() => { + if ((!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved)) { + void approveAndSupply(); + } else { + void signAndSupply(); + } + }, [permit2Authorized, useEth, usePermit2Setting, isApproved, approveAndSupply, signAndSupply]); + return ( <> {showProcessModal && ( @@ -136,7 +145,7 @@ export function SupplyModalContent({ onClose, market, refetch, onAmountChange }: void ((!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? approveAndSupply() : signAndSupply())} + onClick={handleSupply} isLoading={isLoadingPermit2 || supplyPending} disabled={inputError !== null || !supplyAmount} variant="primary" diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index c8a49f29..34f33447 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -33,7 +33,7 @@ export function WithdrawModalContent({ position, market, onClose, refetch, onAmo }, [onAmountChange], ); - const { address: account, isConnected, chainId } = useConnection(); + const { address: account, chainId } = useConnection(); // Prefer the market prop (which has fresh state) over position.market const activeMarket = market ?? position?.market; @@ -103,6 +103,10 @@ export function WithdrawModalContent({ position, market, onClose, refetch, onAmo }); }, [account, activeMarket, position, withdrawAmount, sendTransaction, toast]); + const handleWithdraw = useCallback(() => { + void withdraw(); + }, [withdraw]); + if (!activeMarket) { return (
@@ -140,7 +144,7 @@ export function WithdrawModalContent({ position, market, onClose, refetch, onAmo void withdraw()} + onClick={handleWithdraw} isLoading={isConfirming} disabled={!withdrawAmount} variant="primary" From 14ef92a3fb9d84b7f46f55cd899c101d3f9e2cfe Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 15 Dec 2025 12:33:37 +0800 Subject: [PATCH 3/8] chore: lint --- src/components/ui/ExecuteTransactionButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/ExecuteTransactionButton.tsx b/src/components/ui/ExecuteTransactionButton.tsx index 12dfbbaa..d18c9992 100644 --- a/src/components/ui/ExecuteTransactionButton.tsx +++ b/src/components/ui/ExecuteTransactionButton.tsx @@ -128,7 +128,7 @@ export function ExecuteTransactionButton({ Switching...
) : ( - switchChainText ?? defaultSwitchText + (switchChainText ?? defaultSwitchText) )} ); From 21180463c263f426fa205707b515734a4711b421 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 15 Dec 2025 13:00:08 +0800 Subject: [PATCH 4/8] docs: update docs --- .../components/deployment/DeploymentModal.tsx | 21 +++----- docs/ARCHITECTURE.md | 49 +++++++++++++++++++ .../Borrow/AddCollateralAndBorrow.tsx | 3 +- .../ui/ExecuteTransactionButton.tsx | 7 --- 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/app/autovault/components/deployment/DeploymentModal.tsx b/app/autovault/components/deployment/DeploymentModal.tsx index a460b763..5bc1169c 100644 --- a/app/autovault/components/deployment/DeploymentModal.tsx +++ b/app/autovault/components/deployment/DeploymentModal.tsx @@ -5,7 +5,6 @@ import { Checkbox } from '@heroui/react'; import { FaCube } from 'react-icons/fa'; import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; -import { Spinner } from '@/components/common/Spinner'; import { useMarkets } from '@/contexts/MarketsContext'; import type { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults'; import { useUserBalances } from '@/hooks/useUserBalances'; @@ -57,6 +56,13 @@ function DeploymentModalContent({ isOpen, onOpenChange, existingVaults }: Deploy void createVault(); }, [createVault]); + const getButtonText = useCallback(() => { + if (isDeploying) return 'Deploying...'; + if (balancesLoading || marketsLoading) return 'Loading...'; + if (!selectedTokenAndNetwork) return 'Select Asset & Network'; + return 'Deploy Vault'; + }, [isDeploying, balancesLoading, marketsLoading, selectedTokenAndNetwork]); + return ( - {isDeploying ? ( -
- - Deploying... -
- ) : balancesLoading || marketsLoading ? ( - 'Loading...' - ) : selectedTokenAndNetwork ? ( - 'Deploy Vault' - ) : ( - 'Select Asset & Network' - )} + {getButtonText()}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0e2351ef..5ebf92fa 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -194,6 +194,55 @@ const vault = getVaultByAddress(vaultAddress, chainId); **Vault data source**: `src/data-sources/morpho-api/vaults.ts` +--- + +## Transaction Pattern + +**ExecuteTransactionButton** handles wallet connection + chain switching automatically. + +**Standard Pattern**: +```typescript +// 1. Transaction hook (approval + execution logic) +const { approveAndExecute, signAndExecute, isLoading } = useXTransaction({ ... }); + +// 2. Named callback with useCallback +const handleExecute = useCallback(() => { + if (!isApproved) { + void approveAndExecute(); + } else { + void signAndExecute(); + } +}, [isApproved, approveAndExecute, signAndExecute]); + +// 3. ExecuteTransactionButton (handles connection/chain switching) + + Execute + +``` + +**Dynamic Button Text**: +```typescript +const getButtonText = () => { + if (isDeploying) return 'Deploying...'; + if (!ready) return 'Select Item'; + return 'Execute'; +}; + + + {getButtonText()} + +``` + +**Rules**: +- Always use `useCallback` for onClick handlers +- Never put complex logic directly in `onClick` +- Button shows "Connect Wallet" / "Switch Chain" / action text automatically + --- ## Key Directories diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index d41bd88a..82c90c1f 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -291,13 +291,14 @@ export function AddCollateralAndBorrow({
-
+
{borrowInputError &&

{borrowInputError}

}
diff --git a/src/components/ui/ExecuteTransactionButton.tsx b/src/components/ui/ExecuteTransactionButton.tsx index d18c9992..8ec8ef64 100644 --- a/src/components/ui/ExecuteTransactionButton.tsx +++ b/src/components/ui/ExecuteTransactionButton.tsx @@ -7,9 +7,6 @@ import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { getNetworkName } from '@/utils/networks'; type ExecuteTransactionButtonProps = Omit & { - /** - * The target chain ID that the transaction needs to execute on - */ targetChainId: number; /** @@ -17,9 +14,6 @@ type ExecuteTransactionButtonProps = Omit & */ onClick: () => void; - /** - * Whether the transaction is currently loading/pending - */ isLoading?: boolean; /** @@ -35,7 +29,6 @@ type ExecuteTransactionButtonProps = Omit & /** * Optional custom text for the switch chain state - * @default "Switch to [Network Name]" */ switchChainText?: string; }; From d7564cd0cf20a0e7bde79dba647723a6f358f74b Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 15 Dec 2025 13:32:00 +0800 Subject: [PATCH 5/8] docs: reviews --- .claude/settings.local.json | 5 ++++- src/components/ui/ExecuteTransactionButton.tsx | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b68cf6cb..3491d615 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -43,7 +43,10 @@ "WebFetch(domain:medium.com)", "Bash(pnpm info:*)", "Bash(for file in )", - "Bash(timeout 30 npx tsc:*)" + "Bash(timeout 30 npx tsc:*)", + "Bash(for file in \"src/components/WithdrawModalContent.tsx\" \"src/components/Borrow/WithdrawCollateralAndRepay.tsx\" \"app/positions/components/RebalanceModal.tsx\" \"app/autovault/components/deployment/DeploymentModal.tsx\" \"app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx\")", + "Bash(do echo \"=== $file ===\" git show \"fix/permit-and-deposit-switch-chain:$file\")", + "Bash(git diff:*)" ], "deny": [] } diff --git a/src/components/ui/ExecuteTransactionButton.tsx b/src/components/ui/ExecuteTransactionButton.tsx index 8ec8ef64..99fc1d84 100644 --- a/src/components/ui/ExecuteTransactionButton.tsx +++ b/src/components/ui/ExecuteTransactionButton.tsx @@ -6,6 +6,12 @@ import { Spinner } from '@/components/common/Spinner'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { getNetworkName } from '@/utils/networks'; +/** + * UI delay after chain switch to allow wagmi state to update + * Prevents button flicker during network transition + */ +const CHAIN_SWITCH_UI_DELAY_MS = 500; + type ExecuteTransactionButtonProps = Omit & { targetChainId: number; @@ -85,8 +91,8 @@ export function ExecuteTransactionButton({ setIsSwitching(true); try { switchToNetwork(); - // Wait a bit for the switch to complete - await new Promise((resolve) => setTimeout(resolve, 500)); + // Wait for wagmi state to update after chain switch + await new Promise((resolve) => setTimeout(resolve, CHAIN_SWITCH_UI_DELAY_MS)); } finally { setIsSwitching(false); } @@ -96,8 +102,9 @@ export function ExecuteTransactionButton({ if (!isConnected) { return (