Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add liquidity sources setting/panel in Settings #1130

Merged
merged 1 commit into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/components/swapv2/LiquiditySourcesPanel/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react'
import { Search } from 'react-feather'
import styled from 'styled-components'
import { t } from '@lingui/macro'

type Props = {
text: string
setText: (txt: string) => void
}

const SearchBarWrapper = styled.div`
width: 100%;
height: 36px;
position: relative;
`

const Input = styled.input`
width: 100%;
height: 100%;

padding: 8px 36px 8px 12px;

background: ${({ theme }) => theme.buttonBlack};
border: 0px;
border-radius: 40px;
color: inherit;
outline: none;

font-size: 14px;
font-weight: 500;
line-height: 20px;
`

const IconWrapper = styled.div`
position: absolute;
right: 12px;
top: 0;

width: 18px;
height: 100%;

display: flex;
align-items: center;

color: ${({ theme }) => theme.subText};
`

const SearchBar: React.FC<Props> = ({ text, setText }) => {
return (
<SearchBarWrapper>
<Input value={text} onChange={e => setText(e.target.value)} placeholder={t`Search for a liquidity source`} />
<IconWrapper>
<Search />
</IconWrapper>
</SearchBarWrapper>
)
}

export default SearchBar
134 changes: 134 additions & 0 deletions src/components/swapv2/LiquiditySourcesPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { useState } from 'react'
import { Flex, Box } from 'rebass'
import { ArrowLeft } from 'react-feather'
import styled from 'styled-components'
import { t } from '@lingui/macro'

import { useActiveWeb3React } from 'hooks'
import useDebounce from 'hooks/useDebounce'
import useAggregatorStats from 'hooks/useAggregatorStats'
import { dexListConfig } from 'constants/dexes'

import SearchBar from './SearchBar'

type Props = {
onBack: () => void
}

const BackIconWrapper = styled(ArrowLeft)`
height: 20px;
width: 20px;
margin-right: 10px;
cursor: pointer;
path {
stroke: ${({ theme }) => theme.text} !important;
}
`

const BackText = styled.span`
font-size: 18px;
font-weight: 500;
color: ${({ theme }) => theme.text};
`

const SourceList = styled.div`
width: 100%;
height: 300px;
max-height: 300px;
overflow: scroll;

display: flex;
flex-direction: column;
row-gap: 24px;
`

const Source = styled.div`
width: 100%;
height: 32px;

display: flex;
align-items: center;
column-gap: 16px;
`

const ImageWrapper = styled.div`
width: 32px;
height: 32px;

display: flex;
align-items: center;

img {
width: 100%;
height: auto;
}
`

const SourceName = styled.span`
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: ${({ theme }) => theme.text};
`

const LiquiditySourcesPanel: React.FC<Props> = ({ onBack }) => {
const { chainId } = useActiveWeb3React()
const [searchText, setSearchText] = useState('')
const debouncedSearchText = useDebounce(searchText.toLowerCase(), 200)

const { data, error } = useAggregatorStats(chainId)
if (error || !data) {
onBack()
return null
}

const dexIDs = Object.keys(data.pools)
if (dexIDs.length === 0) {
onBack()
return null
}

const visibleDEXes = dexIDs
.map(id => dexListConfig[id])
.filter(Boolean)
.filter(({ name }) => name.toLowerCase().includes(debouncedSearchText))

return (
<Box width="100%">
<Flex
width={'100%'}
flexDirection={'column'}
sx={{
rowGap: '20px',
}}
>
<Flex
alignItems="center"
sx={{
// this is to make the arrow stay exactly where it stays in Swap panel
marginTop: '5px',
}}
>
<BackIconWrapper onClick={onBack}></BackIconWrapper>
<BackText>{t`Liquidity Sources`}</BackText>
</Flex>

<SearchBar text={searchText} setText={setSearchText} />

<SourceList>
{visibleDEXes.map(({ name, icon }) => (
<Source key={name}>
<ImageWrapper>
<img src={icon} />
</ImageWrapper>

<SourceName>{name}</SourceName>
</Source>
))}
</SourceList>
</Flex>
</Box>
)
}

export default LiquiditySourcesPanel
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react'
import styled from 'styled-components'
import { isMobile } from 'react-device-detect'
import { t, Trans } from '@lingui/macro'
import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'

import QuestionHelper from 'components/QuestionHelper'
import useAggregatorStats from 'hooks/useAggregatorStats'
import { useActiveWeb3React } from 'hooks'

type Props = {
onClick: () => void
}

const SettingLabel = styled.span`
font-size: ${isMobile ? '14px' : '12px'};
color: ${({ theme }) => theme.text};
font-weight: 400;
line-height: 16px;
`

const Group = styled.div`
display: flex;
align-items: center;
color: ${({ theme }) => theme.subText};
font-size: ${isMobile ? '14px' : '12px'};
`

const NumberOfSources = styled.span`
color: ${({ theme }) => theme.text};
font-weight: 400;
line-height: 16px;
`

const LiquiditySourcesSetting: React.FC<Props> = ({ onClick }) => {
const { chainId } = useActiveWeb3React()
const { data, error } = useAggregatorStats(chainId)

if (error || !data) {
return null
}

const numberOfDEXes = Object.keys(data.pools).length
if (numberOfDEXes === 0) {
return null
}

return (
<Flex
justifyContent="space-between"
alignItems="center"
sx={{
cursor: 'pointer',
}}
onClick={onClick}
>
<Group>
<SettingLabel>
<Trans>Liquidity Sources</Trans>
</SettingLabel>
<QuestionHelper text={t`Your trade is routed through one or more of these liquidity sources`} />
</Group>

<Group>
<NumberOfSources>{numberOfDEXes}</NumberOfSources>
<ChevronRight />
</Group>
</Flex>
)
}

