diff --git a/packages/synapse-interface/components/Portfolio/components/HoverTooltip.tsx b/packages/synapse-interface/components/HoverTooltip.tsx similarity index 55% rename from packages/synapse-interface/components/Portfolio/components/HoverTooltip.tsx rename to packages/synapse-interface/components/HoverTooltip.tsx index 88e01b5999..d27d5a8aab 100644 --- a/packages/synapse-interface/components/Portfolio/components/HoverTooltip.tsx +++ b/packages/synapse-interface/components/HoverTooltip.tsx @@ -1,21 +1,35 @@ import React, { useState } from 'react' -export const HoverTooltip = ({ children, hoverContent }) => { +export const HoverTooltip = ({ + children, + hoverContent, + isActive = true, +}: { + children: React.ReactNode + hoverContent: React.ReactNode + isActive?: boolean +}) => { const [showTooltip, setShowTooltip] = useState(false) const activateTooltip = () => setShowTooltip(true) const hideTooltip = () => setShowTooltip(false) - return ( -
- {children} - {hoverContent} -
- ) + if (!isActive) { + return
{children}
+ } else { + return ( +
+ {children} + {hoverContent ? ( + {hoverContent} + ) : null} +
+ ) + } } const Tooltip = ({ diff --git a/packages/synapse-interface/components/Portfolio/components/GasTokenAsset.tsx b/packages/synapse-interface/components/Portfolio/components/GasTokenAsset.tsx index 19e22d005c..cae8ccb317 100644 --- a/packages/synapse-interface/components/Portfolio/components/GasTokenAsset.tsx +++ b/packages/synapse-interface/components/Portfolio/components/GasTokenAsset.tsx @@ -2,7 +2,7 @@ import React from 'react' import Image from 'next/image' import { Token } from '@/utils/types' import { getParsedBalance } from '@/utils/getParsedBalance' -import { HoverTooltip } from './HoverTooltip' +import { HoverTooltip } from '../../HoverTooltip' import GasIcon from '@/components/icons/GasIcon' export const GasTokenAsset = ({ diff --git a/packages/synapse-interface/components/Portfolio/components/PortfolioAssetActionButton.tsx b/packages/synapse-interface/components/Portfolio/components/PortfolioAssetActionButton.tsx index 2c159e4be9..601f589554 100644 --- a/packages/synapse-interface/components/Portfolio/components/PortfolioAssetActionButton.tsx +++ b/packages/synapse-interface/components/Portfolio/components/PortfolioAssetActionButton.tsx @@ -15,7 +15,7 @@ export const PortfolioAssetActionButton = ({ id="portfolio-asset-action-button" className={` py-1 px-6 rounded-sm - border border-synapsePurple + border border-fuchsia-400 ${!isDisabled && 'cursor-pointer hover:bg-surface active:opacity-70'} `} onClick={selectCallback} diff --git a/packages/synapse-interface/components/Portfolio/components/PortfolioTokenAsset.tsx b/packages/synapse-interface/components/Portfolio/components/PortfolioTokenAsset.tsx index be3b17044b..607034b855 100644 --- a/packages/synapse-interface/components/Portfolio/components/PortfolioTokenAsset.tsx +++ b/packages/synapse-interface/components/Portfolio/components/PortfolioTokenAsset.tsx @@ -1,21 +1,19 @@ import React, { useCallback } from 'react' -import _ from 'lodash' +import { zeroAddress } from 'viem' +import { isNumber } from 'lodash' +import Image from 'next/image' import { useAppDispatch } from '@/store/hooks' -import { - setFromChainId, - setFromToken, - updateFromValue, -} from '@/slices/bridge/reducer' +import { setFromChainId, setFromToken } from '@/slices/bridge/reducer' import { Token } from '@/utils/types' import { inputRef } from '../../StateManagedBridge/InputContainer' -import Image from 'next/image' import { useBridgeState } from '@/slices/bridge/hooks' import { PortfolioAssetActionButton } from './PortfolioAssetActionButton' -import { trimTrailingZeroesAfterDecimal } from '@/utils/trimTrailingZeroesAfterDecimal' -import { zeroAddress } from 'viem' -import GasIcon from '@/components/icons/GasIcon' -import { HoverTooltip } from './HoverTooltip' +import { HoverTooltip } from '@/components/HoverTooltip' import { getParsedBalance } from '@/utils/getParsedBalance' +import { useGasEstimator } from '@/utils/hooks/useGasEstimator' +import GasIcon from '@/components/icons/GasIcon' +import { trimTrailingZeroesAfterDecimal } from '@/utils/trimTrailingZeroesAfterDecimal' +import { formatAmount } from '@/utils/formatAmount' const handleFocusOnBridgeInput = () => { inputRef.current?.focus() @@ -37,30 +35,26 @@ export const PortfolioTokenAsset = ({ const dispatch = useAppDispatch() const { fromChainId, fromToken } = useBridgeState() const { icon, symbol, decimals, addresses } = token - - const tokenDecimals = _.isNumber(decimals) + const tokenAddress = addresses[portfolioChainId] + const tokenDecimals = isNumber(decimals) ? decimals : decimals[portfolioChainId] - const parsedBalance = getParsedBalance(balance, tokenDecimals, 3) - const parsedBalanceLong = getParsedBalance(balance, tokenDecimals, 8) + const parsedBalance = getParsedBalance(balance, tokenDecimals) + const formattedBalance = formatAmount(parsedBalance) const isDisabled = false - const isTokenSelected = - fromToken === token && fromChainId === portfolioChainId + const isPortfolioChainSelected = fromChainId === portfolioChainId + const isTokenSelected = isPortfolioChainSelected && fromToken === token + const isGasToken = tokenAddress === zeroAddress - const handleFromSelectionCallback = useCallback(() => { + const { maxBridgeableGas } = useGasEstimator() + + const handleFromSelectionCallback = useCallback(async () => { dispatch(setFromChainId(portfolioChainId)) dispatch(setFromToken(token)) handleFocusOnBridgeInput() - dispatch( - updateFromValue( - trimTrailingZeroesAfterDecimal(getParsedBalance(balance, tokenDecimals)) - ) - ) - }, [token, balance, portfolioChainId]) - - const isBridgeableGasToken = addresses[portfolioChainId] === zeroAddress + }, [token, portfolioChainId]) return (
-
+
{`${symbol} - {parsedBalanceLong} {symbol} -
+ isPortfolioChainSelected && isGasToken && maxBridgeableGas ? ( +
+ Available:{' '} + {trimTrailingZeroesAfterDecimal(maxBridgeableGas.toFixed(8))}{' '} + {symbol} +
+ ) : ( +
+ {parsedBalance} {symbol} +
+ ) } >
- {parsedBalance} {symbol} + {formattedBalance} {symbol}
- {isBridgeableGasToken ? ( + {isGasToken && ( Gas token
} > - ) : null} + )}
{ ${!isMounted && 'border-opacity-30'} ${ isFocused || isSearchInputActive - ? 'border-synapsePurple bg-tint' + ? 'border-fuchsia-400 bg-tint' : 'border-separator bg-transparent' } `} diff --git a/packages/synapse-interface/components/Portfolio/components/SingleNetworkPortfolio.tsx b/packages/synapse-interface/components/Portfolio/components/SingleNetworkPortfolio.tsx index 978980e5d5..392edc169e 100644 --- a/packages/synapse-interface/components/Portfolio/components/SingleNetworkPortfolio.tsx +++ b/packages/synapse-interface/components/Portfolio/components/SingleNetworkPortfolio.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useEffect } from 'react' import { Address } from 'viem' import { useDispatch } from 'react-redux' import _, { isArray } from 'lodash' diff --git a/packages/synapse-interface/components/Portfolio/components/ViewSearchAddressBanner.tsx b/packages/synapse-interface/components/Portfolio/components/ViewSearchAddressBanner.tsx index 432e724125..7f548d31d0 100644 --- a/packages/synapse-interface/components/Portfolio/components/ViewSearchAddressBanner.tsx +++ b/packages/synapse-interface/components/Portfolio/components/ViewSearchAddressBanner.tsx @@ -15,7 +15,7 @@ export const ViewSearchAddressBanner = ({ id="view-search-address-banner" className={` flex justify-between p-3 mb-3 - border border-synapsePurple rounded-sm + border border-fuchsia-400 rounded-sm `} style={{ background: diff --git a/packages/synapse-interface/components/StateManagedBridge/AvailableBalance.tsx b/packages/synapse-interface/components/StateManagedBridge/AvailableBalance.tsx new file mode 100644 index 0000000000..da1ee49204 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedBridge/AvailableBalance.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { joinClassNames } from '@/utils/joinClassNames' + +export const AvailableBalance = ({ + balance, + maxBridgeableBalance, + isGasToken, + isGasEstimateLoading, + isDisabled, +}: { + balance?: string + maxBridgeableBalance?: number + gasCost?: string + isGasToken: boolean + isGasEstimateLoading: boolean + isDisabled: boolean +}) => { + const labelClassName = joinClassNames({ + space: 'block', + text: 'text-xxs md:text-xs', + cursor: 'cursor-default', + }) + + if (isDisabled) { + return null + } else if (isGasToken && isGasEstimateLoading) { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeWarnings.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeWarnings.tsx index 0cd92355b7..429507eb1e 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeWarnings.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeWarnings.tsx @@ -29,7 +29,7 @@ export const ConfirmDestinationAddressWarning = () => { onChange={handleCheckboxChange} className={` cursor-pointer border rounded-[4px] border-secondary - text-synapsePurple bg-transparent outline-none + text-fuchsia-400 bg-transparent outline-none focus:!outline-0 focus:ring-0 focus:!border-0 active:!outline-0 active:ring-0 active:!border-0 `} diff --git a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx index e2365a6756..b17b6b285d 100644 --- a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx @@ -1,18 +1,17 @@ +import { isNull, isNumber } from 'lodash' +import toast from 'react-hot-toast' import React, { useEffect, useState, useCallback, useMemo } from 'react' -import { useDispatch } from 'react-redux' import { useAccount } from 'wagmi' - +import { useAppDispatch } from '@/store/hooks' import { initialState, + updateFromValue, setFromChainId, setFromToken, - updateFromValue, } from '@/slices/bridge/reducer' -import MiniMaxButton from '../buttons/MiniMaxButton' import { ChainSelector } from '@/components/ui/ChainSelector' import { TokenSelector } from '@/components/ui/TokenSelector' import { AmountInput } from '@/components/ui/AmountInput' -import { formatBigIntToString } from '@/utils/bigint/format' import { cleanNumberInput } from '@/utils/cleanNumberInput' import { ConnectToNetworkButton, @@ -26,36 +25,93 @@ import { usePortfolioState } from '@/slices/portfolio/hooks' import { BridgeSectionContainer } from '@/components/ui/BridgeSectionContainer' import { BridgeAmountContainer } from '@/components/ui/BridgeAmountContainer' import { useFromTokenListArray } from './hooks/useFromTokenListArray' +import { AvailableBalance } from './AvailableBalance' +import { useGasEstimator } from '../../utils/hooks/useGasEstimator' +import { getParsedBalance } from '@/utils/getParsedBalance' +import { MaxButton } from './MaxButton' +import { formatAmount } from '../../utils/formatAmount' export const inputRef = React.createRef() export const InputContainer = () => { - const { fromChainId, fromToken, fromValue } = useBridgeState() + const dispatch = useAppDispatch() + const { chain, isConnected } = useAccount() + const { balances } = usePortfolioState() + const { fromChainId, toChainId, fromToken, toToken, fromValue } = + useBridgeState() const [showValue, setShowValue] = useState('') - const [hasMounted, setHasMounted] = useState(false) - const { balances } = usePortfolioState() + const { addresses, decimals } = fromToken || {} + const tokenDecimals = isNumber(decimals) ? decimals : decimals?.[fromChainId] + const balance: bigint = balances[fromChainId]?.find( + (token) => token.tokenAddress === addresses?.[fromChainId] + )?.balance + const parsedBalance = getParsedBalance(balance, tokenDecimals) + const formattedBalance = formatAmount(parsedBalance) + + const hasValidFromSelections: boolean = useMemo(() => { + return Boolean(fromChainId && fromToken) + }, [fromChainId, fromToken]) + + const hasValidInputSelections: boolean = useMemo(() => { + return Boolean(fromChainId && fromToken && toChainId && toToken) + }, [fromChainId, toChainId, fromToken, toToken]) + + const { + isLoading, + isGasToken, + parsedGasCost, + maxBridgeableGas, + hasValidGasEstimateInputs, + estimateBridgeableBalanceCallback, + } = useGasEstimator() + + const isInputMax = + maxBridgeableGas?.toString() === fromValue || parsedBalance === fromValue + + const onMaxBalance = useCallback(async () => { + if (hasValidGasEstimateInputs()) { + const bridgeableBalance = await estimateBridgeableBalanceCallback() + + if (isNull(bridgeableBalance)) { + dispatch(updateFromValue(parsedBalance)) + } else if (bridgeableBalance > 0) { + dispatch(updateFromValue(bridgeableBalance?.toString())) + } else { + dispatch(updateFromValue('0.0')) + toast.error('Gas fees likely exceeds your balance.', { + id: 'toast-error-not-enough-gas', + duration: 10000, + }) + } + } else { + dispatch(updateFromValue(parsedBalance)) + } + }, [ + fromChainId, + fromToken, + parsedBalance, + hasValidGasEstimateInputs, + estimateBridgeableBalanceCallback, + ]) useEffect(() => { setHasMounted(true) }, []) - const { isConnected } = useAccount() - const { chain } = useAccount() - - const dispatch = useDispatch() - - const parsedBalance = balances[fromChainId]?.find( - (token) => token.tokenAddress === fromToken?.addresses[fromChainId] - )?.parsedBalance - - const balance = balances[fromChainId]?.find( - (token) => token.tokenAddress === fromToken?.addresses[fromChainId] - )?.balance + const connectedStatus = useMemo(() => { + if (hasMounted && !isConnected) { + return + } else if (hasMounted && isConnected && fromChainId === chain?.id) { + return + } else if (hasMounted && isConnected && fromChainId !== chain?.id) { + return + } + }, [chain, fromChainId, isConnected, hasMounted]) useEffect(() => { - if (fromToken && fromToken?.decimals[fromChainId]) { + if (fromToken && tokenDecimals) { setShowValue(fromValue) } @@ -83,24 +139,6 @@ export const InputContainer = () => { } } - const onMaxBalance = useCallback(() => { - dispatch( - updateFromValue( - formatBigIntToString(balance, fromToken?.decimals[fromChainId]) - ) - ) - }, [balance, fromChainId, fromToken]) - - const connectedStatus = useMemo(() => { - if (hasMounted && !isConnected) { - return - } else if (hasMounted && isConnected && fromChainId === chain?.id) { - return - } else if (hasMounted && isConnected && fromChainId !== chain?.id) { - return - } - }, [chain, fromChainId, isConnected, hasMounted]) - return (
@@ -109,21 +147,30 @@ export const InputContainer = () => {
- - {hasMounted && isConnected && ( - + + + - )} +
) diff --git a/packages/synapse-interface/components/StateManagedBridge/MaxButton.tsx b/packages/synapse-interface/components/StateManagedBridge/MaxButton.tsx new file mode 100644 index 0000000000..425b78bcdd --- /dev/null +++ b/packages/synapse-interface/components/StateManagedBridge/MaxButton.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { joinClassNames } from '@/utils/joinClassNames' + +export const MaxButton = ({ onClick, isHidden }) => { + const buttonClassName = joinClassNames({ + display: `${isHidden ? 'hidden' : 'block'}`, + spacing: 'px-1.5', + text: 'text-fuchsia-400 text-xxs md:text-xs', + animation: 'transition-all duration-150 transform-gpu', + hover: 'hover:opacity-70 cursor-pointer', + }) + + return ( + + ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx index 383cd07ac9..4e9f0bd71b 100644 --- a/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx +++ b/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react' import { useDispatch } from 'react-redux' import { useAccount } from 'wagmi' - -import MiniMaxButton from '@/components/buttons/MiniMaxButton' import { TokenSelector } from '@/components/ui/TokenSelector' import { formatBigIntToString, stringToBigInt } from '@/utils/bigint/format' import { cleanNumberInput } from '@/utils/cleanNumberInput' @@ -26,6 +24,11 @@ import { CHAINS_BY_ID } from '@/constants/chains' import { useSwapChainListArray } from '@/components/StateManagedSwap//hooks/useSwapChainListArray' import { useSwapFromTokenListArray } from '@/components/StateManagedSwap/hooks/useSwapFromTokenListOverlay' import { AmountInput } from '@/components/ui/AmountInput' +import { joinClassNames } from '@/utils/joinClassNames' +import { MaxButton } from '../StateManagedBridge/MaxButton' +import { trimTrailingZeroesAfterDecimal } from '@/utils/trimTrailingZeroesAfterDecimal' +import { formatAmount } from '@/utils/formatAmount' +import { getParsedBalance } from '@/utils/getParsedBalance' export const SwapInputContainer = () => { const inputRef = useRef(null) @@ -50,9 +53,12 @@ export const SwapInputContainer = () => { (token) => token.tokenAddress === swapFromToken?.addresses[swapChainId] ) - const parsedBalance = tokenData?.parsedBalance - const balance = tokenData?.balance + const decimals = tokenData?.token?.decimals[swapChainId] + const parsedBalance = getParsedBalance(balance, decimals) + const formattedBalance = formatAmount(parsedBalance) + + const isInputMax = parsedBalance === swapFromValue useEffect(() => { if ( @@ -90,7 +96,9 @@ export const SwapInputContainer = () => { const onMaxBalance = useCallback(() => { dispatch( updateSwapFromValue( - formatBigIntToString(balance, swapFromToken.decimals[swapChainId]) + trimTrailingZeroesAfterDecimal( + formatBigIntToString(balance, swapFromToken.decimals[swapChainId]) + ) ) ) }, [balance, swapChainId, swapFromToken]) @@ -107,6 +115,12 @@ export const SwapInputContainer = () => { } }, [chain, swapChainId, isConnected, hasMounted]) + const labelClassName = joinClassNames({ + space: 'block', + textColor: 'text-xxs md:text-xs', + cursor: 'cursor-default', + }) + return (
@@ -115,22 +129,27 @@ export const SwapInputContainer = () => {
- - - {hasMounted && isConnected && ( - + - )} +
+ {hasMounted && isConnected && ( + + )} + +
+
) diff --git a/packages/synapse-interface/components/buttons/MiniMaxButton.tsx b/packages/synapse-interface/components/buttons/MiniMaxButton.tsx deleted file mode 100644 index b82edde14e..0000000000 --- a/packages/synapse-interface/components/buttons/MiniMaxButton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { joinClassNames } from '@/utils/joinClassNames' - -type MaxButtonTypes = { - disabled: boolean - onClickBalance: () => void -} - -export default function MaxButton({ - disabled, - onClickBalance, -}: MaxButtonTypes) { - const className = joinClassNames({ - space: 'px-4 py-1 -ml-1 mr-1 rounded', - background: 'bg-zinc-100 dark:bg-separator', // TODO: Remove - // background: 'bg-zinc-100 dark:bg-zinc-700', - border: 'border border-zinc-200 dark:border-transparent', - hover: 'enabled:hover:border-zinc-400 enabled:hover:dark:border-zinc-500', - disabled: 'disabled:opacity-60 disabled:cursor-default', - }) - - return ( - - ) -} diff --git a/packages/synapse-interface/components/ui/AmountInput.tsx b/packages/synapse-interface/components/ui/AmountInput.tsx index 441f8b0d9d..b7bc8b9e03 100644 --- a/packages/synapse-interface/components/ui/AmountInput.tsx +++ b/packages/synapse-interface/components/ui/AmountInput.tsx @@ -5,24 +5,17 @@ import { joinClassNames } from '@/utils/joinClassNames' interface AmountInputTypes { inputRef?: React.RefObject disabled?: boolean - hasMounted?: boolean - isConnected?: boolean isLoading?: boolean showValue: string handleFromValueChange?: (event: React.ChangeEvent) => void - parsedBalance?: string - onMaxBalance?: () => void } + export function AmountInput({ inputRef, disabled = false, - hasMounted, - isConnected, isLoading = false, showValue, handleFromValueChange, - parsedBalance, - onMaxBalance, }: AmountInputTypes) { const inputClassName = joinClassNames({ unset: 'bg-transparent border-none p-0', @@ -32,15 +25,8 @@ export function AmountInput({ focus: 'focus:outline-none focus:ring-0 focus:border-none', }) - const labelClassName = joinClassNames({ - space: 'block', - textColor: 'text-xxs md:text-xs', - animation: 'transition-all duration-150 transform-gpu', - hover: 'hover:opacity-70 cursor-pointer', - }) - return ( -
+ <> {isLoading ? ( ) : ( @@ -59,16 +45,6 @@ export function AmountInput({ maxLength={79} /> )} - {hasMounted && isConnected && !disabled && ( - - )} -
+ ) } diff --git a/packages/synapse-interface/components/ui/SelectSpecificTokenButton.tsx b/packages/synapse-interface/components/ui/SelectSpecificTokenButton.tsx index a89b12480a..feb161728f 100644 --- a/packages/synapse-interface/components/ui/SelectSpecificTokenButton.tsx +++ b/packages/synapse-interface/components/ui/SelectSpecificTokenButton.tsx @@ -1,5 +1,5 @@ import _ from 'lodash' -import { memo, useEffect, useRef } from 'react' +import { memo, useRef } from 'react' import Image from 'next/image' import { type Token, type ActionTypes } from '@/utils/types' @@ -10,6 +10,8 @@ import { findChainIdsWithPausedToken } from '@/constants/tokens' import { getActiveStyleForButton, getHoverStyleForButton } from '@/styles/hover' import { joinClassNames } from '@/utils/joinClassNames' import { useSwapState } from '@/slices/swap/hooks' +import { formatAmount } from '@/utils/formatAmount' +import { getParsedBalance } from '@/utils/getParsedBalance' export const SelectSpecificTokenButton = ({ showAllChains, @@ -81,9 +83,13 @@ const ButtonContent = memo( }) => { const portfolioBalances = usePortfolioBalances() - const parsedBalance = portfolioBalances[chainId]?.find( + const tokenData = portfolioBalances[chainId]?.find( (tb) => tb.token.addresses[chainId] === token.addresses[chainId] - )?.parsedBalance + ) + const decimals = tokenData?.token?.decimals[chainId] + const balance = tokenData?.balance + const parsedBalance = getParsedBalance(balance, decimals) + const formattedBalance = formatAmount(parsedBalance) return (
@@ -91,7 +97,7 @@ const ButtonContent = memo( token={token} showAllChains={showAllChains} isOrigin={isOrigin} - parsedBalance={parsedBalance} + parsedBalance={formattedBalance} />
) diff --git a/packages/synapse-interface/components/ui/SelectorWrapper.tsx b/packages/synapse-interface/components/ui/SelectorWrapper.tsx index 58592bed81..c1d1a690c8 100644 --- a/packages/synapse-interface/components/ui/SelectorWrapper.tsx +++ b/packages/synapse-interface/components/ui/SelectorWrapper.tsx @@ -78,7 +78,7 @@ export const SelectorWrapper = ({ const itemName = selectedItem?.['symbol' in selectedItem ? 'symbol' : 'name'] return ( -
+