diff --git a/src/components/actions/ActionHandler.tsx b/src/components/actions/ActionHandler.tsx index 7d5167c..837d052 100644 --- a/src/components/actions/ActionHandler.tsx +++ b/src/components/actions/ActionHandler.tsx @@ -96,7 +96,7 @@ export default function ActionHandler({ actionType, tokenType }: ActionHandlerPr const displayTokenSymbol = config.usesShares ? sharesSymbol : formatTokenSymbol(tokenSymbol); const displayDecimals = config.usesShares ? sharesDecimals : tokenDecimals; - const { isLoadingPreview, previewData, receive, provide } = useActionPreview({ + const { previewData, receive, provide } = useActionPreview({ amount, actionType, tokenType, @@ -314,7 +314,6 @@ export default function ActionHandler({ actionType, tokenType }: ActionHandlerPr ) : undefined diff --git a/src/components/actions/SafeActionHandler.tsx b/src/components/actions/SafeActionHandler.tsx index 0b411a9..67bed6e 100644 --- a/src/components/actions/SafeActionHandler.tsx +++ b/src/components/actions/SafeActionHandler.tsx @@ -105,7 +105,7 @@ export default function SafeActionHandler({ actionType, tokenType }: SafeActionH const displayTokenSymbol = config.usesShares ? sharesSymbol : formatTokenSymbol(tokenSymbol); const displayDecimals = config.usesShares ? sharesDecimals : tokenDecimals; - const { isLoadingPreview, previewData, receive, provide } = useActionPreview({ + const { previewData, receive, provide } = useActionPreview({ amount, actionType, tokenType, @@ -458,7 +458,6 @@ export default function SafeActionHandler({ actionType, tokenType }: SafeActionH ) : undefined diff --git a/src/components/ui/PreviewBox.tsx b/src/components/ui/PreviewBox.tsx index 2fcd6de..f472adb 100644 --- a/src/components/ui/PreviewBox.tsx +++ b/src/components/ui/PreviewBox.tsx @@ -12,14 +12,12 @@ export interface PreviewItem { interface PreviewBoxProps { receive: PreviewItem[]; provide: PreviewItem[]; - isLoading: boolean; title?: string; } export const PreviewBox: React.FC = ({ receive, provide, - isLoading, title = 'Preview' }) => { const { @@ -61,7 +59,6 @@ export const PreviewBox: React.FC = ({

{title}

- {isLoading && " (Loading...)"}
{provide.length > 0 && ( diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx index d572c20..d8c407b 100644 --- a/src/components/ui/Tabs.tsx +++ b/src/components/ui/Tabs.tsx @@ -1,77 +1,49 @@ -import { ActionType } from '@/types/actions'; - interface TabItem { value: T; label: string; } -interface TabsPropsGeneric { +interface TabsProps { activeTab: T; - setActiveTab: React.Dispatch>; + setActiveTab: React.Dispatch> | ((tab: T) => void); tabs: TabItem[]; + isProcessing?: boolean; className?: string; } -interface TabsPropsAction { - activeTab: ActionType; - setActiveTab: React.Dispatch>; - tabs?: never; - className?: string; -} - -type TabsProps = T extends ActionType - ? TabsPropsAction - : TabsPropsGeneric; - -export default function Tabs({ - activeTab, - setActiveTab, +export default function Tabs({ + activeTab, + setActiveTab, tabs, - className -}: TabsProps | TabsPropsGeneric) { + isProcessing, + className, +}: TabsProps) { + const safeSetActiveTab = (tab: T) => { + if (isProcessing && tab !== activeTab) return; + setActiveTab(tab); + }; + const tabClass = (tab: T) => - `flex-1 py-2 px-4 rounded-lg font-medium transition-colors focus:outline-none focus:ring-0 ${activeTab === tab - ? 'bg-indigo-600 text-white' - : 'bg-gray-100 text-gray-700 hover:bg-gray-200' + `flex-1 py-2 px-4 rounded-lg font-medium transition-colors focus:outline-none focus:ring-0 ${isProcessing && tab !== activeTab + ? 'bg-gray-200 text-gray-400 cursor-not-allowed opacity-60 border-0' + : activeTab === tab + ? 'bg-indigo-600 text-white' + : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }`; - // If custom tabs are provided, use them - if (tabs) { - return ( -
-
- {tabs.map((tab) => ( - - ))} -
-
- ); - } - - // Default action tabs layout return (
-
- - -
-
- - +
+ {tabs.map((tab) => ( + + ))}
); diff --git a/src/components/vault/Actions.tsx b/src/components/vault/Actions.tsx index 6ab7410..9f543e9 100644 --- a/src/components/vault/Actions.tsx +++ b/src/components/vault/Actions.tsx @@ -3,6 +3,7 @@ import Tabs from '@/components/ui/Tabs'; import ActionWrapper from '@/components/actions/ActionWrapper'; import { ActionType } from '@/types/actions'; import DexLink from './DexLink'; +import { ACTIONS_TABS } from '@/constants'; interface ActionsProps { isSafe?: boolean; @@ -13,7 +14,11 @@ export default function Actions({ isSafe = false }: ActionsProps) { return (
- +
diff --git a/src/components/vault/ActionsDropdown.tsx b/src/components/vault/ActionsDropdown.tsx index da3f6d1..6b50b49 100644 --- a/src/components/vault/ActionsDropdown.tsx +++ b/src/components/vault/ActionsDropdown.tsx @@ -3,12 +3,13 @@ import Tabs from '@/components/ui/Tabs'; import ActionWrapper from '@/components/actions/ActionWrapper'; import { ActionType } from '@/types/actions'; import DexLink from './DexLink'; +import { ACTIONS_TABS } from '@/constants'; interface ActionsProps { isSafe?: boolean; } -export default function ActionsDropdown({ isSafe = false } : ActionsProps ) { +export default function ActionsDropdown({ isSafe = false }: ActionsProps) { const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState('deposit'); @@ -33,22 +34,10 @@ export default function ActionsDropdown({ isSafe = false } : ActionsProps ) { className={`transition-all duration-200 overflow-hidden ${isOpen ? 'max-h-[2000px] opacity-100 p-3' : 'max-h-0 opacity-0 pb-0' }`} > - +
); } - -export function Actions({ isSafe = false }: ActionsProps) { - const [activeTab, setActiveTab] = useState('deposit'); - - return ( -
- - - -
- ); -} \ No newline at end of file diff --git a/src/components/vault/AuctionHandler.tsx b/src/components/vault/AuctionHandler.tsx index dfb54a8..184d558 100644 --- a/src/components/vault/AuctionHandler.tsx +++ b/src/components/vault/AuctionHandler.tsx @@ -15,7 +15,6 @@ export default function AuctionHandler({ futureBorrowAssets, futureCollateralAss const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); - const [isLoadingPreview, setIsLoadingPreview] = useState(false); const [isLoadingMax, setIsLoadingMax] = useState(false); const [previewData, setPreviewData] = useState<{ @@ -148,8 +147,6 @@ export default function AuctionHandler({ futureBorrowAssets, futureCollateralAss return; } - setIsLoadingPreview(true); - try { let result: bigint; @@ -169,8 +166,6 @@ export default function AuctionHandler({ futureBorrowAssets, futureCollateralAss } catch (err) { console.error('Error loading preview:', err); setPreviewData(null); - } finally { - setIsLoadingPreview(false); } }; @@ -345,7 +340,6 @@ export default function AuctionHandler({ futureBorrowAssets, futureCollateralAss ); diff --git a/src/components/vault/FlashLoanDepositWithdraw.tsx b/src/components/vault/FlashLoanDepositWithdraw.tsx index 0d50a29..bd13a8c 100644 --- a/src/components/vault/FlashLoanDepositWithdraw.tsx +++ b/src/components/vault/FlashLoanDepositWithdraw.tsx @@ -24,6 +24,7 @@ export default function FlashLoanDepositWithdraw() { const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState(availableTabs[0]?.value || 'deposit'); + const [isProcessing, setIsProcessing] = useState(false); if (availableTabs.length === 0) { return null; @@ -52,10 +53,18 @@ export default function FlashLoanDepositWithdraw() { > {availableTabs.length > 1 && (
- +
)} - + ); diff --git a/src/components/vault/FlashLoanDepositWithdrawForm.tsx b/src/components/vault/FlashLoanDepositWithdrawForm.tsx index af69151..a474bb6 100644 --- a/src/components/vault/FlashLoanDepositWithdrawForm.tsx +++ b/src/components/vault/FlashLoanDepositWithdrawForm.tsx @@ -11,6 +11,7 @@ export default function FlashLoanDepositWithdrawForm() { ] const [activeTab, setActiveTab] = useState(tabs[0]?.value || 'deposit'); + const [isProcessing, setIsProcessing] = useState(false); return (
@@ -19,10 +20,18 @@ export default function FlashLoanDepositWithdrawForm() { > {tabs.length > 1 && (
- +
)} - +
); diff --git a/src/components/vault/FlashLoanDepositWithdrawHandler.tsx b/src/components/vault/FlashLoanDepositWithdrawHandler.tsx index f7f0e75..70ff7ee 100644 --- a/src/components/vault/FlashLoanDepositWithdrawHandler.tsx +++ b/src/components/vault/FlashLoanDepositWithdrawHandler.tsx @@ -9,7 +9,9 @@ import { formatUsdValue, wrapEthToWstEth, calculateEthWrapForFlashLoan, - processInput + processInput, + isShowWrapPreview, + isZeroOrNan } from '@/utils'; import { PreviewBox, @@ -37,6 +39,7 @@ type ActionType = 'deposit' | 'withdraw'; interface FlashLoanDepositWithdrawHandlerProps { actionType: ActionType; + setIsProcessing: React.Dispatch>; } const GAS_RESERVE_MULTIPLIER = 3n; @@ -48,7 +51,10 @@ const MINT_SLIPPAGE_DIVIDER = 1000000; const FLASH_LOAN_DEPOSIT_WITHDRAW_PRECISION_DIVIDEND = 99999; const FLASH_LOAN_DEPOSIT_WITHDRAW_PRECISION_DIVIDER = 100000; -export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoanDepositWithdrawHandlerProps) { +export default function FlashLoanDepositWithdrawHandler({ + actionType, + setIsProcessing +}: FlashLoanDepositWithdrawHandlerProps) { const [inputValue, setInputValue] = useState(''); const [estimatedShares, setEstimatedShares] = useState(null); const [wrapError, setWrapError] = useState(''); @@ -91,23 +97,20 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa const helperAddress = actionType === 'deposit' ? flashLoanMintHelperAddress : flashLoanRedeemHelperAddress; // Check if this is a wstETH vault that supports ETH input - const isWstETHVault = actionType === 'deposit' && collateralToken && isWstETHAddress(collateralTokenAddress || ''); + const isWstETHVault = collateralToken && isWstETHAddress(collateralTokenAddress || ''); const { - isLoadingPreview, previewData, receive, provide, isErrorLoadingPreview, invalidRebalanceMode } = useFlashLoanPreview({ - sharesToProcess: estimatedShares, helperType: actionType === 'deposit' ? 'mint' : 'redeem', - mintHelper: flashLoanMintHelper, - redeemHelper: flashLoanRedeemHelper, - collateralTokenDecimals, + sharesToProcess: estimatedShares, sharesBalance, - sharesDecimals, + mintHelper: flashLoanMintHelper, + redeemHelper: flashLoanRedeemHelper }); const rawInputSymbol = actionType === 'deposit' ? (isWstETHVault ? 'ETH' : collateralTokenSymbol) : borrowTokenSymbol; @@ -136,12 +139,13 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa setWrapError(''); setWrapSuccess(''); - // Something very ugly here, should be rewrited in future - setUseEthWrapToWSTETH(true); + setUseEthWrapToWSTETH(actionType !== 'withdraw'); setShowWarning(false); }, [actionType]); + const isInputZeroOrNaN = isZeroOrNan(inputValue); + const isInputMoreThanMax = useIsAmountMoreThanMax({ amount: inputValue, max: maxAmount, @@ -221,7 +225,7 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa }); const calculateShares = async () => { - if (!inputValue || !vaultLens) { + if (isInputZeroOrNaN || !vaultLens) { setEstimatedShares(null); setShowWarning(false); return; @@ -322,7 +326,7 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa useEffect(() => { // Reset state if input is empty or invalid - if (!inputValue || !estimatedShares || estimatedShares <= 0n) { + if (isInputZeroOrNaN || !estimatedShares || estimatedShares <= 0n) { setPreviewedWstEthAmount(null); setEthToWrapValue(''); setHasInsufficientBalance(false); @@ -394,7 +398,6 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa }, [ previewData, estimatedShares, - actionType, useEthWrapToWSTETH, isWstETHVault, collateralTokenBalance, @@ -428,37 +431,47 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa setWrapError(''); setWrapSuccess(''); + setIsProcessing(true); - // If using ETH input for wstETH vault, wrap ETH to wstETH first - if (useEthWrapToWSTETH && isWstETHVault && ethToWrapValue && provider && signer) { - setIsWrapping(true); - const ethAmount = parseEther(ethToWrapValue); - - const wrapResult = await wrapEthToWstEth( - provider, - signer, - ethAmount, - address, - setWrapSuccess, - setWrapError - ); - - setIsWrapping(false); + try { + // If using ETH input for wstETH vault, wrap ETH to wstETH first + if (useEthWrapToWSTETH && isWstETHVault && ethToWrapValue && provider && signer) { + setIsWrapping(true); + const ethAmount = parseEther(ethToWrapValue); + + const wrapResult = await wrapEthToWstEth( + provider, + signer, + ethAmount, + address, + setWrapSuccess, + setWrapError + ); + + if (!wrapResult) { + setIsWrapping(false); + setIsProcessing(false); + return; // Error already set by wrapEthToWstEth, finally will handle processing state + } - if (!wrapResult) { - return; // Error already set by wrapEthToWstEth + // Refresh balances to get updated wstETH balance + await refreshBalances(); + setIsWrapping(false); } - // Refresh balances to get updated wstETH balance - await refreshBalances(); - } - - const success = await flashLoan.execute(); + const success = await flashLoan.execute(); - if (success) { - setInputValue(''); - setEstimatedShares(null); - setEthToWrapValue(''); + if (success) { + setInputValue(''); + setEstimatedShares(null); + setEthToWrapValue(''); + } + } catch (err) { + console.error('Error in handling flash loan submit:', err); + } finally { + setIsProcessing(false); + // Safety check to ensure isWrapping is also disabled if something failed during wrap + setIsWrapping(false); } }; @@ -493,6 +506,18 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa const userBalance = actionType === 'deposit' ? collateralTokenBalance : sharesBalance; const userBalanceToken = actionType === 'deposit' ? formatTokenSymbol(collateralTokenSymbol) : sharesSymbol; + const shouldShowWrapPreview = isShowWrapPreview({ + inputValue, + isInputMoreThanMax, + isAmountLessThanMin, + invalidRebalanceMode, + hasInsufficientBalance, + isErrorLoadingPreview, + showWarning, + isWrapping, + flashLoanLoading: flashLoan.loading + }); + return (
@@ -578,7 +603,7 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa

ETH will be automatically wrapped to wstETH

- {previewedWstEthAmount && ethToWrapValue && ( + {previewedWstEthAmount && ethToWrapValue && shouldShowWrapPreview && (

→ Will wrap ETH to ~ wstETH

@@ -588,7 +613,7 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa )} - {!inputValue ? null : + {isInputZeroOrNaN ? null : isInputMoreThanMax && !flashLoan.loading && !isWrapping ? ( ) : isErrorLoadingPreview ? ( - ) : estimatedShares !== null && estimatedShares > 0n && previewData && !!inputValue ? ( + ) : estimatedShares !== null && estimatedShares > 0n && previewData && !isInputZeroOrNaN ? ( ) : null} @@ -617,7 +641,6 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa type="submit" disabled={ flashLoan.loading || - !inputValue || !estimatedShares || estimatedShares <= 0n || flashLoan.isApproving || @@ -627,7 +650,9 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa invalidRebalanceMode || isInputMoreThanMax || isMinMoreThanMax || - isAmountLessThanMin + isAmountLessThanMin || + !previewData || + isInputZeroOrNaN } className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed" > diff --git a/src/components/vault/FlashLoanHelper.tsx b/src/components/vault/FlashLoanHelper.tsx index fc1e898..c679ef6 100644 --- a/src/components/vault/FlashLoanHelper.tsx +++ b/src/components/vault/FlashLoanHelper.tsx @@ -24,6 +24,7 @@ export default function FlashLoanHelper() { const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState(availableTabs[0]?.value || 'mint'); + const [isProcessing, setIsProcessing] = useState(false); if (availableTabs.length === 0) { return null; @@ -54,10 +55,18 @@ export default function FlashLoanHelper() { > {availableTabs.length > 1 && (
- +
)} - +
); diff --git a/src/components/vault/FlashLoanHelperHandler.tsx b/src/components/vault/FlashLoanHelperHandler.tsx index f92861f..7c2b50e 100644 --- a/src/components/vault/FlashLoanHelperHandler.tsx +++ b/src/components/vault/FlashLoanHelperHandler.tsx @@ -9,7 +9,9 @@ import { formatUsdValue, wrapEthToWstEth, calculateEthWrapForFlashLoan, - processInput + processInput, + isShowWrapPreview, + isZeroOrNan } from '@/utils'; import { PreviewBox, @@ -36,6 +38,7 @@ type HelperType = 'mint' | 'redeem'; interface FlashLoanHelperHandlerProps { helperType: HelperType; + setIsProcessing: React.Dispatch>; } const GAS_RESERVE_MULTIPLIER = 3n; @@ -47,7 +50,10 @@ const MINT_MAX_SLIPPAGE_DIVIDER = 1000000; const MINT_SLIPPAGE_DIVIDEND = 1000001; const MINT_SLIPPAGE_DIVIDER = 1000000; -export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHandlerProps) { +export default function FlashLoanHelperHandler({ + helperType, + setIsProcessing +}: FlashLoanHelperHandlerProps) { const [inputValue, setInputValue] = useState(''); const [sharesToProcess, setSharesToProcess] = useState(null); const [wrapError, setWrapError] = useState(''); @@ -89,23 +95,20 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa const helperAddress = helperType === 'mint' ? flashLoanMintHelperAddress : flashLoanRedeemHelperAddress; // Check if this is a wstETH vault that supports ETH input - const isWstETHVault = helperType === 'mint' && collateralToken && isWstETHAddress(collateralTokenAddress || ''); + const isWstETHVault = collateralToken && isWstETHAddress(collateralTokenAddress || ''); const { - isLoadingPreview, previewData, receive, provide, isErrorLoadingPreview, invalidRebalanceMode } = useFlashLoanPreview({ - sharesToProcess, helperType, - mintHelper: flashLoanMintHelper, - redeemHelper: flashLoanRedeemHelper, - collateralTokenDecimals, + sharesToProcess, sharesBalance, - sharesDecimals, + mintHelper: flashLoanMintHelper, + redeemHelper: flashLoanRedeemHelper }); const maxAmountUsd = useMaxAmountUsd({ @@ -125,14 +128,17 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa setPreviewedWstEthAmount(null); setEthToWrapValue(''); setEffectiveCollateralBalance(''); - setUseEthWrapToWSTETH(true); setHasInsufficientBalance(false); + setUseEthWrapToWSTETH(helperType !== 'redeem'); + setWrapError(''); setWrapSuccess(''); }, [helperType]); + const isInputZeroOrNaN = isZeroOrNan(inputValue); + const isInputMoreThanMax = useIsAmountMoreThanMax({ amount: inputValue, max: maxAmount, @@ -325,7 +331,6 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa }, [ previewData, sharesToProcess, - helperType, useEthWrapToWSTETH, isWstETHVault, collateralTokenBalance, @@ -359,37 +364,46 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa setWrapError(''); setWrapSuccess(''); + setIsProcessing(true); + + try { + // If using ETH input for wstETH vault, wrap ETH to wstETH first + if (useEthWrapToWSTETH && isWstETHVault && ethToWrapValue && provider && signer) { + setIsWrapping(true); + const ethAmount = parseEther(ethToWrapValue); + + const wrapResult = await wrapEthToWstEth( + provider, + signer, + ethAmount, + address, + setWrapSuccess, + setWrapError + ); + + if (!wrapResult) { + setIsWrapping(false); + setIsProcessing(false); + return; // Error already set by wrapEthToWstEth, finally will handle processing state + } - // If using ETH input for wstETH vault, wrap ETH to wstETH first - if (useEthWrapToWSTETH && isWstETHVault && ethToWrapValue && provider && signer) { - setIsWrapping(true); - const ethAmount = parseEther(ethToWrapValue); - - const wrapResult = await wrapEthToWstEth( - provider, - signer, - ethAmount, - address, - setWrapSuccess, - setWrapError - ); - - setIsWrapping(false); - - if (!wrapResult) { - return; // Error already set by wrapEthToWstEth + // Refresh balances to get updated wstETH balance + await refreshBalances(); + setIsWrapping(false); } - // Refresh balances to get updated wstETH balance - await refreshBalances(); - } - - const success = await flashLoan.execute(); - - if (success) { - setInputValue(''); - setSharesToProcess(null); - setEthToWrapValue(''); + const success = await flashLoan.execute(); + + if (success) { + setInputValue(''); + setSharesToProcess(null); + setEthToWrapValue(''); + } + } catch (err) { + console.error('Error in handling flash loan helper submit:', err); + } finally { + setIsProcessing(false); + setIsWrapping(false); } }; @@ -413,6 +427,17 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa const userBalance = helperType === 'mint' ? effectiveCollateralBalance : sharesBalance; const userBalanceToken = helperType === 'mint' ? formatTokenSymbol(collateralTokenSymbol) : sharesSymbol; + const shouldShowWrapPreview = isShowWrapPreview({ + inputValue, + isInputMoreThanMax, + isAmountLessThanMin, + invalidRebalanceMode, + hasInsufficientBalance, + isErrorLoadingPreview, + isWrapping, + flashLoanLoading: flashLoan.loading + }); + return (
@@ -480,7 +505,7 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa

ETH will be wrapped to wstETH before the flash loan mint

- {previewedWstEthAmount && ethToWrapValue && ( + {previewedWstEthAmount && ethToWrapValue && shouldShowWrapPreview && (

→ Will receive ~ wstETH from wrapping and use ~ from balance

@@ -490,7 +515,7 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa )} - {!inputValue ? null : isInputMoreThanMax && !flashLoan.loading ? ( + {isInputZeroOrNaN ? null : isInputMoreThanMax && !flashLoan.loading ? ( @@ -508,7 +533,6 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa ) : null} @@ -519,14 +543,15 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa flashLoan.loading || flashLoan.isApproving || isWrapping || - !inputValue || !sharesToProcess || hasInsufficientBalance || isErrorLoadingPreview || invalidRebalanceMode || isMinMoreThanMax || isInputMoreThanMax || - isAmountLessThanMin + isAmountLessThanMin || + !previewData || + isInputZeroOrNaN } className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed" > diff --git a/src/components/vault/LowLevelRebalanceHandler.tsx b/src/components/vault/LowLevelRebalanceHandler.tsx index e2b6d88..c9ad298 100644 --- a/src/components/vault/LowLevelRebalanceHandler.tsx +++ b/src/components/vault/LowLevelRebalanceHandler.tsx @@ -19,7 +19,6 @@ export default function LowLevelRebalanceHandler({ rebalanceType, actionType }: const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); - const [isLoadingPreview, setIsLoadingPreview] = useState(false); const [isLoadingMax, setIsLoadingMax] = useState(false); const [previewData, setPreviewData] = useState<{ @@ -118,8 +117,6 @@ export default function LowLevelRebalanceHandler({ rebalanceType, actionType }: return; } - setIsLoadingPreview(true); - try { let result: any; @@ -145,8 +142,6 @@ export default function LowLevelRebalanceHandler({ rebalanceType, actionType }: } catch (err) { console.error('Error loading preview:', err); setPreviewData(null); - } finally { - setIsLoadingPreview(false); } }; @@ -469,7 +464,6 @@ export default function LowLevelRebalanceHandler({ rebalanceType, actionType }: ); diff --git a/src/components/vault/NftMintBanner.tsx b/src/components/vault/NftMintBanner.tsx index 797146a..2fb2303 100644 --- a/src/components/vault/NftMintBanner.tsx +++ b/src/components/vault/NftMintBanner.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useVaultContext } from '@/contexts'; +import { useVaultContext, useAppContext } from '@/contexts'; export default function NftMintBanner() { const { @@ -7,19 +7,24 @@ export default function NftMintBanner() { isWhitelistedToMintNft, nftTotalSupply } = useVaultContext(); + const { address } = useAppContext(); const [isDismissed, setIsDismissed] = useState(true); useEffect(() => { - const dismissed = localStorage.getItem('nft_mint_banner_closed'); + if (!address) return; + const dismissed = localStorage.getItem(`nft_mint_banner_closed_${address.toLowerCase()}`); if (dismissed !== 'true') { setIsDismissed(false); + } else { + setIsDismissed(true); } - }, []); + }, [address]); const handleClose = () => { + if (!address) return; setIsDismissed(true); - localStorage.setItem('nft_mint_banner_closed', 'true'); + localStorage.setItem(`nft_mint_banner_closed_${address.toLowerCase()}`, 'true'); }; const isVisible = !isDismissed && !hasNft && isWhitelistedToMintNft && nftTotalSupply < 1024; diff --git a/src/constants.ts b/src/constants.ts index f2a3ed9..13237c8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,5 @@ import { parseEther } from "ethers"; +import { ActionType } from '@/types/actions'; const SEPOLIA_WETH_ADDRESSES = [ '0x2d5ee574e710219a521449679a4a7f2b43f046ad', @@ -103,4 +104,11 @@ export const SAFE_HELPER_ADDRESSES: Record; provide: Array<{ amount: bigint; tokenType: TokenType }>; @@ -42,91 +39,103 @@ export const useFlashLoanPreview = ({ redeemHelper, sharesBalance, }: UseFlashLoanPreviewParams): UseFlashLoanPreviewReturn => { - const [isLoadingPreview, setIsLoadingPreview] = useState(false); const [isErrorLoadingPreview, setIsErrorLoadingPreview] = useState(false); const [invalidRebalanceMode, setInvalidRebalanceMode] = useState(false); const [previewData, setPreviewData] = useState(null); - const loadPreview = async (shares: bigint | null) => { - if ( - shares === null || shares <= 0n || - helperType === 'mint' && !mintHelper || - helperType === 'redeem' && !redeemHelper - ) { + const intervalRef = useRef(null); + + const isValid = + sharesToProcess !== null && + sharesToProcess > 0n && + ( + (helperType === 'mint' && !!mintHelper) || + (helperType === 'redeem' && !!redeemHelper) + ); + + const loadPreview = async () => { + if (!isValid) { setPreviewData(null); return; } - + setIsErrorLoadingPreview(false); setInvalidRebalanceMode(false); - setIsLoadingPreview(true); try { let amount: bigint; + if (helperType === 'mint') { - // returns collateral required - amount = await mintHelper!.previewMintSharesWithFlashLoanCollateral(shares); + amount = await mintHelper!.previewMintSharesWithFlashLoanCollateral( + sharesToProcess! + ); } else { - // returns borrow tokens to receive - amount = await redeemHelper!.previewRedeemSharesWithCurveAndFlashLoanBorrow(shares); + amount = await redeemHelper!.previewRedeemSharesWithCurveAndFlashLoanBorrow( + sharesToProcess! + ); } + amount = reduceByPrecisionBuffer(amount); - setPreviewData({ amount }); + setPreviewData({ amount }); } catch (err: any) { - setIsErrorLoadingPreview(true); console.error('Error loading preview:', err); + setIsErrorLoadingPreview(true); - if (err.message.includes('InvalidRebalanceMode')) { + if (err?.message?.includes('InvalidRebalanceMode')) { setInvalidRebalanceMode(true); } setPreviewData(null); - } finally { - setIsLoadingPreview(false); } }; + // Immediate first load + // Start auto-refresh every 6s AFTER first load useEffect(() => { - const timeoutId = setTimeout(() => { - loadPreview(sharesToProcess); - }, 500); + if (!isValid) { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + return; + } + + loadPreview(); + + // auto refresh + intervalRef.current = setInterval(() => { + loadPreview(); + }, 6000); - return () => clearTimeout(timeoutId); - }, [sharesToProcess, helperType, sharesBalance]); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [sharesToProcess, helperType, mintHelper, redeemHelper, sharesBalance]); - // Reset preview data and errors when helper type changes useEffect(() => { - setPreviewData(null); setIsErrorLoadingPreview(false); setInvalidRebalanceMode(false); + setPreviewData(null); }, [helperType]); - const getReceiveAndProvide = () => { - const receive: Array<{ amount: bigint; tokenType: TokenType }> = []; - const provide: Array<{ amount: bigint; tokenType: TokenType }> = []; - - if (!sharesToProcess || sharesToProcess <= 0n || !previewData) { - return { receive, provide }; - } + const receive: Array<{ amount: bigint; tokenType: TokenType }> = []; + const provide: Array<{ amount: bigint; tokenType: TokenType }> = []; + if (previewData && sharesToProcess && sharesToProcess > 0n) { if (helperType === 'mint') { - // For mint: provide collateral, receive shares provide.push({ amount: previewData.amount, tokenType: 'collateral' }); receive.push({ amount: sharesToProcess, tokenType: 'shares' }); } else { - // For redeem: provide shares, receive borrow tokens provide.push({ amount: sharesToProcess, tokenType: 'shares' }); receive.push({ amount: previewData.amount, tokenType: 'borrow' }); } - - return { receive, provide }; - }; - - const { receive, provide } = getReceiveAndProvide(); + } return { - isLoadingPreview, previewData, receive, provide, diff --git a/src/utils/index.ts b/src/utils/index.ts index c9780e0..36b873a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -23,4 +23,6 @@ export { formatUsdValue } from './formatUsdValue'; export { limitDecimals } from './limitDecimals'; export { processInput } from './processInput'; export { applyGasSlippage } from './applyGasSlippage'; +export { isShowWrapPreview } from './isShowWrapPreview'; +export { isZeroOrNan } from './isZeroOrNan'; export * from './api'; diff --git a/src/utils/isShowWrapPreview.ts b/src/utils/isShowWrapPreview.ts new file mode 100644 index 0000000..3c28257 --- /dev/null +++ b/src/utils/isShowWrapPreview.ts @@ -0,0 +1,43 @@ +type ErrorFlags = { + isInputMoreThanMax: boolean; + isAmountLessThanMin: boolean; + invalidRebalanceMode: boolean; + hasInsufficientBalance: boolean; + isErrorLoadingPreview: boolean; + showWarning?: boolean; // for FlashLoanDepositWithdrawHandler + flashLoanLoading: boolean; + isWrapping: boolean; + inputValue?: string | null; +}; + +export function isShowWrapPreview(flags: ErrorFlags) { + const { + inputValue, + isInputMoreThanMax, + isAmountLessThanMin, + invalidRebalanceMode, + hasInsufficientBalance, + isErrorLoadingPreview, + showWarning = false, + flashLoanLoading, + isWrapping, + } = flags; + + const hasAnyErrorOrWarning = + isInputMoreThanMax || + isAmountLessThanMin || + invalidRebalanceMode || + hasInsufficientBalance || + isErrorLoadingPreview || + showWarning; + + const shouldShowWrapPreview = + isWrapping || + ( + !!inputValue && + !flashLoanLoading && + !hasAnyErrorOrWarning + ); + + return shouldShowWrapPreview; +} diff --git a/src/utils/isZeroOrNan.ts b/src/utils/isZeroOrNan.ts new file mode 100644 index 0000000..0b59472 --- /dev/null +++ b/src/utils/isZeroOrNan.ts @@ -0,0 +1,4 @@ +export const isZeroOrNan = (value: string) => { + const parsed = parseFloat(value); + return parsed === 0 || isNaN(parsed); +} \ No newline at end of file