export default LiquiditySourcesSetting
6 changes: 5 additions & 1 deletion src/components/swapv2/SwapSettingsPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ import useTopTrendingSoonTokensInCurrentNetwork from 'components/TopTrendingSoon
import TransactionTimeLimitSetting from './TransactionTimeLimitSetting'
import SlippageSetting from './SlippageSetting'
import AdvancedModeSetting from './AdvancedModeSetting'
import LiquiditySourcesSetting from './LiquiditySourcesSetting'

type Props = {
className?: string
onBack: () => void
onClickLiquiditySources: () => void
}

const BackIconWrapper = styled(ArrowLeft)`
Expand All @@ -51,7 +53,7 @@ const BackText = styled.span`
color: ${({ theme }) => theme.text};
`

const SettingsPanel: React.FC<Props> = ({ className, onBack }) => {
const SettingsPanel: React.FC<Props> = ({ className, onBack, onClickLiquiditySources }) => {
const theme = useTheme()

const { data: topTrendingSoonTokens } = useTopTrendingSoonTokensInCurrentNetwork()
Expand Down Expand Up @@ -131,6 +133,8 @@ const SettingsPanel: React.FC<Props> = ({ className, onBack }) => {

<AdvancedModeSetting />

<LiquiditySourcesSetting onClick={onClickLiquiditySources} />

<Flex
sx={{
flexDirection: 'column',
Expand Down
12 changes: 6 additions & 6 deletions src/constants/dexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ export const dexListConfig: DexList = {
},
firebird: {
name: 'Firebird',
icon: 'https://app.firebird.finance/favicon.png',
icon: 'https://s2.coinmarketcap.com/static/img/exchanges/64x64/1500.png',
},
oneswap: {
name: 'OneSwap',
icon: 'https://app.firebird.finance/favicon.png',
icon: 'https://s2.coinmarketcap.com/static/img/exchanges/64x64/1170.png',
},
wault: {
name: 'Wault',
icon: 'https://wault.finance/wp-content/uploads/2021/04/cropped-wault-new-favicon-32x32.png',
icon: 'https://s2.coinmarketcap.com/static/img/coins/64x64/9478.png',
},
curve: {
name: 'Curve',
Expand All @@ -76,7 +76,7 @@ export const dexListConfig: DexList = {
},
'iron-stable': {
name: 'IronSwap',
icon: 'https://iron.finance/icons/icon-72x72.png',
icon: 'https://s2.coinmarketcap.com/static/img/coins/64x64/10484.png',
},
polydex: {
name: 'PolyDex',
Expand Down Expand Up @@ -128,7 +128,7 @@ export const dexListConfig: DexList = {
},
pantherswap: {
name: 'PantherSwap',
icon: 'https://pantherswap.com/favicon.ico',
icon: 'https://s2.coinmarketcap.com/static/img/coins/64x64/9778.png',
},
nerve: {
name: 'Nerve',
Expand Down Expand Up @@ -176,7 +176,7 @@ export const dexListConfig: DexList = {
},
axial: {
name: 'Axial',
icon: 'https://assets.website-files.com/6189dee5e79d6e8f7e214eba/618bf2f3e40e777d4210a84f_favicon.ico',
icon: 'https://s2.coinmarketcap.com/static/img/coins/64x64/14396.png',
},
lydia: {
name: 'Lydia',
Expand Down
51 changes: 51 additions & 0 deletions src/hooks/useAggregatorStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import useSWR from 'swr'
import { ChainId } from '@kyberswap/ks-sdk-core'

import { NETWORKS_INFO } from 'constants/networks'

type Response = {
pools: Record<
string,
{
poolSize: number
tvl: number
tokenSize: number
}
>
}

// It's recommended to use NETWORKS_INFO[chainId].route,
// but very unfortunately that BE uses `bsc` instead of `bnb`
const chainIdMapping: Partial<Record<ChainId, string>> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use NETWORKS_INFO variable
NETWORKS_INFO[chainId].route

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. They are all the same, except for BSC. I need bsc instead of bnb (as in its route), so I decided to manually do mapping like this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we deal with backend update bsc to bnb =)) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok let me ask

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BE refused this change request

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok this seems better

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank @nguyenhoaidanh. as long as it's not changed frequently, it's OK

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it never changes :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more than reusable. If you have a deep search on NETWORK_INFOS, there are fields only use once but still lie there.

So why would we put it there? It's the answers for the situation that everytime we think about a piece of data that different accross networks, we first think it must stay in NETWORK_INFOS.

Also, might you didn't care about maintainability, when we add a new chain, everything lie in one place is much more easily to find than many piece of data lie everywhere in the repository.

So in your case, what if we gonna add new chain like Celo for example. I fill up every infomation in constants/networks/celo.ts, but I missed this object, and this object dont shown up with error. So no one gonna known that I have missed this obj.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. It's bad 👍

[ChainId.BSCMAINNET]: 'bsc',
}

const useAggregatorStats = (chainId?: ChainId) => {
const chainString = chainId ? chainIdMapping[chainId] || NETWORKS_INFO[chainId].route : ''

return useSWR<Response>(`${process.env.REACT_APP_AGGREGATOR_API}/${chainString}/stats`, async (url: string) => {
if (!chainId || !chainString) {
const err = `chain (${chainId}) is not supported`
console.error(err)
throw err
}

const response = await fetch(url)
if (response.ok) {
const data = await response.json()
if (data && data.pools) {
return data
}

const err = `no pools found on ${chainString}`
console.error(err)
throw err
}

const err = `fetching stats on ${chainString} failed`
console.error(err)
throw err
})
}

export default useAggregatorStats
Loading