diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index 8f859bd4727..92efecd4daa 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -946,6 +946,7 @@ "price-impact": "Price Impact", "loss-tips": "You're losing {{usd}}. Try a smaller amount in a small market.", "Auto": "Auto", + "no-quote-found": "No quote found", "rabbyFee": { "title": "Rabby fee", "swapDesc": "Rabby Wallet will always find the best possible rate from top aggregators and verify the reliability of their offers. Rabby charges a 0.25% fee (0% for wrapping), which is automatically included in the quote.", diff --git a/src/ui/component/ChainIcon.tsx b/src/ui/component/ChainIcon.tsx index cda6de43965..8e2fc9b9057 100644 --- a/src/ui/component/ChainIcon.tsx +++ b/src/ui/component/ChainIcon.tsx @@ -211,6 +211,7 @@ const ChainIcon = ({ align={{ offset: [0, 2], }} + {...tooltipProps} > { getCustomRPC(); }, [chain]); + return ( { const { t } = useTranslation(); const [query, setQuery] = useState(''); diff --git a/src/ui/views/Bridge/Component/BridgeContent.tsx b/src/ui/views/Bridge/Component/BridgeContent.tsx index 3e18a4a97d1..61827833841 100644 --- a/src/ui/views/Bridge/Component/BridgeContent.tsx +++ b/src/ui/views/Bridge/Component/BridgeContent.tsx @@ -394,7 +394,7 @@ export const BridgeContent = () => { -
+
{selectedBridgeQuote && ( { - if (quoteLoading) { + if (quoteLoading || (!sourceLogo && !sourceName)) { return { showLoss: false, diff: '', @@ -124,14 +124,17 @@ export const BridgeShowMore = ({ }, [isBestQuote]); useEffect(() => { - if ((!quoteLoading && data?.showLoss) || slippageError) { + if ( + (!quoteLoading && sourceLogo && sourceName && data?.showLoss) || + slippageError + ) { setOpen(true); } - }, [quoteLoading, data?.showLoss]); + }, [quoteLoading, data?.showLoss, sourceLogo, sourceName]); return (
-
+
)} - + {sourceName} + {!sourceLogo && !sourceName ? ( + - + ) : null}
)} diff --git a/src/ui/views/Bridge/hooks/slippage.tsx b/src/ui/views/Bridge/hooks/slippage.tsx index 9c36f534cf7..81bdb78e719 100644 --- a/src/ui/views/Bridge/hooks/slippage.tsx +++ b/src/ui/views/Bridge/hooks/slippage.tsx @@ -1,22 +1,9 @@ import { useCallback, useMemo, useState } from 'react'; -type SlippageType = 'swap' | 'bridge'; +export const useBridgeSlippage = () => { + const [slippageState, setSlippageState] = useState('1'); -const DEFAULT = { - swap: '0.1', - bridge: '1', -}; -const SLIPPAGE_RANGE = { - swap: [0.1, 10], - bridge: [0.2, 3], -}; - -export const useSwapAndBridgeSlippage = (type: SlippageType) => { - const [slippageState, setSlippageState] = useState(DEFAULT[type]); - - const slippage = useMemo(() => slippageState || DEFAULT[type], [ - slippageState, - ]); + const slippage = useMemo(() => slippageState || '1', [slippageState]); const [slippageChanged, setSlippageChanged] = useState(false); const [autoSlippage, setAutoSlippage] = useState(true); @@ -28,10 +15,8 @@ export const useSwapAndBridgeSlippage = (type: SlippageType) => { const [isSlippageLow, isSlippageHigh] = useMemo(() => { return [ - slippageState?.trim() !== '' && - Number(slippageState || 0) < SLIPPAGE_RANGE[type][0], - slippageState?.trim() !== '' && - Number(slippageState || 0) > SLIPPAGE_RANGE[type][1], + slippageState?.trim() !== '' && Number(slippageState || 0) < 0.2, + slippageState?.trim() !== '' && Number(slippageState || 0) > 3, ]; }, [slippageState]); diff --git a/src/ui/views/Bridge/hooks/token.tsx b/src/ui/views/Bridge/hooks/token.tsx index e512f5c912c..a16da280da7 100644 --- a/src/ui/views/Bridge/hooks/token.tsx +++ b/src/ui/views/Bridge/hooks/token.tsx @@ -11,8 +11,8 @@ import { useRefreshId, useSetQuoteVisible, useSetRefreshId } from './context'; import { getChainDefaultToken, tokenAmountBn } from '@/ui/utils/token'; import BigNumber from 'bignumber.js'; import stats from '@/stats'; -import { useSwapAndBridgeSlippage } from './slippage'; import { isNaN } from 'lodash'; +import { useBridgeSlippage } from './slippage'; export interface SelectedBridgeQuote extends Omit { shouldApproveToken?: boolean; @@ -119,7 +119,7 @@ export const useBridge = () => { const [amount, setAmount] = useState(''); - const slippageObj = useSwapAndBridgeSlippage('bridge'); + const slippageObj = useBridgeSlippage(); const [recommendFromToken, setRecommendFromToken] = useState(); diff --git a/src/ui/views/Dashboard/components/CurrentConnection/index.tsx b/src/ui/views/Dashboard/components/CurrentConnection/index.tsx index 198cd93703f..d02e46c3bc2 100644 --- a/src/ui/views/Dashboard/components/CurrentConnection/index.tsx +++ b/src/ui/views/Dashboard/components/CurrentConnection/index.tsx @@ -125,7 +125,7 @@ export const CurrentConnection = memo((props: CurrentConnectionProps) => { handleRemove(site!.origin)} />
diff --git a/src/ui/views/Swap/Component/Main.tsx b/src/ui/views/Swap/Component/Main.tsx index 75e37e6a2c2..55c5839b00e 100644 --- a/src/ui/views/Swap/Component/Main.tsx +++ b/src/ui/views/Swap/Component/Main.tsx @@ -15,7 +15,6 @@ import { useWallet } from '@/ui/utils'; import clsx from 'clsx'; import { QuoteList } from './Quotes'; import { useQuoteVisible, useSetQuoteVisible, useSetRefreshId } from '../hooks'; -import { InfoCircleFilled } from '@ant-design/icons'; import { DEX_ENUM, DEX_SPENDER_WHITELIST } from '@rabby-wallet/rabby-swap'; import { useDispatch } from 'react-redux'; import { useRbiSource } from '@/ui/utils/ga-event'; @@ -40,6 +39,7 @@ import { SwapTokenItem } from './Token'; import { BridgeSwitchBtn } from '../../Bridge/Component/BridgeSwitchButton'; import { BridgeShowMore } from '../../Bridge/Component/BridgeShowMore'; import { ReactComponent as RcIconWarningCC } from '@/ui/assets/warning-cc.svg'; +import useDebounceValue from '@/ui/hooks/useDebounceValue'; const getDisabledTips: SelectChainItemProps['disabledTips'] = (ctx) => { const chainItem = findChainByServerID(ctx.chain.serverId); @@ -360,7 +360,7 @@ export const Main = () => { }, }); - const [showMoreOpen, setShowMoreOpen] = useState(false); + const [showMoreOpen, setShowMoreOpen] = useState(!autoSlippage); const [sourceName, sourceLogo] = useMemo(() => { if (activeProvider?.name) { @@ -373,7 +373,7 @@ export const Main = () => { return ['', '']; }, [isWrapToken, activeProvider?.name]); - const noQuote = useMemo( + const noQuoteOrigin = useMemo( () => Number(inputAmount) > 0 && !inSufficient && @@ -393,11 +393,17 @@ export const Main = () => { ] ); + const noQuote = useDebounceValue(noQuoteOrigin, 16); + + if (noQuote && !showMoreOpen) { + setShowMoreOpen(true); + } + return (
-
+
{ hideTestnetTab={true} chainRenderClassName={clsx('pl-[30px] text-[13px] font-medium')} title={
{t('page.bridge.select-chain')}
} + drawerHeight={540} + showClosableIcon />
@@ -484,13 +492,42 @@ export const Main = () => { />
+ {inSufficient || noQuote ? ( + + } + banner + message={ + + {inSufficient + ? t('page.swap.insufficient-balance') + : t('page.swap.no-quote-found')} + + } + /> + ) : null} + {Number(inputAmount) > 0 && !inSufficient && !!amountAvailable && !!payToken && - !!receiveToken && - !!activeProvider && ( -
+ !!receiveToken && ( +
{
)} - {inSufficient || noQuote ? ( - - } - banner - message={ - - {inSufficient - ? t('page.swap.insufficient-balance') - : t('page.bridge.no-quote-found')} - - } - /> - ) : null}
.ant-input { + height: 46px; font-weight: 500; + box-shadow: none; + border-radius: 4px; + border: 1px solid transparent; + background: transparent !important; font-size: 24px; text-align: right; - border-width: 0px !important; - border-color: transparent; padding-right: 0; } &.ant-input-affix-wrapper:not(.ant-input-affix-wrapper-disabled):hover { @@ -70,7 +66,7 @@ interface SwapTokenItemProps { value: string; chainId: string; onTokenChange: (token: TokenItem) => void; - onValueChange?: React.ChangeEventHandler; + onValueChange?: (s: string) => void; label?: React.ReactNode; slider?: number; onChangeSlider?: (value: number) => void; @@ -99,6 +95,8 @@ export const SwapTokenItem = (props: SwapTokenItemProps) => { const dispatch = useRabbyDispatch(); + const inputRef = useRef(); + const isFrom = type === 'from'; const [balance, usdValue] = useMemo(() => { @@ -117,10 +115,16 @@ export const SwapTokenItem = (props: SwapTokenItemProps) => { }, [token, valueLoading, value]); const onTokenSelect = useCallback( - (token: TokenItem) => { - onTokenChange(token); + (newToken: TokenItem) => { + onTokenChange(newToken); + if (isFrom && newToken.id !== token?.id) { + onValueChange?.(''); + setTimeout(() => { + inputRef?.current?.focus?.(); + }); + } if (type === 'to') { - dispatch.swap.setRecentSwapToToken(token); + dispatch.swap.setRecentSwapToToken(newToken); } }, [onTokenChange, type] @@ -151,7 +155,12 @@ export const SwapTokenItem = (props: SwapTokenItemProps) => { }); }, [isWrapQuote, currentQuote?.name, currentQuote?.quote]); - const inputRef = useRef(); + const onInputChange: React.ChangeEventHandler = useCallback( + (e) => { + onValueChange?.(e.target.value); + }, + [] + ); useLayoutEffect(() => { if (token?.id && isFrom) { @@ -166,16 +175,16 @@ export const SwapTokenItem = (props: SwapTokenItemProps) => { {isFrom ? t('page.swap.from') : t('page.swap.to')} {isFrom && ( -
+
- + {slider}%
@@ -210,7 +219,7 @@ export const SwapTokenItem = (props: SwapTokenItemProps) => { spellCheck={false} placeholder="0" value={value} - onChange={onValueChange} + onChange={onInputChange} ref={inputRef as any} readOnly={!isFrom} className={clsx( diff --git a/src/ui/views/Swap/hooks/quote.tsx b/src/ui/views/Swap/hooks/quote.tsx index b9e96712ee2..b2c8250c438 100644 --- a/src/ui/views/Swap/hooks/quote.tsx +++ b/src/ui/views/Swap/hooks/quote.tsx @@ -1,8 +1,7 @@ -import { CEX, DEX, ETH_USDT_CONTRACT, SWAP_FEE_ADDRESS } from '@/constant'; +import { DEX, ETH_USDT_CONTRACT, SWAP_FEE_ADDRESS } from '@/constant'; import { formatUsdValue, isSameAddress, useWallet } from '@/ui/utils'; import { CHAINS, CHAINS_ENUM } from '@debank/common'; import { - CEXQuote, ExplainTxResponse, TokenItem, Tx, diff --git a/src/ui/views/Swap/hooks/slippage.tsx b/src/ui/views/Swap/hooks/slippage.tsx new file mode 100644 index 00000000000..65d3130ae68 --- /dev/null +++ b/src/ui/views/Swap/hooks/slippage.tsx @@ -0,0 +1,70 @@ +import { useRabbyDispatch, useRabbySelector } from '@/ui/store'; +import { useCallback, useMemo, useState } from 'react'; + +const useSlippageStore = () => { + const { autoSlippage, isCustomSlippage } = useRabbySelector((store) => ({ + autoSlippage: !!store.swap.autoSlippage, + isCustomSlippage: !!store.swap.isCustomSlippage, + })); + + const dispatch = useRabbyDispatch(); + + const setAutoSlippage = useCallback( + (bool: boolean) => { + dispatch.swap.setAutoSlippage(bool); + }, + [dispatch] + ); + + const setIsCustomSlippage = useCallback( + (bool: boolean) => { + dispatch.swap.setIsCustomSlippage(bool); + }, + [dispatch] + ); + + return { + autoSlippage, + isCustomSlippage, + setAutoSlippage, + setIsCustomSlippage, + }; +}; + +export const useSwapSlippage = () => { + const previousSlippage = useRabbySelector((s) => s.swap.slippage || ''); + const [slippageState, setSlippageState] = useState(previousSlippage || '0.1'); + + const setSlippageOnStore = useRabbyDispatch().swap.setSlippage; + + const slippage = useMemo(() => slippageState || '0.1', [slippageState]); + const [slippageChanged, setSlippageChanged] = useState(false); + + const setSlippage = useCallback( + (slippage: string) => { + setSlippageOnStore(slippage); + setSlippageState(slippage); + }, + [setSlippageOnStore] + ); + + const [isSlippageLow, isSlippageHigh] = useMemo(() => { + return [ + slippageState?.trim() !== '' && Number(slippageState || 0) < 0.1, + slippageState?.trim() !== '' && Number(slippageState || 0) > 10, + ]; + }, [slippageState]); + + const slippageStore = useSlippageStore(); + + return { + slippageChanged, + setSlippageChanged, + slippageState, + isSlippageLow, + isSlippageHigh, + slippage, + setSlippage, + ...slippageStore, + }; +}; diff --git a/src/ui/views/Swap/hooks/token.tsx b/src/ui/views/Swap/hooks/token.tsx index fed38e3d090..de7c5462a6e 100644 --- a/src/ui/views/Swap/hooks/token.tsx +++ b/src/ui/views/Swap/hooks/token.tsx @@ -21,7 +21,7 @@ import { useAsyncInitializeChainList } from '@/ui/hooks/useChain'; import { SWAP_SUPPORT_CHAINS } from '@/constant'; import { findChain } from '@/utils/chain'; import { GasLevelType } from '../Component/ReserveGasPopup'; -import { useSwapAndBridgeSlippage } from '../../Bridge/hooks/slippage'; +import { useSwapSlippage } from './slippage'; const useTokenInfo = ({ userAddress, @@ -199,7 +199,7 @@ export const useTokenPair = (userAddress: string) => { [payToken] ); - const slippageObj = useSwapAndBridgeSlippage('swap'); + const slippageObj = useSwapSlippage(); const [currentProvider, setOriActiveProvider] = useState< QuoteProvider | undefined @@ -223,9 +223,8 @@ export const useTokenPair = (userAddress: string) => { const [passGasPrice, setUseGasPrice] = useState(false); - const handleAmountChange: React.ChangeEventHandler = useCallback( - (e) => { - const v = e.target.value; + const handleAmountChange = useCallback( + (v: string) => { if (!/^\d*(\.\d*)?$/.test(v)) { return; }