diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 7691560..0170c85 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,13 +1,45 @@ +import { useState, useEffect, useRef } from "react"; import ConnectWallet from "./connect/ConnectWallet"; import Container from "./ui/Container"; +import SettingsPopup from "./SettingsPopup"; export default function Header() { + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const settingsRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (settingsRef.current && !settingsRef.current.contains(event.target as Node)) { + setIsSettingsOpen(false); + } + }; + + if (isSettingsOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [isSettingsOpen]); + return (
LTV Protocol
- + +
+ + + setIsSettingsOpen(false)} /> +
diff --git a/src/components/SettingsPopup.tsx b/src/components/SettingsPopup.tsx new file mode 100644 index 0000000..9ddbbce --- /dev/null +++ b/src/components/SettingsPopup.tsx @@ -0,0 +1,63 @@ +import { useState, useEffect } from 'react'; +import { MainView } from './settings/MainView'; +import { NetworkSelectView } from './settings/NetworkSelectView'; +import { RpcConfigView } from './settings/RpcConfigView'; + +interface SettingsPopupProps { + isOpen: boolean; + onClose: () => void; +} + +type View = 'main' | 'network-select' | 'rpc-config'; + +export default function SettingsPopup({ isOpen, onClose }: SettingsPopupProps) { + const [currentView, setCurrentView] = useState('main'); + const [selectedNetwork, setSelectedNetwork] = useState(null); + + useEffect(() => { + if (!isOpen) { + // Reset navigation state when closed + setCurrentView('main'); + setSelectedNetwork(null); + } + }, [isOpen]); + + if (!isOpen) return null; + + const handleNetworkSelect = (chainId: string) => { + setSelectedNetwork(chainId); + setCurrentView('rpc-config'); + }; + + return ( +
+
+ {currentView === 'main' && ( + setCurrentView('network-select')} + onClose={onClose} + /> + )} + + {currentView === 'network-select' && ( + setCurrentView('main')} + onClose={onClose} + /> + )} + + {currentView === 'rpc-config' && selectedNetwork && ( + setCurrentView('network-select')} + onClose={onClose} + /> + )} +
+
+ ); +} 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/settings/MainView.tsx b/src/components/settings/MainView.tsx new file mode 100644 index 0000000..d0d8a47 --- /dev/null +++ b/src/components/settings/MainView.tsx @@ -0,0 +1,23 @@ +import { SettingsHeader } from './SettingsHeader'; + +interface MainViewProps { + onNavigateToNetworkSelect: () => void; + onClose: () => void; +} + +export function MainView({ onNavigateToNetworkSelect, onClose }: MainViewProps) { + return ( +
+ + +
+ ); +}; diff --git a/src/components/settings/NetworkSelectView.tsx b/src/components/settings/NetworkSelectView.tsx new file mode 100644 index 0000000..a89fd66 --- /dev/null +++ b/src/components/settings/NetworkSelectView.tsx @@ -0,0 +1,27 @@ +import { SettingsHeader } from './SettingsHeader'; +import { NETWORKS_LIST } from '@/constants'; + +interface NetworkSelectViewProps { + onSelectNetwork: (chainId: string) => void; + onBack: () => void; + onClose: () => void; +} + +export function NetworkSelectView({ onSelectNetwork, onBack, onClose }: NetworkSelectViewProps) { + return ( +
+ + + {NETWORKS_LIST.map((network) => ( + + ))} +
+ ); +} diff --git a/src/components/settings/RpcConfigView.tsx b/src/components/settings/RpcConfigView.tsx new file mode 100644 index 0000000..16c63a1 --- /dev/null +++ b/src/components/settings/RpcConfigView.tsx @@ -0,0 +1,171 @@ +import { useState, useEffect } from 'react'; +import { SettingsHeader } from './SettingsHeader'; +import { NETWORK_CONFIGS } from '@/constants'; +import { useAppContext } from '@/contexts'; +import { JsonRpcProvider } from 'ethers'; +import { WarningMessage, SuccessMessage } from '@/components/ui'; + +interface RpcConfigViewProps { + selectedNetwork: string; + onBack: () => void; + onClose: () => void; +} + +export function RpcConfigView({ selectedNetwork, onBack, onClose }: RpcConfigViewProps) { + const { refreshPublicProvider } = useAppContext(); + + const [rpcUrl, setRpcUrl] = useState(''); + const [activeRpcDisplay, setActiveRpcDisplay] = useState(''); + + const [isCustomRpcSet, setIsCustomRpcSet] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + const getDefaultUrl = () => { + return NETWORK_CONFIGS[selectedNetwork]?.rpcUrls[0] ?? ''; + }; + + useEffect(() => { + // Load current RPC + const custom = localStorage.getItem(`custom_rpc_${selectedNetwork}`); + const defaultUrl = getDefaultUrl(); + + // If custom is set, show it in input. If not, input is empty. + setRpcUrl(custom || ''); + // Active is custom if exists, else it's default + setActiveRpcDisplay(custom || defaultUrl); + + + setIsCustomRpcSet(!!custom); + setError(null); + setSuccess(null); + }, [selectedNetwork]); + + const validateAndSave = async () => { + setError(null); + setSuccess(null); + setIsLoading(true); + + try { + if (!rpcUrl.startsWith('http://') && !rpcUrl.startsWith('https://')) { + throw new Error('URL must start with http:// or https://'); + } + + const tempProvider = new JsonRpcProvider(rpcUrl); + const network = await tempProvider.getNetwork(); + + const returnedChainId = network.chainId.toString(); + + const networkConfig = NETWORK_CONFIGS[selectedNetwork]; + const expectedChainIdBigInt = networkConfig?.chainIdBigInt; + + if (expectedChainIdBigInt && network.chainId !== expectedChainIdBigInt) { + throw new Error(`Chain ID mismatch. Expected ${expectedChainIdBigInt}, got ${returnedChainId}`); + } + + const currentStored = localStorage.getItem(`custom_rpc_${selectedNetwork}`); + if (currentStored !== rpcUrl) { + localStorage.setItem(`custom_rpc_${selectedNetwork}`, rpcUrl); + await refreshPublicProvider(); + } + + setIsCustomRpcSet(true); + setActiveRpcDisplay(rpcUrl); + setSuccess('RPC updated successfully!'); + } catch (err: any) { + console.error("RPC Validation failed:", err); + const msg = err.message || ''; + + if ( + msg.includes('NetworkError') || + msg.includes('Failed to fetch') || + msg.includes('connection refused') || + err.code === "NETWORK_ERROR" + ) { + setError('This RPC URL is not responding.'); + } else { + setError('Failed to connect to RPC'); + } + } finally { + setIsLoading(false); + } + }; + + const handleReset = async () => { + setIsLoading(true); + setError(null); + + try { + const defaultUrl = getDefaultUrl(); + setRpcUrl(''); + setActiveRpcDisplay(defaultUrl); + + localStorage.removeItem(`custom_rpc_${selectedNetwork}`); + await refreshPublicProvider(); + setIsCustomRpcSet(false); + setSuccess('Reset to default successfully.'); + } catch (err: any) { + setError('Failed to reset RPC'); + } finally { + setIsLoading(false); + } + }; + + // Determine if save should be disabled + const isInputEmpty = !rpcUrl || rpcUrl.trim() === ''; + const isSaveDisabled = isLoading || isInputEmpty; + + return ( +
+ + +
+ +
+ {activeRpcDisplay} +
+ + + setRpcUrl(e.target.value)} + disabled={isLoading} + className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + placeholder="https://..." + /> +
+ + {error && } + {success && } + +
+ + {isCustomRpcSet && ( + + )} +
+
+ ); +}; diff --git a/src/components/settings/SettingsHeader.tsx b/src/components/settings/SettingsHeader.tsx new file mode 100644 index 0000000..dc65096 --- /dev/null +++ b/src/components/settings/SettingsHeader.tsx @@ -0,0 +1,27 @@ +interface SettingsHeaderProps { + title: string; + onClose: () => void; + onBack?: () => void; +} + +export function SettingsHeader({ title, onClose, onBack }: SettingsHeaderProps) { + return ( +
+
+ {onBack && ( + + )} + {title} +
+ +
+ ); +}; 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..2f95deb 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); } }; @@ -180,7 +175,7 @@ export default function AuctionHandler({ futureBorrowAssets, futureCollateralAss }, 500); return () => clearTimeout(timeoutId); - }, [amount, auctionType]); + }, [amount, auctionType, vaultLens]); const checkAndApproveRequiredAssets = async () => { if (!address || !vaultAddress || !amount) { @@ -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..7d53c85 100644 --- a/src/components/vault/FlashLoanDepositWithdrawHandler.tsx +++ b/src/components/vault/FlashLoanDepositWithdrawHandler.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { parseUnits, parseEther, formatUnits, formatEther } from 'ethers'; import { useAppContext, useVaultContext } from '@/contexts'; import { @@ -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, @@ -172,7 +176,7 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa return amount * BigInt(FLASH_LOAN_DEPOSIT_WITHDRAW_PRECISION_DIVIDEND) / BigInt(FLASH_LOAN_DEPOSIT_WITHDRAW_PRECISION_DIVIDER); } - const loadMinAvailable = async () => { + const loadMinAvailable = useCallback(async () => { if (!vaultLens || !publicProvider || !vaultAddress || !sharesDecimals) return; const [, deltaShares] = await vaultLens.previewLowLevelRebalanceBorrow(0); @@ -211,7 +215,7 @@ export default function FlashLoanDepositWithdrawHandler({ actionType }: FlashLoa setMinDeposit('0'); setMinWithdraw('0'); } - }; + }, [vaultLens, publicProvider, vaultAddress, sharesDecimals]); useAdaptiveInterval(loadMinAvailable, { initialDelay: 12000, @@ -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..9b399ea 100644 --- a/src/components/vault/FlashLoanHelperHandler.tsx +++ b/src/components/vault/FlashLoanHelperHandler.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { parseUnits, parseEther, formatUnits } from 'ethers'; import { useAppContext, useVaultContext } from '@/contexts'; import { @@ -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, @@ -197,7 +203,7 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa setMaxAmount(sharesBalance); } - const loadMinAvailable = async () => { + const loadMinAvailable = useCallback(async () => { if (!vaultLens || !publicProvider || !vaultAddress || !sharesDecimals) return; const [, deltaShares] = await vaultLens.previewLowLevelRebalanceBorrow(0); @@ -230,7 +236,7 @@ export default function FlashLoanHelperHandler({ helperType }: FlashLoanHelperHa setMinMint('0'); setMinRedeem('0'); } - }; + }, [vaultLens, publicProvider, vaultAddress, sharesDecimals]); useAdaptiveInterval(loadMinAvailable, { initialDelay: 12000, @@ -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..c079b30 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', @@ -60,21 +61,47 @@ export const MAINNET_NETWORK = { blockExplorerUrls: ['https://etherscan.io'] }; -export const NETWORK_CONFIGS = { +export interface NetworkConfig { + chainId: string; + chainIdBigInt: bigint; + chainName: string; + name: string; + urlParam: string; + color: string; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; + rpcUrls: string[]; + blockExplorerUrls: string[]; +} + +export const NETWORK_CONFIGS: Record = { [SEPOLIA_CHAIN_ID_STRING]: { ...SEPOLIA_NETWORK, chainId: SEPOLIA_CHAIN_ID_HEX, + chainIdBigInt: SEPOLIA_CHAIN_ID, name: 'Sepolia', - urlParam: 'sepolia' + urlParam: 'sepolia', + color: 'bg-blue-500' }, [MAINNET_CHAIN_ID_STRING]: { ...MAINNET_NETWORK, chainId: MAINNET_CHAIN_ID_HEX, + chainIdBigInt: MAINNET_CHAIN_ID, name: 'Ethereum', - urlParam: 'ethereum' + urlParam: 'ethereum', + color: 'bg-green-500' } }; +// Helper to get networks as array for iteration +export const NETWORKS_LIST = Object.entries(NETWORK_CONFIGS).map(([key, config]) => ({ + ...config, + chainIdString: key +})); + // Only these networks are supported. Any unrecognized network parameter will default to Sepolia. export const URL_PARAM_TO_CHAIN_ID = { 'sepolia': SEPOLIA_CHAIN_ID_STRING, @@ -103,4 +130,11 @@ export const SAFE_HELPER_ADDRESSES: Record Promise; signTermsOfUse: () => Promise; + refreshPublicProvider: () => Promise; } const AppContext = createContext(undefined); @@ -93,27 +94,27 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { const getNetworkFromUrl = useCallback(() => { const urlParams = new URLSearchParams(window.location.search); const networkParam = urlParams.get('network'); - + if (!networkParam) { setUnrecognizedNetworkParam(false); return null; } - + const recognizedNetwork = URL_PARAM_TO_CHAIN_ID[networkParam as keyof typeof URL_PARAM_TO_CHAIN_ID]; - + if (!recognizedNetwork) { console.warn(`Unrecognized network parameter: "${networkParam}".`); setUnrecognizedNetworkParam(true); return null; } - + setUnrecognizedNetworkParam(false); return recognizedNetwork; }, []); const getDefaultNetwork = useCallback(() => { const urlNetwork = getNetworkFromUrl(); - return urlNetwork ?? '11155111'; + return urlNetwork ?? '1'; }, [getNetworkFromUrl]); useEffect(() => { @@ -184,21 +185,37 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { } }, [currentNetwork, signer]); + const createPublicProvider = useCallback((networkId: string): JsonRpcProvider | null => { + const config = NETWORK_CONFIGS[networkId]; + if (!config) return null; + + const customRpc = localStorage.getItem(`custom_rpc_${networkId}`); + const rpcUrl = customRpc || config.rpcUrls[0]; + return new JsonRpcProvider(rpcUrl); + }, []); + + const refreshPublicProvider = useCallback(async () => { + const networkId = currentNetwork || getDefaultNetwork(); + const newProvider = createPublicProvider(networkId); + if (newProvider) { + setPublicProvider(newProvider); + } + }, [currentNetwork, getDefaultNetwork, createPublicProvider]); + useEffect(() => { const defaultNetwork = getDefaultNetwork(); - const networkConfig = (NETWORK_CONFIGS as any)[defaultNetwork]; - if (networkConfig) { - const newPublicProvider = new JsonRpcProvider(networkConfig.rpcUrls[0]); - setPublicProvider(newPublicProvider); + const newProvider = createPublicProvider(defaultNetwork); + if (newProvider) { + setPublicProvider(newProvider); setCurrentNetwork(defaultNetwork); } else { setPublicProvider(null); setCurrentNetwork(null); } - }, [getDefaultNetwork]); + }, [getDefaultNetwork, createPublicProvider]); const switchToNetwork = useCallback(async (chainId: string) => { - const networkConfig = (NETWORK_CONFIGS as any)[chainId]; + const networkConfig = NETWORK_CONFIGS[chainId]; if (!networkConfig) { console.error('Unknown network chain ID:', chainId); return; @@ -207,15 +224,17 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { const urlParam = Object.keys(URL_PARAM_TO_CHAIN_ID).find( key => URL_PARAM_TO_CHAIN_ID[key as keyof typeof URL_PARAM_TO_CHAIN_ID] === chainId ); - + if (urlParam) { const url = new URL(window.location.href); url.searchParams.set('network', urlParam); window.history.pushState({}, '', url.toString()); } - const newPublicProvider = new JsonRpcProvider(networkConfig.rpcUrls[0]); - setPublicProvider(newPublicProvider); + const newPublicProvider = createPublicProvider(chainId); + if (newPublicProvider) { + setPublicProvider(newPublicProvider); + } setCurrentNetwork(chainId); if (provider) { @@ -237,18 +256,18 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { } } } - }, [provider]); + }, [provider, createPublicProvider]); useEffect(() => { const autoSwitchToUrlNetwork = async () => { if (!isConnected || !provider) return; - + const urlNetwork = getNetworkFromUrl(); if (!urlNetwork) return; - + const currentChainId = chainId?.toString(); if (currentChainId === urlNetwork) return; - + try { await switchToNetwork(urlNetwork); } catch (error) { @@ -303,9 +322,8 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { // Only do this if there's no URL network param, or if it matches const urlNetwork = getNetworkFromUrl(); if (!urlNetwork || urlNetwork === chainIdString) { - const networkConfig = (NETWORK_CONFIGS as any)[chainIdString]; - if (networkConfig) { - const newPublicProvider = new JsonRpcProvider(networkConfig.rpcUrls[0]); + const newPublicProvider = createPublicProvider(chainIdString); + if (newPublicProvider) { setPublicProvider(newPublicProvider); } } @@ -327,7 +345,7 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { const tempProvider = new BrowserProvider(wallet.provider); const tempSigner = await tempProvider.getSigner(); const currentAddress = await tempSigner.getAddress(); - + if (expectedAddress && expectedAddress.toLowerCase() !== currentAddress.toLowerCase()) { console.warn("Address mismatch, user selected another account"); } @@ -340,7 +358,7 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { const urlParam = Object.keys(URL_PARAM_TO_CHAIN_ID).find( key => URL_PARAM_TO_CHAIN_ID[key as keyof typeof URL_PARAM_TO_CHAIN_ID] === chainIdString ); - + if (urlParam) { const url = new URL(window.location.href); url.searchParams.set('network', urlParam); @@ -413,9 +431,9 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { const eip1193Provider = rawProvider as unknown as { on: (event: string, listener: (...args: any[]) => void) => void; removeListener?: (event: string, listener: (...args: any[]) => void) => void; - }; + }; - if (eip1193Provider && typeof eip1193Provider.on === 'function') { + if (eip1193Provider && typeof eip1193Provider.on === 'function') { const onAccountsChangedHandler = async (accounts: string[]) => { if (accounts.length > 0 && provider) { const signer = await provider.getSigner(); @@ -434,12 +452,12 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { const chainIdString = chainIdBigInt.toString(); // Update URL parameter to match the new network - const networkConfig = (NETWORK_CONFIGS as any)[chainIdString]; + const networkConfig = NETWORK_CONFIGS[chainIdString]; if (networkConfig) { const urlParam = Object.keys(URL_PARAM_TO_CHAIN_ID).find( key => URL_PARAM_TO_CHAIN_ID[key as keyof typeof URL_PARAM_TO_CHAIN_ID] === chainIdString ); - + if (urlParam) { const url = new URL(window.location.href); url.searchParams.set('network', urlParam); @@ -447,8 +465,10 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { } // Update publicProvider to match the new network - const newPublicProvider = new JsonRpcProvider(networkConfig.rpcUrls[0]); - setPublicProvider(newPublicProvider); + const newPublicProvider = createPublicProvider(chainIdString); + if (newPublicProvider) { + setPublicProvider(newPublicProvider); + } } // Update all wallet connection state (this also sets currentNetwork, chainId, etc.) @@ -532,7 +552,7 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { try { const signature = await signer.signMessage(termsText); - + const result = await submitTermsOfUseSignature(address, signature, currentNetwork); if (result && result.success) { setIsTermsSigned(true); @@ -665,6 +685,7 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { isTermsBlockingUI, checkTermsStatus, signTermsOfUse, + refreshPublicProvider }; return {children}; diff --git a/src/contexts/VaultContext.tsx b/src/contexts/VaultContext.tsx index 149c997..8c0dc6f 100644 --- a/src/contexts/VaultContext.tsx +++ b/src/contexts/VaultContext.tsx @@ -69,6 +69,8 @@ interface VaultContextType { // Flash loan helpers flashLoanMintHelper: FlashLoanMintHelper | null; flashLoanRedeemHelper: FlashLoanRedeemHelper | null; + flashLoanMintHelperLens: FlashLoanMintHelper | null; + flashLoanRedeemHelperLens: FlashLoanRedeemHelper | null; flashLoanMintHelperAddress: string | null; flashLoanRedeemHelperAddress: string | null; // Vault existence @@ -162,6 +164,8 @@ export const VaultContextProvider = ({ children, vaultAddress, params }: { child const [vaultLens, setVaultLens] = useState(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/useActionPreview.ts b/src/hooks/useActionPreview.ts index 10f4a78..918e809 100644 --- a/src/hooks/useActionPreview.ts +++ b/src/hooks/useActionPreview.ts @@ -81,7 +81,7 @@ export const useActionPreview = ({ }, 500); return () => clearTimeout(timeoutId); - }, [amount, actionType, tokenType]); + }, [amount, actionType, tokenType, vaultLens, displayDecimals, isBorrow]); // Reset preview data when action/token changes useEffect(() => { @@ -136,4 +136,3 @@ export const useActionPreview = ({ provide, }; }; - diff --git a/src/hooks/useFlashLoanPreview.ts b/src/hooks/useFlashLoanPreview.ts index 2d4f04d..4992e59 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 }); @@ -93,7 +91,7 @@ export const useFlashLoanPreview = ({ }, 500); return () => clearTimeout(timeoutId); - }, [sharesToProcess, helperType, sharesBalance]); + }, [sharesToProcess, helperType, sharesBalance, mintHelperLens, redeemHelperLens]); // Reset preview data and errors when helper type changes useEffect(() => { 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