= (e) => {
+ const v = e.target.value;
+ if (!/^\d*(\.\d*)?$/.test(v)) {
+ return;
+ }
+ setInput(v);
+ onChange && onChange(v);
+ };
+
+ useEffect(() => {
+ setQueryConds((prev) => ({
+ ...prev,
+ chainServerId: chainId,
+ }));
+ }, [chainId]);
+
+ if (tokenRender) {
+ return (
+ <>
+ {typeof tokenRender === 'function'
+ ? tokenRender?.({ token, openTokenModal: handleSelectToken })
+ : tokenRender}
+ {chainId && (
+
+ )}
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+ {token ? (
+
+
+ {getTokenSymbol(token)}
+
+
+ ) : (
+
+ Select Token
+
+
+ )}
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+};
+
+const TokenWrapper = styled.div`
+ /* width: 92px; */
+ /* height: 30px; */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ padding: 4px;
+ border-radius: 4px;
+ &:hover {
+ background: rgba(134, 151, 255, 0.3);
+ }
+`;
+
+const SelectTips = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 150px;
+ height: 32px;
+ color: #fff;
+ background: var(--r-blue-default, #7084ff);
+ border-radius: 4px;
+ font-weight: 500;
+ font-size: 20px;
+ line-height: 23px;
+ & svg {
+ margin-left: 4px;
+ filter: brightness(1000);
+ }
+`;
+
+export interface TokenSelectorProps {
+ visible: boolean;
+ list: TokenItem[];
+ isLoading?: boolean;
+ onConfirm(item: TokenItem): void;
+ onCancel(): void;
+ placeholder?: string;
+ chainId?: string;
+ disabledTips?: string;
+ supportChains?: CHAINS_ENUM[] | undefined;
+ itemRender?: (
+ token: TokenItem,
+ supportChains?: CHAINS_ENUM[]
+ ) => React.ReactNode;
+ onSearch: (q: string) => void;
+}
+
+const TokenSelector = ({
+ visible,
+ list,
+ onConfirm,
+ onCancel,
+ onSearch,
+ isLoading = false,
+ placeholder,
+ chainId: chainServerId,
+ disabledTips,
+ supportChains,
+ itemRender,
+}: TokenSelectorProps) => {
+ const { t } = useTranslation();
+ const [query, setQuery] = useState('');
+ const [isInputActive, setIsInputActive] = useState(false);
+
+ useDebounce(
+ () => {
+ onSearch(query);
+ },
+ 150,
+ [query]
+ );
+
+ const handleQueryChange = (value: string) => {
+ setQuery(value);
+ };
+
+ const displayList = useMemo(() => {
+ return list || [];
+ }, [list]);
+
+ const handleInputFocus = () => {
+ setIsInputActive(true);
+ };
+
+ const handleInputBlur = () => {
+ setIsInputActive(false);
+ };
+
+ useEffect(() => {
+ if (!visible) {
+ setQuery('');
+ }
+ }, [visible]);
+
+ const isEmpty = list.length <= 0;
+
+ const isSearchAddr = useMemo(() => {
+ const v = query?.trim() || '';
+ return v.length === 42 && v.toLowerCase().startsWith('0x');
+ }, [query]);
+
+ const NoDataUI = useMemo(
+ () =>
+ isLoading ? (
+
+ {Array(10)
+ .fill(1)
+ .map((_, i) => (
+
+ ))}
+
+ ) : (
+
+
+
+ {!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',
+ })}
+
+ >
+ )}
+
+ ),
+ [isLoading, t, isSearchAddr, chainServerId]
+ );
+
+ return (
+
+ }
+ >
+ {/* Select a token */}
+ {t('component.TokenSelector.header.title')}
+
+ }
+ // Search by Name / Address
+ placeholder={
+ placeholder ?? t('component.TokenSelector.searchInput.placeholder')
+ }
+ allowClear
+ value={query}
+ onChange={(e) => handleQueryChange(e.target.value)}
+ autoFocus
+ onFocus={handleInputFocus}
+ onBlur={handleInputBlur}
+ />
+
+
+ {
+
+ }
+
+ );
+};
+
+const DefaultLoading = () => (
+
+);
+
+export default BridgeToTokenSelect;
diff --git a/src/ui/views/Bridge/Component/BridgeToken.tsx b/src/ui/views/Bridge/Component/BridgeToken.tsx
new file mode 100644
index 00000000000..7b2690a59fa
--- /dev/null
+++ b/src/ui/views/Bridge/Component/BridgeToken.tsx
@@ -0,0 +1,226 @@
+import ChainSelectorInForm from '@/ui/component/ChainSelector/InForm';
+import TokenSelect from '@/ui/component/TokenSelect';
+import { findChainByEnum } from '@/utils/chain';
+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 { useTranslation } from 'react-i18next';
+import { TokenRender } from '../../Swap/Component/TokenRender';
+import { formatTokenAmount, formatUsdValue } from '@/ui/utils';
+import BigNumber from 'bignumber.js';
+import { MaxButton } from '../../SendToken/components/MaxButton';
+import { tokenAmountBn } from '@/ui/utils/token';
+import SkeletonInput from 'antd/lib/skeleton/Input';
+import styled from 'styled-components';
+import BridgeToTokenSelect from './BridgeToTokenSelect';
+import { ReactComponent as RcIconInfoCC } from 'ui/assets/info-cc.svg';
+import { useSetSettingVisible } from '../hooks';
+
+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;
+ & > .ant-input {
+ color: var(--r-neutral-title1, #192945);
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ 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 {
+ color: var(--r-neutral-foot, #6a7587);
+ }
+ &::-webkit-inner-spin-button,
+ &::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+`;
+
+export const BridgeToken = ({
+ type = 'from',
+ token,
+ chain,
+ // excludeTokens,
+ excludeChains,
+ onChangeToken,
+ onChangeChain,
+ value,
+ onInputChange,
+ valueLoading,
+ fromChainId,
+ fromTokenId,
+}: {
+ type?: 'from' | 'to';
+ token?: TokenItem;
+ chain?: CHAINS_ENUM;
+ // excludeTokens?: TokenItem['id'][];
+ excludeChains?: CHAINS_ENUM[];
+ onChangeToken: (token: TokenItem) => void;
+ onChangeChain: (chain: CHAINS_ENUM) => void;
+ value?: string | number;
+ onInputChange?: (v: string) => void;
+ valueLoading?: boolean;
+ fromChainId?: string;
+ fromTokenId?: string;
+}) => {
+ const { t } = useTranslation();
+
+ const isFromToken = type === 'from';
+
+ const name = type === 'from' ? t('page.bridge.From') : t('page.bridge.To');
+ const chainObj = findChainByEnum(chain);
+
+ const openFeePopup = useSetSettingVisible();
+
+ const useValue = useMemo(() => {
+ if (token && value) {
+ return formatUsdValue(
+ new BigNumber(value).multipliedBy(token.price || 0).toString()
+ );
+ }
+ return '$0.00';
+ }, [token?.price, value]);
+
+ const inputChange = React.useCallback(
+ (e: React.ChangeEvent) => {
+ onInputChange?.(e.target.value);
+ },
+ [onInputChange]
+ );
+
+ const handleMax = React.useCallback(() => {
+ if (token) {
+ onInputChange?.(tokenAmountBn(token)?.toString(10));
+ }
+ }, [token?.raw_amount_hex_str, onInputChange]);
+
+ return (
+
+
+ {name}
+
+
+
+
+
+ {valueLoading ? (
+
+ ) : (
+
+ )}
+ {type === 'to' ? (
+ }
+ />
+ ) : (
+ }
+ />
+ )}
+
+
+
+
+ {useValue}
+ {type === 'to' && !!value && (
+ openFeePopup(true)}
+ viewBox="0 0 14 14"
+ className="w-14 h-14 text-r-neutral-foot cursor-pointer"
+ />
+ )}
+
+
+
+ {t('page.bridge.Balance')}
+
+ {token
+ ? formatTokenAmount(tokenAmountBn(token).toString(10)) || '0'
+ : 0}
+
+ {isFromToken && (
+ {t('page.swap.max')}
+ )}
+
+
+
+
+ );
+};
diff --git a/src/ui/views/Bridge/hooks/history.tsx b/src/ui/views/Bridge/hooks/history.tsx
index b0d9ee089b6..1a5c350b04e 100644
--- a/src/ui/views/Bridge/hooks/history.tsx
+++ b/src/ui/views/Bridge/hooks/history.tsx
@@ -134,7 +134,6 @@ export const useBridgeHistory = () => {
useEffect(() => {
if (!noMore && inViewport && !loadingMore && loadMore && isInBridge) {
- console.log('loadMore');
loadMore();
}
}, [inViewport, loadMore, loading, loadingMore, noMore, isInBridge]);
diff --git a/src/ui/views/Bridge/hooks/slippage.tsx b/src/ui/views/Bridge/hooks/slippage.tsx
new file mode 100644
index 00000000000..c50f5f2f5f3
--- /dev/null
+++ b/src/ui/views/Bridge/hooks/slippage.tsx
@@ -0,0 +1,67 @@
+import { useRabbyDispatch, useRabbySelector } from '@/ui/store';
+import { useCallback, useMemo, useState } from 'react';
+
+export const useBridgeSlippage = () => {
+ const previousSlippage = useRabbySelector((s) => s.bridge.slippage || '1');
+ const [slippageState, setSlippageState] = useState(previousSlippage);
+
+ const setSlippageOnStore = useRabbyDispatch().bridge.setSlippage;
+
+ const slippage = useMemo(() => slippageState || '0.03', [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.2,
+ slippageState?.trim() !== '' && Number(slippageState || 0) > 3,
+ ];
+ }, [slippageState]);
+
+ return {
+ slippageChanged,
+ setSlippageChanged,
+ slippageState,
+ isSlippageLow,
+ isSlippageHigh,
+ slippage,
+ setSlippage,
+ };
+};
+
+export const useBridgeSlippageStore = () => {
+ const { autoSlippage, isCustomSlippage } = useRabbySelector((store) => ({
+ autoSlippage: store.bridge.autoSlippage,
+ isCustomSlippage: !!store.bridge.isCustomSlippage,
+ }));
+
+ const dispatch = useRabbyDispatch();
+
+ const setAutoSlippage = useCallback(
+ (bool: boolean) => {
+ dispatch.bridge.setAutoSlippage(bool);
+ },
+ [dispatch]
+ );
+
+ const setIsCustomSlippage = useCallback(
+ (bool: boolean) => {
+ dispatch.bridge.setIsCustomSlippage(bool);
+ },
+ [dispatch]
+ );
+
+ return {
+ autoSlippage,
+ isCustomSlippage,
+ setAutoSlippage,
+ setIsCustomSlippage,
+ };
+};
diff --git a/src/ui/views/Bridge/hooks/token.tsx b/src/ui/views/Bridge/hooks/token.tsx
index 85806f8b771..dcf107c42a3 100644
--- a/src/ui/views/Bridge/hooks/token.tsx
+++ b/src/ui/views/Bridge/hooks/token.tsx
@@ -1,43 +1,91 @@
-import { useRabbyDispatch, useRabbySelector } from '@/ui/store';
-import { isSameAddress, useWallet } from '@/ui/utils';
-import { CHAINS, CHAINS_ENUM } from '@debank/common';
-import { TokenItem } from '@rabby-wallet/rabby-api/dist/types';
-import BigNumber from 'bignumber.js';
+import { CHAINS_ENUM, ETH_USDT_CONTRACT } from '@/constant';
+import { useAsyncInitializeChainList } from '@/ui/hooks/useChain';
+import { useRabbySelector } from '@/ui/store';
+import { formatUsdValue, isSameAddress, useWallet } from '@/ui/utils';
+import { findChain, findChainByEnum, findChainByServerID } from '@/utils/chain';
+import { BridgeQuote, TokenItem } from '@rabby-wallet/rabby-api/dist/types';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { useAsync, useDebounce } from 'react-use';
-
+import { useAsyncFn, useDebounce } from 'react-use';
+import useAsync from 'react-use/lib/useAsync';
import {
useQuoteVisible,
useRefreshId,
useSetQuoteVisible,
useSetRefreshId,
- useSettingVisible,
} from './context';
-import { useAsyncInitializeChainList } from '@/ui/hooks/useChain';
-import { ETH_USDT_CONTRACT } from '@/constant';
-import { findChain } from '@/utils/chain';
-import { BridgeQuote } from '@/background/service/openapi';
+import { getChainDefaultToken, tokenAmountBn } from '@/ui/utils/token';
+import BigNumber from 'bignumber.js';
import stats from '@/stats';
-import useDebounceValue from '@/ui/hooks/useDebounceValue';
-
-const useTokenInfo = ({
- userAddress,
- chain,
- defaultToken,
-}: {
- userAddress?: string;
- chain?: CHAINS_ENUM;
- defaultToken?: TokenItem;
-}) => {
+import { useBridgeSlippage, useBridgeSlippageStore } from './slippage';
+import { isNaN } from 'lodash';
+
+export interface SelectedBridgeQuote extends Omit {
+ shouldApproveToken?: boolean;
+ shouldTwoStepApprove?: boolean;
+ loading?: boolean;
+ tx?: BridgeQuote['tx'];
+ manualClick?: boolean;
+}
+
+export const tokenPriceImpact = (
+ fromToken?: TokenItem,
+ toToken?: TokenItem,
+ fromAmount?: string | number,
+ toAmount?: string | number
+) => {
+ const notReady = [fromToken, toToken, fromAmount, toAmount].some((e) =>
+ isNaN(e)
+ );
+
+ if (notReady) {
+ return;
+ }
+
+ const fromUsdBn = new BigNumber(fromAmount || 0).times(fromToken?.price || 0);
+ const toUsdBn = new BigNumber(toAmount || 0).times(toToken?.price || 0);
+
+ const cut = toUsdBn.minus(fromUsdBn).div(fromUsdBn).times(100);
+
+ return {
+ showLoss: cut.lte(-5),
+ lossUsd: formatUsdValue(toUsdBn.minus(fromUsdBn).abs().toString()),
+ diff: cut.abs().toFixed(2),
+ fromUsd: formatUsdValue(fromUsdBn.toString(10)),
+ toUsd: formatUsdValue(toUsdBn.toString(10)),
+ };
+};
+
+const useToken = (type: 'from' | 'to') => {
const refreshId = useRefreshId();
+
+ const userAddress = useRabbySelector(
+ (s) => s.account.currentAccount?.address
+ );
const wallet = useWallet();
- const [token, setToken] = useState(defaultToken);
+
+ const [chain, setChain] = useState();
+
+ const [token, setToken] = useState();
+
+ const switchChain = useCallback(
+ (changeChain?: CHAINS_ENUM, resetToken = true) => {
+ setChain(changeChain);
+ if (resetToken) {
+ if (type === 'from') {
+ setToken(changeChain ? getChainDefaultToken(changeChain) : undefined);
+ } else {
+ setToken(undefined);
+ }
+ }
+ },
+ [type]
+ );
const { value, loading, error } = useAsync(async () => {
if (userAddress && token?.id && chain) {
const data = await wallet.openapi.getToken(
userAddress,
- CHAINS[chain].serverId,
+ findChainByEnum(chain)!.serverId,
token.id
);
return data;
@@ -54,171 +102,176 @@ const useTokenInfo = ({
[value, error, loading]
);
- if (error) {
- console.error('token info error', chain, token?.symbol, token?.id, error);
- }
- return [token, setToken] as const;
+ return [chain, token, setToken, switchChain] as const;
};
-export interface SelectedBridgeQuote extends Omit {
- shouldApproveToken?: boolean;
- shouldTwoStepApprove?: boolean;
- loading?: boolean;
- tx?: BridgeQuote['tx'];
- manualClick?: boolean;
-}
-
-export const useTokenPair = (userAddress: string) => {
- const dispatch = useRabbyDispatch();
+export const useBridge = () => {
+ const userAddress = useRabbySelector(
+ (s) => s.account.currentAccount?.address
+ );
const refreshId = useRefreshId();
- const {
- initialSelectedChain,
- oChain,
- defaultSelectedFromToken,
- defaultSelectedToToken,
- } = useRabbySelector((state) => {
- return {
- initialSelectedChain: state.bridge.$$initialSelectedChain,
- oChain: state.bridge.selectedChain || CHAINS_ENUM.ETH,
- defaultSelectedFromToken: state.bridge.selectedFromToken,
- defaultSelectedToToken: state.bridge.selectedToToken,
- };
- });
-
- const [chain, setChain] = useState(oChain);
- const handleChain = (c: CHAINS_ENUM) => {
- setChain(c);
- dispatch.bridge.setSelectedChain(c);
- };
-
- const [payToken, setPayToken] = useTokenInfo({
- userAddress,
- chain: defaultSelectedFromToken?.chain
- ? findChain({ serverId: defaultSelectedFromToken?.chain })?.enum
- : undefined,
- defaultToken: defaultSelectedFromToken,
- });
-
- const [receiveToken, setReceiveToken] = useTokenInfo({
- userAddress,
- chain,
- defaultToken: defaultSelectedToToken,
- });
-
- const setSelectedBridgeQuote: React.Dispatch<
- React.SetStateAction
- > = useCallback((p) => {
- if (expiredTimer.current) {
- clearTimeout(expiredTimer.current);
- }
- setExpired(false);
- expiredTimer.current = setTimeout(() => {
- setExpired(true);
- }, 1000 * 30);
- setOriSelectedBridgeQuote(p);
- }, []);
+ const setRefreshId = useSetRefreshId();
- const switchChain = useCallback(
- (c: CHAINS_ENUM, opts?: { payTokenId?: string; changeTo?: boolean }) => {
- handleChain(c);
- setPayToken(undefined);
- setReceiveToken(undefined);
- setPayAmount('');
- setSelectedBridgeQuote(undefined);
- },
- [handleChain, setSelectedBridgeQuote, setPayToken, setReceiveToken]
+ const wallet = useWallet();
+ const [fromChain, fromToken, setFromToken, switchFromChain] = useToken(
+ 'from'
);
+ const [toChain, toToken, setToToken, switchToChain] = useToken('to');
- const supportedChains = useRabbySelector((s) => s.bridge.supportedChains);
-
- useAsyncInitializeChainList({
- // NOTICE: now `useTokenPair` is only used for swap page, so we can use `SWAP_SUPPORT_CHAINS` here
- supportChains: supportedChains,
- onChainInitializedAsync: (firstEnum) => {
- // only init chain if it's not cached before
- if (!initialSelectedChain) {
- switchChain(firstEnum);
- }
- },
- });
-
- useEffect(() => {
- dispatch.bridge.setSelectedFromToken(payToken);
- }, [payToken]);
-
- useEffect(() => {
- dispatch.bridge.setSelectedToToken(receiveToken);
- }, [receiveToken]);
+ const [amount, setAmount] = useState('');
- const [inputAmount, setPayAmount] = useState('');
+ const slippageObj = useBridgeSlippage();
- const debouncePayAmount = useDebounceValue(inputAmount, 300);
+ const [recommendFromToken, setRecommendFromToken] = useState();
const [selectedBridgeQuote, setOriSelectedBridgeQuote] = useState<
SelectedBridgeQuote | undefined
>();
- const expiredTimer = useRef();
const [expired, setExpired] = useState(false);
- const handleAmountChange: React.ChangeEventHandler = useCallback(
- (e) => {
- const v = e.target.value;
- if (!/^\d*(\.\d*)?$/.test(v)) {
- return;
+ const expiredTimer = useRef();
+
+ const inSufficient = useMemo(
+ () =>
+ fromToken
+ ? tokenAmountBn(fromToken).lt(amount)
+ : new BigNumber(0).lt(amount),
+ [fromToken, amount]
+ );
+
+ const getRecommendToChain = async (chain: CHAINS_ENUM) => {
+ if (userAddress) {
+ const latestTx = await wallet.openapi.getBridgeHistoryList({
+ user_addr: userAddress,
+ start: 0,
+ limit: 1,
+ });
+ const latestToToken = latestTx?.history_list?.[0]?.to_token;
+ if (latestToToken) {
+ const chainEnum = findChainByServerID(latestToToken.chain);
+ if (chainEnum) {
+ switchToChain(chainEnum.enum);
+ setToToken(latestToToken);
+ }
+ } else {
+ const data = await wallet.openapi.getRecommendBridgeToChain({
+ from_chain_id: findChainByEnum(chain)!.serverId,
+ });
+ switchToChain(findChainByServerID(data.to_chain_id)?.enum);
+ }
+ }
+ };
+
+ const {
+ value: isSameToken,
+ loading: isSameTokenLoading,
+ } = useAsync(async () => {
+ if (fromChain && fromToken?.id && toChain && toToken?.id) {
+ try {
+ const data = await wallet.openapi.isSameBridgeToken({
+ from_chain_id: findChainByEnum(fromChain)!.serverId,
+ from_token_id: fromToken?.id,
+ to_chain_id: findChainByEnum(toChain)!.serverId,
+ to_token_id: toToken?.id,
+ });
+ return data?.every((e) => e.is_same);
+ } catch (error) {
+ return false;
}
- setPayAmount(v);
+ }
+ return false;
+ }, [fromChain, fromToken?.id, toChain, toToken?.id]);
+
+ const { autoSlippage } = useBridgeSlippageStore();
+
+ useEffect(() => {
+ if (!isSameTokenLoading && autoSlippage) {
+ slippageObj.setSlippage(isSameToken ? '1' : '3');
+ }
+ }, [autoSlippage, isSameToken, isSameTokenLoading]);
+
+ const supportedChains = useRabbySelector((s) => s.bridge.supportedChains);
+ // the most worth chain is the first
+ useAsyncInitializeChainList({
+ supportChains: supportedChains,
+ onChainInitializedAsync: (firstEnum) => {
+ switchFromChain(firstEnum);
+ getRecommendToChain(firstEnum);
},
- []
- );
+ });
- const handleBalance = useCallback(() => {
- if (payToken) {
- setPayAmount(tokenAmountBn(payToken).toString(10));
+ const handleAmountChange = useCallback((v: string) => {
+ if (!/^\d*(\.\d*)?$/.test(v)) {
+ return;
}
- }, [payToken]);
+ setAmount(v);
+ }, []);
- const inSufficient = useMemo(
- () =>
- payToken
- ? tokenAmountBn(payToken).lt(debouncePayAmount)
- : new BigNumber(0).lt(debouncePayAmount),
- [payToken, debouncePayAmount]
- );
+ const switchToken = useCallback(() => {
+ switchFromChain(toChain, false);
+ switchToChain(fromChain, false);
+ setFromToken(toToken);
+ setToToken(fromToken);
+ }, [
+ setFromToken,
+ toToken,
+ setToToken,
+ fromToken,
+ switchFromChain,
+ toChain,
+ switchToChain,
+ fromChain,
+ ]);
const [quoteList, setQuotesList] = useState([]);
+ const setSelectedBridgeQuote: React.Dispatch<
+ React.SetStateAction
+ > = useCallback((p) => {
+ if (expiredTimer.current) {
+ clearTimeout(expiredTimer.current);
+ }
+ setExpired(false);
+ expiredTimer.current = setTimeout(() => {
+ setExpired(true);
+ setRefreshId((e) => e + 1);
+ }, 1000 * 30);
+ setOriSelectedBridgeQuote(p);
+ }, []);
+
useEffect(() => {
setQuotesList([]);
setSelectedBridgeQuote(undefined);
- }, [payToken?.id, receiveToken?.id, chain, debouncePayAmount, inSufficient]);
+ }, [fromToken?.id, toToken?.id, fromChain, toChain, amount, inSufficient]);
const visible = useQuoteVisible();
- useEffect(() => {
- if (!visible) {
- setQuotesList([]);
- }
- }, [visible]);
-
- const setRefreshId = useSetRefreshId();
+ // useEffect(() => {
+ // if (!visible) {
+ // setQuotesList([]);
+ // }
+ // }, [visible]);
const aggregatorsList = useRabbySelector(
(s) => s.bridge.aggregatorsList || []
);
const fetchIdRef = useRef(0);
- const wallet = useWallet();
- const { loading: quoteLoading, error: quotesError } = useAsync(async () => {
+ const [
+ { loading: quoteLoading, error: quotesError },
+ getQuoteList,
+ ] = useAsyncFn(async () => {
if (
!inSufficient &&
userAddress &&
- payToken?.id &&
- receiveToken?.id &&
- receiveToken &&
- chain &&
- Number(debouncePayAmount) > 0 &&
+ fromToken?.id &&
+ toToken?.id &&
+ toToken &&
+ fromChain &&
+ toChain &&
+ Number(amount) > 0 &&
aggregatorsList.length > 0
) {
fetchIdRef.current += 1;
@@ -236,32 +289,74 @@ export const useTokenPair = (userAddress: string) => {
setSelectedBridgeQuote(undefined);
- const originData = await wallet.openapi
- .getBridgeQuoteList({
- aggregator_ids: aggregatorsList.map((e) => e.id).join(','),
- from_token_id: payToken.id,
- user_addr: userAddress,
- from_chain_id: payToken.chain,
- from_token_raw_amount: new BigNumber(debouncePayAmount)
- .times(10 ** payToken.decimals)
- .toFixed(0, 1)
- .toString(),
- to_chain_id: receiveToken.chain,
- to_token_id: receiveToken.id,
- })
- .catch((e) => {
- if (currentFetchId === fetchIdRef.current) {
- stats.report('bridgeQuoteResult', {
- aggregatorIds: aggregatorsList.map((e) => e.id).join(','),
- fromChainId: payToken.chain,
- fromTokenId: payToken.id,
- toTokenId: receiveToken.id,
- toChainId: receiveToken.chain,
- status: 'fail',
- });
- }
- })
- .finally(() => {});
+ const originData: Omit[] = [];
+
+ const getQUoteV2 = async (alternativeToken?: TokenItem) =>
+ await Promise.allSettled(
+ aggregatorsList.map(async (bridgeAggregator) => {
+ const data = await wallet.openapi
+ .getBridgeQuoteV2({
+ aggregator_id: bridgeAggregator.id,
+ user_addr: userAddress,
+ from_chain_id: alternativeToken?.chain || fromToken.chain,
+ from_token_id: alternativeToken?.id || fromToken.id,
+ from_token_raw_amount: alternativeToken
+ ? new BigNumber(amount)
+ .times(fromToken.price)
+ .div(alternativeToken.price)
+ .times(10 ** alternativeToken.decimals)
+ .toFixed(0, 1)
+ .toString()
+ : new BigNumber(amount)
+ .times(10 ** fromToken.decimals)
+ .toFixed(0, 1)
+ .toString(),
+ to_chain_id: toToken.chain,
+ to_token_id: toToken.id,
+ slippage: new BigNumber(slippageObj.slippageState)
+ .div(100)
+ .toString(10),
+ })
+ .catch((e) => {
+ if (
+ currentFetchId === fetchIdRef.current &&
+ !alternativeToken
+ ) {
+ stats.report('bridgeQuoteResult', {
+ aggregatorIds: bridgeAggregator.id,
+ fromChainId: fromToken.chain,
+ fromTokenId: fromToken.id,
+ toTokenId: toToken.id,
+ toChainId: toToken.chain,
+ status: 'fail',
+ });
+ }
+ });
+
+ if (alternativeToken) {
+ if (data && currentFetchId === fetchIdRef.current) {
+ setRecommendFromToken(alternativeToken);
+ return;
+ }
+ }
+ if (data && currentFetchId === fetchIdRef.current) {
+ originData.push(...data);
+ }
+ if (currentFetchId === fetchIdRef.current) {
+ stats.report('bridgeQuoteResult', {
+ aggregatorIds: bridgeAggregator.id,
+ fromChainId: fromToken.chain,
+ fromTokenId: fromToken.id,
+ toTokenId: toToken.id,
+ toChainId: toToken.chain,
+ status: data?.length ? 'success' : 'none',
+ });
+ }
+ return data;
+ })
+ );
+
+ await getQUoteV2();
const data = originData?.filter(
(quote) =>
@@ -272,12 +367,39 @@ export const useTokenPair = (userAddress: string) => {
);
if (currentFetchId === fetchIdRef.current) {
+ setPending(false);
+
+ if (data.length < 1) {
+ try {
+ const recommendFromToken = await wallet.openapi.getRecommendFromToken(
+ {
+ user_addr: userAddress,
+ from_chain_id: fromToken.chain,
+ from_token_id: fromToken.id,
+ from_token_amount: new BigNumber(amount)
+ .times(10 ** fromToken.decimals)
+ .toFixed(0, 1)
+ .toString(),
+ to_chain_id: toToken.chain,
+ to_token_id: toToken.id,
+ }
+ );
+ if (recommendFromToken?.token_list?.[0]) {
+ await getQUoteV2(recommendFromToken?.token_list?.[0]);
+ } else {
+ setRecommendFromToken(undefined);
+ }
+ } catch (error) {
+ setRecommendFromToken(undefined);
+ }
+ }
+
stats.report('bridgeQuoteResult', {
aggregatorIds: aggregatorsList.map((e) => e.id).join(','),
- fromChainId: payToken.chain,
- fromTokenId: payToken.id,
- toTokenId: receiveToken.id,
- toChainId: receiveToken.chain,
+ fromChainId: fromToken.chain,
+ fromTokenId: fromToken.id,
+ toTokenId: toToken.id,
+ toChainId: toToken.chain,
status: data ? (data?.length === 0 ? 'none' : 'success') : 'fail',
});
}
@@ -294,23 +416,23 @@ export const useTokenPair = (userAddress: string) => {
}
let tokenApproved = false;
let allowance = '0';
- const fromChain = findChain({ serverId: payToken?.chain });
- if (payToken?.id === fromChain?.nativeTokenAddress) {
+ const fromChain = findChain({ serverId: fromToken?.chain });
+ if (fromToken?.id === fromChain?.nativeTokenAddress) {
tokenApproved = true;
} else {
allowance = await wallet.getERC20Allowance(
- payToken.chain,
- payToken.id,
+ fromToken.chain,
+ fromToken.id,
quote.approve_contract_id
);
tokenApproved = new BigNumber(allowance).gte(
- new BigNumber(debouncePayAmount).times(10 ** payToken.decimals)
+ new BigNumber(amount).times(10 ** fromToken.decimals)
);
}
let shouldTwoStepApprove = false;
if (
fromChain?.enum === CHAINS_ENUM.ETH &&
- isSameAddress(payToken.id, ETH_USDT_CONTRACT) &&
+ isSameAddress(fromToken.id, ETH_USDT_CONTRACT) &&
Number(allowance) !== 0 &&
!tokenApproved
) {
@@ -356,12 +478,53 @@ export const useTokenPair = (userAddress: string) => {
aggregatorsList,
refreshId,
userAddress,
- payToken?.id,
- receiveToken?.id,
- chain,
- debouncePayAmount,
+ fromToken?.id,
+ toToken?.id,
+ fromChain,
+ toChain,
+ amount,
+ slippageObj.slippage,
]);
+ const [pending, setPending] = useState(false);
+
+ useEffect(() => {
+ if (
+ !inSufficient &&
+ userAddress &&
+ fromToken?.id &&
+ toToken?.id &&
+ toToken &&
+ fromChain &&
+ toChain &&
+ Number(amount) > 0 &&
+ aggregatorsList.length > 0
+ ) {
+ setPending(true);
+ } else {
+ setPending(false);
+ }
+ }, [
+ inSufficient,
+ userAddress,
+ fromToken?.id,
+ toToken?.id,
+ toToken,
+ fromChain,
+ toChain,
+ Number(amount),
+ aggregatorsList.length,
+ refreshId,
+ ]);
+
+ const [, cancelDebounce] = useDebounce(
+ () => {
+ getQuoteList();
+ },
+ 300,
+ [getQuoteList]
+ );
+
const [bestQuoteId, setBestQuoteId] = useState<
| {
bridgeId: string;
@@ -373,20 +536,20 @@ export const useTokenPair = (userAddress: string) => {
const openQuote = useSetQuoteVisible();
const openQuotesList = useCallback(() => {
- setQuotesList([]);
- setRefreshId((e) => e + 1);
+ // setQuotesList([]);
+ // setRefreshId((e) => e + 1);
openQuote(true);
}, []);
useEffect(() => {
- if (!quoteLoading && receiveToken && quoteList.every((e) => !e.loading)) {
+ if (!quoteLoading && toToken && quoteList.every((e) => !e.loading)) {
const sortedList = quoteList?.sort((b, a) => {
return new BigNumber(a.to_token_amount)
- .times(receiveToken.price || 1)
+ .times(toToken.price || 1)
.minus(a.gas_fee.usd_value)
.minus(
new BigNumber(b.to_token_amount)
- .times(receiveToken.price || 1)
+ .times(toToken.price || 1)
.minus(b.gas_fee.usd_value)
)
.toNumber();
@@ -406,7 +569,7 @@ export const useTokenPair = (userAddress: string) => {
);
}
}
- }, [quoteList, quoteLoading, receiveToken]);
+ }, [quoteList, quoteLoading, toToken]);
if (quotesError) {
console.error('quotesError', quotesError);
@@ -415,39 +578,49 @@ export const useTokenPair = (userAddress: string) => {
useEffect(() => {
setExpired(false);
setSelectedBridgeQuote(undefined);
- }, [payToken?.id, receiveToken?.id, chain, debouncePayAmount, inSufficient]);
+ setRecommendFromToken(undefined);
+ }, [fromToken?.id, toToken?.id, fromChain, toChain, amount, inSufficient]);
+
+ const showLoss = useMemo(() => {
+ if (selectedBridgeQuote) {
+ return !!tokenPriceImpact(
+ fromToken,
+ toToken,
+ amount,
+ selectedBridgeQuote?.to_token_amount
+ )?.showLoss;
+ }
+ return false;
+ }, [fromToken, toToken, amount, selectedBridgeQuote]);
return {
- chain,
- switchChain,
+ fromChain,
+ fromToken,
+ setFromToken,
+ switchFromChain,
+ toChain,
+ toToken,
+ setToToken,
+ switchToChain,
+ switchToken,
+
+ recommendFromToken,
- payToken,
- setPayToken,
- receiveToken,
- setReceiveToken,
-
- handleAmountChange,
- handleBalance,
- debouncePayAmount,
- setPayAmount,
- inputAmount,
+ expired,
inSufficient,
+ amount,
+ handleAmountChange,
+ showLoss,
- quoteLoading,
- quoteList,
- selectedBridgeQuote,
- setSelectedBridgeQuote,
openQuotesList,
+ quoteLoading: pending || quoteLoading,
+ quoteList,
bestQuoteId,
+ selectedBridgeQuote,
- expired,
+ setSelectedBridgeQuote,
+ ...slippageObj,
};
};
-
-function tokenAmountBn(token: TokenItem) {
- return new BigNumber(token?.raw_amount_hex_str || 0, 16).div(
- 10 ** (token?.decimals || 1)
- );
-}
diff --git a/src/ui/views/Swap/Component/TokenRender.tsx b/src/ui/views/Swap/Component/TokenRender.tsx
index 9b32e24381d..d44d2df4056 100644
--- a/src/ui/views/Swap/Component/TokenRender.tsx
+++ b/src/ui/views/Swap/Component/TokenRender.tsx
@@ -5,6 +5,8 @@ import { ReactComponent as RcIconRcArrowDownTriangle } from '@/ui/assets/swap/ar
import { TokenItem } from '@rabby-wallet/rabby-api/dist/types';
import { getTokenSymbol } from '@/ui/utils/token';
import { useTranslation } from 'react-i18next';
+import { ReactComponent as RcImgArrowDown } from '@/ui/assets/swap/arrow-down.svg';
+
const TokenRenderWrapper = styled.div`
width: 150px;
height: 46px;
@@ -18,6 +20,17 @@ const TokenRenderWrapper = styled.div`
color: #13141a;
border: 1px solid transparent;
cursor: pointer;
+ &.bridge {
+ height: 40px;
+ width: auto;
+ border-radius: 8px;
+ background: var(--r-neutral-card2, #f2f4f7);
+ padding: 8px 12px;
+
+ .token {
+ gap: 6px;
+ }
+ }
&:hover {
background: rgba(134, 151, 255, 0.2);
}
@@ -55,13 +68,17 @@ const TokenRenderWrapper = styled.div`
export const TokenRender = ({
openTokenModal,
token,
+ type = 'swap',
}: {
token?: TokenItem | undefined;
openTokenModal: () => void;
+ type?: 'swap' | 'bridge';
}) => {
const { t } = useTranslation();
+ const isBridge = type === 'bridge';
+
return (
-
+
{token ? (
{getTokenSymbol(token)}
-
+ {isBridge ? (
+
+ ) : (
+
+ )}
) : (
{t('page.swap.select-token')}
-
+ {isBridge ? (
+
+ ) : (
+
+ )}
)}
diff --git a/yarn.lock b/yarn.lock
index 87e6c1ad51b..f0f55338848 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4781,10 +4781,10 @@
resolved "https://registry.yarnpkg.com/@rabby-wallet/rabby-action/-/rabby-action-0.1.4.tgz#c82e7c8b538b7dfd94506c4f89f78aca7ca880ef"
integrity sha512-6ttnlpGHcO2v/qyYo8epBbSutfS9OZXfXr9mfapuveoBvzqUwvE6ej3bsmdtc+qFhFeH7HeiwASpY6xaTM4E1w==
-"@rabby-wallet/rabby-api@0.8.3":
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/@rabby-wallet/rabby-api/-/rabby-api-0.8.3.tgz#1454f0d6df905c0e98a6bc0a0c55ae7aee3df74d"
- integrity sha512-lcZ3zo+VgnHmsk95d+WoplpCnBeW/5WoAxG1oj6r6iWKiQbndpy9UvKX6lvJb7SWrR7xDc3eXXuCepgVFPQoqg==
+"@rabby-wallet/rabby-api@0.8.4-beta.0":
+ version "0.8.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@rabby-wallet/rabby-api/-/rabby-api-0.8.4-beta.0.tgz#1ae4418a79a6e9d3b50305f50d8951ae0a82e0cb"
+ integrity sha512-XsLXk2rtacIG4fh5NUa3CYT3oYvu9V9+56P+YdgsxBZGMpKQL+NJJ2Bq8oN7gLoz1g13FtRAWDHjHuR9DOiuSw==
dependencies:
"@rabby-wallet/rabby-sign" "0.4.0"
axios "^0.27.2"
@@ -19220,8 +19220,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
- name string-width-cjs
+"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -19248,6 +19247,15 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
+string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -19300,8 +19308,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
- name strip-ansi-cjs
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -19322,6 +19329,13 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
+strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-ansi@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@@ -21309,8 +21323,7 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
- name wrap-ansi-cjs
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -21327,6 +21340,15 @@ wrap-ansi@^2.0.0:
string-width "^1.0.1"
strip-ansi "^3.0.1"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"