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..a9f83c4 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(''); @@ -70,8 +76,8 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa const { vaultLens, vaultAddress, - flashLoanMintHelper, - flashLoanRedeemHelper, + flashLoanMintHelperLens, + flashLoanRedeemHelperLens, flashLoanMintHelperAddress, flashLoanRedeemHelperAddress, collateralToken, @@ -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, + mintHelperLens: flashLoanMintHelperLens, + redeemHelperLens: flashLoanRedeemHelperLens }); 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; @@ -236,8 +240,6 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa } if (actionType === 'deposit') { - if (!flashLoanMintHelper || !publicProvider) return; - let shares = await vaultLens.convertToShares(inputAmount); if (!shares) return; @@ -251,11 +253,11 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa return; } - if (!flashLoanRedeemHelper) return; + if (!flashLoanRedeemHelperLens) return; let shares = await findSharesForEthWithdraw({ amount: inputAmount, - helper: flashLoanRedeemHelper, + helper: flashLoanRedeemHelperLens, vaultLens }); @@ -279,7 +281,7 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa useEffect(() => { const timeoutId = setTimeout(calculateShares, 500); return () => clearTimeout(timeoutId); - }, [inputValue, actionType, vaultLens, flashLoanMintHelper, flashLoanRedeemHelper, publicProvider, isMaxWithdraw]); + }, [inputValue, actionType, vaultLens, flashLoanRedeemHelperLens, publicProvider, isMaxWithdraw]); const setMaxDeposit = async () => { if (!vaultLens) return; @@ -297,14 +299,14 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa }; const setMaxWithdraw = async () => { - if (!flashLoanRedeemHelper || !sharesBalance) { + if (!flashLoanRedeemHelperLens || !sharesBalance) { setMaxAmount('0'); return; } try { const rawShares = parseUnits(sharesBalance, Number(sharesDecimals)); - const maxWeth = await flashLoanRedeemHelper.previewRedeemSharesWithCurveAndFlashLoanBorrow(rawShares); + const maxWeth = await flashLoanRedeemHelperLens.previewRedeemSharesWithCurveAndFlashLoanBorrow(rawShares); setMaxAmount(formatEther(maxWeth)); } catch (err) { console.error("Error calculating max withdraw:", err); @@ -322,7 +324,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 +396,6 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa }, [ previewData, estimatedShares, - actionType, useEthWrapToWSTETH, isWstETHVault, collateralTokenBalance, @@ -428,37 +429,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 +504,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 +601,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 +611,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 +639,6 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa type="submit" disabled={ flashLoan.loading || - !inputValue || !estimatedShares || estimatedShares <= 0n || flashLoan.isApproving || @@ -627,7 +648,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..e0e2a33 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(''); @@ -68,8 +74,8 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa const { vaultLens, vaultAddress, - flashLoanMintHelper, - flashLoanRedeemHelper, + flashLoanMintHelperLens, + flashLoanRedeemHelperLens, flashLoanMintHelperAddress, flashLoanRedeemHelperAddress, collateralToken, @@ -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, + mintHelperLens: flashLoanMintHelperLens, + redeemHelperLens: flashLoanRedeemHelperLens }); 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(null); const [borrowTokenLens, setBorrowTokenLens] = useState(null); const [collateralTokenLens, setCollateralTokenLens] = useState(null); + const [flashLoanMintHelperLens, setFlashLoanMintHelperLens] = useState(null); + const [flashLoanRedeemHelperLens, setFlashLoanRedeemHelperLens] = useState(null); const [sharesDecimals, setSharesDecimals] = useState(18n); const [borrowTokenDecimals, setBorrowTokenDecimals] = useState(18n); const [collateralTokenDecimals, setCollateralTokenDecimals] = useState(18n); @@ -323,6 +327,19 @@ export const VaultContextProvider = ({ children, vaultAddress, params }: { child } } + // Initialize lens helpers (always, using publicProvider) + if (vaultConfig?.flashLoanMintHelperAddress && vaultConfig.flashLoanMintHelperAddress !== '') { + setFlashLoanMintHelperLens(FlashLoanMintHelper__factory.connect(vaultConfig.flashLoanMintHelperAddress, publicProvider)); + } else { + setFlashLoanMintHelperLens(null); + } + + if (vaultConfig?.flashLoanRedeemHelperAddress && vaultConfig.flashLoanRedeemHelperAddress !== '') { + setFlashLoanRedeemHelperLens(FlashLoanRedeemHelper__factory.connect(vaultConfig.flashLoanRedeemHelperAddress, publicProvider)); + } else { + setFlashLoanRedeemHelperLens(null); + } + if (!vaultConfig?.sharesSymbol) { const symbol = await vaultLensInstance.symbol(); setSharesSymbol(symbol); @@ -1022,6 +1039,8 @@ export const VaultContextProvider = ({ children, vaultAddress, params }: { child description, flashLoanMintHelper, flashLoanRedeemHelper, + flashLoanMintHelperLens, + flashLoanRedeemHelperLens, flashLoanMintHelperAddress, flashLoanRedeemHelperAddress, vaultExists, diff --git a/src/hooks/useFlashLoanPreview.ts b/src/hooks/useFlashLoanPreview.ts index 2d4f04d..f618827 100644 --- a/src/hooks/useFlashLoanPreview.ts +++ b/src/hooks/useFlashLoanPreview.ts @@ -17,13 +17,11 @@ interface PreviewData { } interface UseFlashLoanPreviewParams { - sharesToProcess: bigint | null; helperType: HelperType; - mintHelper: FlashLoanMintHelper | null; - redeemHelper: FlashLoanRedeemHelper | null; - collateralTokenDecimals: bigint; sharesBalance: string; - sharesDecimals: bigint; + sharesToProcess: bigint | null; + mintHelperLens: FlashLoanMintHelper | null; + redeemHelperLens: FlashLoanRedeemHelper | null; } interface UseFlashLoanPreviewReturn { @@ -38,8 +36,8 @@ interface UseFlashLoanPreviewReturn { export const useFlashLoanPreview = ({ sharesToProcess, helperType, - mintHelper, - redeemHelper, + mintHelperLens, + redeemHelperLens, sharesBalance, }: UseFlashLoanPreviewParams): UseFlashLoanPreviewReturn => { const [isLoadingPreview, setIsLoadingPreview] = useState(false); @@ -50,13 +48,13 @@ export const useFlashLoanPreview = ({ const loadPreview = async (shares: bigint | null) => { if ( shares === null || shares <= 0n || - helperType === 'mint' && !mintHelper || - helperType === 'redeem' && !redeemHelper + helperType === 'mint' && !mintHelperLens || + helperType === 'redeem' && !redeemHelperLens ) { setPreviewData(null); return; } - + setIsErrorLoadingPreview(false); setInvalidRebalanceMode(false); setIsLoadingPreview(true); @@ -65,10 +63,10 @@ export const useFlashLoanPreview = ({ let amount: bigint; if (helperType === 'mint') { // returns collateral required - amount = await mintHelper!.previewMintSharesWithFlashLoanCollateral(shares); + amount = await mintHelperLens!.previewMintSharesWithFlashLoanCollateral(shares); } else { // returns borrow tokens to receive - amount = await redeemHelper!.previewRedeemSharesWithCurveAndFlashLoanBorrow(shares); + amount = await redeemHelperLens!.previewRedeemSharesWithCurveAndFlashLoanBorrow(shares); } amount = reduceByPrecisionBuffer(amount); setPreviewData({ amount }); 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