- {success ? (
-
- ) : (
-
- )}
+
+
-
+
{title}
-
- {summary}
-
+ {summary && (
+
+ {summary}
+
+ )}
diff --git a/src/components/Popups/TransactionPopup.tsx b/src/components/Popups/TransactionPopup.tsx
index c3a4cf2c56..ee887ecf9b 100644
--- a/src/components/Popups/TransactionPopup.tsx
+++ b/src/components/Popups/TransactionPopup.tsx
@@ -8,6 +8,7 @@ import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import IconSuccess from 'assets/svg/notification_icon_success.svg'
import IconFailure from 'assets/svg/notification_icon_failure.svg'
+import { NotificationType } from 'state/application/hooks'
const RowNoFlex = styled(AutoRow)`
flex-wrap: nowrap;
@@ -124,17 +125,17 @@ export const SUMMARY: {
export default function TransactionPopup({
hash,
- success,
+ notiType,
type,
summary,
}: {
hash: string
- success?: boolean
+ notiType: NotificationType
type?: string
summary?: string
}) {
const { chainId } = useActiveWeb3React()
-
+ const success = notiType === NotificationType.SUCCESS
const theme = useContext(ThemeContext)
return (
diff --git a/src/components/Popups/index.tsx b/src/components/Popups/index.tsx
index f5649a2d5f..38454faf0c 100644
--- a/src/components/Popups/index.tsx
+++ b/src/components/Popups/index.tsx
@@ -1,73 +1,39 @@
import React from 'react'
import styled from 'styled-components'
import { useActivePopups } from 'state/application/hooks'
-import { AutoColumn } from '../Column'
import PopupItem from './PopupItem'
-import { useURLWarningVisible, useRebrandingAnnouncement } from 'state/user/hooks'
+import { Z_INDEXS } from 'constants/styles'
-const MobilePopupWrapper = styled.div<{ height: string | number }>`
- position: absolute;
- z-index: 9999;
- max-width: 100%;
- height: ${({ height }) => height};
- margin: ${({ height }) => (height ? '20px auto;' : 0)};
- display: none;
-
- ${({ theme }) => theme.mediaWidth.upToSmall`
- display: block;
- `};
-`
-
-const MobilePopupInner = styled.div`
- height: 99%;
- overflow-x: auto;
- overflow-y: hidden;
- display: flex;
- flex-direction: column;
- -webkit-overflow-scrolling: touch;
- ::-webkit-scrollbar {
- display: none;
- }
-`
-
-const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: string }>`
+const FixedPopupColumn = styled.div`
position: fixed;
- top: ${({ extraPadding }) => extraPadding};
+ top: 108px;
right: 1rem;
- max-width: 355px !important;
width: 100%;
- z-index: 9999;
-
- ${({ theme }) => theme.mediaWidth.upToSmall`
- display: none;
+ z-index: ${Z_INDEXS.POPUP_NOTIFICATION};
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ ${({ theme }) => theme.mediaWidth.upToMedium`
+ left: 0;
+ right: 0;
+ top: 15px;
+ align-items: center;
`};
`
export default function Popups() {
- // get all popups
const activePopups = useActivePopups()
-
- const urlWarningActive = useURLWarningVisible()
- const rebrandingAnnouncement = useRebrandingAnnouncement()
-
return (
- <>
-
- {activePopups.map(item => (
-
- ))}
-
-
0 ? 'auto' : 0}>
-
- {activePopups // reverse so new items up front
- .map(item => (
-
- ))}
-
-
- >
+
+ {activePopups.map(item => (
+
+ ))}
+
)
}
diff --git a/src/components/SearchModal/CurrencySearch.tsx b/src/components/SearchModal/CurrencySearch.tsx
index 65fc04cb6c..3a69266122 100644
--- a/src/components/SearchModal/CurrencySearch.tsx
+++ b/src/components/SearchModal/CurrencySearch.tsx
@@ -30,7 +30,7 @@ import Row, { RowBetween, RowFixed } from '../Row'
import Column from '../Column'
import CommonBases from './CommonBases'
import CurrencyList from './CurrencyList'
-import { filterTokens } from './filtering'
+import { filterTokens } from 'utils/filtering'
import SortButton from './SortButton'
import { useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput, Separator } from './styleds'
diff --git a/src/components/SearchModal/ImportToken.tsx b/src/components/SearchModal/ImportToken.tsx
index bcb2351654..a2d5f81bec 100644
--- a/src/components/SearchModal/ImportToken.tsx
+++ b/src/components/SearchModal/ImportToken.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useCallback, useEffect } from 'react'
import { Token, Currency } from '@kyberswap/ks-sdk-core'
import styled from 'styled-components'
import { t, Trans } from '@lingui/macro'
@@ -7,8 +7,8 @@ import Card from 'components/Card'
import { AutoColumn } from 'components/Column'
import { RowBetween, RowFixed } from 'components/Row'
import CurrencyLogo from 'components/CurrencyLogo'
-import { ArrowLeft, AlertCircle } from 'react-feather'
-import { transparentize } from 'polished'
+import { ArrowLeft, AlertCircle, CornerDownLeft } from 'react-feather'
+import { rgba, transparentize } from 'polished'
import useTheme from 'hooks/useTheme'
import { ButtonPrimary } from 'components/Button'
import { SectionBreak } from 'components/swap/styleds'
@@ -40,7 +40,16 @@ const AddressText = styled(TYPE.blue)`
`}
`
+const IconEnterWrapper = styled.div`
+ position: absolute;
+ background-color: ${({ theme }) => rgba(theme.background, 0.45)};
+ border-radius: 20px;
+ padding: 6px 15px 4px 15px;
+ right: 13px;
+`
+
interface ImportProps {
+ enterToImport?: boolean
tokens: Token[]
onBack?: () => void
list?: TokenList
@@ -48,12 +57,35 @@ interface ImportProps {
handleCurrencySelect?: (currency: Currency) => void
}
-export function ImportToken({ tokens, onBack, onDismiss, handleCurrencySelect, list }: ImportProps) {
+export function ImportToken({
+ enterToImport = false,
+ tokens,
+ onBack,
+ onDismiss,
+ handleCurrencySelect,
+ list,
+}: ImportProps) {
const theme = useTheme()
const { chainId } = useActiveWeb3React()
const addToken = useAddUserToken()
+ const onClickImport = useCallback(() => {
+ tokens.forEach(addToken)
+ handleCurrencySelect?.(tokens[0])
+ }, [tokens, addToken, handleCurrencySelect])
+ useEffect(() => {
+ function onKeydown(e: KeyboardEvent) {
+ if (e.key === 'Enter' && enterToImport) {
+ e.preventDefault()
+ onClickImport()
+ }
+ }
+ window.addEventListener('keydown', onKeydown)
+ return () => {
+ window.removeEventListener('keydown', onKeydown)
+ }
+ }, [onClickImport, enterToImport])
return (
@@ -69,7 +101,17 @@ export function ImportToken({ tokens, onBack, onDismiss, handleCurrencySelect, l
- {t`This token doesn't appear on the active token list(s). Make sure this is the token that you want to trade.`}
+ {tokens.length > 1 ? (
+
+ These tokens don't appear on the active token list(s). Make sure these are the tokens that you want to
+ trade.
+
+ ) : (
+
+ This token doesn't appear on the active token list(s). Make sure this is the token that you want to
+ trade.
+
+ )}
{tokens.map(token => {
@@ -123,13 +165,16 @@ export function ImportToken({ tokens, onBack, onDismiss, handleCurrencySelect, l
borderRadius="20px"
padding="10px 1rem"
margin="16px 0 0"
- onClick={() => {
- tokens.map(token => addToken(token))
- handleCurrencySelect && handleCurrencySelect(tokens[0])
- }}
+ onClick={onClickImport}
className=".token-dismiss-button"
+ style={{ position: 'relative' }}
>
Import
+ {enterToImport && (
+
+
+
+ )}
diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx
index c3e5bcebfa..b445fe9403 100644
--- a/src/components/Settings/index.tsx
+++ b/src/components/Settings/index.tsx
@@ -77,7 +77,7 @@ const MenuFlyoutBrowserStyle = css`
const StyledLabel = styled.div`
font-size: ${isMobile ? '14px' : '12px'};
color: ${({ theme }) => theme.text};
- font-weigh: 400;
+ font-weight: 400;
line-height: 20px;
`
export default function SettingsTab() {
diff --git a/src/components/TokenWarningModal/index.tsx b/src/components/TokenWarningModal/index.tsx
index 784539aab1..6f67f02946 100644
--- a/src/components/TokenWarningModal/index.tsx
+++ b/src/components/TokenWarningModal/index.tsx
@@ -17,7 +17,7 @@ export default function TokenWarningModal({
}) {
return (
-
+
)
}
diff --git a/src/components/TradingViewChart/index.tsx b/src/components/TradingViewChart/index.tsx
index eccdb6afb6..ce68c9afd6 100644
--- a/src/components/TradingViewChart/index.tsx
+++ b/src/components/TradingViewChart/index.tsx
@@ -10,7 +10,7 @@ import * as ReactDOMServer from 'react-dom/server'
import { isMobile } from 'react-device-detect'
import { useDatafeed } from './datafeed'
import { Currency } from '@kyberswap/ks-sdk-core'
-import { Z_INDEXS } from 'styles'
+import { Z_INDEXS } from 'constants/styles'
const ProLiveChartWrapper = styled.div<{ fullscreen: boolean }>`
margin-top: 10px;
@@ -172,7 +172,6 @@ function ProLiveChart({
auto_save_delay: 2,
saved_data: localStorageState,
}
-
const tvWidget = new window.TradingView.widget(widgetOptions)
tvWidget.onChartReady(() => {
diff --git a/src/components/swapv2/MobileTokenInfo.tsx b/src/components/swapv2/MobileTokenInfo.tsx
index 1787e8aaf5..72dcfd3332 100644
--- a/src/components/swapv2/MobileTokenInfo.tsx
+++ b/src/components/swapv2/MobileTokenInfo.tsx
@@ -2,7 +2,7 @@ import React, { useContext } from 'react'
import { MobileModalWrapper, StyledActionButtonSwapForm } from 'components/swapv2/styleds'
import { Flex, Text } from 'rebass'
import { ButtonText } from 'theme/components'
-import { X } from 'react-feather'
+
import { ThemeContext } from 'styled-components'
import { isMobile, MobileView } from 'react-device-detect'
import { useModalOpen, useToggleModal } from 'state/application/hooks'
@@ -12,7 +12,7 @@ import { Trans, t } from '@lingui/macro'
import { Field } from 'state/swap/actions'
import { Currency } from '@kyberswap/ks-sdk-core'
import TokenInfo from 'components/swapv2/TokenInfo'
-import { Info } from 'react-feather'
+import { X, Info } from 'react-feather'
import { MouseoverTooltip } from 'components/Tooltip'
function MobileTradeRoutes({
diff --git a/src/components/swapv2/PairSuggestion/ListPair.tsx b/src/components/swapv2/PairSuggestion/ListPair.tsx
new file mode 100644
index 0000000000..94cc4a41c0
--- /dev/null
+++ b/src/components/swapv2/PairSuggestion/ListPair.tsx
@@ -0,0 +1,158 @@
+import { Trans, t } from '@lingui/macro'
+import { Z_INDEXS } from 'constants/styles'
+import { useActiveWeb3React } from 'hooks'
+import PairSuggestionItem from './PairSuggestionItem'
+import useTheme from 'hooks/useTheme'
+import React from 'react'
+import { Star, AlertTriangle } from 'react-feather'
+import { Flex, Text } from 'rebass'
+import styled, { css } from 'styled-components'
+import { Container, MAX_FAVORITE_PAIRS } from './index'
+import { SuggestionPairData } from './request'
+import { isFavoritePair } from './utils'
+
+const Break = styled.div`
+ border-top: 1px solid ${({ theme }) => theme.border};
+`
+
+const Title = styled.div`
+ font-size: 13px;
+ color: ${({ theme }) => theme.subText};
+ margin: 1em 0 1em 0;
+`
+
+const MenuFlyout = styled.div<{ showList: boolean; hasShadow?: boolean }>`
+ overflow: auto;
+ background-color: ${({ theme, showList }) => (showList ? theme.tabActive : theme.background)};
+ border-radius: 20px;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+ top: 55px;
+ left: 0;
+ right: 0;
+ outline: none;
+ z-index: ${Z_INDEXS.SUGGESTION_PAIR};
+ ${({ hasShadow }) =>
+ hasShadow
+ ? css`
+ box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.16);
+ position: absolute;
+ `
+ : css`
+ box-shadow: unset;
+ position: unset;
+ `};
+`
+
+const TextWithIcon = ({ text, icon, color }: { text: string; icon?: JSX.Element; color: string }) => (
+
+
+ {icon}
+ {text}
+
+
+)
+
+export default function ListPair({
+ isShowListPair,
+ isFullFavoritePair,
+ hasShadow,
+ suggestedAmount,
+ favoritePairs,
+ suggestedPairs,
+ isSearch,
+ selectedIndex,
+ onSelectPair,
+ onClickStar,
+}: {
+ isFullFavoritePair: boolean
+ suggestedAmount: string
+ hasShadow?: boolean
+ selectedIndex: number
+ isShowListPair: boolean
+ favoritePairs: SuggestionPairData[]
+ suggestedPairs: SuggestionPairData[]
+ isSearch: boolean
+ onSelectPair: (item: SuggestionPairData) => void
+ onClickStar: (item: SuggestionPairData) => void
+}) {
+ const theme = useTheme()
+ const { account } = useActiveWeb3React()
+ const isShowNotfound = isSearch && !suggestedPairs.length && !favoritePairs.length
+ const isShowNotfoundFavoritePair = !favoritePairs.length && !isSearch
+
+ return isShowListPair ? (
+
+ {isShowNotfound && (
+ }
+ />
+ )}
+ {account && (
+ <>
+ {!isSearch && (
+
+
+
+ Favourites
+
+ {favoritePairs.length}/{MAX_FAVORITE_PAIRS}
+
+
+
+
+ )}
+ {isShowNotfoundFavoritePair && (
+ }
+ text={t`Your favourite pairs will appear here`}
+ />
+ )}
+ {favoritePairs.map((item, i) => (
+ onSelectPair(item)}
+ onClickStar={() => onClickStar(item)}
+ amount={suggestedAmount}
+ isActive={selectedIndex === i}
+ data={item}
+ isFavorite
+ key={item.tokenIn + item.tokenOut}
+ isFullFavoritePair={isFullFavoritePair}
+ />
+ ))}
+ {!isSearch && }
+ >
+ )}
+ {suggestedPairs.length > 0 && (
+ <>
+ {!isSearch && (
+
+
+ Top traded pairs
+
+
+ )}
+ {suggestedPairs.map((item, i) => (
+ onSelectPair(item)}
+ onClickStar={() => onClickStar(item)}
+ amount={suggestedAmount}
+ isActive={selectedIndex === favoritePairs.length + i}
+ data={item}
+ key={item.tokenIn + item.tokenOut}
+ isFavorite={isFavoritePair(favoritePairs, item)}
+ isFullFavoritePair={isFullFavoritePair}
+ />
+ ))}
+ >
+ )}
+
+
+
+ ) : null
+}
diff --git a/src/components/swapv2/PairSuggestion/PairSuggestionItem.tsx b/src/components/swapv2/PairSuggestion/PairSuggestionItem.tsx
new file mode 100644
index 0000000000..0ac4abf35c
--- /dev/null
+++ b/src/components/swapv2/PairSuggestion/PairSuggestionItem.tsx
@@ -0,0 +1,103 @@
+import React from 'react'
+import { useAllTokens } from 'hooks/Tokens'
+import useTheme from 'hooks/useTheme'
+import { Flex, Text } from 'rebass'
+import styled from 'styled-components'
+import { SuggestionPairData } from './request'
+import { Star } from 'react-feather'
+import { isMobile } from 'react-device-detect'
+import Logo from 'components/Logo'
+import { useActiveWeb3React } from 'hooks'
+import { isActivePair } from './utils'
+import { rgba } from 'polished'
+import { MouseoverTooltip } from 'components/Tooltip'
+import { t } from '@lingui/macro'
+
+const ItemWrapper = styled.div<{ isActive: boolean }>`
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: ${({ theme, isActive }) => (isActive ? rgba(theme.buttonBlack, 0.5) : 'transparent')};
+ padding: 1em;
+ &:hover {
+ background-color: ${({ theme }) => rgba(theme.buttonBlack, 0.5)};
+ }
+`
+
+const StyledLogo = styled(Logo)`
+ width: 20px;
+ height: 20px;
+ border-radius: 100%;
+`
+
+type PropsType = {
+ onClickStar: () => void
+ onSelectPair: () => void
+ data: SuggestionPairData
+ isActive: boolean
+ amount: string
+ isFavorite?: boolean
+ isFullFavoritePair?: boolean
+}
+export default function SuggestItem({
+ data,
+ isFavorite,
+ isFullFavoritePair,
+ isActive,
+ amount,
+ onClickStar,
+ onSelectPair,
+}: PropsType) {
+ const theme = useTheme()
+ const activeTokens = useAllTokens(true)
+ const { account } = useActiveWeb3React()
+ const { tokenInSymbol, tokenOutSymbol, tokenInImgUrl, tokenOutImgUrl, tokenInName, tokenOutName } = data
+
+ const handleClickStar = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ onClickStar()
+ }
+
+ const isTokenNotImport = !isActivePair(activeTokens, data)
+ const star = (
+
+ )
+ return (
+
+
+
+
+
+
+
+
+ {amount} {tokenInSymbol} - {tokenOutSymbol}
+
+
+ {tokenInName} - {tokenOutName}
+
+
+
+
+ {!isTokenNotImport &&
+ account &&
+ (isFullFavoritePair && !isMobile ? (
+ {star}
+ ) : (
+ star
+ ))}
+
+
+ )
+}
diff --git a/src/components/swapv2/PairSuggestion/SearchInput.tsx b/src/components/swapv2/PairSuggestion/SearchInput.tsx
new file mode 100644
index 0000000000..697096c89f
--- /dev/null
+++ b/src/components/swapv2/PairSuggestion/SearchInput.tsx
@@ -0,0 +1,139 @@
+import { t } from '@lingui/macro'
+import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel'
+import React, { RefObject, forwardRef } from 'react'
+import { BrowserView, isMobile, isMacOs } from 'react-device-detect'
+import { Command, Search } from 'react-feather'
+import { Flex } from 'rebass'
+import styled, { css } from 'styled-components'
+const SearchWrapper = styled.div<{ showList: boolean }>`
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ position: relative;
+ width: 100%;
+ border-radius: 20px;
+ background-color: ${({ theme, showList }) => (showList ? theme.tabActive : theme.background)};
+ height: 45px;
+`
+const SearchInput = styled.input<{ hasBorder?: boolean }>`
+ ::placeholder {
+ color: ${({ theme }) => theme.border};
+ }
+ transition: border 100ms;
+ color: ${({ theme }) => theme.text};
+ background: none;
+ border: none;
+ outline: none;
+ padding: 16px;
+ padding-left: 35px;
+ width: 100%;
+ font-size: 16px;
+ ${({ theme, hasBorder }) =>
+ hasBorder
+ ? css`
+ border-radius: 20px;
+ border: 1px solid ${theme.primary};
+ `
+ : css`
+ border: none;
+ `};
+`
+
+const DisabledFrame = styled.div`
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+`
+
+const SearchIcon = styled(Search)<{ showList: boolean }>`
+ position: absolute;
+ left: 10px;
+ color: ${({ theme, showList }) => (showList ? theme.subText : theme.border)};
+ font-size: 14px;
+`
+const InputIcon = styled.div`
+ background: ${({ theme }) => theme.buttonBlack};
+ padding: 3px 8px;
+ margin-right: 10px;
+ border-radius: 22px;
+ font-size: 12px;
+ color: ${({ theme }) => theme.subText};
+ cursor: pointer;
+`
+
+type Props = {
+ value: string
+ isShowListPair: boolean
+ disabled?: boolean
+ ref: RefObject
+ hasBorder?: boolean
+ showListView: () => void
+ hideListView: () => void
+ onChangeInput: (value: string) => void
+ onKeyPressInput: (e: React.KeyboardEvent) => void
+}
+
+export default forwardRef(function SearchComponent(
+ { isShowListPair, hasBorder, disabled, value, onChangeInput, onKeyPressInput, showListView, hideListView },
+ ref,
+) {
+ const onChange = (event: React.FormEvent) => {
+ const { value } = event.currentTarget
+ onChangeInput(value)
+ }
+
+ const onBlurInput = (e: React.FocusEvent) => {
+ if (isMobile) return
+ const relate = e.relatedTarget as HTMLDivElement
+ if (relate && relate.classList.contains('no-blur')) {
+ return // press star / import icon
+ }
+ hideListView()
+ }
+
+ const { mixpanelHandler } = useMixpanel()
+
+ const showListViewWithTracking = () => {
+ showListView()
+ mixpanelHandler(MIXPANEL_TYPE.TAS_PRESS_CTRL_K, 'mouse click')
+ }
+
+ return (
+
+
+
+ {disabled && }
+
+ {isShowListPair ? (
+
+ Esc
+
+ ) : (
+
+
+ {isMacOs ? (
+ <>
+ K
+ >
+ ) : (
+ Ctrl+K
+ )}
+
+
+ )}
+
+
+ )
+})
diff --git a/src/components/swapv2/PairSuggestion/index.tsx b/src/components/swapv2/PairSuggestion/index.tsx
new file mode 100644
index 0000000000..f081ca8594
--- /dev/null
+++ b/src/components/swapv2/PairSuggestion/index.tsx
@@ -0,0 +1,296 @@
+import React, { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'
+import styled from 'styled-components'
+import { reqAddFavoritePair, reqGetSuggestionPair, reqRemoveFavoritePair, SuggestionPairData } from './request'
+import { debounce } from 'lodash'
+import ListPair from './ListPair'
+import SearchInput from './SearchInput'
+import { ChainId, NativeCurrency, Token } from '@kyberswap/ks-sdk-core'
+import { BrowserView, isMobile, MobileView } from 'react-device-detect'
+import Modal from 'components/Modal'
+import { useActiveWeb3React } from 'hooks'
+import { filterTokens } from 'utils/filtering'
+import { ETHER_ADDRESS } from 'constants/index'
+import { nativeOnChain } from 'constants/tokens'
+import useParsedQueryString from 'hooks/useParsedQueryString'
+import { useHistory } from 'react-router-dom'
+import { stringify } from 'qs'
+import { findLogoAndSortPair, getAddressParam, isActivePair, isFavoritePair } from './utils'
+import { useAllTokens } from 'hooks/Tokens'
+import { t } from '@lingui/macro'
+import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel'
+import { NotificationType, useNotify } from 'state/application/hooks'
+
+const Wrapper = styled.div`
+ position: relative;
+ width: 100%;
+`
+
+const WrapperPopup = styled(Wrapper)`
+ height: 75vh;
+ background-color: ${({ theme }) => theme.tabActive};
+`
+
+export const Container = styled.div`
+ padding-left: 1em;
+ padding-right: 1em;
+ display: flex;
+ flex-direction: column;
+ row-gap: 1em;
+`
+
+export const MAX_FAVORITE_PAIRS = 3
+
+type Props = {
+ onSelectSuggestedPair: (
+ fromToken: NativeCurrency | Token | undefined | null,
+ toToken: NativeCurrency | Token | undefined | null,
+ amount: string,
+ ) => void
+ setShowModalImportToken: (val: boolean) => void
+}
+
+export type PairSuggestionHandle = {
+ onConfirmImportToken: () => void
+}
+
+export default forwardRef(function PairSuggestionInput(
+ { onSelectSuggestedPair, setShowModalImportToken },
+ ref,
+) {
+ const [searchQuery, setSearchQuery] = useState('')
+
+ const [selectedIndex, setSelectedIndex] = useState(0) // index selected when press up/down arrow
+ const [isShowListPair, setIsShowListPair] = useState(false)
+
+ const [suggestedPairs, setSuggestions] = useState([])
+ const [favoritePairs, setListFavorite] = useState([])
+
+ const [suggestedAmount, setSuggestedAmount] = useState('')
+ const [totalFavoritePair, setTotalFavoritePair] = useState(0) // to save actual total suggestedPairs because when searching, suggestedPairs being filter
+
+ const { account, chainId } = useActiveWeb3React()
+ const qs = useParsedQueryString()
+ const history = useHistory()
+ const { mixpanelHandler } = useMixpanel()
+
+ const refLoading = useRef(false) // prevent spam call api
+ const refInput = useRef(null)
+
+ const activeTokens = useAllTokens(true)
+
+ const findToken = (search: string): NativeCurrency | Token | undefined => {
+ if (search.toLowerCase() === ETHER_ADDRESS.toLowerCase()) {
+ return nativeOnChain(chainId as ChainId)
+ }
+ return filterTokens(Object.values(activeTokens), search)[0]
+ }
+
+ const focusInput = () => {
+ const input = refInput.current
+ if (!input) return
+ input.focus()
+ input?.setSelectionRange(searchQuery.length, searchQuery.length) // fix focus input cursor at front (ios)
+ }
+
+ const searchSuggestionPair = (keyword = '') => {
+ reqGetSuggestionPair(chainId, account, keyword)
+ .then(({ recommendedPairs = [], favoritePairs = [], amount }) => {
+ setSuggestions(findLogoAndSortPair(activeTokens, recommendedPairs, chainId))
+ setListFavorite(findLogoAndSortPair(activeTokens, favoritePairs, chainId))
+ setSuggestedAmount(amount || '')
+ if (!keyword) setTotalFavoritePair(favoritePairs.length)
+ })
+ .catch(e => {
+ console.log(e)
+ setSuggestions([])
+ setListFavorite([])
+ })
+ keyword && mixpanelHandler(MIXPANEL_TYPE.TAS_TYPING_KEYWORD, keyword)
+ }
+
+ const searchDebounce = useCallback(debounce(searchSuggestionPair, 300), [chainId, account])
+ const notify = useNotify()
+ const addToFavorite = (item: SuggestionPairData) => {
+ focusInput()
+ if (refLoading.current) return // prevent spam api
+ if (totalFavoritePair === MAX_FAVORITE_PAIRS && isMobile) {
+ // PC we already has tool tip
+ notify({
+ title: t`You can only favorite up to three token pairs.`,
+ type: NotificationType.WARNING,
+ })
+ return
+ }
+ refLoading.current = true
+ reqAddFavoritePair(item, account, chainId)
+ .then(() => {
+ searchSuggestionPair(searchQuery)
+ setTotalFavoritePair(prev => prev + 1)
+ })
+ .catch(console.error)
+ .finally(() => {
+ refLoading.current = false
+ })
+ mixpanelHandler(MIXPANEL_TYPE.TAS_LIKE_PAIR, { token_1: item.tokenIn, token_2: item.tokenOut })
+ }
+
+ const removeFavorite = (item: SuggestionPairData) => {
+ focusInput()
+ if (refLoading.current) return // prevent spam api
+ refLoading.current = true
+ reqRemoveFavoritePair(item, account, chainId)
+ .then(() => {
+ searchSuggestionPair(searchQuery)
+ setTotalFavoritePair(prev => prev - 1)
+ })
+ .catch(console.error)
+ .finally(() => {
+ refLoading.current = false
+ })
+ mixpanelHandler(MIXPANEL_TYPE.TAS_DISLIKE_PAIR, { token_1: item.tokenIn, token_2: item.tokenOut })
+ }
+
+ const onClickStar = (item: SuggestionPairData) => {
+ if (isFavoritePair(favoritePairs, item)) {
+ removeFavorite(item)
+ } else {
+ addToFavorite(item)
+ }
+ }
+
+ const hideListView = () => {
+ setIsShowListPair(false)
+ setSelectedIndex(0)
+ refInput.current?.blur()
+ }
+ const showListView = () => {
+ setIsShowListPair(true)
+ focusInput()
+ }
+
+ useEffect(() => {
+ function onKeydown(e: KeyboardEvent) {
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
+ // cmd+k or ctrl+k
+ e.preventDefault()
+ showListView()
+ mixpanelHandler(MIXPANEL_TYPE.TAS_PRESS_CTRL_K, 'keyboard hotkey')
+ }
+ }
+ window.addEventListener('keydown', onKeydown)
+ return () => {
+ window.removeEventListener('keydown', onKeydown)
+ }
+ }, [mixpanelHandler])
+
+ useEffect(() => {
+ if (isShowListPair) {
+ searchDebounce(searchQuery)
+ }
+ }, [isShowListPair, searchQuery, searchDebounce])
+
+ useEffect(() => {
+ setSearchQuery('')
+ }, [chainId])
+
+ const onChangeInput = (value: string) => {
+ setSearchQuery(value)
+ searchDebounce(value)
+ }
+
+ const onSelectPair = (item: SuggestionPairData) => {
+ mixpanelHandler(MIXPANEL_TYPE.TAS_SELECT_PAIR, `${item.tokenIn} to ${item.tokenOut}`)
+ if (!isActivePair(activeTokens, item)) {
+ // show import modal
+ const newQs = {
+ ...qs,
+ inputCurrency: getAddressParam(item.tokenIn, chainId),
+ outputCurrency: getAddressParam(item.tokenOut, chainId),
+ }
+ history.push({
+ search: stringify(newQs),
+ })
+ setShowModalImportToken(true)
+ return
+ }
+ // select pair fill input swap form
+ const fromToken = findToken(item.tokenIn)
+ const toToken = findToken(item.tokenOut)
+ onSelectSuggestedPair(fromToken, toToken, suggestedAmount)
+ setIsShowListPair(false)
+ }
+
+ useImperativeHandle(ref, () => ({
+ onConfirmImportToken() {
+ setIsShowListPair(false)
+ if (suggestedAmount) {
+ onSelectSuggestedPair(null, null, suggestedAmount) // fill input amount
+ }
+ },
+ }))
+
+ const onKeyPressInput = (e: React.KeyboardEvent) => {
+ const lastIndex = suggestedPairs.length + favoritePairs.length - 1
+ switch (e.key) {
+ case 'ArrowDown':
+ if (selectedIndex < lastIndex) {
+ setSelectedIndex(prev => prev + 1)
+ } else setSelectedIndex(0)
+ break
+ case 'ArrowUp':
+ if (selectedIndex > 0) {
+ setSelectedIndex(prev => prev - 1)
+ } else setSelectedIndex(lastIndex)
+ break
+ case 'Escape':
+ hideListView()
+ break
+ case 'Enter':
+ const selectedPair = favoritePairs.concat(suggestedPairs)[selectedIndex]
+ onSelectPair(selectedPair)
+ break
+ default:
+ break
+ }
+ }
+
+ const propsListPair = {
+ suggestedAmount,
+ selectedIndex,
+ isSearch: !!searchQuery,
+ isShowListPair,
+ suggestedPairs,
+ favoritePairs,
+ isFullFavoritePair: totalFavoritePair === MAX_FAVORITE_PAIRS,
+ onClickStar,
+ onSelectPair,
+ }
+
+ const propsSearch = {
+ isShowListPair,
+ value: searchQuery,
+ showListView,
+ hideListView,
+ onChangeInput,
+ onKeyPressInput,
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+})
diff --git a/src/components/swapv2/PairSuggestion/request.ts b/src/components/swapv2/PairSuggestion/request.ts
new file mode 100644
index 0000000000..8d29ff0de1
--- /dev/null
+++ b/src/components/swapv2/PairSuggestion/request.ts
@@ -0,0 +1,56 @@
+import { ChainId } from '@kyberswap/ks-sdk-core'
+import Axios from 'axios'
+const SUGGEST_PAIR_API = process.env.REACT_APP_TYPE_AND_SWAP_URL
+
+const formatData = (obj: any) => {
+ Object.keys(obj).forEach(key => {
+ if (obj[key] === undefined || obj[key] === '') {
+ delete obj[key]
+ }
+ })
+ return obj
+}
+
+export type SuggestionPairData = {
+ tokenIn: string
+ tokenInSymbol: string
+ tokenInImgUrl: string
+ tokenOut: string
+ tokenOutSymbol: string
+ tokenOutImgUrl: string
+ tokenInName: string
+ tokenOutName: string
+}
+
+export function reqGetSuggestionPair(
+ chainId: ChainId | undefined,
+ wallet: string | null | undefined,
+ query: string,
+): Promise<{ favoritePairs: SuggestionPairData[]; recommendedPairs: SuggestionPairData[]; amount: string }> {
+ return Axios.get(`${SUGGEST_PAIR_API}/v1/suggested-pairs`, { params: formatData({ chainId, query, wallet }) }).then(
+ ({ data }) => data.data,
+ )
+}
+
+export function reqRemoveFavoritePair(
+ item: SuggestionPairData,
+ wallet: string | null | undefined,
+ chainId: ChainId | undefined,
+): Promise {
+ return Axios.delete(`${SUGGEST_PAIR_API}/v1/favorite-pairs`, {
+ data: { wallet, chainId: chainId + '', tokenIn: item.tokenIn, tokenOut: item.tokenOut },
+ })
+}
+
+export function reqAddFavoritePair(
+ item: SuggestionPairData,
+ wallet: string | null | undefined,
+ chainId: ChainId | undefined,
+): Promise {
+ return Axios.post(`${SUGGEST_PAIR_API}/v1/favorite-pairs`, {
+ wallet,
+ chainId: chainId + '',
+ tokenIn: item.tokenIn,
+ tokenOut: item.tokenOut,
+ })
+}
diff --git a/src/components/swapv2/PairSuggestion/utils.ts b/src/components/swapv2/PairSuggestion/utils.ts
new file mode 100644
index 0000000000..ceed07840e
--- /dev/null
+++ b/src/components/swapv2/PairSuggestion/utils.ts
@@ -0,0 +1,48 @@
+import { ChainId } from '@kyberswap/ks-sdk-core'
+import { ETHER_ADDRESS } from 'constants/index'
+import { nativeOnChain } from 'constants/tokens'
+import { AllTokenType } from 'hooks/Tokens'
+import { getTokenLogoURL } from 'utils'
+import { currencyId } from 'utils/currencyId'
+import { SuggestionPairData } from './request'
+
+export const isFavoritePair = (favoritePairs: SuggestionPairData[], item: SuggestionPairData) => {
+ return favoritePairs.some(({ tokenIn, tokenOut }) => item.tokenIn === tokenIn && item.tokenOut === tokenOut)
+}
+
+// address is lowercase
+const isTokenInWhiteList = (activeTokens: AllTokenType, address: string) =>
+ address.toLowerCase() === ETHER_ADDRESS.toLowerCase() ? true : !!activeTokens[address]
+
+// at least tokenIn or tokeOut not in whitelist
+export const isActivePair = (activeTokens: AllTokenType, pair: SuggestionPairData) =>
+ isTokenInWhiteList(activeTokens, pair.tokenIn) && isTokenInWhiteList(activeTokens, pair.tokenOut)
+
+export const findLogoAndSortPair = (
+ activeTokens: AllTokenType,
+ list: SuggestionPairData[],
+ chainId: ChainId | undefined,
+) => {
+ return list
+ .map(token => {
+ // find logo
+ if (!token.tokenInImgUrl) {
+ token.tokenInImgUrl = getTokenLogoURL(token.tokenIn, chainId)
+ }
+ if (!token.tokenOutImgUrl) {
+ token.tokenOutImgUrl = getTokenLogoURL(token.tokenOut, chainId)
+ }
+ return token
+ })
+ .sort((a, b) => {
+ // sort token pair in whitelist appear first
+ const activeA = [isTokenInWhiteList(activeTokens, a.tokenIn), isTokenInWhiteList(activeTokens, a.tokenOut)]
+ const activeB = [isTokenInWhiteList(activeTokens, b.tokenIn), isTokenInWhiteList(activeTokens, b.tokenOut)]
+ return activeA.filter(Boolean).length > activeB.filter(Boolean).length ? -1 : 1
+ })
+}
+
+export const getAddressParam = (address: string, chainId: ChainId | undefined) =>
+ address.toLowerCase() === ETHER_ADDRESS.toLowerCase() && chainId
+ ? currencyId(nativeOnChain(chainId), chainId)
+ : address
diff --git a/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx b/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx
index 831f193c58..ae770f28d6 100644
--- a/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx
+++ b/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx
@@ -128,16 +128,9 @@ export function HowToSwap({
+
How to swap {symbol1} to {symbol2}?
-
+
}
>
diff --git a/src/components/swapv2/styleds.tsx b/src/components/swapv2/styleds.tsx
index f25d9cfd73..5594677afd 100644
--- a/src/components/swapv2/styleds.tsx
+++ b/src/components/swapv2/styleds.tsx
@@ -8,7 +8,7 @@ import { AutoColumn } from '../Column'
import { errorFriendly } from 'utils/dmm'
import { ReactComponent as Alert } from '../../assets/images/alert.svg'
import Modal, { ModalProps } from 'components/Modal'
-import { Z_INDEXS } from 'styles'
+import { Z_INDEXS } from 'constants/styles'
export const PageWrapper = styled.div`
display: flex;
diff --git a/src/hooks/StakingRewards.json b/src/constants/abis/StakingRewards.json
similarity index 100%
rename from src/hooks/StakingRewards.json
rename to src/constants/abis/StakingRewards.json
diff --git a/src/styles.ts b/src/constants/styles.ts
similarity index 57%
rename from src/styles.ts
rename to src/constants/styles.ts
index 3eb2950185..557b36c880 100644
--- a/src/styles.ts
+++ b/src/constants/styles.ts
@@ -2,4 +2,9 @@ export const Z_INDEXS = {
ICON_SUPPORT: 99,
LIVE_CHART: 99999,
MOBILE_MODAL: 999999,
+
+ POPUP_NOTIFICATION: 9999,
+
+ SWAP_FORM: 1,
+ SUGGESTION_PAIR: 2,
}
diff --git a/src/hooks/Tokens.ts b/src/hooks/Tokens.ts
index 996e004aaa..2c09d7c17f 100644
--- a/src/hooks/Tokens.ts
+++ b/src/hooks/Tokens.ts
@@ -1,11 +1,11 @@
import { parseBytes32String } from '@ethersproject/strings'
import { Currency, Token, ChainId, NativeCurrency } from '@kyberswap/ks-sdk-core'
import { useMemo } from 'react'
-import { TokenAddressMap, useAllLists, useCombinedActiveList, useInactiveListUrls } from 'state/lists/hooks'
+import { ListType, TokenAddressMap, useAllLists, useCombinedActiveList, useInactiveListUrls } from 'state/lists/hooks'
import { NEVER_RELOAD, useSingleCallResult, useMultipleContractSingleData } from 'state/multicall/hooks'
import { useUserAddedTokens } from 'state/user/hooks'
import { isAddress } from 'utils'
-import { createTokenFilterFunction } from 'components/SearchModal/filtering'
+import { createTokenFilterFunction } from 'utils/filtering'
import { useActiveWeb3React } from 'hooks/index'
import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
import { arrayify } from 'ethers/lib/utils'
@@ -16,7 +16,11 @@ import { Interface } from '@ethersproject/abi'
import { ZERO_ADDRESS } from 'constants/index'
// reduce token map into standard address <-> Token mapping, optionally include user added tokens
-function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean): { [address: string]: Token } {
+function useTokensFromMap(
+ tokenMap: TokenAddressMap,
+ includeUserAdded: boolean,
+ lowercaseAddress?: boolean,
+): { [address: string]: Token } {
const { chainId } = useActiveWeb3React()
const userAddedTokens = useUserAddedTokens()
@@ -25,7 +29,8 @@ function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean):
// reduce to just tokens
const mapWithoutUrls = Object.keys(tokenMap[chainId]).reduce<{ [address: string]: Token }>((newMap, address) => {
- newMap[address] = tokenMap[chainId][address].token
+ const key = lowercaseAddress ? address.toLowerCase() : address
+ newMap[key] = tokenMap[chainId][address].token
return newMap
}, {})
@@ -35,7 +40,8 @@ function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean):
// reduce into all ALL_TOKENS filtered by the current chain
.reduce<{ [address: string]: Token }>(
(tokenMap, token) => {
- tokenMap[token.address] = token
+ const key = lowercaseAddress ? token.address.toLowerCase() : token.address
+ tokenMap[key] = token
return tokenMap
},
// must make a copy because reduce modifies the map, and we do not
@@ -46,12 +52,13 @@ function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean):
}
return mapWithoutUrls
- }, [chainId, userAddedTokens, tokenMap, includeUserAdded])
+ }, [chainId, userAddedTokens, tokenMap, includeUserAdded, lowercaseAddress])
}
-export function useAllTokens(): { [address: string]: Token } {
+export type AllTokenType = { [address: string]: Token }
+export function useAllTokens(lowercaseAddress = false): AllTokenType {
const allTokens = useCombinedActiveList()
- return useTokensFromMap(allTokens, true)
+ return useTokensFromMap(allTokens, true, lowercaseAddress)
}
export function useIsTokenActive(token: Token | undefined | null): boolean {
@@ -212,31 +219,49 @@ export function useCurrency(currencyId: string | undefined): Currency | null | u
return isETH ? nativeOnChain(chainId as ChainId) : token
}
+export function searchInactiveTokenLists({
+ search,
+ minResults = 10,
+ activeTokens,
+ chainId,
+ inactiveUrls,
+ activeList,
+}: {
+ search: string | undefined
+ minResults: number
+ activeTokens: AllTokenType
+ chainId: ChainId | undefined
+ inactiveUrls: string[]
+ activeList: ListType
+}): WrappedTokenInfo[] {
+ if (!search || search.trim().length === 0) return []
+ const tokenFilter = createTokenFilterFunction(search)
+ const result: WrappedTokenInfo[] = []
+ const addressSet: { [address: string]: true } = {}
+ for (const url of inactiveUrls) {
+ const list = activeList[url].current
+ if (!list) continue
+ for (const tokenInfo of list.tokens) {
+ if (tokenInfo.chainId === chainId && tokenFilter(tokenInfo)) {
+ const wrapped: WrappedTokenInfo = new WrappedTokenInfo(tokenInfo, list)
+ if (!activeTokens[wrapped.address] && !addressSet[wrapped.address]) {
+ addressSet[wrapped.address] = true
+ result.push(wrapped)
+ if (result.length >= minResults) return result
+ }
+ }
+ }
+ }
+ return result
+}
+
export function useSearchInactiveTokenLists(search: string | undefined, minResults = 10): WrappedTokenInfo[] {
- const lists = useAllLists()
+ const activeList = useAllLists()
const inactiveUrls = useInactiveListUrls()
const { chainId } = useActiveWeb3React()
const activeTokens = useAllTokens()
return useMemo(() => {
- if (!search || search.trim().length === 0) return []
- const tokenFilter = createTokenFilterFunction(search)
- const result: WrappedTokenInfo[] = []
- const addressSet: { [address: string]: true } = {}
- for (const url of inactiveUrls) {
- const list = lists[url].current
- if (!list) continue
- for (const tokenInfo of list.tokens) {
- if (tokenInfo.chainId === chainId && tokenFilter(tokenInfo)) {
- const wrapped: WrappedTokenInfo = new WrappedTokenInfo(tokenInfo, list)
- if (!activeTokens[wrapped.address] && !addressSet[wrapped.address]) {
- addressSet[wrapped.address] = true
- result.push(wrapped)
- if (result.length >= minResults) return result
- }
- }
- }
- }
- return result
- }, [activeTokens, chainId, inactiveUrls, lists, minResults, search])
+ return searchInactiveTokenLists({ activeTokens, chainId, inactiveUrls, activeList, minResults, search })
+ }, [activeTokens, chainId, inactiveUrls, activeList, minResults, search])
}
diff --git a/src/hooks/useActiveNetwork.ts b/src/hooks/useActiveNetwork.ts
index 316e4ab9b8..2012aa8173 100644
--- a/src/hooks/useActiveNetwork.ts
+++ b/src/hooks/useActiveNetwork.ts
@@ -9,7 +9,7 @@ import { ChainId } from '@kyberswap/ks-sdk-core'
import { useAppDispatch } from 'state/hooks'
import { updateChainIdWhenNotConnected } from 'state/application/actions'
import { UnsupportedChainIdError } from '@web3-react/core'
-import { useAddPopup } from 'state/application/hooks'
+import { NotificationType, useNotify } from 'state/application/hooks'
import { t } from '@lingui/macro'
const getAddNetworkParams = (chainId: ChainId) => ({
@@ -38,7 +38,7 @@ export function useActiveNetwork() {
const location = useLocation()
const qs = useParsedQueryString()
const dispatch = useAppDispatch()
- const addPopup = useAddPopup()
+ const notify = useNotify()
const locationWithoutNetworkId = useMemo(() => {
// Delete networkId from qs object
@@ -47,7 +47,6 @@ export function useActiveNetwork() {
return { ...location, search: stringify({ ...qsWithoutNetworkId }) }
}, [location, qs])
-
const changeNetwork = useCallback(
async (desiredChainId: ChainId, successCallback?: () => void, failureCallback?: () => void) => {
const switchNetworkParams = {
@@ -79,12 +78,10 @@ export function useActiveNetwork() {
try {
await activeProvider.request({ method: 'wallet_addEthereumChain', params: [addNetworkParams] })
if (chainId !== desiredChainId) {
- addPopup({
- simple: {
- title: t`Failed to switch network`,
- success: false,
- summary: t`In order to use KyberSwap on ${NETWORKS_INFO[desiredChainId].name}, you must change the network in your wallet.`,
- },
+ notify({
+ title: t`Failed to switch network`,
+ type: NotificationType.ERROR,
+ summary: t`In order to use KyberSwap on ${NETWORKS_INFO[desiredChainId].name}, you must change the network in your wallet.`,
})
}
successCallback && successCallback()
@@ -96,18 +93,16 @@ export function useActiveNetwork() {
// handle other "switch" errors
console.error(switchError)
failureCallback && failureCallback()
- addPopup({
- simple: {
- title: t`Failed to switch network`,
- success: false,
- summary: t`In order to use KyberSwap on ${NETWORKS_INFO[desiredChainId].name}, you must change the network in your wallet.`,
- },
+ notify({
+ title: t`Failed to switch network`,
+ type: NotificationType.ERROR,
+ summary: t`In order to use KyberSwap on ${NETWORKS_INFO[desiredChainId].name}, you must change the network in your wallet.`,
})
}
}
}
},
- [dispatch, history, library, locationWithoutNetworkId, error, addPopup, chainId],
+ [dispatch, history, library, locationWithoutNetworkId, error, notify, chainId],
)
useEffect(() => {
diff --git a/src/hooks/useLiveChartData.ts b/src/hooks/useLiveChartData.ts
index 321dafa1dd..bd1241adc5 100644
--- a/src/hooks/useLiveChartData.ts
+++ b/src/hooks/useLiveChartData.ts
@@ -1,216 +1,217 @@
-import { useMemo } from 'react'
-import { Token, ChainId, WETH } from '@kyberswap/ks-sdk-core'
-import { useActiveWeb3React } from 'hooks'
-import useSWR from 'swr'
-import { getUnixTime, subHours } from 'date-fns'
-import { NETWORKS_INFO } from 'constants/networks'
-
-export enum LiveDataTimeframeEnum {
- HOUR = '1H',
- FOUR_HOURS = '4H',
- DAY = '1D',
- WEEK = '1W',
- MONTH = '1M',
- SIX_MONTHS = '6M',
-}
-
-const getTimeFrameHours = (timeFrame: LiveDataTimeframeEnum) => {
- switch (timeFrame) {
- case LiveDataTimeframeEnum.HOUR:
- return 1
- case LiveDataTimeframeEnum.FOUR_HOURS:
- return 4
- case LiveDataTimeframeEnum.DAY:
- return 24
- case LiveDataTimeframeEnum.WEEK:
- return 7 * 24
- case LiveDataTimeframeEnum.MONTH:
- return 30 * 24
- case LiveDataTimeframeEnum.SIX_MONTHS:
- return 180 * 24
- default:
- return 7 * 24
- }
-}
-const generateCoingeckoUrl = (
- chainId: ChainId,
- address: string | undefined,
- timeFrame: LiveDataTimeframeEnum | 'live',
-): string => {
- const timeTo = getUnixTime(new Date())
- const timeFrom =
- timeFrame === 'live' ? timeTo - 1000 : getUnixTime(subHours(new Date(), getTimeFrameHours(timeFrame)))
-
- return `https://api.coingecko.com/api/v3/coins/${
- NETWORKS_INFO[chainId || ChainId.MAINNET].coingeckoNetworkId
- }/contract/${address}/market_chart/range?vs_currency=usd&from=${timeFrom}&to=${timeTo}`
-}
-const getClosestPrice = (prices: any[], time: number) => {
- let closestIndex = 0
- prices.forEach((item, index) => {
- if (Math.abs(item[0] - time) < Math.abs(prices[closestIndex][0] - time)) {
- closestIndex = index
- }
- })
- return prices[closestIndex][0] - time > 10000000 ? 0 : prices[closestIndex][1]
-}
-
-export interface ChartDataInfo {
- readonly time: number
- readonly value: number
-}
-
-const liveDataApi: { [chainId in ChainId]?: string } = {
- [ChainId.MAINNET]: `${process.env.REACT_APP_AGGREGATOR_API}/ethereum/tokens`,
- [ChainId.BSCMAINNET]: `${process.env.REACT_APP_AGGREGATOR_API}/bsc/tokens`,
- [ChainId.MATIC]: `${process.env.REACT_APP_AGGREGATOR_API}/polygon/tokens`,
- [ChainId.AVAXMAINNET]: `${process.env.REACT_APP_AGGREGATOR_API}/avalanche/tokens`,
- [ChainId.FANTOM]: `${process.env.REACT_APP_AGGREGATOR_API}/fantom/tokens`,
- [ChainId.CRONOS]: `${process.env.REACT_APP_AGGREGATOR_API}/cronos/tokens`,
- [ChainId.ARBITRUM]: `${process.env.REACT_APP_AGGREGATOR_API}/arbitrum/tokens`,
-}
-const fetchKyberDataSWR = async (url: string) => {
- const res = await fetch(url)
- if (!res.ok) throw new Error()
- if (res.status === 204) {
- throw new Error('No content')
- }
- return res.json()
-}
-
-const fetchCoingeckoDataSWR = async (tokenAddresses: any, chainId: any, timeFrame: any): Promise => {
- return await Promise.all(
- [tokenAddresses[0], tokenAddresses[1]].map(address =>
- fetch(generateCoingeckoUrl(chainId, address, timeFrame)).then(res => {
- if (!res.ok) throw new Error()
- if (res.status === 204) {
- throw new Error('No content')
- }
- return res.json()
- }),
- ),
- )
-}
-
-export default function useLiveChartData(tokens: (Token | null | undefined)[], timeFrame: LiveDataTimeframeEnum) {
- const { chainId } = useActiveWeb3React()
-
- const isReverse = useMemo(() => {
- if (!tokens || !tokens[0] || !tokens[1] || tokens[0].equals(tokens[1])) return false
- const [token0] = tokens[0].sortsBefore(tokens[1]) ? [tokens[0], tokens[1]] : [tokens[1], tokens[0]]
- return token0 !== tokens[0]
- }, [tokens])
-
- const tokenAddresses = useMemo(
- () =>
- tokens
- .filter(Boolean)
- .map(token => (token?.isNative ? WETH[chainId || ChainId.MAINNET].address : token?.address)?.toLowerCase()),
- [tokens, chainId],
- )
- const { data: kyberData, error: kyberError } = useSWR(
- tokenAddresses[0] && tokenAddresses[1]
- ? `https://price-chart.kyberswap.com/api/price-chart?chainId=${chainId}&timeWindow=${timeFrame.toLowerCase()}&tokenIn=${
- tokenAddresses[0]
- }&tokenOut=${tokenAddresses[1]}`
- : null,
- fetchKyberDataSWR,
- {
- shouldRetryOnError: false,
- revalidateOnFocus: false,
- revalidateIfStale: false,
- },
- )
- const isKyberDataNotValid = useMemo(() => {
- if (kyberError || kyberData === null) return true
- if (kyberData && kyberData.length === 0) return true
- if (
- kyberData &&
- kyberData.length > 0 &&
- kyberData.every((item: any) => !item.token0Price || item.token0Price === '0')
- )
- return true
- return false
- }, [kyberError, kyberData])
-
- const { data: coingeckoData, error: coingeckoError } = useSWR(
- isKyberDataNotValid ? [tokenAddresses, chainId, timeFrame] : null,
- fetchCoingeckoDataSWR,
- {
- shouldRetryOnError: false,
- revalidateOnFocus: false,
- revalidateIfStale: false,
- },
- )
-
- const chartData = useMemo(() => {
- if (!isKyberDataNotValid && kyberData && kyberData.length > 0) {
- return kyberData
- .sort((a: any, b: any) => parseInt(a.timestamp) - parseInt(b.timestamp))
- .map((item: any) => {
- return {
- time: parseInt(item.timestamp) * 1000,
- value: !isReverse ? item.token0Price : item.token1Price || 0,
- }
- })
- } else if (coingeckoData && coingeckoData[0]?.prices?.length > 0 && coingeckoData[1]?.prices?.length > 0) {
- const [data1, data2] = coingeckoData
- return data1.prices.map((item: number[]) => {
- const closestPrice = getClosestPrice(data2.prices, item[0])
- return { time: item[0], value: closestPrice > 0 ? parseFloat((item[1] / closestPrice).toPrecision(6)) : 0 }
- })
- } else return []
- }, [kyberData, coingeckoData, isKyberDataNotValid, isReverse])
-
- const error = (!!kyberError && !!coingeckoError) || chartData.length === 0
-
- const { data: liveKyberData } = useSWR(
- !isKyberDataNotValid && kyberData && chainId
- ? liveDataApi[chainId] + `?ids=${tokenAddresses[0]},${tokenAddresses[1]}`
- : null,
- fetchKyberDataSWR,
- {
- refreshInterval: 60000,
- shouldRetryOnError: false,
- revalidateOnFocus: false,
- revalidateIfStale: false,
- },
- )
- const { data: liveCoingeckoData } = useSWR(
- isKyberDataNotValid && coingeckoData ? [tokenAddresses, chainId, 'live'] : null,
- fetchCoingeckoDataSWR,
- {
- refreshInterval: 60000,
- shouldRetryOnError: false,
- revalidateOnFocus: false,
- revalidateIfStale: false,
- },
- )
- const latestData = useMemo(() => {
- if (isKyberDataNotValid) {
- if (liveCoingeckoData) {
- const [data1, data2] = liveCoingeckoData
- if (data1.prices?.length > 0 && data2.prices?.length > 0) {
- const newValue = parseFloat(
- (data1.prices[data1.prices.length - 1][1] / data2.prices[data2.prices.length - 1][1]).toPrecision(6),
- )
- return { time: new Date().getTime(), value: newValue }
- }
- }
- } else {
- if (liveKyberData) {
- const value =
- liveKyberData && tokenAddresses[0] && tokenAddresses[1]
- ? liveKyberData[tokenAddresses[0]]?.price / liveKyberData[tokenAddresses[1]]?.price
- : 0
- if (value) return { time: new Date().getTime(), value: value }
- }
- }
- return null
- }, [liveKyberData, liveCoingeckoData, isKyberDataNotValid, tokenAddresses])
- return {
- data: useMemo(() => (latestData ? [...chartData, latestData] : chartData), [latestData, chartData]),
- error: error,
- loading: chartData.length === 0 && !error,
- }
-}
+import { useMemo } from 'react'
+import { Token, ChainId, WETH } from '@kyberswap/ks-sdk-core'
+import { useActiveWeb3React } from 'hooks'
+import useSWR from 'swr'
+import { getUnixTime, subHours } from 'date-fns'
+import { NETWORKS_INFO } from 'constants/networks'
+import { COINGECKO_API_URL } from 'constants/index'
+
+export enum LiveDataTimeframeEnum {
+ HOUR = '1H',
+ FOUR_HOURS = '4H',
+ DAY = '1D',
+ WEEK = '1W',
+ MONTH = '1M',
+ SIX_MONTHS = '6M',
+}
+
+const getTimeFrameHours = (timeFrame: LiveDataTimeframeEnum) => {
+ switch (timeFrame) {
+ case LiveDataTimeframeEnum.HOUR:
+ return 1
+ case LiveDataTimeframeEnum.FOUR_HOURS:
+ return 4
+ case LiveDataTimeframeEnum.DAY:
+ return 24
+ case LiveDataTimeframeEnum.WEEK:
+ return 7 * 24
+ case LiveDataTimeframeEnum.MONTH:
+ return 30 * 24
+ case LiveDataTimeframeEnum.SIX_MONTHS:
+ return 180 * 24
+ default:
+ return 7 * 24
+ }
+}
+const generateCoingeckoUrl = (
+ chainId: ChainId,
+ address: string | undefined,
+ timeFrame: LiveDataTimeframeEnum | 'live',
+): string => {
+ const timeTo = getUnixTime(new Date())
+ const timeFrom =
+ timeFrame === 'live' ? timeTo - 1000 : getUnixTime(subHours(new Date(), getTimeFrameHours(timeFrame)))
+
+ return `${COINGECKO_API_URL}/coins/${
+ NETWORKS_INFO[chainId || ChainId.MAINNET].coingeckoNetworkId
+ }/contract/${address}/market_chart/range?vs_currency=usd&from=${timeFrom}&to=${timeTo}`
+}
+const getClosestPrice = (prices: any[], time: number) => {
+ let closestIndex = 0
+ prices.forEach((item, index) => {
+ if (Math.abs(item[0] - time) < Math.abs(prices[closestIndex][0] - time)) {
+ closestIndex = index
+ }
+ })
+ return prices[closestIndex][0] - time > 10000000 ? 0 : prices[closestIndex][1]
+}
+
+export interface ChartDataInfo {
+ readonly time: number
+ readonly value: number
+}
+
+const liveDataApi: { [chainId in ChainId]?: string } = {
+ [ChainId.MAINNET]: `${process.env.REACT_APP_AGGREGATOR_API}/ethereum/tokens`,
+ [ChainId.BSCMAINNET]: `${process.env.REACT_APP_AGGREGATOR_API}/bsc/tokens`,
+ [ChainId.MATIC]: `${process.env.REACT_APP_AGGREGATOR_API}/polygon/tokens`,
+ [ChainId.AVAXMAINNET]: `${process.env.REACT_APP_AGGREGATOR_API}/avalanche/tokens`,
+ [ChainId.FANTOM]: `${process.env.REACT_APP_AGGREGATOR_API}/fantom/tokens`,
+ [ChainId.CRONOS]: `${process.env.REACT_APP_AGGREGATOR_API}/cronos/tokens`,
+ [ChainId.ARBITRUM]: `${process.env.REACT_APP_AGGREGATOR_API}/arbitrum/tokens`,
+}
+const fetchKyberDataSWR = async (url: string) => {
+ const res = await fetch(url)
+ if (!res.ok) throw new Error()
+ if (res.status === 204) {
+ throw new Error('No content')
+ }
+ return res.json()
+}
+
+const fetchCoingeckoDataSWR = async (tokenAddresses: any, chainId: any, timeFrame: any): Promise => {
+ return await Promise.all(
+ [tokenAddresses[0], tokenAddresses[1]].map(address =>
+ fetch(generateCoingeckoUrl(chainId, address, timeFrame)).then(res => {
+ if (!res.ok) throw new Error()
+ if (res.status === 204) {
+ throw new Error('No content')
+ }
+ return res.json()
+ }),
+ ),
+ )
+}
+
+export default function useLiveChartData(tokens: (Token | null | undefined)[], timeFrame: LiveDataTimeframeEnum) {
+ const { chainId } = useActiveWeb3React()
+
+ const isReverse = useMemo(() => {
+ if (!tokens || !tokens[0] || !tokens[1] || tokens[0].equals(tokens[1])) return false
+ const [token0] = tokens[0].sortsBefore(tokens[1]) ? [tokens[0], tokens[1]] : [tokens[1], tokens[0]]
+ return token0 !== tokens[0]
+ }, [tokens])
+
+ const tokenAddresses = useMemo(
+ () =>
+ tokens
+ .filter(Boolean)
+ .map(token => (token?.isNative ? WETH[chainId || ChainId.MAINNET].address : token?.address)?.toLowerCase()),
+ [tokens, chainId],
+ )
+ const { data: kyberData, error: kyberError } = useSWR(
+ tokenAddresses[0] && tokenAddresses[1]
+ ? `https://price-chart.kyberswap.com/api/price-chart?chainId=${chainId}&timeWindow=${timeFrame.toLowerCase()}&tokenIn=${
+ tokenAddresses[0]
+ }&tokenOut=${tokenAddresses[1]}`
+ : null,
+ fetchKyberDataSWR,
+ {
+ shouldRetryOnError: false,
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ },
+ )
+ const isKyberDataNotValid = useMemo(() => {
+ if (kyberError || kyberData === null) return true
+ if (kyberData && kyberData.length === 0) return true
+ if (
+ kyberData &&
+ kyberData.length > 0 &&
+ kyberData.every((item: any) => !item.token0Price || item.token0Price === '0')
+ )
+ return true
+ return false
+ }, [kyberError, kyberData])
+
+ const { data: coingeckoData, error: coingeckoError } = useSWR(
+ isKyberDataNotValid ? [tokenAddresses, chainId, timeFrame] : null,
+ fetchCoingeckoDataSWR,
+ {
+ shouldRetryOnError: false,
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ },
+ )
+
+ const chartData = useMemo(() => {
+ if (!isKyberDataNotValid && kyberData && kyberData.length > 0) {
+ return kyberData
+ .sort((a: any, b: any) => parseInt(a.timestamp) - parseInt(b.timestamp))
+ .map((item: any) => {
+ return {
+ time: parseInt(item.timestamp) * 1000,
+ value: !isReverse ? item.token0Price : item.token1Price || 0,
+ }
+ })
+ } else if (coingeckoData && coingeckoData[0]?.prices?.length > 0 && coingeckoData[1]?.prices?.length > 0) {
+ const [data1, data2] = coingeckoData
+ return data1.prices.map((item: number[]) => {
+ const closestPrice = getClosestPrice(data2.prices, item[0])
+ return { time: item[0], value: closestPrice > 0 ? parseFloat((item[1] / closestPrice).toPrecision(6)) : 0 }
+ })
+ } else return []
+ }, [kyberData, coingeckoData, isKyberDataNotValid, isReverse])
+
+ const error = (!!kyberError && !!coingeckoError) || chartData.length === 0
+
+ const { data: liveKyberData } = useSWR(
+ !isKyberDataNotValid && kyberData && chainId
+ ? liveDataApi[chainId] + `?ids=${tokenAddresses[0]},${tokenAddresses[1]}`
+ : null,
+ fetchKyberDataSWR,
+ {
+ refreshInterval: 60000,
+ shouldRetryOnError: false,
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ },
+ )
+ const { data: liveCoingeckoData } = useSWR(
+ isKyberDataNotValid && coingeckoData ? [tokenAddresses, chainId, 'live'] : null,
+ fetchCoingeckoDataSWR,
+ {
+ refreshInterval: 60000,
+ shouldRetryOnError: false,
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ },
+ )
+ const latestData = useMemo(() => {
+ if (isKyberDataNotValid) {
+ if (liveCoingeckoData) {
+ const [data1, data2] = liveCoingeckoData
+ if (data1.prices?.length > 0 && data2.prices?.length > 0) {
+ const newValue = parseFloat(
+ (data1.prices[data1.prices.length - 1][1] / data2.prices[data2.prices.length - 1][1]).toPrecision(6),
+ )
+ return { time: new Date().getTime(), value: newValue }
+ }
+ }
+ } else {
+ if (liveKyberData) {
+ const value =
+ liveKyberData && tokenAddresses[0] && tokenAddresses[1]
+ ? liveKyberData[tokenAddresses[0]]?.price / liveKyberData[tokenAddresses[1]]?.price
+ : 0
+ if (value) return { time: new Date().getTime(), value: value }
+ }
+ }
+ return null
+ }, [liveKyberData, liveCoingeckoData, isKyberDataNotValid, tokenAddresses])
+ return {
+ data: useMemo(() => (latestData ? [...chartData, latestData] : chartData), [latestData, chartData]),
+ error: error,
+ loading: chartData.length === 0 && !error,
+ }
+}
diff --git a/src/hooks/useMixpanel.ts b/src/hooks/useMixpanel.ts
index f474f65570..4f72f2bfaf 100644
--- a/src/hooks/useMixpanel.ts
+++ b/src/hooks/useMixpanel.ts
@@ -111,6 +111,13 @@ export enum MIXPANEL_TYPE {
CAMPAIGN_WALLET_CONNECTED,
TRANSAK_BUY_CRYPTO_CLICKED,
TRANSAK_DOWNLOAD_WALLET_CLICKED,
+
+ // type and swap
+ TAS_TYPING_KEYWORD,
+ TAS_SELECT_PAIR,
+ TAS_LIKE_PAIR,
+ TAS_DISLIKE_PAIR,
+ TAS_PRESS_CTRL_K,
}
export const NEED_CHECK_SUBGRAPH_TRANSACTION_TYPES = [
@@ -590,6 +597,30 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?:
mixpanel.track('Buy Crypto - To purchase crypto on Transak "Buy Now”')
break
}
+
+ // type and swap
+ case MIXPANEL_TYPE.TAS_TYPING_KEYWORD: {
+ mixpanel.track('Type and Swap - Typed on the text box', { text: payload })
+ break
+ }
+ case MIXPANEL_TYPE.TAS_SELECT_PAIR: {
+ mixpanel.track('Type and Swap - Selected an option', { option: payload })
+ break
+ }
+ case MIXPANEL_TYPE.TAS_LIKE_PAIR: {
+ mixpanel.track('Type and Swap - Favorite a token pair', payload)
+ break
+ }
+ case MIXPANEL_TYPE.TAS_DISLIKE_PAIR: {
+ mixpanel.track('Type and Swap - Un-favorite a token pair', payload)
+ break
+ }
+ case MIXPANEL_TYPE.TAS_PRESS_CTRL_K: {
+ mixpanel.track('Type and Swap - User click Ctrl + K (or Cmd + K) or Clicked on the text box', {
+ navigation: payload,
+ })
+ break
+ }
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/src/pages/SwapV2/index.tsx b/src/pages/SwapV2/index.tsx
index 042a7bd3c6..fd088c75cd 100644
--- a/src/pages/SwapV2/index.tsx
+++ b/src/pages/SwapV2/index.tsx
@@ -1,4 +1,4 @@
-import { ChainId, Currency, CurrencyAmount, Token } from '@kyberswap/ks-sdk-core'
+import { ChainId, Currency, CurrencyAmount, NativeCurrency, Token } from '@kyberswap/ks-sdk-core'
import JSBI from 'jsbi'
import React, { useCallback, useContext, useEffect, useMemo, useState, useRef } from 'react'
import { AlertTriangle } from 'react-feather'
@@ -34,6 +34,7 @@ import {
TabContainer,
TabWrapper,
Wrapper,
+ StyledActionButtonSwapForm,
} from 'components/swapv2/styleds'
import TokenWarningModal from 'components/TokenWarningModal'
import ProgressSteps from 'components/ProgressSteps'
@@ -76,6 +77,7 @@ import TokenInfoV2 from 'components/swapv2/TokenInfoV2'
import MobileLiveChart from 'components/swapv2/MobileLiveChart'
import MobileTradeRoutes from 'components/swapv2/MobileTradeRoutes'
import MobileTokenInfo from 'components/swapv2/MobileTokenInfo'
+import PairSuggestion, { PairSuggestionHandle } from 'components/swapv2/PairSuggestion'
import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel'
import { currencyId } from 'utils/currencyId'
import Banner from 'components/Banner'
@@ -85,12 +87,11 @@ import { NETWORKS_INFO, SUPPORTED_NETWORKS } from 'constants/networks'
import { useActiveNetwork } from 'hooks/useActiveNetwork'
import { convertToSlug, getNetworkSlug, getSymbolSlug } from 'utils/string'
import { checkPairInWhiteList, convertSymbol } from 'utils/tokenInfo'
-import { filterTokensWithExactKeyword } from 'components/SearchModal/filtering'
+import { filterTokensWithExactKeyword } from 'utils/filtering'
import { nativeOnChain } from 'constants/tokens'
import usePrevious from 'hooks/usePrevious'
import SettingsPanel from 'components/swapv2/SwapSettingsPanel'
import TransactionSettingsIcon from 'components/Icons/TransactionSettingsIcon'
-import { StyledActionButtonSwapForm } from 'components/swapv2/styleds'
import GasPriceTrackerPanel from 'components/swapv2/GasPriceTrackerPanel'
import LiquiditySourcesPanel from 'components/swapv2/LiquiditySourcesPanel'
import useParsedQueryString from 'hooks/useParsedQueryString'
@@ -98,6 +99,9 @@ import { ReactComponent as TutorialSvg } from 'assets/svg/play_circle_outline.sv
import Tutorial, { TutorialType } from 'components/Tutorial'
import { MouseoverTooltip } from 'components/Tooltip'
import { reportException } from 'utils/sentry'
+import { Z_INDEXS } from 'constants/styles'
+import { stringify } from 'qs'
+import { debounce } from 'lodash'
const TutorialIcon = styled(TutorialSvg)`
width: 22px;
@@ -146,7 +150,7 @@ const highlight = (theme: DefaultTheme) => keyframes`
export const AppBodyWrapped = styled(BodyWrapper)`
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.04);
- z-index: 1;
+ z-index: ${Z_INDEXS.SWAP_FORM};
padding: 16px 16px 24px;
margin-top: 0;
@@ -179,7 +183,10 @@ export default function Swap({ history }: RouteComponentProps) {
const isShowTokenInfoSetting = useShowTokenInfo()
const qs = useParsedQueryString()
- const shouldHighlightSwapBox = (qs.highlightBox as string) === 'true'
+ const refSuggestPair = useRef(null)
+ const [showingPairSuggestionImport, setShowingPairSuggestionImport] = useState(false) // show modal import when click pair suggestion
+
+ const shouldHighlightSwapBox = qs.highlightBox === 'true'
const [isSelectCurencyMannual, setIsSelectCurencyMannual] = useState(false) // true when: select token input, output mannualy or click rotate token.
// else select via url
@@ -291,8 +298,19 @@ export default function Swap({ history }: RouteComponentProps) {
// reset if they close warning without tokens in params
const handleDismissTokenWarning = useCallback(() => {
- setDismissTokenWarning(true)
- }, [])
+ if (showingPairSuggestionImport) {
+ setShowingPairSuggestionImport(false)
+ } else {
+ setDismissTokenWarning(true)
+ }
+ }, [showingPairSuggestionImport])
+
+ const handleConfirmTokenWarning = useCallback(() => {
+ handleDismissTokenWarning()
+ if (showingPairSuggestionImport) {
+ refSuggestPair.current?.onConfirmImportToken() // callback from children
+ }
+ }, [handleDismissTokenWarning, showingPairSuggestionImport])
// modal and loading
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
@@ -454,9 +472,17 @@ export default function Swap({ history }: RouteComponentProps) {
return filterTokensWithExactKeyword(Object.values(defaultTokens), keyword)[0]
}
- const navigate = (url: string) => {
- history.push(`${url}${window.location.search}`) // keep query params
- }
+ const navigate = useCallback(
+ (url: string) => {
+ const newQs = { ...qs }
+ // /swap/net/symA-to-symB?inputCurrency= addressC/symC &outputCurrency= addressD/symD
+ delete newQs.outputCurrency
+ delete newQs.inputCurrency
+ delete newQs.networkId
+ history.push(`${url}?${stringify(newQs)}`) // keep query params
+ },
+ [history, qs],
+ )
function findTokenPairFromUrl() {
let { fromCurrency, toCurrency, network } = getUrlMatchParams()
@@ -471,7 +497,7 @@ export default function Swap({ history }: RouteComponentProps) {
const isSame = fromCurrency && fromCurrency === toCurrency
if (!toCurrency || isSame) {
- // net/xxx
+ // net/symbol
const fromToken = findToken(fromCurrency)
if (fromToken) {
onCurrencySelection(Field.INPUT, fromToken)
@@ -511,7 +537,7 @@ export default function Swap({ history }: RouteComponentProps) {
}
const checkAutoSelectTokenFromUrl = () => {
- // check case: `/swap/net/x-to-y` or `/swap/net/x` is valid
+ // check case: `/swap/net/sym-to-sym` or `/swap/net/sym` is valid
const { fromCurrency, network } = getUrlMatchParams()
if (!fromCurrency || !network) return
@@ -529,13 +555,29 @@ export default function Swap({ history }: RouteComponentProps) {
}
}
- const syncUrl = () => {
- const symbolIn = getSymbolSlug(currencyIn)
- const symbolOut = getSymbolSlug(currencyOut)
- if (symbolIn && symbolOut && chainId) {
- navigate(`/swap/${getNetworkSlug(chainId)}/${symbolIn}-to-${symbolOut}`)
- }
- }
+ const syncUrl = useCallback(
+ (currencyIn: Currency | undefined, currencyOut: Currency | undefined) => {
+ const symbolIn = getSymbolSlug(currencyIn)
+ const symbolOut = getSymbolSlug(currencyOut)
+ if (symbolIn && symbolOut && chainId) {
+ navigate(`/swap/${getNetworkSlug(chainId)}/${symbolIn}-to-${symbolOut}`)
+ }
+ },
+ [navigate, chainId],
+ )
+
+ const onSelectSuggestedPair = useCallback(
+ (
+ fromToken: NativeCurrency | Token | undefined | null,
+ toToken: NativeCurrency | Token | undefined | null,
+ amount: string,
+ ) => {
+ if (fromToken) onCurrencySelection(Field.INPUT, fromToken)
+ if (toToken) onCurrencySelection(Field.OUTPUT, toToken)
+ if (amount) handleTypeInput(amount)
+ },
+ [handleTypeInput, onCurrencySelection],
+ )
const tokenImports: Token[] = useUserAddedTokens()
const prevTokenImports = usePrevious(tokenImports) || []
@@ -592,9 +634,35 @@ export default function Swap({ history }: RouteComponentProps) {
}, [])
useEffect(() => {
- if (isSelectCurencyMannual) syncUrl() // when we select token mannual
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currencyIn, currencyOut])
+ if (isSelectCurencyMannual) syncUrl(currencyIn, currencyOut) // when we select token manual
+ }, [currencyIn, currencyOut, isSelectCurencyMannual, syncUrl])
+
+ const refLoadedCurrency = useRef<{
+ currencyIn: Currency | null | undefined
+ currencyOut: Currency | null | undefined
+ }>({ currencyIn: null, currencyOut: null })
+
+ useEffect(() => {
+ refLoadedCurrency.current = { currencyIn: loadedInputCurrency, currencyOut: loadedOutputCurrency }
+ }, [loadedInputCurrency, loadedOutputCurrency])
+
+ const checkParamWrong = useCallback(() => {
+ const { currencyIn, currencyOut } = refLoadedCurrency.current
+ if (!currencyIn || !currencyOut) {
+ const newQuery = { ...qs }
+ if (!currencyIn) delete newQuery.inputCurrency
+ if (!currencyOut) delete newQuery.outputCurrency
+ history.replace({
+ search: stringify(newQuery),
+ })
+ }
+ }, [qs, history])
+
+ // swap?inputCurrency=xxx&outputCurrency=yyy. xxx yyy not exist in chain => remove params => select default pair
+ const checkParamWrongDebounce = useCallback(debounce(checkParamWrong, 300), [])
+ useEffect(() => {
+ checkParamWrongDebounce()
+ }, [chainId, checkParamWrongDebounce])
useEffect(() => {
if (isExpertMode) {
@@ -611,15 +679,16 @@ export default function Swap({ history }: RouteComponentProps) {
}, [allowedSlippage])
const shareUrl = useMemo(() => {
- return currencies && currencyIn && currencyOut
- ? window.location.origin +
- `/swap?inputCurrency=${currencyId(currencyIn as Currency, chainId)}&outputCurrency=${currencyId(
- currencyOut as Currency,
- chainId,
- )}&networkId=${chainId}`
- : window.location.origin + `/swap?networkId=${chainId}`
+ return `${window.location.origin}/swap?networkId=${chainId}${
+ currencyIn && currencyOut
+ ? `&${stringify({
+ inputCurrency: currencyId(currencyIn, chainId),
+ outputCurrency: currencyId(currencyOut, chainId),
+ })}`
+ : ''
+ }`
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currencies, currencyIn, currencyOut, chainId, currencyId, window.location.origin])
+ }, [currencyIn, currencyOut, chainId, currencyId, window.location.origin])
const { isInWhiteList: isPairInWhiteList, canonicalUrl } = checkPairInWhiteList(
chainId,
@@ -629,6 +698,9 @@ export default function Swap({ history }: RouteComponentProps) {
const shouldRenderTokenInfo = isShowTokenInfoSetting && currencyIn && currencyOut && isPairInWhiteList
+ const isShowModalImportToken =
+ importTokensNotInDefault.length > 0 && (!dismissTokenWarning || showingPairSuggestionImport)
+
return (
<>
{/**
@@ -638,9 +710,9 @@ export default function Swap({ history }: RouteComponentProps) {
0 && !dismissTokenWarning}
+ isOpen={isShowModalImportToken}
tokens={importTokensNotInDefault}
- onConfirm={handleDismissTokenWarning}
+ onConfirm={handleConfirmTokenWarning}
onDismiss={handleDismissTokenWarning}
/>
@@ -695,6 +767,14 @@ export default function Swap({ history }: RouteComponentProps) {
+
+
+
+
{activeTab === TAB.SWAP && (
<>
diff --git a/src/pages/TrueSight/hooks/useGetCoinGeckoChartData.ts b/src/pages/TrueSight/hooks/useGetCoinGeckoChartData.ts
index b67b929993..b3d88c7833 100644
--- a/src/pages/TrueSight/hooks/useGetCoinGeckoChartData.ts
+++ b/src/pages/TrueSight/hooks/useGetCoinGeckoChartData.ts
@@ -2,6 +2,7 @@
import { useMemo, useRef } from 'react'
import { TrueSightTimeframe } from 'pages/TrueSight/index'
import { NETWORKS_INFO, TRUESIGHT_NETWORK_TO_CHAINID } from 'constants/networks'
+import { COINGECKO_API_URL } from 'constants/index'
import useSWRImmutable from 'swr/immutable'
export interface CoinGeckoChartData {
@@ -48,15 +49,15 @@ export default function useGetCoinGeckoChartData(
const from = to - (timeframe === TrueSightTimeframe.ONE_DAY ? 24 * 3600 : 24 * 3600 * 7)
const chainId = TRUESIGHT_NETWORK_TO_CHAINID[tokenNetwork]
const coinGeckoNetworkId = NETWORKS_INFO[chainId].coingeckoNetworkId
- let url = `https://api.coingecko.com/api/v3/coins/${coinGeckoNetworkId}/contract/${tokenAddress.toLowerCase()}/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
+ let url = `${COINGECKO_API_URL}/coins/${coinGeckoNetworkId}/contract/${tokenAddress.toLowerCase()}/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
if (tokenAddress === 'bnb') {
- url = `https://api.coingecko.com/api/v3/coins/binancecoin/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
+ url = `${COINGECKO_API_URL}/coins/binancecoin/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
} else if (tokenNetwork === 'bsc' && tokenAddress === '0x1Fa4a73a3F0133f0025378af00236f3aBDEE5D63') {
// NEAR
- url = `https://api.coingecko.com/api/v3/coins/near/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
+ url = `${COINGECKO_API_URL}/coins/near/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
} else if (tokenNetwork === 'eth' && tokenAddress === '0x7c8161545717a334f3196e765d9713f8042EF338') {
// CAKE
- url = `https://api.coingecko.com/api/v3/coins/binance-smart-chain/contract/0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
+ url = `${COINGECKO_API_URL}/coins/binance-smart-chain/contract/0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82/market_chart/range?vs_currency=usd&from=${from}&to=${to}`
}
if (Date.now() - latestRequestingTime.current < FETCHING_COINGECKO_CHART_DATA_OFFSET) {
// Too Many Request
diff --git a/src/pages/TrueSight/hooks/useGetTrendingSoonTokenId.ts b/src/pages/TrueSight/hooks/useGetTrendingSoonTokenId.ts
index 0b61543449..9c24ba0110 100644
--- a/src/pages/TrueSight/hooks/useGetTrendingSoonTokenId.ts
+++ b/src/pages/TrueSight/hooks/useGetTrendingSoonTokenId.ts
@@ -9,25 +9,26 @@ export default function useGetTrendingSoonTokenId(token?: Token): number | undef
useEffect(() => {
const asyncF = async () => {
- setTokenId(undefined)
- if (token) {
- const { address } = token
- const url24h = `${process.env.REACT_APP_TRUESIGHT_API}/api/v1/trending-soon?timeframe=24h&page_number=0&page_size=${TRENDING_SOON_MAX_ITEMS}&search_token_address=${address}`
- // const url7d = `${process.env.REACT_APP_TRUESIGHT_API}/api/v1/trending-soon?timeframe=7d&page_number=0&page_size=${TRENDING_SOON_MAX_ITEMS}&search_token_address=${address}`
- const responses = await Promise.all([fetch(url24h)])
- for (let i = 0; i < responses.length; i++) {
- const response = responses[i]
- if (response.ok) {
- const { data }: { data: TrueSightTokenResponse } = await response.json()
- if (data.tokens.length) {
- setTokenId(data.tokens[0].token_id)
- return
+ try {
+ setTokenId(undefined)
+ if (token) {
+ const { address } = token
+ const url24h = `${process.env.REACT_APP_TRUESIGHT_API}/api/v1/trending-soon?timeframe=24h&page_number=0&page_size=${TRENDING_SOON_MAX_ITEMS}&search_token_address=${address}`
+ // const url7d = `${process.env.REACT_APP_TRUESIGHT_API}/api/v1/trending-soon?timeframe=7d&page_number=0&page_size=${TRENDING_SOON_MAX_ITEMS}&search_token_address=${address}`
+ const responses = await Promise.all([fetch(url24h)])
+ for (let i = 0; i < responses.length; i++) {
+ const response = responses[i]
+ if (response.ok) {
+ const { data }: { data: TrueSightTokenResponse } = await response.json()
+ if (data.tokens.length) {
+ setTokenId(data.tokens[0].token_id)
+ return
+ }
}
}
}
- }
+ } catch (error) {}
}
-
asyncF()
}, [token])
diff --git a/src/state/application/actions.ts b/src/state/application/actions.ts
index d8d313de91..68688f30ad 100644
--- a/src/state/application/actions.ts
+++ b/src/state/application/actions.ts
@@ -2,37 +2,32 @@ import { createAction } from '@reduxjs/toolkit'
import { TokenList } from '@uniswap/token-lists'
import { ChainId } from '@kyberswap/ks-sdk-core'
import { GasPrice } from './reducer'
+import { NotificationType } from './hooks'
+export type PopupContentTxn = {
+ hash: string
+ notiType: NotificationType
+ type?: string
+ summary?: string
+}
+export type PopupContentListUpdate = {
+ listUrl: string
+ oldList: TokenList
+ newList: TokenList
+ auto: boolean
+}
+export type PopupContentSimple = {
+ title: string
+ summary?: string
+ type: NotificationType
+}
+
+export enum PopupType {
+ TRANSACTION,
+ LIST_UPDATE,
+ SIMPLE,
+}
-export type PopupContent =
- | {
- txn: {
- hash: string
- success: boolean
- type?: string
- summary?: string
- }
- }
- | {
- listUpdate: {
- listUrl: string
- oldList: TokenList
- newList: TokenList
- auto: boolean
- }
- }
- | {
- simple: {
- title: string
- success: boolean
- summary: string
- }
- }
- | {
- truesightNoti: {
- title: string
- body: string
- }
- }
+export type PopupContent = PopupContentTxn | PopupContentListUpdate | PopupContentSimple
export enum ApplicationModal {
NETWORK,
@@ -67,9 +62,12 @@ export enum ApplicationModal {
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('application/updateBlockNumber')
export const setOpenModal = createAction('application/setOpenModal')
-export const addPopup = createAction<{ key?: string; removeAfterMs?: number | null; content: PopupContent }>(
- 'application/addPopup',
-)
+export const addPopup = createAction<{
+ key?: string
+ removeAfterMs?: number | null
+ content: PopupContent
+ popupType: PopupType
+}>('application/addPopup')
export const removePopup = createAction<{ key: string }>('application/removePopup')
export const updatePrommETHPrice = createAction<{
currentPrice: string
diff --git a/src/state/application/hooks.ts b/src/state/application/hooks.ts
index b20d44528f..5d65d10084 100644
--- a/src/state/application/hooks.ts
+++ b/src/state/application/hooks.ts
@@ -16,6 +16,9 @@ import {
updateETHPrice,
updatePrommETHPrice,
updateKNCPrice,
+ PopupType,
+ PopupContentTxn,
+ PopupContentSimple,
} from './actions'
import { getPercentChange, getBlockFromTimestamp } from 'utils'
import { useDeepCompareEffect } from 'react-use'
@@ -115,17 +118,49 @@ export function useTrueSightUnsubscribeModalToggle(): () => void {
}
// returns a function that allows adding a popup
-export function useAddPopup(): (content: PopupContent, key?: string) => void {
+function useAddPopup(): (
+ content: PopupContent,
+ popupType: PopupType,
+ key?: string,
+ removeAfterMs?: number | null,
+) => void {
const dispatch = useDispatch()
return useCallback(
- (content: PopupContent, key?: string) => {
- dispatch(addPopup({ content, key }))
+ (content: PopupContent, popupType: PopupType, key?: string, removeAfterMs?: number | null) => {
+ dispatch(addPopup({ content, key, popupType, removeAfterMs }))
},
[dispatch],
)
}
+export enum NotificationType {
+ SUCCESS,
+ ERROR,
+ WARNING,
+}
+// simple notify with text and description
+export const useNotify = () => {
+ const addPopup = useAddPopup()
+ return useCallback(
+ (data: PopupContentSimple, removeAfterMs = 3000) => {
+ addPopup(data, PopupType.SIMPLE, data.title, removeAfterMs)
+ },
+ [addPopup],
+ )
+}
+
+// popup notify transaction
+export const useTransactionNotify = () => {
+ const addPopup = useAddPopup()
+ return useCallback(
+ (data: PopupContentTxn) => {
+ addPopup(data, PopupType.TRANSACTION, data.hash)
+ },
+ [addPopup],
+ )
+}
+
// returns a function that allows removing a popup via its key
export function useRemovePopup(): (key: string) => void {
const dispatch = useDispatch()
diff --git a/src/state/application/reducer.ts b/src/state/application/reducer.ts
index e50aef7524..65ea9ab058 100644
--- a/src/state/application/reducer.ts
+++ b/src/state/application/reducer.ts
@@ -12,9 +12,16 @@ import {
updateChainIdWhenNotConnected,
setGasPrice,
updatePrommETHPrice,
+ PopupType,
} from './actions'
-type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
+type PopupList = Array<{
+ key: string
+ show: boolean
+ content: PopupContent
+ removeAfterMs: number | null
+ popupType: PopupType
+}>
type ETHPrice = {
currentPrice?: string
@@ -63,13 +70,14 @@ export default createReducer(initialState, builder =>
.addCase(setOpenModal, (state, action) => {
state.openModal = action.payload
})
- .addCase(addPopup, (state, { payload: { content, key, removeAfterMs = 15000 } }) => {
+ .addCase(addPopup, (state, { payload: { content, key, removeAfterMs = 15000, popupType } }) => {
state.popupList = (key ? state.popupList.filter(popup => popup.key !== key) : state.popupList).concat([
{
key: key || nanoid(),
show: true,
content,
removeAfterMs,
+ popupType,
},
])
})
diff --git a/src/state/farms/promm/hooks.ts b/src/state/farms/promm/hooks.ts
index 6e88bc9f5b..bf48ba121a 100644
--- a/src/state/farms/promm/hooks.ts
+++ b/src/state/farms/promm/hooks.ts
@@ -33,25 +33,6 @@ export const useProMMFarms = () => {
return useSelector((state: AppState) => state.prommFarms)
}
-export const useProMMFarmsFetchOnlyOne = () => {
- const { data: farms } = useProMMFarms()
- const getProMMFarm = useGetProMMFarms()
-
- const firstRender = useRef(true)
-
- const { chainId } = useActiveWeb3React()
- const previousChainId = usePrevious(chainId)
-
- useEffect(() => {
- if ((!Object.keys(farms).length && firstRender.current) || chainId !== previousChainId) {
- getProMMFarm()
- firstRender.current = false
- }
- }, [previousChainId, farms, getProMMFarm, chainId])
-
- return farms
-}
-
export const useGetProMMFarms = () => {
const dispatch = useAppDispatch()
const { chainId, account } = useActiveWeb3React()
@@ -185,6 +166,25 @@ export const useGetProMMFarms = () => {
return getProMMFarms
}
+export const useProMMFarmsFetchOnlyOne = () => {
+ const { data: farms } = useProMMFarms()
+ const getProMMFarm = useGetProMMFarms()
+
+ const firstRender = useRef(true)
+
+ const { chainId } = useActiveWeb3React()
+ const previousChainId = usePrevious(chainId)
+
+ useEffect(() => {
+ if ((!Object.keys(farms).length && firstRender.current) || chainId !== previousChainId) {
+ getProMMFarm()
+ firstRender.current = false
+ }
+ }, [previousChainId, farms, getProMMFarm, chainId])
+
+ return farms
+}
+
export const useFarmAction = (address: string) => {
const addTransactionWithType = useTransactionAdder()
const contract = useProMMFarmContract(address)
diff --git a/src/state/lists/hooks.ts b/src/state/lists/hooks.ts
index b0e4b50f20..c977a122c6 100644
--- a/src/state/lists/hooks.ts
+++ b/src/state/lists/hooks.ts
@@ -67,14 +67,15 @@ function listToTokenMap(list: TokenList): TokenAddressMap {
const TRANSFORMED_DEFAULT_TOKEN_LIST = listToTokenMap(DEFAULT_TOKEN_LIST)
// returns all downloaded current lists
-export function useAllLists(): {
+export type ListType = {
readonly [url: string]: {
readonly current: TokenList | null
readonly pendingUpdate: TokenList | null
readonly loadingRequestId: string | null
readonly error: string | null
}
-} {
+}
+export function useAllLists(): ListType {
return useSelector(state => state.lists.byUrl)
}
diff --git a/src/state/transactions/updater.tsx b/src/state/transactions/updater.tsx
index 4459c6861f..a5db7f70e3 100644
--- a/src/state/transactions/updater.tsx
+++ b/src/state/transactions/updater.tsx
@@ -4,7 +4,7 @@ import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { ethers } from 'ethers'
import { BigNumber } from '@ethersproject/bignumber'
import { useActiveWeb3React } from '../../hooks'
-import { useAddPopup, useBlockNumber } from '../application/hooks'
+import { NotificationType, useBlockNumber, useTransactionNotify } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { checkedTransaction, finalizeTransaction } from './actions'
import { AGGREGATOR_ROUTER_SWAPPED_EVENT_TOPIC } from 'constants/index'
@@ -42,7 +42,6 @@ export default function Updater(): null {
const transactions = useMemo(() => (chainId ? state[chainId] ?? {} : {}), [chainId, state])
// show popup on confirm
- const addPopup = useAddPopup()
const parseTransactionType = useCallback(
(receipt: TransactionReceipt): string | undefined => {
@@ -97,6 +96,7 @@ export default function Updater(): null {
[transactions],
)
const { mixpanelHandler, subgraphMixpanelHandler } = useMixpanel()
+ const transactionNotify = useTransactionNotify()
useEffect(() => {
if (!chainId || !library || !lastBlockNumber) return
@@ -129,17 +129,12 @@ export default function Updater(): null {
}),
)
- addPopup(
- {
- txn: {
- hash,
- success: receipt.status === 1,
- type: parseTransactionType(receipt),
- summary: parseTransactionSummary(receipt),
- },
- },
+ transactionNotify({
hash,
- )
+ notiType: receipt.status === 1 ? NotificationType.SUCCESS : NotificationType.ERROR,
+ type: parseTransactionType(receipt),
+ summary: parseTransactionSummary(receipt),
+ })
if (receipt.status === 1 && transaction && transaction.arbitrary) {
switch (transaction.type) {
case 'Swap': {
@@ -185,16 +180,7 @@ export default function Updater(): null {
})
// eslint-disable-next-line
- }, [
- chainId,
- library,
- transactions,
- lastBlockNumber,
- dispatch,
- addPopup,
- parseTransactionSummary,
- parseTransactionType,
- ])
+ }, [chainId, library, transactions, lastBlockNumber, dispatch, parseTransactionSummary, parseTransactionType])
return null
}
diff --git a/src/theme/index.tsx b/src/theme/index.tsx
index 880f38904b..4054d19b68 100644
--- a/src/theme/index.tsx
+++ b/src/theme/index.tsx
@@ -8,7 +8,7 @@ import styled, {
import { useIsDarkMode } from 'state/user/hooks'
import { Text, TextProps } from 'rebass'
import { Colors } from './styled'
-import { Z_INDEXS } from 'styles'
+import { Z_INDEXS } from 'constants/styles'
export * from './components'
@@ -92,10 +92,13 @@ export function colors(darkMode: boolean): Colors {
bg20: darkMode ? '#243036' : '#F5F5F5',
bg21: darkMode
? 'linear-gradient(90deg, rgba(29, 122, 95, 0.5) 0%, rgba(29, 122, 95, 0) 100%)'
- : 'linear-gradient(90deg, rgba(49, 203, 158, 0.15) 0%, rgba(49, 203, 158, 0) 100%)',
+ : 'linear-gradient(90deg, rgba(49, 203, 158, 0.15) 0%, rgba(49, 203, 158, 0) 100%)', // success
bg22: darkMode
? 'linear-gradient(90deg, rgba(255, 83, 123, 0.4) 0%, rgba(255, 83, 123, 0) 100%)'
- : 'linear-gradient(90deg, rgba(255, 83, 123, 0.15) 0%, rgba(255, 83, 123, 0) 100%)',
+ : 'linear-gradient(90deg, rgba(255, 83, 123, 0.15) 0%, rgba(255, 83, 123, 0) 100%)', // error
+ bg23: darkMode
+ ? 'linear-gradient(90deg, rgba(255, 153, 1, 0.5) 0%, rgba(255, 153, 1, 0) 100%)'
+ : 'linear-gradient(90deg, rgba(255, 153, 1, 0.5) 0%, rgba(255, 153, 1, 0) 100%)', // warning
//specialty colors
modalBG: darkMode ? 'rgba(0,0,0,.425)' : 'rgba(0,0,0,0.3)',
diff --git a/src/theme/styled.d.ts b/src/theme/styled.d.ts
index 54bbfc7ac8..0f6bca0a24 100644
--- a/src/theme/styled.d.ts
+++ b/src/theme/styled.d.ts
@@ -51,6 +51,7 @@ export interface Colors {
bg20: Color
bg21: Color
bg22: Color
+ bg23: Color
buttonBlack: Color
buttonGray: Color
diff --git a/src/components/SearchModal/filtering.ts b/src/utils/filtering.ts
similarity index 63%
rename from src/components/SearchModal/filtering.ts
rename to src/utils/filtering.ts
index d01da6b09f..fa7aa1e8c9 100644
--- a/src/components/SearchModal/filtering.ts
+++ b/src/utils/filtering.ts
@@ -1,5 +1,4 @@
-import { useMemo } from 'react'
-import { isAddress } from '../../utils'
+import { isAddress } from 'utils'
import { Token } from '@kyberswap/ks-sdk-core'
import { TokenInfo } from '@uniswap/token-lists'
@@ -46,37 +45,3 @@ export function filterTokensWithExactKeyword(tokens
const filterExact = result.filter(e => (e.symbol ? e.symbol.toLowerCase() === search.toLowerCase() : true)) // Exact Keyword
return filterExact.length ? filterExact : result
}
-
-export function useSortedTokensByQuery(tokens: Token[] | undefined, searchQuery: string): Token[] {
- return useMemo(() => {
- if (!tokens) {
- return []
- }
-
- const symbolMatch = searchQuery
- .toLowerCase()
- .split(/\s+/)
- .filter(s => s.length > 0)
-
- if (symbolMatch.length > 1) {
- return tokens
- }
-
- const exactMatches: Token[] = []
- const symbolSubtrings: Token[] = []
- const rest: Token[] = []
-
- // sort tokens by exact match -> subtring on symbol match -> rest
- tokens.map(token => {
- if (token.symbol?.toLowerCase() === symbolMatch[0]) {
- return exactMatches.push(token)
- } else if (token.symbol?.toLowerCase().startsWith(searchQuery.toLowerCase().trim())) {
- return symbolSubtrings.push(token)
- } else {
- return rest.push(token)
- }
- })
-
- return [...exactMatches, ...symbolSubtrings, ...rest]
- }, [tokens, searchQuery])
-}
diff --git a/src/utils/mumbaiTokenMapping.ts b/src/utils/mumbaiTokenMapping.ts
index 539fe50230..a0c06d14e9 100644
--- a/src/utils/mumbaiTokenMapping.ts
+++ b/src/utils/mumbaiTokenMapping.ts
@@ -1,27 +1,16 @@
+const MapAddress: { [key: string]: string } = {
+ '0x9c3c9283d3e44854697cd22d3faa240cfb032889': '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0',
+ '0xfd1f9381cb641dc76fe8087dbcf8ea84a2c77cbe': '0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202',
+ '0x19395624c030a11f58e820c3aefb1f5960d9742a': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
+ '0x2cec76b26a8d96bf3072d34a01bb3a4ede7c06be': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ '0x064b91bda6d178dfe03835de9450bfe78201c43f': '0xdAC17F958D2ee523a2206206994597C13D831ec7',
+ '0x5e2de02472ac02736b43054f095837725a5870ef': '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ '0x326c977e6efc84e512bb9c30f76e30c160ed06fb': '0x514910771AF9Ca656af840dff83E8264EcF986CA',
+}
export const getMumbaiTokenLogoURL = (address: string) => {
let uri
- if (address?.toLowerCase() === '0x9c3c9283d3e44854697cd22d3faa240cfb032889') {
- address = '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0'
- }
- if (address?.toLowerCase() === '0xfd1f9381cb641dc76fe8087dbcf8ea84a2c77cbe') {
- address = '0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202'
- }
- if (address?.toLowerCase() === '0x19395624c030a11f58e820c3aefb1f5960d9742a') {
- address = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
- }
- if (address?.toLowerCase() === '0x2cec76b26a8d96bf3072d34a01bb3a4ede7c06be') {
- address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
- }
- if (address?.toLowerCase() === '0x064b91bda6d178dfe03835de9450bfe78201c43f') {
- address = '0xdAC17F958D2ee523a2206206994597C13D831ec7'
- }
- if (address?.toLowerCase() === '0x5e2de02472ac02736b43054f095837725a5870ef') {
- address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
- }
- if (address?.toLowerCase() === '0x326c977e6efc84e512bb9c30f76e30c160ed06fb') {
- address = '0x514910771AF9Ca656af840dff83E8264EcF986CA'
- }
+ if (address) address = MapAddress[address.toLowerCase()] || address
if (!uri) {
uri = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`