diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index 43fadcb3705..d11fd50ee17 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -963,6 +963,7 @@ "Select": "Select", "select-chain": "Select Chain", "no-quote-found": "No quote found. Please try other token pairs.", + "no-quote": "No Quote", "title": "Bridge", "history": "Bridge history", "the-following-bridge-route-are-found": "Found following route", @@ -2397,7 +2398,7 @@ "token": "Token", "value": "Value", "liquidity": "Liquidity", - "liquidityTips": "The higher the historical trade volume, the more likely bridge will be successful.", + "liquidityTips": "The higher the historical trade volume, the more likely the bridge will succeed.", "low": "Low", "high": "High" }, diff --git a/src/ui/assets/match-cc.svg b/src/ui/assets/match-cc.svg new file mode 100644 index 00000000000..7f9188daa48 --- /dev/null +++ b/src/ui/assets/match-cc.svg @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/src/ui/component/ChainSelector/InForm.tsx b/src/ui/component/ChainSelector/InForm.tsx index 5e32eeb8d2f..bf3a6b621aa 100644 --- a/src/ui/component/ChainSelector/InForm.tsx +++ b/src/ui/component/ChainSelector/InForm.tsx @@ -137,6 +137,7 @@ interface ChainSelectorProps { mini?: boolean; hideTestnetTab?: boolean; excludeChains?: CHAINS_ENUM[]; + drawerHeight?: number; } export default function ChainSelectorInForm({ value, @@ -151,6 +152,7 @@ export default function ChainSelectorInForm({ mini, hideTestnetTab = false, excludeChains, + drawerHeight, }: ChainSelectorProps) { const [showSelectorModal, setShowSelectorModal] = useState(showModal); @@ -182,6 +184,7 @@ export default function ChainSelectorInForm({ /> {!readonly && ( void; }) => React.ReactNode) | React.ReactNode; + disabledTips?: React.ReactNode; + drawerHeight?: string | number; } const defaultExcludeTokens = []; @@ -90,6 +92,8 @@ const TokenSelect = ({ loading = false, tokenRender, useSwapTokenList = false, + disabledTips = 'Not supported', + drawerHeight, }: TokenSelectProps) => { const [queryConds, setQueryConds] = useState({ keyword: '', @@ -167,7 +171,7 @@ const TokenSelect = ({ currentAccount?.address, queryConds.keyword, queryConds.chainServerId, - isSwapType ? false : true + isSwapType || type === 'bridgeFrom' ? false : true ); const availableToken = useMemo(() => { @@ -227,6 +231,7 @@ const TokenSelect = ({ : tokenRender} {queryConds.chainServerId && ( )} @@ -299,8 +304,9 @@ const TokenSelect = ({ type={type} placeholder={placeholder} chainId={queryConds.chainServerId} - disabledTips={'Not supported'} + disabledTips={disabledTips} supportChains={SWAP_SUPPORT_CHAINS} + drawerHeight={drawerHeight} /> )} diff --git a/src/ui/component/TokenSelector/index.tsx b/src/ui/component/TokenSelector/index.tsx index 3fe6b7645cc..c51dc0679b6 100644 --- a/src/ui/component/TokenSelector/index.tsx +++ b/src/ui/component/TokenSelector/index.tsx @@ -19,6 +19,7 @@ import { ReactComponent as RcIconChainFilterClose } from 'ui/assets/chain-select import { ReactComponent as RcIconCloseCC } from 'ui/assets/component/close-cc.svg'; import { isNil } from 'lodash'; import ThemeIcon from '../ThemeMode/ThemeIcon'; +import { ReactComponent as RcIconMatchCC } from '@/ui/assets/match-cc.svg'; export const isSwapTokenType = (s: string) => ['swapFrom', 'swapTo'].includes(s); @@ -42,8 +43,9 @@ export interface TokenSelectorProps { type?: 'default' | 'swapFrom' | 'swapTo' | 'bridgeFrom'; placeholder?: string; chainId: string; - disabledTips?: string; + disabledTips?: React.ReactNode; supportChains?: CHAINS_ENUM[] | undefined; + drawerHeight?: number | string; } const filterTestnetTokenItem = (token: TokenItem) => { @@ -63,6 +65,7 @@ const TokenSelector = ({ chainId: chainServerId, disabledTips, supportChains, + drawerHeight = '580px', }: TokenSelectorProps) => { const { t } = useTranslation(); const [query, setQuery] = useState(''); @@ -153,6 +156,24 @@ const TokenSelector = ({ return v.length === 42 && v.toLowerCase().startsWith('0x'); }, [query]); + const bridgeFromNoDataTip = useMemo(() => { + if (type === 'bridgeFrom') { + return ( +
+ + +

+ {t('component.TokenSelector.noTokens')} +

+
+ ); + } + return null; + }, [type]); + const NoDataUI = useMemo( () => isLoading ? ( @@ -163,6 +184,8 @@ const TokenSelector = ({ ))} + ) : type === 'bridgeFrom' ? ( + <>{bridgeFromNoDataTip} ) : (
), - [isLoading, isSwapType, t, isSearchAddr, chainServerId] + [ + isLoading, + isSwapType, + t, + isSearchAddr, + chainServerId, + bridgeFromNoDataTip, + type, + ] ); useEffect(() => { @@ -241,19 +272,21 @@ const TokenSelector = ({ const bridgeFromItemRender = (token: TokenItem) => { const chainItem = findChain({ serverId: token.chain }); const disabled = - !!supportChains?.length && - chainItem && - !supportChains.includes(chainItem.enum); + (!!supportChains?.length && + !!chainItem && + !supportChains.includes(chainItem.enum)) || + new BigNumber(token?.raw_amount_hex_str || token.amount || 0).lte(0); return (
  • { selectedBridgeQuote, setSelectedBridgeQuote, - expired, slippage, slippageState, @@ -147,22 +126,8 @@ export const BridgeContent = () => { const { t } = useTranslation(); const btnText = useMemo(() => { - // if (selectedBridgeQuote && expired) { - // return t('page.bridge.price-expired-refresh-route'); - // } - if (selectedBridgeQuote?.shouldApproveToken) { - return t('page.bridge.approve-and-bridge', { - name: selectedBridgeQuote?.aggregator.name || '', - }); - } - if (selectedBridgeQuote?.aggregator.name) { - return t('page.bridge.bridge-via-x', { - name: selectedBridgeQuote?.aggregator.name, - }); - } - return t('page.bridge.title'); - }, [selectedBridgeQuote, expired, t]); + }, []); const wallet = useWallet(); const rbiSource = useRbiSource(); @@ -410,6 +375,23 @@ export const BridgeContent = () => { }, }); + const noQuote = + !inSufficient && + !!fromToken && + !!toToken && + Number(amount) > 0 && + !quoteLoading && + !quoteList?.length; + + const btnDisabled = + inSufficient || + !fromToken || + !toToken || + !amountAvailable || + !selectedBridgeQuote || + quoteLoading || + !quoteList?.length; + return (
    { valueLoading={quoteLoading} value={selectedBridgeQuote?.to_token_amount} excludeChains={fromChain ? [fromChain] : undefined} + noQuote={noQuote} />
    @@ -463,23 +446,12 @@ export const BridgeContent = () => { openQuotesList={openQuotesList} /> )} - {fromToken && - toToken && - Number(amount) > 0 && - !quoteLoading && - !quoteList?.length && - recommendFromToken && ( - - )} + {noQuote && recommendFromToken && ( + + )}
    - {inSufficient || - (fromToken && - toToken && - Number(amount) > 0 && - !quoteLoading && - !quoteList?.length && - !recommendFromToken) ? ( + {inSufficient || (noQuote && !recommendFromToken) ? ( { @@ -521,7 +493,7 @@ export const BridgeContent = () => { className="h-[48px] text-white text-[16px] font-medium" onClick={() => { if (fetchingBridgeQuote) return; - if (!selectedBridgeQuote || expired) { + if (!selectedBridgeQuote) { refresh((e) => e + 1); return; @@ -556,13 +528,7 @@ export const BridgeContent = () => { // gotoBridge(); handleBridge(); }} - disabled={ - !fromToken || - !toToken || - !amountAvailable || - inSufficient || - !selectedBridgeQuote - } + disabled={btnDisabled} > {btnText} diff --git a/src/ui/views/Bridge/Component/BridgeQuoteItem.tsx b/src/ui/views/Bridge/Component/BridgeQuoteItem.tsx index f8d7f65a3bd..8f5eeb0a2ce 100644 --- a/src/ui/views/Bridge/Component/BridgeQuoteItem.tsx +++ b/src/ui/views/Bridge/Component/BridgeQuoteItem.tsx @@ -32,9 +32,7 @@ interface QuoteItemProps extends SelectedBridgeQuote { isBestQuote?: boolean; bestQuoteUsd: string; sortIncludeGasFee: boolean; - setSelectedBridgeQuote?: React.Dispatch< - React.SetStateAction - >; + setSelectedBridgeQuote?: (quote: SelectedBridgeQuote) => void; onlyShow?: boolean; loading?: boolean; inSufficient?: boolean; diff --git a/src/ui/views/Bridge/Component/BridgeQuotes.tsx b/src/ui/views/Bridge/Component/BridgeQuotes.tsx index d75c0535462..e89704cd0a4 100644 --- a/src/ui/views/Bridge/Component/BridgeQuotes.tsx +++ b/src/ui/views/Bridge/Component/BridgeQuotes.tsx @@ -22,9 +22,7 @@ interface QuotesProps { visible: boolean; onClose: () => void; payAmount: string; - setSelectedBridgeQuote: React.Dispatch< - React.SetStateAction - >; + setSelectedBridgeQuote: (quote?: SelectedBridgeQuote) => void; sortIncludeGasFee: boolean; } diff --git a/src/ui/views/Bridge/Component/BridgeShowMore.tsx b/src/ui/views/Bridge/Component/BridgeShowMore.tsx index 40ea3744a23..f5bd8a5ebd2 100644 --- a/src/ui/views/Bridge/Component/BridgeShowMore.tsx +++ b/src/ui/views/Bridge/Component/BridgeShowMore.tsx @@ -11,7 +11,7 @@ import { BridgeSlippage } from './BridgeSlippage'; import { tokenPriceImpact } from '../hooks'; const dottedClassName = - 'h-0 flex-1 border-b-[0.5px] border-dotted border-rabby-neutral-line'; + 'h-0 flex-1 border-b-[1px] border-solid border-rabby-neutral-line'; export const BridgeShowMore = ({ openQuotesList, @@ -47,11 +47,11 @@ export const BridgeShowMore = ({ return (
    -
    +
    )} - {sourceName} + {sourceName}
    diff --git a/src/ui/views/Bridge/Component/BridgeSlippage.tsx b/src/ui/views/Bridge/Component/BridgeSlippage.tsx index 5a1de910700..154dcf4728d 100644 --- a/src/ui/views/Bridge/Component/BridgeSlippage.tsx +++ b/src/ui/views/Bridge/Component/BridgeSlippage.tsx @@ -32,13 +32,14 @@ const SlippageItem = styled.div` border-radius: 6px; overflow: hidden; - &.input { + &.input-wrapper { border: 1px solid var(--r-neutral-line, #e0e5ec); background: var(--r-neutral-card-1, #fff); } &:hover, &.active { + color: var(--r-blue-default, #7084ff); background: var(--r-blue-light1, #eef1ff); border: 1px solid var(--r-blue-default, #7084ff); } @@ -198,7 +199,11 @@ export const BridgeSlippage = memo((props: SlippageProps) => { {t('page.swap.slippage-tolerance')} - + {displaySlippage}% {/* { setIsCustomSlippage(true); }} className={clsx( - 'input', + 'input-wrapper', 'flex-1', isCustomSlippage && 'active', tips && 'error' )} > void; }) => React.ReactNode) | React.ReactNode; + drawerHeight?: string | number; } const defaultExcludeTokens = []; @@ -98,6 +99,7 @@ const BridgeToTokenSelect = ({ value, loading = false, tokenRender, + drawerHeight, }: TokenSelectProps) => { const [queryConds, setQueryConds] = useState({ keyword: '', @@ -108,6 +110,8 @@ const BridgeToTokenSelect = ({ ); const wallet = useWallet(); + const supportChains = useRabbySelector((s) => s.bridge.supportedChains); + const handleCurrentTokenChange = (token: TokenItem) => { onChange && onChange(''); onTokenChange(token); @@ -128,26 +132,23 @@ const BridgeToTokenSelect = ({ setTokenSelectorVisible(true); }; - const { - value: swapTokenList, - loading: swapTokenListLoading, - } = useAsync(async () => { - if (fromChainId) { + const { value: tokenList, loading: tokenListLoading } = useAsync(async () => { + if (fromChainId && chainId) { const list = await wallet.openapi.getBridgeToTokenList({ from_chain_id: fromChainId, from_token_id: fromTokenId, - // @ts-expect-error 123 + // @ts-expect-error to_chain_id to_chain_id: chainId, q: queryConds.keyword, }); return list?.token_list; } return []; - }, [currentAccount, tokenSelectorVisible]); + }, [currentAccount, chainId, tokenSelectorVisible, queryConds.keyword]); const allDisplayTokens = useMemo(() => { - return swapTokenList; - }, [swapTokenList]); + return tokenList; + }, [tokenList]); const availableToken = useMemo(() => { const allTokens = allDisplayTokens; @@ -158,11 +159,11 @@ const BridgeToTokenSelect = ({ const displayTokenList = useSortToken(availableToken); - const isListLoading = swapTokenListLoading; + const isListLoading = tokenListLoading; - const handleSearchTokens = React.useCallback(async (ctx) => { + const handleSearchTokens = React.useCallback(async (keyword) => { setQueryConds({ - keyword: ctx.keyword, + keyword, }); }, []); @@ -192,6 +193,7 @@ const BridgeToTokenSelect = ({ : tokenRender} {chainId && ( )} @@ -253,6 +255,7 @@ const BridgeToTokenSelect = ({ )} ); @@ -316,6 +319,7 @@ export interface TokenSelectorProps { supportChains?: CHAINS_ENUM[] ) => React.ReactNode; onSearch: (q: string) => void; + height?: number | string; } const TokenSelector = ({ @@ -330,6 +334,7 @@ const TokenSelector = ({ disabledTips, supportChains, itemRender, + height = '580px', }: TokenSelectorProps) => { const { t } = useTranslation(); const [query, setQuery] = useState(''); @@ -383,37 +388,15 @@ const TokenSelector = ({ ))}
    ) : ( -
    - no site + - {!query || isSearchAddr ? ( -

    - {t('component.TokenSelector.noTokens')} -

    - ) : ( - <> -

    - {t('component.TokenSelector.noMatch')} -

    -

    - {/* Try to search contract address on {{ chainName }} */} - {t('component.TokenSelector.noMatchSuggestion', { - chainName: - findChain({ - serverId: chainServerId, - })?.name || 'chain', - })} -

    - - )} +

    + {t('component.TokenSelector.noTokens')} +

    ), [isLoading, t, isSearchAddr, chainServerId] @@ -422,7 +405,7 @@ const TokenSelector = ({ return (
    - { -
      -
    • -
      {t('component.TokenSelector.bridge.token')}
      -
      - {t('component.TokenSelector.bridge.liquidity')} - - - -
      -
    • - {isEmpty - ? NoDataUI - : displayList.map((_token) => { - const token = (_token as any) as TokenItem & { - trade_volume_level: 'low' | 'high'; - }; - if (itemRender) { - return itemRender(token, supportChains); - } - const chainItem = findChain({ serverId: token.chain }); - const disabled = - !!supportChains?.length && - chainItem && - !supportChains.includes(chainItem.enum); - - return ( - +
      {t('component.TokenSelector.bridge.token')}
      +
      + {t('component.TokenSelector.bridge.liquidity')} + + + +
      +
    + +
      + {isEmpty + ? NoDataUI + : displayList.map((_token) => { + const token = (_token as any) as TokenItem & { + trade_volume_level: 'low' | 'high'; + }; + if (itemRender) { + return itemRender(token, supportChains); + } + const chainItem = findChain({ serverId: token.chain }); + const disabled = + !!supportChains?.length && + chainItem && + !supportChains.includes(chainItem.enum); + + return ( + +
    • !disabled && onConfirm(token)} > -
    • !disabled && onConfirm(token)} - > -
      - -
      - - {getTokenSymbol(token)} - - - {findChainByServerID(token.chain)?.name || ''} - -
      +
      + +
      + + {getTokenSymbol(token)} + + + {findChainByServerID(token.chain)?.name || ''} +
      - -
      - -
      +
      + +
      + +
      +
      -
      - - {token?.trade_volume_level === 'high' - ? t('component.TokenSelector.bridge.high') - : t('component.TokenSelector.bridge.low')} - -
      + /> + + {token?.trade_volume_level === 'high' + ? t('component.TokenSelector.bridge.high') + : t('component.TokenSelector.bridge.low')} +
      -
    • -
      - ); - })} -
    - } +
    +
  • +
    + ); + })} + ); }; diff --git a/src/ui/views/Bridge/Component/BridgeToken.tsx b/src/ui/views/Bridge/Component/BridgeToken.tsx index 7b2690a59fa..266bbc1612b 100644 --- a/src/ui/views/Bridge/Component/BridgeToken.tsx +++ b/src/ui/views/Bridge/Component/BridgeToken.tsx @@ -5,7 +5,7 @@ import { CHAINS_ENUM } from '@debank/common'; import { TokenItem } from '@rabby-wallet/rabby-api/dist/types'; import { Input } from 'antd'; import clsx from 'clsx'; -import React, { useMemo } from 'react'; +import React, { useLayoutEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { TokenRender } from '../../Swap/Component/TokenRender'; import { formatTokenAmount, formatUsdValue } from '@/ui/utils'; @@ -17,16 +17,16 @@ import styled from 'styled-components'; import BridgeToTokenSelect from './BridgeToTokenSelect'; import { ReactComponent as RcIconInfoCC } from 'ui/assets/info-cc.svg'; import { useSetSettingVisible } from '../hooks'; +import { useRabbySelector } from '@/ui/store'; const StyledInput = styled(Input)` - /* height: 46px; */ color: var(--r-neutral-title1, #192945); font-size: 24px; font-style: normal; font-weight: 500; line-height: normal; - /* border: 1px solid var(--r-neutral-line, #d3d8e0); */ background: transparent !important; + padding-left: 0; & > .ant-input { color: var(--r-neutral-title1, #192945); font-size: 24px; @@ -36,27 +36,11 @@ const StyledInput = styled(Input)` border-width: 0px !important; border-color: transparent; } - /* &.ant-input-affix-wrapper:not(.ant-input-affix-wrapper-disabled):hover { - border-width: 1px !important; - } */ - - /* &:active { - border: 1px solid transparent; - } */ - /* &:focus, - &:focus-within { - border-width: 1px !important; - border-color: var(--r-blue-default, #7084ff) !important; - } - &:hover { - border-width: 1px !important; - border-color: var(--r-blue-default, #7084ff) !important; - box-shadow: none; - } */ - &:placeholder-shown { + &::placeholder { color: var(--r-neutral-foot, #6a7587); } + &::-webkit-inner-spin-button, &::-webkit-outer-spin-button { -webkit-appearance: none; @@ -77,6 +61,7 @@ export const BridgeToken = ({ valueLoading, fromChainId, fromTokenId, + noQuote, }: { type?: 'from' | 'to'; token?: TokenItem; @@ -87,12 +72,16 @@ export const BridgeToken = ({ onChangeChain: (chain: CHAINS_ENUM) => void; value?: string | number; onInputChange?: (v: string) => void; + valueLoading?: boolean; fromChainId?: string; fromTokenId?: string; + noQuote?: boolean; }) => { const { t } = useTranslation(); + const supportedChains = useRabbySelector((s) => s.bridge.supportedChains); + const isFromToken = type === 'from'; const name = type === 'from' ? t('page.bridge.From') : t('page.bridge.To'); @@ -100,6 +89,19 @@ export const BridgeToken = ({ const openFeePopup = useSetSettingVisible(); + const inputRef = useRef(); + + useLayoutEffect(() => { + if (type === 'from') { + inputRef.current?.focus(); + } + }, []); + + const showNoQuote = useMemo(() => type === 'to' && !!noQuote, [ + type, + noQuote, + ]); + const useValue = useMemo(() => { if (token && value) { return formatUsdValue( @@ -144,6 +146,8 @@ export const BridgeToken = ({ onChange={onChangeChain} title={t('page.bridge.select-chain')} excludeChains={excludeChains} + supportChains={supportedChains} + drawerHeight={540} />
    @@ -160,14 +164,16 @@ export const BridgeToken = ({ /> ) : ( )} {type === 'to' ? ( } /> ) : ( } /> )} diff --git a/src/ui/views/Bridge/hooks/token.tsx b/src/ui/views/Bridge/hooks/token.tsx index 5514cd709f1..acb4e7746d7 100644 --- a/src/ui/views/Bridge/hooks/token.tsx +++ b/src/ui/views/Bridge/hooks/token.tsx @@ -129,8 +129,6 @@ export const useBridge = () => { SelectedBridgeQuote | undefined >(); - const [expired, setExpired] = useState(false); - const expiredTimer = useRef(); const inSufficient = useMemo( @@ -232,33 +230,24 @@ export const useBridge = () => { const [quoteList, setQuotesList] = useState([]); - const setSelectedBridgeQuote: React.Dispatch< - React.SetStateAction - > = useCallback((p) => { - if (expiredTimer.current) { + const setSelectedBridgeQuote = useCallback((quote?: SelectedBridgeQuote) => { + if (!quote?.manualClick && expiredTimer.current) { clearTimeout(expiredTimer.current); } - setExpired(false); - expiredTimer.current = setTimeout(() => { - setExpired(true); - setRefreshId((e) => e + 1); - }, 1000 * 30); - setOriSelectedBridgeQuote(p); + if (!quote?.manualClick) { + expiredTimer.current = setTimeout(() => { + setRefreshId((e) => e + 1); + }, 1000 * 30); + } + setOriSelectedBridgeQuote(quote); }, []); useEffect(() => { setQuotesList([]); setSelectedBridgeQuote(undefined); + setRecommendFromToken(undefined); }, [fromToken?.id, toToken?.id, fromChain, toChain, amount, inSufficient]); - const visible = useQuoteVisible(); - - // useEffect(() => { - // if (!visible) { - // setQuotesList([]); - // } - // }, [visible]); - const aggregatorsList = useRabbySelector( (s) => s.bridge.aggregatorsList || [] ); @@ -292,8 +281,6 @@ export const useBridge = () => { return e?.map((e) => ({ ...e, loading: true })); }); - setSelectedBridgeQuote(undefined); - const originData: Omit[] = []; const getQUoteV2 = async (alternativeToken?: TokenItem) => @@ -397,6 +384,8 @@ export const useBridge = () => { } catch (error) { setRecommendFromToken(undefined); } + + setSelectedBridgeQuote(undefined); } stats.report('bridgeQuoteResult', { @@ -541,8 +530,6 @@ export const useBridge = () => { const openQuote = useSetQuoteVisible(); const openQuotesList = useCallback(() => { - // setQuotesList([]); - // setRefreshId((e) => e + 1); openQuote(true); }, []); @@ -569,9 +556,14 @@ export const useBridge = () => { aggregatorId: sortedList[0]?.aggregator?.id, }); - setSelectedBridgeQuote((preItem) => - preItem?.manualClick ? preItem : sortedList[0] - ); + let useQuote = sortedList[0]; + + setOriSelectedBridgeQuote((preItem) => { + useQuote = preItem?.manualClick ? preItem : sortedList[0]; + return preItem; + }); + + setSelectedBridgeQuote(useQuote); } } }, [quoteList, quoteLoading, toToken]); @@ -580,12 +572,6 @@ export const useBridge = () => { console.error('quotesError', quotesError); } - useEffect(() => { - setExpired(false); - setSelectedBridgeQuote(undefined); - setRecommendFromToken(undefined); - }, [fromToken?.id, toToken?.id, fromChain, toChain, amount, inSufficient]); - const showLoss = useMemo(() => { if (selectedBridgeQuote) { return !!tokenPriceImpact( @@ -611,8 +597,6 @@ export const useBridge = () => { recommendFromToken, - expired, - inSufficient, amount, handleAmountChange,