Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ace518311 | ||
|
|
67c776c995 | ||
|
|
719754c46c | ||
|
|
9170af888e | ||
|
|
b258f557d1 | ||
|
|
9d8c7f8e12 | ||
|
|
9c44e61e23 | ||
|
|
71db11b6ac | ||
|
|
db3328c8d9 | ||
|
|
34dfb41a1e | ||
|
|
e77fcd21dc | ||
|
|
0b7846ee1d | ||
|
|
f450d34d69 | ||
|
|
76ab349b9e | ||
|
|
5c3c1c67f5 | ||
|
|
9efd5da1f7 | ||
|
|
8fd894f2d1 | ||
|
|
cc22183388 | ||
|
|
9175dd10cc | ||
|
|
bbd50f066d | ||
|
|
2291e3ec20 | ||
|
|
28d8f0b0bb | ||
|
|
8bed3900ba | ||
|
|
a1000c6576 | ||
|
|
267204d98e | ||
|
|
74f50f1b7e | ||
|
|
a70aa41df2 | ||
|
|
587b659816 | ||
|
|
5388cab779 | ||
|
|
cadd68fb37 | ||
|
|
ab8ce37ceb | ||
|
|
5a9a71ae2d | ||
|
|
b93fd230bd | ||
|
|
e9a11bb604 | ||
|
|
c5afbedb3e | ||
|
|
5b2c44522e | ||
|
|
7fd4005154 | ||
|
|
48eab0d0ac | ||
|
|
c9ee1b3b32 | ||
|
|
eb4c305eff | ||
|
|
d9825622f1 | ||
|
|
8975086a69 | ||
|
|
ddf88345a9 | ||
|
|
32ac25556b | ||
|
|
50a599c005 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support
|
||||
url: https://discord.gg/EwFs3Pp
|
||||
url: https://discord.gg/FCfyBSbCU5
|
||||
about: Please ask and answer questions here
|
||||
- name: List a token
|
||||
url: https://github.com/Uniswap/default-token-list#adding-a-token
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -3,10 +3,10 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
jobs:
|
||||
run-linters:
|
||||
|
||||
4
.github/workflows/tests.yaml
vendored
4
.github/workflows/tests.yaml
vendored
@@ -2,10 +2,10 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
|
||||
@@ -12,7 +12,7 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
|
||||
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
|
||||
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
|
||||
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
@@ -3,18 +3,9 @@ describe('Lists', () => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('defaults to uniswap list', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
|
||||
})
|
||||
|
||||
// @TODO check if default lists are active when we have them
|
||||
it('change list', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#currency-search-change-list-button').click()
|
||||
cy.get('#list-row-tokens-1inch-eth .select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', '1inch')
|
||||
cy.get('#currency-search-change-list-button').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
|
||||
cy.get('.list-token-manage-button').click()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
describe('Warning', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap?outputCurrency=0x0a40f26d74274b7f22b28556a27b35d97ce08e0a')
|
||||
})
|
||||
|
||||
it('Check that warning is displayed', () => {
|
||||
cy.get('.token-warning-container').should('be.visible')
|
||||
})
|
||||
|
||||
it('Check that warning hides after button dismissal', () => {
|
||||
cy.get('.token-dismiss-button').should('be.disabled')
|
||||
cy.get('.understand-checkbox').click()
|
||||
cy.get('.token-dismiss-button').should('not.be.disabled')
|
||||
cy.get('.token-dismiss-button').click()
|
||||
cy.get('.token-warning-container').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
17
package.json
17
package.json
@@ -31,7 +31,7 @@
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/sdk": "3.0.3",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.17",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.19",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
@@ -82,18 +82,24 @@
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"serve": "^11.3.0",
|
||||
"serve": "^11.3.2",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"styled-components": "^4.2.0",
|
||||
"typescript": "^3.8.3",
|
||||
"use-count-up": "^2.2.5",
|
||||
"wcag-contrast": "^3.0.0"
|
||||
"wcag-contrast": "^3.0.0",
|
||||
"workbox-core": "^6.1.0",
|
||||
"workbox-expiration": "^6.1.0",
|
||||
"workbox-precaching": "^6.1.0",
|
||||
"workbox-routing": "^6.1.0",
|
||||
"workbox-strategies": "^6.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@walletconnect/web3-provider": "1.1.1-alpha.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start:service-worker": "yarn build && yarn serve -s build",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
@@ -117,5 +123,8 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"license": "GPL-3.0-or-later"
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@uniswap/default-token-list": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"short_name": "Uniswap",
|
||||
"name": "Uniswap",
|
||||
"background_color": "#fff",
|
||||
"display": "standalone",
|
||||
"homepage_url": "https://app.uniswap.org",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./images/192x192_App_Icon.png",
|
||||
@@ -16,7 +17,8 @@
|
||||
}
|
||||
],
|
||||
"orientation": "portrait",
|
||||
"display": "standalone",
|
||||
"theme_color": "#ff007a",
|
||||
"background_color": "#fff"
|
||||
"name": "Uniswap",
|
||||
"short_name": "Uniswap",
|
||||
"start_url": ".",
|
||||
"theme_color": "#ff007a"
|
||||
}
|
||||
|
||||
BIN
src/assets/images/token-list-logo.png
Normal file
BIN
src/assets/images/token-list-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src/assets/images/tokenlistsgrouped.png
Normal file
BIN
src/assets/images/tokenlistsgrouped.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
30
src/assets/svg/tokenlist.svg
Normal file
30
src/assets/svg/tokenlist.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg width="225" height="225" viewBox="0 0 225 225" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M74.8125 190.529C65.7561 190.513 55.5298 183.748 51.9715 175.42L19.9417 100.456C16.3834 92.1277 20.8404 85.39 29.8968 85.4068L111.417 85.5579C120.473 85.5747 130.699 92.3395 134.258 100.668L166.288 175.632C169.846 183.96 165.389 190.697 156.332 190.681L74.8125 190.529Z" fill="#131313"/>
|
||||
<path d="M92.1541 164.065C83.0977 164.049 72.8715 157.284 69.3132 148.956L28.3003 52.9672C24.7419 44.6391 29.199 37.9015 38.2554 37.9182L142.638 38.1117C151.695 38.1285 161.921 44.8933 165.479 53.2214L206.492 149.21C210.051 157.538 205.594 164.276 196.537 164.259L92.1541 164.065Z" fill="white"/>
|
||||
<path d="M92.1541 164.065C83.0977 164.049 72.8715 157.284 69.3132 148.956L28.3003 52.9672C24.7419 44.6391 29.199 37.9015 38.2554 37.9182L142.638 38.1117C151.695 38.1285 161.921 44.8933 165.479 53.2214L206.492 149.21C210.051 157.538 205.594 164.276 196.537 164.259L92.1541 164.065Z" fill="url(#paint0_radial)"/>
|
||||
<path d="M92.1541 164.065C83.0977 164.049 72.8715 157.284 69.3132 148.956L28.3003 52.9672C24.7419 44.6391 29.199 37.9015 38.2554 37.9182L142.638 38.1117C151.695 38.1285 161.921 44.8933 165.479 53.2214L206.492 149.21C210.051 157.538 205.594 164.276 196.537 164.259L92.1541 164.065Z" fill="url(#paint1_radial)"/>
|
||||
<path d="M92.1541 164.065C83.0977 164.049 72.8715 157.284 69.3132 148.956L28.3003 52.9672C24.7419 44.6391 29.199 37.9015 38.2554 37.9182L142.638 38.1117C151.695 38.1285 161.921 44.8933 165.479 53.2214L206.492 149.21C210.051 157.538 205.594 164.276 196.537 164.259L92.1541 164.065Z" fill="url(#paint2_radial)"/>
|
||||
<path d="M92.1541 164.065C83.0977 164.049 72.8715 157.284 69.3132 148.956L28.3003 52.9672C24.7419 44.6391 29.199 37.9015 38.2554 37.9182L142.638 38.1117C151.695 38.1285 161.921 44.8933 165.479 53.2214L206.492 149.21C210.051 157.538 205.594 164.276 196.537 164.259L92.1541 164.065Z" fill="url(#paint3_radial)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M92.958 165.95C82.7695 165.931 71.265 158.321 67.2619 148.952L26.2489 52.9632C22.2458 43.5941 27.26 36.0143 37.4485 36.0332L141.832 36.2266C152.02 36.2455 163.525 43.8559 167.528 53.225L208.541 149.214C212.544 158.583 207.53 166.163 197.341 166.144L92.958 165.95ZM71.3614 148.959C74.475 156.246 83.4229 162.166 91.3473 162.18L195.73 162.374C203.655 162.388 207.555 156.493 204.441 149.206L163.428 53.2174C160.315 45.9304 151.367 40.0111 143.442 39.9964L39.0592 39.803C31.1349 39.7883 27.2349 45.6837 30.3485 52.9708L71.3614 148.959Z" fill="#131313"/>
|
||||
<path d="M68.565 53.3425C81.1781 53.3659 95.4205 62.7875 100.376 74.3862C105.332 85.985 99.1246 95.3687 86.5115 95.3454C73.8984 95.322 59.6559 85.9004 54.7001 74.3016C49.7443 62.7028 55.9518 53.3191 68.565 53.3425Z" fill="#131313"/>
|
||||
<path d="M90.6891 104.981C103.302 105.004 117.545 114.425 122.5 126.024C127.456 137.623 121.249 147.007 108.636 146.983C96.0225 146.96 81.7801 137.538 76.8243 125.94C71.8685 114.341 78.076 104.957 90.6891 104.981Z" fill="#131313"/>
|
||||
<path d="M147.538 105.142C160.151 105.166 174.394 114.587 179.349 126.186C184.305 137.785 178.098 147.168 165.485 147.145C152.871 147.122 138.629 137.7 133.673 126.101C128.717 114.503 134.925 105.119 147.538 105.142Z" fill="#131313"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(134.41 68.3006) rotate(-33.9533) scale(90.6795 83.3208)">
|
||||
<stop offset="0.661458" stop-color="#C4FCF8"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(42.7873 129.218) rotate(-24.1606) scale(213.359 196.045)">
|
||||
<stop stop-color="#FF0099" stop-opacity="0.9"/>
|
||||
<stop offset="0.770833" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(176.854 148.655) rotate(-53.4908) scale(107.342 98.6309)">
|
||||
<stop stop-color="#FFEC43"/>
|
||||
<stop offset="0.805707" stop-color="#FFF6A8" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(57.5443 53.4752) rotate(20.3896) scale(137.027 125.907)">
|
||||
<stop offset="0.125" stop-color="#5886FE" stop-opacity="0.46"/>
|
||||
<stop offset="0.673044" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
12
src/components/Blocklist/index.tsx
Normal file
12
src/components/Blocklist/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React, { ReactNode, useMemo } from 'react'
|
||||
import { BLOCKED_ADDRESSES } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
export default function Blocklist({ children }: { children: ReactNode }) {
|
||||
const { account } = useActiveWeb3React()
|
||||
const blocked: boolean = useMemo(() => Boolean(account && BLOCKED_ADDRESSES.indexOf(account) !== -1), [account])
|
||||
if (blocked) {
|
||||
return <div>Blocked address</div>
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const Base = styled(RebassButton)<{
|
||||
width: ${({ width }) => (width ? width : '100%')};
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
border-radius: 20px;
|
||||
border-radius: ${({ borderRadius }) => borderRadius && borderRadius};
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
@@ -53,13 +53,15 @@ export const ButtonPrimary = styled(Base)`
|
||||
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)};
|
||||
background-color: ${({ theme, altDisabledStyle, disabled }) =>
|
||||
altDisabledStyle ? (disabled ? theme.bg3 : theme.primary1) : theme.bg3};
|
||||
color: ${({ theme, altDisabledStyle, disabled }) =>
|
||||
altDisabledStyle ? (disabled ? theme.text3 : 'white') : theme.text3};
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')};
|
||||
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.5' : '1')};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -97,15 +99,13 @@ export const ButtonGray = styled(Base)`
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg4)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg4)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg4)};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -159,6 +159,26 @@ export const ButtonPink = styled(Base)`
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonUNIGradient = styled(ButtonPrimary)`
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
background: radial-gradient(174.47% 188.91% at 1.84% 0%, #ff007a 0%, #2172e5 100%), #edeef2;
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
white-space: no-wrap;
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonOutlined = styled(Base)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
background-color: transparent;
|
||||
@@ -190,10 +210,10 @@ export const ButtonEmpty = styled(Base)`
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
@@ -288,6 +308,17 @@ export function ButtonDropdown({ disabled = false, children, ...rest }: { disabl
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropdownGrey({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonGray {...rest} disabled={disabled} style={{ borderRadius: '20px' }}>
|
||||
<RowBetween>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>{children}</div>
|
||||
<ChevronDown size={24} />
|
||||
</RowBetween>
|
||||
</ButtonGray>
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonOutlined {...rest} disabled={disabled}>
|
||||
|
||||
@@ -3,8 +3,8 @@ import styled from 'styled-components'
|
||||
import { CardProps, Text } from 'rebass'
|
||||
import { Box } from 'rebass/styled-components'
|
||||
|
||||
const Card = styled(Box)<{ padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: 100%;
|
||||
const Card = styled(Box)<{ width?: string; padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
padding: ${({ padding }) => padding};
|
||||
@@ -18,6 +18,10 @@ export const LightCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
`
|
||||
|
||||
export const LightGreyCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
||||
export const GreyCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Currency, Pair } from '@uniswap/sdk'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
|
||||
@@ -13,6 +13,7 @@ import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '../../hooks/useTheme'
|
||||
|
||||
const InputRow = styled.div<{ selected: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
@@ -154,7 +155,7 @@ export default function CurrencyInputPanel({
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account } = useActiveWeb3React()
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useContext(ThemeContext)
|
||||
const theme = useTheme()
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
|
||||
@@ -7,7 +7,7 @@ import useHttpLocations from '../../hooks/useHttpLocations'
|
||||
import { WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import Logo from '../Logo'
|
||||
|
||||
const getTokenLogoURL = (address: string) =>
|
||||
export const getTokenLogoURL = (address: string) =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
|
||||
const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
@@ -22,6 +22,7 @@ const StyledLogo = styled(Logo)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
border-radius: ${({ size }) => size};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
background-color: ${({ theme }) => theme.white};
|
||||
`
|
||||
|
||||
export default function CurrencyLogo({
|
||||
@@ -42,7 +43,6 @@ export default function CurrencyLogo({
|
||||
if (currency instanceof WrappedTokenInfo) {
|
||||
return [...uriLocations, getTokenLogoURL(currency.address)]
|
||||
}
|
||||
|
||||
return [getTokenLogoURL(currency.address)]
|
||||
}
|
||||
return []
|
||||
|
||||
@@ -17,7 +17,7 @@ import { CountUp } from 'use-count-up'
|
||||
import { TYPE, ExternalLink } from '../../theme'
|
||||
|
||||
import { YellowCard } from '../Card'
|
||||
import Settings from '../Settings'
|
||||
import { Moon, Sun } from 'react-feather'
|
||||
import Menu from '../Menu'
|
||||
|
||||
import Row, { RowFixed } from '../Row'
|
||||
@@ -83,7 +83,11 @@ const HeaderControls = styled.div`
|
||||
const HeaderElement = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
/* addresses safari's lack of support for "gap" */
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
flex-direction: row-reverse;
|
||||
@@ -123,9 +127,6 @@ const AccountElement = styled.div<{ active: boolean }>`
|
||||
:focus {
|
||||
border: 1px solid blue;
|
||||
}
|
||||
/* :hover {
|
||||
background-color: ${({ theme, active }) => (!active ? theme.bg2 : theme.bg4)};
|
||||
} */
|
||||
`
|
||||
|
||||
const UNIAmount = styled(AccountElement)`
|
||||
@@ -257,6 +258,35 @@ const StyledExternalLink = styled(ExternalLink).attrs({
|
||||
`}
|
||||
`
|
||||
|
||||
export const StyledMenuButton = styled.button`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
margin-left: 8px;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const NETWORK_LABELS: { [chainId in ChainId]?: string } = {
|
||||
[ChainId.RINKEBY]: 'Rinkeby',
|
||||
[ChainId.ROPSTEN]: 'Ropsten',
|
||||
@@ -269,7 +299,8 @@ export default function Header() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
|
||||
const [isDark] = useDarkModeManager()
|
||||
// const [isDark] = useDarkModeManager()
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
const toggleClaimModal = useToggleSelfClaimModal()
|
||||
|
||||
@@ -294,7 +325,7 @@ export default function Header() {
|
||||
<HeaderRow>
|
||||
<Title href=".">
|
||||
<UniIcon>
|
||||
<img width={'24px'} src={isDark ? LogoDark : Logo} alt="logo" />
|
||||
<img width={'24px'} src={darkMode ? LogoDark : Logo} alt="logo" />
|
||||
</UniIcon>
|
||||
</Title>
|
||||
<HeaderLinks>
|
||||
@@ -378,7 +409,9 @@ export default function Header() {
|
||||
</AccountElement>
|
||||
</HeaderElement>
|
||||
<HeaderElementWrap>
|
||||
<Settings />
|
||||
<StyledMenuButton onClick={() => toggleDarkMode()}>
|
||||
{darkMode ? <Moon size={20} /> : <Sun size={20} />}
|
||||
</StyledMenuButton>
|
||||
<Menu />
|
||||
</HeaderElementWrap>
|
||||
</HeaderControls>
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function Menu() {
|
||||
<Code size={14} />
|
||||
Code
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://discord.gg/EwFs3Pp">
|
||||
<MenuItem id="link" href="https://discord.gg/FCfyBSbCU5">
|
||||
<MessageCircle size={14} />
|
||||
Discord
|
||||
</MenuItem>
|
||||
|
||||
@@ -115,7 +115,13 @@ export default function Modal({
|
||||
{fadeTransition.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
unstable_lockFocusAcrossFrames={false}
|
||||
>
|
||||
<StyledDialogContent
|
||||
{...(isMobile
|
||||
? {
|
||||
|
||||
@@ -6,7 +6,11 @@ import { NavLink, Link as HistoryLink } from 'react-router-dom'
|
||||
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
// import QuestionHelper from '../QuestionHelper'
|
||||
import Settings from '../Settings'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { AppDispatch } from 'state'
|
||||
import { resetMintState } from 'state/mint/actions'
|
||||
|
||||
const Tabs = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
@@ -69,32 +73,34 @@ export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
|
||||
export function FindPoolTabs() {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>Import Pool</ActiveText>
|
||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||
<Settings />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export function AddRemoveTabs({ adding, creating }: { adding: boolean; creating: boolean }) {
|
||||
// reset states on back
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
|
||||
<HistoryLink
|
||||
to="/pool"
|
||||
onClick={() => {
|
||||
adding && dispatch(resetMintState())
|
||||
}}
|
||||
>
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>{creating ? 'Create a pair' : adding ? 'Add Liquidity' : 'Remove Liquidity'}</ActiveText>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
<Settings />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useCallback, useMemo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { acceptListUpdate } from '../../state/lists/actions'
|
||||
@@ -12,6 +13,11 @@ import { ButtonSecondary } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
export const ChangesList = styled.ul`
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export default function ListUpdatePopup({
|
||||
popKey,
|
||||
listUrl,
|
||||
@@ -64,7 +70,7 @@ export default function ListUpdatePopup({
|
||||
An update is available for the token list "{oldList.name}" (
|
||||
{listVersionLabel(oldList.version)} to {listVersionLabel(newList.version)}).
|
||||
</Text>
|
||||
<ul>
|
||||
<ChangesList>
|
||||
{tokensAdded.length > 0 ? (
|
||||
<li>
|
||||
{tokensAdded.map((token, i) => (
|
||||
@@ -88,7 +94,7 @@ export default function ListUpdatePopup({
|
||||
</li>
|
||||
) : null}
|
||||
{numTokensChanged > 0 ? <li>{numTokensChanged} tokens updated</li> : null}
|
||||
</ul>
|
||||
</ChangesList>
|
||||
</div>
|
||||
<AutoRow>
|
||||
<div style={{ flexGrow: 1, marginRight: 12 }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSBI, Pair, Percent } from '@uniswap/sdk'
|
||||
import { JSBI, Pair, Percent, TokenAmount } from '@uniswap/sdk'
|
||||
import { darken } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
@@ -22,8 +22,9 @@ import Card, { GreyCard, LightCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
import { Dots } from '../swap/styleds'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
|
||||
export const FixedHeightRow = styled(RowBetween)`
|
||||
height: 24px;
|
||||
@@ -47,6 +48,7 @@ interface PositionCardProps {
|
||||
pair: Pair
|
||||
showUnwrapped?: boolean
|
||||
border?: string
|
||||
stakedBalance?: TokenAmount // optional balance to indicate that liquidity is deposited in mining pool
|
||||
}
|
||||
|
||||
export function MinimalPositionCard({ pair, showUnwrapped = false, border }: PositionCardProps) {
|
||||
@@ -157,7 +159,7 @@ export function MinimalPositionCard({ pair, showUnwrapped = false, border }: Pos
|
||||
)
|
||||
}
|
||||
|
||||
export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
export default function FullPositionCard({ pair, border, stakedBalance }: PositionCardProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const currency0 = unwrappedToken(pair.token0)
|
||||
@@ -165,9 +167,12 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const userDefaultPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
|
||||
|
||||
// if staked balance balance provided, add to standard liquidity amount
|
||||
const userPoolBalance = stakedBalance ? userDefaultPoolBalance?.add(stakedBalance) : userDefaultPoolBalance
|
||||
|
||||
const poolTokenPercentage =
|
||||
!!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
? new Percent(userPoolBalance.raw, totalPoolTokens.raw)
|
||||
@@ -192,13 +197,12 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
<CardNoise />
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
|
||||
<AutoRow gap="8px">
|
||||
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
|
||||
</AutoRow>
|
||||
<RowFixed gap="8px">
|
||||
<ButtonEmpty
|
||||
padding="6px 8px"
|
||||
@@ -208,7 +212,6 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
>
|
||||
{showMore ? (
|
||||
<>
|
||||
{' '}
|
||||
Manage
|
||||
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
||||
</>
|
||||
@@ -226,12 +229,22 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
<AutoColumn gap="8px">
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
Your total pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
{stakedBalance && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pool tokens in rewards pool:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{stakedBalance.toSignificant(4)}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
@@ -273,7 +286,9 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
Your pool share:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
{poolTokenPercentage
|
||||
? (poolTokenPercentage.toFixed(2) === '0.00' ? '<0.01' : poolTokenPercentage.toFixed(2)) + '%'
|
||||
: '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
|
||||
@@ -285,26 +300,39 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
View accrued fees and analytics<span style={{ fontSize: '11px' }}>↗</span>
|
||||
</ExternalLink>
|
||||
</ButtonSecondary>
|
||||
<RowBetween marginTop="10px">
|
||||
{userDefaultPoolBalance && JSBI.greaterThan(userDefaultPoolBalance.raw, BIG_INT_ZERO) && (
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
width="48%"
|
||||
>
|
||||
Add
|
||||
</ButtonPrimary>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
width="48%"
|
||||
to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
>
|
||||
Remove
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
)}
|
||||
{stakedBalance && JSBI.greaterThan(stakedBalance.raw, BIG_INT_ZERO) && (
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
width="48%"
|
||||
to={`/uni/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
width="100%"
|
||||
>
|
||||
Add
|
||||
Manage Liquidity in Rewards Pool
|
||||
</ButtonPrimary>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
as={Link}
|
||||
width="48%"
|
||||
to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}
|
||||
>
|
||||
Remove
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import styled from 'styled-components'
|
||||
import { Box } from 'rebass/styled-components'
|
||||
|
||||
const Row = styled(Box)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: 100%;
|
||||
const Row = styled(Box)<{
|
||||
width?: string
|
||||
align?: string
|
||||
justify?: string
|
||||
padding?: string
|
||||
border?: string
|
||||
borderRadius?: string
|
||||
}>`
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
display: flex;
|
||||
padding: 0;
|
||||
align-items: ${({ align }) => (align ? align : 'center')};
|
||||
align-items: ${({ align }) => align ?? 'center'};
|
||||
justify-content: ${({ justify }) => justify ?? 'flex-start'};
|
||||
padding: ${({ padding }) => padding};
|
||||
border: ${({ border }) => border};
|
||||
border-radius: ${({ borderRadius }) => borderRadius};
|
||||
|
||||
@@ -4,18 +4,23 @@ import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { WrappedTokenInfo, useCombinedActiveList } from '../../state/lists/hooks'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { useIsUserAddedToken } from '../../hooks/Tokens'
|
||||
import { TYPE } from '../../theme'
|
||||
import { useIsUserAddedToken, useAllInactiveTokens } from '../../hooks/Tokens'
|
||||
import Column from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import { RowFixed, RowBetween } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import { FadedSpan, MenuItem } from './styleds'
|
||||
import { MenuItem } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
import ImportRow from './ImportRow'
|
||||
import { wrappedCurrency } from 'utils/wrappedCurrency'
|
||||
import { LightGreyCard } from 'components/Card'
|
||||
import TokenListLogo from '../../assets/svg/tokenlist.svg'
|
||||
import QuestionHelper from 'components/QuestionHelper'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
|
||||
@@ -42,6 +47,14 @@ const Tag = styled.div`
|
||||
margin-right: 4px;
|
||||
`
|
||||
|
||||
const FixedContentRow = styled.div`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
@@ -51,6 +64,10 @@ const TagContainer = styled.div`
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const TokenListLogoWrapper = styled.img`
|
||||
height: 20px;
|
||||
`
|
||||
|
||||
function TokenTags({ currency }: { currency: Currency }) {
|
||||
if (!(currency instanceof WrappedTokenInfo)) {
|
||||
return <span />
|
||||
@@ -93,16 +110,13 @@ function CurrencyRow({
|
||||
otherSelected: boolean
|
||||
style: CSSProperties
|
||||
}) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const { account } = useActiveWeb3React()
|
||||
const key = currencyKey(currency)
|
||||
const selectedTokenList = useSelectedTokenList()
|
||||
const selectedTokenList = useCombinedActiveList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
|
||||
const customAdded = useIsUserAddedToken(currency)
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
// only show add or remove buttons if not on selected list
|
||||
return (
|
||||
<MenuItem
|
||||
@@ -117,34 +131,9 @@ function CurrencyRow({
|
||||
<Text title={currency.name} fontWeight={500}>
|
||||
{currency.symbol}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
{!isOnSelectedList && customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Added by user
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (chainId && currency instanceof Token) removeToken(chainId, currency.address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isOnSelectedList && !customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Found by address
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (currency instanceof Token) addToken(currency)
|
||||
}}
|
||||
>
|
||||
(Add)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
</FadedSpan>
|
||||
<TYPE.darkGray ml="0px" fontSize={'12px'} fontWeight={300}>
|
||||
{currency.name} {!isOnSelectedList && customAdded && '• Added by user'}
|
||||
</TYPE.darkGray>
|
||||
</Column>
|
||||
<TokenTags currency={currency} />
|
||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||
@@ -161,7 +150,10 @@ export default function CurrencyList({
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
fixedListRef,
|
||||
showETH
|
||||
showETH,
|
||||
showImportView,
|
||||
setImportToken,
|
||||
breakIndex
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
@@ -170,8 +162,24 @@ export default function CurrencyList({
|
||||
otherCurrency?: Currency | null
|
||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||
showETH: boolean
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
breakIndex: number | undefined
|
||||
}) {
|
||||
const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH])
|
||||
const itemData: (Currency | undefined)[] = useMemo(() => {
|
||||
let formatted: (Currency | undefined)[] = showETH ? [Currency.ETHER, ...currencies] : currencies
|
||||
if (breakIndex !== undefined) {
|
||||
formatted = [...formatted.slice(0, breakIndex), undefined, ...formatted.slice(breakIndex, formatted.length)]
|
||||
}
|
||||
return formatted
|
||||
}, [breakIndex, currencies, showETH])
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
const inactiveTokens: {
|
||||
[address: string]: Token
|
||||
} = useAllInactiveTokens()
|
||||
|
||||
const Row = useCallback(
|
||||
({ data, index, style }) => {
|
||||
@@ -179,17 +187,62 @@ export default function CurrencyList({
|
||||
const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency))
|
||||
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
|
||||
const handleSelect = () => onCurrencySelect(currency)
|
||||
return (
|
||||
<CurrencyRow
|
||||
style={style}
|
||||
currency={currency}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
/>
|
||||
)
|
||||
|
||||
const token = wrappedCurrency(currency, chainId)
|
||||
|
||||
const showImport = inactiveTokens && token && Object.keys(inactiveTokens).includes(token.address)
|
||||
|
||||
if (index === breakIndex || !data) {
|
||||
return (
|
||||
<FixedContentRow style={style}>
|
||||
<LightGreyCard padding="8px 12px" borderRadius="8px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TokenListLogoWrapper src={TokenListLogo} />
|
||||
<TYPE.main ml="6px" fontSize="12px" color={theme.text1}>
|
||||
Expanded results from inactive Token Lists
|
||||
</TYPE.main>
|
||||
</RowFixed>
|
||||
<QuestionHelper text="Tokens from inactive lists. Import specific tokens below or click 'Manage' to activate more lists." />
|
||||
</RowBetween>
|
||||
</LightGreyCard>
|
||||
</FixedContentRow>
|
||||
)
|
||||
}
|
||||
|
||||
if (showImport && token) {
|
||||
return (
|
||||
<ImportRow
|
||||
style={style}
|
||||
token={token}
|
||||
showImportView={showImportView}
|
||||
setImportToken={setImportToken}
|
||||
dim={true}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<CurrencyRow
|
||||
style={style}
|
||||
currency={currency}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
[onCurrencySelect, otherCurrency, selectedCurrency]
|
||||
[
|
||||
chainId,
|
||||
inactiveTokens,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
selectedCurrency,
|
||||
setImportToken,
|
||||
showImportView,
|
||||
breakIndex,
|
||||
theme.text1
|
||||
]
|
||||
)
|
||||
|
||||
const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), [])
|
||||
|
||||
@@ -1,27 +1,44 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useSelectedListInfo } from '../../state/lists/hooks'
|
||||
import { CloseIcon, LinkStyledButton, TYPE } from '../../theme'
|
||||
import { useAllTokens, useToken, useIsUserAddedToken, useFoundOnInactiveList } from '../../hooks/Tokens'
|
||||
import { CloseIcon, TYPE, ButtonText, IconWrapper } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Card from '../Card'
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import CommonBases from './CommonBases'
|
||||
import CurrencyList from './CurrencyList'
|
||||
import { filterTokens } from './filtering'
|
||||
import SortButton from './SortButton'
|
||||
import { filterTokens, useSortedTokensByQuery } from './filtering'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
import styled from 'styled-components'
|
||||
import useToggle from 'hooks/useToggle'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import ImportRow from './ImportRow'
|
||||
import { Edit } from 'react-feather'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
|
||||
const ContentWrapper = styled(Column)`
|
||||
width: 100%;
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const Footer = styled.div`
|
||||
width: 100%;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
border-top: 1px solid ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
||||
interface CurrencySearchProps {
|
||||
isOpen: boolean
|
||||
@@ -30,7 +47,9 @@ interface CurrencySearchProps {
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
onChangeList: () => void
|
||||
showManageView: () => void
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
@@ -40,20 +59,28 @@ export function CurrencySearch({
|
||||
showCommonBases,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onChangeList
|
||||
showManageView,
|
||||
showImportView,
|
||||
setImportToken
|
||||
}: CurrencySearchProps) {
|
||||
const { t } = useTranslation()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const theme = useTheme()
|
||||
|
||||
// refs for fixed size lists
|
||||
const fixedList = useRef<FixedSizeList>()
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const debouncedQuery = useDebounce(searchQuery, 200)
|
||||
|
||||
const [invertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(searchQuery)
|
||||
const searchToken = useToken(searchQuery)
|
||||
const isAddressSearch = isAddress(debouncedQuery)
|
||||
const searchToken = useToken(debouncedQuery)
|
||||
const searchTokenIsAdded = useIsUserAddedToken(searchToken)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddressSearch) {
|
||||
@@ -66,33 +93,21 @@ export function CurrencySearch({
|
||||
}, [isAddressSearch])
|
||||
|
||||
const showETH: boolean = useMemo(() => {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
const s = debouncedQuery.toLowerCase().trim()
|
||||
return s === '' || s === 'e' || s === 'et' || s === 'eth'
|
||||
}, [searchQuery])
|
||||
}, [debouncedQuery])
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (isAddressSearch) return searchToken ? [searchToken] : []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isAddressSearch, searchToken, allTokens, searchQuery])
|
||||
return filterTokens(Object.values(allTokens), debouncedQuery)
|
||||
}, [allTokens, debouncedQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
const sortedTokens: Token[] = useMemo(() => {
|
||||
return filteredTokens.sort(tokenComparator)
|
||||
}, [filteredTokens, tokenComparator])
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
const filteredSortedTokens = useSortedTokensByQuery(sortedTokens, debouncedQuery)
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
@@ -119,12 +134,12 @@ export function CurrencySearch({
|
||||
const handleEnter = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
const s = debouncedQuery.toLowerCase().trim()
|
||||
if (s === 'eth') {
|
||||
handleCurrencySelect(ETHER)
|
||||
} else if (filteredSortedTokens.length > 0) {
|
||||
if (
|
||||
filteredSortedTokens[0].symbol?.toLowerCase() === searchQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens[0].symbol?.toLowerCase() === debouncedQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens.length === 1
|
||||
) {
|
||||
handleCurrencySelect(filteredSortedTokens[0])
|
||||
@@ -132,83 +147,88 @@ export function CurrencySearch({
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredSortedTokens, handleCurrencySelect, searchQuery]
|
||||
[filteredSortedTokens, handleCurrencySelect, debouncedQuery]
|
||||
)
|
||||
|
||||
const selectedListInfo = useSelectedListInfo()
|
||||
// menu ui
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
// if no results on main list, show option to expand into inactive
|
||||
const inactiveTokens = useFoundOnInactiveList(debouncedQuery)
|
||||
const filteredInactiveTokens: Token[] = useSortedTokensByQuery(inactiveTokens, debouncedQuery)
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<ContentWrapper>
|
||||
<PaddedColumn gap="16px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper text="Find a token by searching for its name or symbol or by pasting its address below." />
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
autoComplete="off"
|
||||
value={searchQuery}
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
</Row>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={selectedCurrency} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div style={{ flex: '1' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
showETH={showETH}
|
||||
currencies={filteredSortedTokens}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={selectedCurrency}
|
||||
fixedListRef={fixedList}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Card>
|
||||
<RowBetween>
|
||||
{selectedListInfo.current ? (
|
||||
<Row>
|
||||
{selectedListInfo.current.logoURI ? (
|
||||
<ListLogo
|
||||
style={{ marginRight: 12 }}
|
||||
logoURI={selectedListInfo.current.logoURI}
|
||||
alt={`${selectedListInfo.current.name} list logo`}
|
||||
/>
|
||||
) : null}
|
||||
<TYPE.main id="currency-search-selected-list-name">{selectedListInfo.current.name}</TYPE.main>
|
||||
</Row>
|
||||
) : null}
|
||||
<LinkStyledButton
|
||||
style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }}
|
||||
onClick={onChangeList}
|
||||
id="currency-search-change-list-button"
|
||||
>
|
||||
{selectedListInfo.current ? 'Change' : 'Select a list'}
|
||||
</LinkStyledButton>
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</Column>
|
||||
{searchToken && !searchTokenIsAdded ? (
|
||||
<Column style={{ padding: '20px 0', height: '100%' }}>
|
||||
<ImportRow token={searchToken} showImportView={showImportView} setImportToken={setImportToken} />
|
||||
</Column>
|
||||
) : filteredSortedTokens?.length > 0 || filteredInactiveTokens?.length > 0 ? (
|
||||
<div style={{ flex: '1' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
showETH={showETH}
|
||||
currencies={
|
||||
filteredInactiveTokens ? filteredSortedTokens.concat(filteredInactiveTokens) : filteredSortedTokens
|
||||
}
|
||||
breakIndex={inactiveTokens && filteredSortedTokens ? filteredSortedTokens.length : undefined}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={selectedCurrency}
|
||||
fixedListRef={fixedList}
|
||||
showImportView={showImportView}
|
||||
setImportToken={setImportToken}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
) : (
|
||||
<Column style={{ padding: '20px', height: '100%' }}>
|
||||
<TYPE.main color={theme.text3} textAlign="center" mb="20px">
|
||||
No results found.
|
||||
</TYPE.main>
|
||||
</Column>
|
||||
)}
|
||||
<Footer>
|
||||
<Row justify="center">
|
||||
<ButtonText onClick={showManageView} color={theme.blue1} className="list-token-manage-button">
|
||||
<RowFixed>
|
||||
<IconWrapper size="16px" marginRight="6px">
|
||||
<Edit />
|
||||
</IconWrapper>
|
||||
<TYPE.main color={theme.blue1}>Manage</TYPE.main>
|
||||
</RowFixed>
|
||||
</ButtonText>
|
||||
</Row>
|
||||
</Footer>
|
||||
</ContentWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Currency } from '@uniswap/sdk'
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import Modal from '../Modal'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import { ListSelect } from './ListSelect'
|
||||
import { ImportToken } from './ImportToken'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import Manage from './Manage'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { ImportList } from './ImportList'
|
||||
|
||||
interface CurrencySearchModalProps {
|
||||
isOpen: boolean
|
||||
@@ -15,6 +18,13 @@ interface CurrencySearchModalProps {
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export enum CurrencyModalView {
|
||||
search,
|
||||
manage,
|
||||
importToken,
|
||||
importList
|
||||
}
|
||||
|
||||
export default function CurrencySearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
@@ -23,12 +33,12 @@ export default function CurrencySearchModal({
|
||||
otherSelectedCurrency,
|
||||
showCommonBases = false
|
||||
}: CurrencySearchModalProps) {
|
||||
const [listView, setListView] = useState<boolean>(false)
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
|
||||
const lastOpen = useLast(isOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !lastOpen) {
|
||||
setListView(false)
|
||||
setModalView(CurrencyModalView.search)
|
||||
}
|
||||
}, [isOpen, lastOpen])
|
||||
|
||||
@@ -40,35 +50,54 @@ export default function CurrencySearchModal({
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
const handleClickChangeList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Change Lists'
|
||||
})
|
||||
setListView(true)
|
||||
}, [])
|
||||
const handleClickBack = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Back'
|
||||
})
|
||||
setListView(false)
|
||||
}, [])
|
||||
// for token import view
|
||||
const prevView = usePrevious(modalView)
|
||||
|
||||
// used for import token flow
|
||||
const [importToken, setImportToken] = useState<Token | undefined>()
|
||||
|
||||
// used for import list
|
||||
const [importList, setImportList] = useState<TokenList | undefined>()
|
||||
const [listURL, setListUrl] = useState<string | undefined>()
|
||||
|
||||
// change min height if not searching
|
||||
const minHeight = modalView === CurrencyModalView.importToken || modalView === CurrencyModalView.importList ? 40 : 80
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={listView ? 40 : 80}>
|
||||
{listView ? (
|
||||
<ListSelect onDismiss={onDismiss} onBack={handleClickBack} />
|
||||
) : (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
|
||||
{modalView === CurrencyModalView.search ? (
|
||||
<CurrencySearch
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
onChangeList={handleClickChangeList}
|
||||
selectedCurrency={selectedCurrency}
|
||||
otherSelectedCurrency={otherSelectedCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
showImportView={() => setModalView(CurrencyModalView.importToken)}
|
||||
setImportToken={setImportToken}
|
||||
showManageView={() => setModalView(CurrencyModalView.manage)}
|
||||
/>
|
||||
) : modalView === CurrencyModalView.importToken && importToken ? (
|
||||
<ImportToken
|
||||
tokens={[importToken]}
|
||||
onDismiss={onDismiss}
|
||||
onBack={() =>
|
||||
setModalView(prevView && prevView !== CurrencyModalView.importToken ? prevView : CurrencyModalView.search)
|
||||
}
|
||||
handleCurrencySelect={handleCurrencySelect}
|
||||
/>
|
||||
) : modalView === CurrencyModalView.importList && importList && listURL ? (
|
||||
<ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
|
||||
) : modalView === CurrencyModalView.manage ? (
|
||||
<Manage
|
||||
onDismiss={onDismiss}
|
||||
setModalView={setModalView}
|
||||
setImportToken={setImportToken}
|
||||
setImportList={setImportList}
|
||||
setListUrl={setListUrl}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
|
||||
163
src/components/SearchModal/ImportList.tsx
Normal file
163
src/components/SearchModal/ImportList.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import ReactGA from 'react-ga'
|
||||
import { TYPE, CloseIcon } from 'theme'
|
||||
import Card from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { RowBetween, RowFixed, AutoRow } from 'components/Row'
|
||||
import { ArrowLeft, AlertTriangle } from 'react-feather'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { transparentize } from 'polished'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { SectionBreak } from 'components/swap/styleds'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import ListLogo from 'components/ListLogo'
|
||||
import { PaddedColumn, Checkbox, TextDot } from './styleds'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { AppDispatch } from 'state'
|
||||
import { useFetchListCallback } from 'hooks/useFetchListCallback'
|
||||
import { removeList, enableList } from 'state/lists/actions'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { useAllLists } from 'state/lists/hooks'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
interface ImportProps {
|
||||
listURL: string
|
||||
list: TokenList
|
||||
onDismiss: () => void
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
}
|
||||
|
||||
export function ImportList({ listURL, list, setModalView, onDismiss }: ImportProps) {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
// user must accept
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
|
||||
const lists = useAllLists()
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
// monitor is list is loading
|
||||
const adding = Boolean(lists[listURL]?.loadingRequestId)
|
||||
const [addError, setAddError] = useState<string | null>(null)
|
||||
|
||||
const handleAddList = useCallback(() => {
|
||||
if (adding) return
|
||||
setAddError(null)
|
||||
fetchList(listURL)
|
||||
.then(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listURL
|
||||
})
|
||||
|
||||
// turn list on
|
||||
dispatch(enableList(listURL))
|
||||
// go back to lists
|
||||
setModalView(CurrencyModalView.manage)
|
||||
})
|
||||
.catch(error => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listURL
|
||||
})
|
||||
setAddError(error.message)
|
||||
dispatch(removeList(listURL))
|
||||
})
|
||||
}, [adding, dispatch, fetchList, listURL, setModalView])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
|
||||
<RowBetween>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.manage)} />
|
||||
<TYPE.mediumHeader>Import List</TYPE.mediumHeader>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<SectionBreak />
|
||||
<PaddedColumn gap="md">
|
||||
<AutoColumn gap="md">
|
||||
<Card backgroundColor={theme.bg2} padding="12px 20px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
{list.logoURI && <ListLogo logoURI={list.logoURI} size="40px" />}
|
||||
<AutoColumn gap="sm" style={{ marginLeft: '20px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.body fontWeight={600} mr="6px">
|
||||
{list.name}
|
||||
</TYPE.body>
|
||||
<TextDot />
|
||||
<TYPE.main fontSize={'16px'} ml="6px">
|
||||
{list.tokens.length} tokens
|
||||
</TYPE.main>
|
||||
</RowFixed>
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listURL}`}>
|
||||
<TYPE.main fontSize={'12px'} color={theme.blue1}>
|
||||
{listURL}
|
||||
</TYPE.main>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</Card>
|
||||
<Card style={{ backgroundColor: transparentize(0.8, theme.red1) }}>
|
||||
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<AlertTriangle stroke={theme.red1} size={32} />
|
||||
<TYPE.body fontWeight={500} fontSize={20} color={theme.red1}>
|
||||
Import at your own risk{' '}
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<TYPE.body fontWeight={500} color={theme.red1}>
|
||||
By adding this list you are implicitly trusting that the data is correct. Anyone can create a list,
|
||||
including creating fake versions of existing lists and lists that claim to represent projects that do
|
||||
not have one.
|
||||
</TYPE.body>
|
||||
<TYPE.body fontWeight={600} color={theme.red1}>
|
||||
If you purchase a token from this list, you may not be able to sell it back.
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
|
||||
<Checkbox
|
||||
name="confirmed"
|
||||
type="checkbox"
|
||||
checked={confirmed}
|
||||
onChange={() => setConfirmed(!confirmed)}
|
||||
/>
|
||||
<TYPE.body ml="10px" fontSize="16px" color={theme.red1} fontWeight={500}>
|
||||
I understand
|
||||
</TYPE.body>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
|
||||
<ButtonPrimary
|
||||
disabled={!confirmed}
|
||||
altDisabledStyle={true}
|
||||
borderRadius="20px"
|
||||
padding="10px 1rem"
|
||||
onClick={handleAddList}
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
{/* </Card> */}
|
||||
</PaddedColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
108
src/components/SearchModal/ImportRow.tsx
Normal file
108
src/components/SearchModal/ImportRow.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { CSSProperties } from 'react'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { AutoRow, RowFixed } from 'components/Row'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { TYPE } from 'theme'
|
||||
import ListLogo from 'components/ListLogo'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import { useCombinedInactiveList } from 'state/lists/hooks'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import styled from 'styled-components'
|
||||
import { useIsUserAddedToken, useIsTokenActive } from 'hooks/Tokens'
|
||||
import { CheckCircle } from 'react-feather'
|
||||
|
||||
const TokenSection = styled.div<{ dim?: boolean }>`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(auto, 1fr) auto;
|
||||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
opacity: ${({ dim }) => (dim ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const CheckIcon = styled(CheckCircle)`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 6px;
|
||||
stroke: ${({ theme }) => theme.green1};
|
||||
`
|
||||
|
||||
const NameOverflow = styled.div`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 140px;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
export default function ImportRow({
|
||||
token,
|
||||
style,
|
||||
dim,
|
||||
showImportView,
|
||||
setImportToken
|
||||
}: {
|
||||
token: Token
|
||||
style?: CSSProperties
|
||||
dim?: boolean
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
}) {
|
||||
// gloabls
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
// check if token comes from list
|
||||
const inactiveTokenList = useCombinedInactiveList()
|
||||
const list = chainId && inactiveTokenList?.[chainId]?.[token.address]?.list
|
||||
|
||||
// check if already active on list or local storage tokens
|
||||
const isAdded = useIsUserAddedToken(token)
|
||||
const isActive = useIsTokenActive(token)
|
||||
|
||||
return (
|
||||
<TokenSection style={style}>
|
||||
<CurrencyLogo currency={token} size={'24px'} style={{ opacity: dim ? '0.6' : '1' }} />
|
||||
<AutoColumn gap="4px" style={{ opacity: dim ? '0.6' : '1' }}>
|
||||
<AutoRow>
|
||||
<TYPE.body fontWeight={500}>{token.symbol}</TYPE.body>
|
||||
<TYPE.darkGray ml="8px" fontWeight={300}>
|
||||
<NameOverflow title={token.name}>{token.name}</NameOverflow>
|
||||
</TYPE.darkGray>
|
||||
</AutoRow>
|
||||
{list && list.logoURI && (
|
||||
<RowFixed>
|
||||
<TYPE.small mr="4px" color={theme.text3}>
|
||||
via {list.name}
|
||||
</TYPE.small>
|
||||
<ListLogo logoURI={list.logoURI} size="12px" />
|
||||
</RowFixed>
|
||||
)}
|
||||
</AutoColumn>
|
||||
{!isActive && !isAdded ? (
|
||||
<ButtonPrimary
|
||||
width="fit-content"
|
||||
padding="6px 12px"
|
||||
fontWeight={500}
|
||||
fontSize="14px"
|
||||
onClick={() => {
|
||||
setImportToken && setImportToken(token)
|
||||
showImportView()
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<RowFixed style={{ minWidth: 'fit-content' }}>
|
||||
<CheckIcon />
|
||||
<TYPE.main color={theme.green1}>Active</TYPE.main>
|
||||
</RowFixed>
|
||||
)}
|
||||
</TokenSection>
|
||||
)
|
||||
}
|
||||
164
src/components/SearchModal/ImportToken.tsx
Normal file
164
src/components/SearchModal/ImportToken.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Token, Currency } from '@uniswap/sdk'
|
||||
import styled from 'styled-components'
|
||||
import { TYPE, CloseIcon } from 'theme'
|
||||
import Card from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { RowBetween, RowFixed, AutoRow } from 'components/Row'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { ArrowLeft, AlertTriangle } from 'react-feather'
|
||||
import { transparentize } from 'polished'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { SectionBreak } from 'components/swap/styleds'
|
||||
import { useAddUserToken } from 'state/user/hooks'
|
||||
import { getEtherscanLink } from 'utils'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { useCombinedInactiveList } from 'state/lists/hooks'
|
||||
import ListLogo from 'components/ListLogo'
|
||||
import { PaddedColumn, Checkbox } from './styleds'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const WarningWrapper = styled(Card)<{ highWarning: boolean }>`
|
||||
background-color: ${({ theme, highWarning }) =>
|
||||
highWarning ? transparentize(0.8, theme.red1) : transparentize(0.8, theme.yellow2)};
|
||||
width: fit-content;
|
||||
`
|
||||
|
||||
const AddressText = styled(TYPE.blue)`
|
||||
font-size: 12px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
font-size: 10px;
|
||||
`}
|
||||
`
|
||||
|
||||
interface ImportProps {
|
||||
tokens: Token[]
|
||||
onBack?: () => void
|
||||
onDismiss?: () => void
|
||||
handleCurrencySelect?: (currency: Currency) => void
|
||||
}
|
||||
|
||||
export function ImportToken({ tokens, onBack, onDismiss, handleCurrencySelect }: ImportProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
// use for showing import source on inactive tokens
|
||||
const inactiveTokenList = useCombinedInactiveList()
|
||||
|
||||
// higher warning severity if either is not on a list
|
||||
const fromLists =
|
||||
(chainId && inactiveTokenList?.[chainId]?.[tokens[0]?.address]?.list) ||
|
||||
(chainId && inactiveTokenList?.[chainId]?.[tokens[1]?.address]?.list)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
|
||||
<RowBetween>
|
||||
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div></div>}
|
||||
<TYPE.mediumHeader>Import {tokens.length > 1 ? 'Tokens' : 'Token'}</TYPE.mediumHeader>
|
||||
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div></div>}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<SectionBreak />
|
||||
<PaddedColumn gap="md">
|
||||
{tokens.map(token => {
|
||||
const list = chainId && inactiveTokenList?.[chainId]?.[token.address]?.list
|
||||
return (
|
||||
<Card backgroundColor={theme.bg2} key={'import' + token.address} className=".token-warning-container">
|
||||
<AutoColumn gap="10px">
|
||||
<AutoRow align="center">
|
||||
<CurrencyLogo currency={token} size={'24px'} />
|
||||
<TYPE.body ml="8px" mr="8px" fontWeight={500}>
|
||||
{token.symbol}
|
||||
</TYPE.body>
|
||||
<TYPE.darkGray fontWeight={300}>{token.name}</TYPE.darkGray>
|
||||
</AutoRow>
|
||||
{chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<AddressText>{token.address}</AddressText>
|
||||
</ExternalLink>
|
||||
)}
|
||||
{list !== undefined ? (
|
||||
<RowFixed>
|
||||
{list.logoURI && <ListLogo logoURI={list.logoURI} size="12px" />}
|
||||
<TYPE.small ml="6px" color={theme.text3}>
|
||||
via {list.name}
|
||||
</TYPE.small>
|
||||
</RowFixed>
|
||||
) : (
|
||||
<WarningWrapper borderRadius="4px" padding="4px" highWarning={true}>
|
||||
<RowFixed>
|
||||
<AlertTriangle stroke={theme.red1} size="10px" />
|
||||
<TYPE.body color={theme.red1} ml="4px" fontSize="10px" fontWeight={500}>
|
||||
Unknown Source
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</WarningWrapper>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
|
||||
<Card
|
||||
style={{ backgroundColor: fromLists ? transparentize(0.8, theme.yellow2) : transparentize(0.8, theme.red1) }}
|
||||
>
|
||||
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<AlertTriangle stroke={fromLists ? theme.yellow2 : theme.red1} size={32} />
|
||||
<TYPE.body fontWeight={600} fontSize={20} color={fromLists ? theme.yellow2 : theme.red1}>
|
||||
Trade at your own risk!
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<TYPE.body fontWeight={400} color={fromLists ? theme.yellow2 : theme.red1}>
|
||||
Anyone can create a token, including creating fake versions of existing tokens that claim to represent
|
||||
projects.
|
||||
</TYPE.body>
|
||||
<TYPE.body fontWeight={600} color={fromLists ? theme.yellow2 : theme.red1}>
|
||||
If you purchase this token, you may not be able to sell it back.
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
|
||||
<Checkbox
|
||||
className=".understand-checkbox"
|
||||
name="confirmed"
|
||||
type="checkbox"
|
||||
checked={confirmed}
|
||||
onChange={() => setConfirmed(!confirmed)}
|
||||
/>
|
||||
<TYPE.body ml="10px" fontSize="16px" color={fromLists ? theme.yellow2 : theme.red1} fontWeight={500}>
|
||||
I understand
|
||||
</TYPE.body>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
<ButtonPrimary
|
||||
disabled={!confirmed}
|
||||
altDisabledStyle={true}
|
||||
borderRadius="20px"
|
||||
padding="10px 1rem"
|
||||
onClick={() => {
|
||||
tokens.map(token => addToken(token))
|
||||
handleCurrencySelect && handleCurrencySelect(tokens[0])
|
||||
}}
|
||||
className=".token-dismiss-button"
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
</PaddedColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { AppDispatch, AppState } from '../../state'
|
||||
import { acceptListUpdate, removeList, selectList } from '../../state/lists/actions'
|
||||
import { useSelectedListUrl } from '../../state/lists/hooks'
|
||||
import { CloseIcon, ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { parseENSAddress } from '../../utils/parseENSAddress'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import { ButtonOutlined, ButtonPrimary, ButtonSecondary } from '../Button'
|
||||
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled.div`
|
||||
max-width: 160px;
|
||||
opacity: 0.6;
|
||||
margin-right: 0.5rem;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
function ListOrigin({ listUrl }: { listUrl: string }) {
|
||||
const ensName = useMemo(() => parseENSAddress(listUrl)?.ensName, [listUrl])
|
||||
const host = useMemo(() => {
|
||||
if (ensName) return undefined
|
||||
const lowerListUrl = listUrl.toLowerCase()
|
||||
if (lowerListUrl.startsWith('ipfs://') || lowerListUrl.startsWith('ipns://')) {
|
||||
return listUrl
|
||||
}
|
||||
try {
|
||||
const url = new URL(listUrl)
|
||||
return url.host
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}, [listUrl, ensName])
|
||||
return <>{ensName ?? host}</>
|
||||
}
|
||||
|
||||
function listUrlRowHTMLId(listUrl: string) {
|
||||
return `list-row-${listUrl.replace(/\./g, '-')}`
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl, onBack }: { listUrl: string; onBack: () => void }) {
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const selectedListUrl = useSelectedListUrl()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const isSelected = listUrl === selectedListUrl
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }]
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const selectThisList = useCallback(() => {
|
||||
if (isSelected) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Select List',
|
||||
label: listUrl
|
||||
})
|
||||
|
||||
dispatch(selectList(listUrl))
|
||||
onBack()
|
||||
}, [dispatch, isSelected, listUrl, onBack])
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<Row key={listUrl} align="center" padding="16px" id={listUrlRowHTMLId(listUrl)}>
|
||||
{list.logoURI ? (
|
||||
<ListLogo style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<Text
|
||||
fontWeight={isSelected ? 500 : 400}
|
||||
fontSize={16}
|
||||
style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{list.name}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
marginTop: '4px'
|
||||
}}
|
||||
>
|
||||
<StyledListUrlText title={listUrl}>
|
||||
<ListOrigin listUrl={listUrl} />
|
||||
</StyledListUrlText>
|
||||
</Row>
|
||||
</Column>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonOutlined
|
||||
style={{
|
||||
width: '2rem',
|
||||
padding: '.8rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px',
|
||||
marginRight: '0.5rem'
|
||||
}}
|
||||
onClick={toggle}
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
<DropDown />
|
||||
</ButtonOutlined>
|
||||
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>View list</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
Remove list
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>Update list</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
{isSelected ? (
|
||||
<ButtonPrimary
|
||||
disabled={true}
|
||||
className="select-button"
|
||||
style={{ width: '5rem', minWidth: '5rem', padding: '0.5rem .35rem', borderRadius: '12px', fontSize: '14px' }}
|
||||
>
|
||||
Selected
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<>
|
||||
<ButtonPrimary
|
||||
className="select-button"
|
||||
style={{
|
||||
width: '5rem',
|
||||
minWidth: '4.5rem',
|
||||
padding: '0.5rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
onClick={selectThisList}
|
||||
>
|
||||
Select
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
|
||||
const AddListButton = styled(ButtonSecondary)`
|
||||
max-width: 4rem;
|
||||
margin-left: 1rem;
|
||||
border-radius: 12px;
|
||||
padding: 10px 18px;
|
||||
`
|
||||
|
||||
const ListContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export function ListSelect({ onDismiss, onBack }: { onDismiss: () => void; onBack: () => void }) {
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const adding = Boolean(lists[listUrlInput]?.loadingRequestId)
|
||||
const [addError, setAddError] = useState<string | null>(null)
|
||||
|
||||
const handleInput = useCallback(e => {
|
||||
setListUrlInput(e.target.value)
|
||||
setAddError(null)
|
||||
}, [])
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const handleAddList = useCallback(() => {
|
||||
if (adding) return
|
||||
setAddError(null)
|
||||
fetchList(listUrlInput)
|
||||
.then(() => {
|
||||
setListUrlInput('')
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listUrlInput
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listUrlInput
|
||||
})
|
||||
setAddError(error.message)
|
||||
dispatch(removeList(listUrlInput))
|
||||
})
|
||||
}, [adding, dispatch, fetchList, listUrlInput])
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const handleEnterKey = useCallback(
|
||||
e => {
|
||||
if (validUrl && e.key === 'Enter') {
|
||||
handleAddList()
|
||||
}
|
||||
},
|
||||
[handleAddList, validUrl]
|
||||
)
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter(listUrl => {
|
||||
return Boolean(lists[listUrl].current)
|
||||
})
|
||||
.sort((u1, u2) => {
|
||||
const { current: l1 } = lists[u1]
|
||||
const { current: l2 } = lists[u2]
|
||||
if (l1 && l2) {
|
||||
return l1.name.toLowerCase() < l2.name.toLowerCase()
|
||||
? -1
|
||||
: l1.name.toLowerCase() === l2.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (l1) return -1
|
||||
if (l2) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists])
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn>
|
||||
<RowBetween>
|
||||
<div>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} />
|
||||
</div>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Manage Lists
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<PaddedColumn gap="14px">
|
||||
<Text fontWeight={600}>
|
||||
Add a list{' '}
|
||||
<QuestionHelper text="Token lists are an open specification for lists of ERC20 tokens. You can use any token list by entering its URL below. Beware that third party token lists can contain fake or malicious ERC20 tokens." />
|
||||
</Text>
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder="https:// or ipfs:// or ENS name"
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnterKey}
|
||||
style={{ height: '2.75rem', borderRadius: 12, padding: '12px' }}
|
||||
/>
|
||||
<AddListButton onClick={handleAddList} disabled={!validUrl}>
|
||||
Add
|
||||
</AddListButton>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<ListContainer>
|
||||
{sortedLists.map(listUrl => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} onBack={onBack} />
|
||||
))}
|
||||
</ListContainer>
|
||||
<Separator />
|
||||
|
||||
<div style={{ padding: '16px', textAlign: 'center' }}>
|
||||
<ExternalLink href="https://tokenlists.org">Browse lists</ExternalLink>
|
||||
</div>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
89
src/components/SearchModal/Manage.tsx
Normal file
89
src/components/SearchModal/Manage.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react'
|
||||
import { PaddedColumn, Separator } from './styleds'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { CloseIcon } from 'theme'
|
||||
import styled from 'styled-components'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { ManageLists } from './ManageLists'
|
||||
import ManageTokens from './ManageTokens'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-bottom: 80px;
|
||||
`
|
||||
|
||||
const ToggleWrapper = styled(RowBetween)`
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
`
|
||||
|
||||
const ToggleOption = styled.div<{ active?: boolean }>`
|
||||
width: 48%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
background-color: ${({ theme, active }) => (active ? theme.bg1 : theme.bg3)};
|
||||
color: ${({ theme, active }) => (active ? theme.text1 : theme.text2)};
|
||||
user-select: none;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Manage({
|
||||
onDismiss,
|
||||
setModalView,
|
||||
setImportList,
|
||||
setImportToken,
|
||||
setListUrl
|
||||
}: {
|
||||
onDismiss: () => void
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportToken: (token: Token) => void
|
||||
setImportList: (list: TokenList) => void
|
||||
setListUrl: (url: string) => void
|
||||
}) {
|
||||
// toggle between tokens and lists
|
||||
const [showLists, setShowLists] = useState(true)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn>
|
||||
<RowBetween>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.search)} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Manage
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<Separator />
|
||||
<PaddedColumn style={{ paddingBottom: 0 }}>
|
||||
<ToggleWrapper>
|
||||
<ToggleOption onClick={() => setShowLists(!showLists)} active={showLists}>
|
||||
Lists
|
||||
</ToggleOption>
|
||||
<ToggleOption onClick={() => setShowLists(!showLists)} active={!showLists}>
|
||||
Tokens
|
||||
</ToggleOption>
|
||||
</ToggleWrapper>
|
||||
</PaddedColumn>
|
||||
{showLists ? (
|
||||
<ManageLists setModalView={setModalView} setImportList={setImportList} setListUrl={setListUrl} />
|
||||
) : (
|
||||
<ManageTokens setModalView={setModalView} setImportToken={setImportToken} />
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
377
src/components/SearchModal/ManageLists.tsx
Normal file
377
src/components/SearchModal/ManageLists.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import React, { memo, useCallback, useMemo, useRef, useState, useEffect } from 'react'
|
||||
import { Settings, CheckCircle } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { AppDispatch, AppState } from '../../state'
|
||||
import { acceptListUpdate, removeList, disableList, enableList } from '../../state/lists/actions'
|
||||
import { useIsListActive, useAllLists, useActiveListUrls } from '../../state/lists/hooks'
|
||||
import { ExternalLink, LinkStyledButton, TYPE, IconWrapper } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { parseENSAddress } from '../../utils/parseENSAddress'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import { ButtonEmpty, ButtonPrimary } from '../Button'
|
||||
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import Row, { RowFixed, RowBetween } from '../Row'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
import { useListColor } from 'hooks/useColor'
|
||||
import useTheme from '../../hooks/useTheme'
|
||||
import ListToggle from '../Toggle/ListToggle'
|
||||
import Card from 'components/Card'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
|
||||
|
||||
const Wrapper = styled(Column)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledTitleText = styled.div<{ active: boolean }>`
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: ${({ theme, active }) => (active ? theme.white : theme.text2)};
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled(TYPE.main)<{ active: boolean }>`
|
||||
font-size: 12px;
|
||||
color: ${({ theme, active }) => (active ? theme.white : theme.text2)};
|
||||
`
|
||||
|
||||
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean }>`
|
||||
background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.bg2)};
|
||||
transition: 200ms;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
function listUrlRowHTMLId(listUrl: string) {
|
||||
return `list-row-${listUrl.replace(/\./g, '-')}`
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const theme = useTheme()
|
||||
const listColor = useListColor(list?.logoURI)
|
||||
const isActive = useIsListActive(listUrl)
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }]
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleEnableList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Enable List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(enableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleDisableList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Disable List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(disableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<RowWrapper active={isActive} bgColor={listColor} key={listUrl} id={listUrlRowHTMLId(listUrl)}>
|
||||
{list.logoURI ? (
|
||||
<ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<StyledTitleText active={isActive}>{list.name}</StyledTitleText>
|
||||
</Row>
|
||||
<RowFixed mt="4px">
|
||||
<StyledListUrlText active={isActive} mr="6px">
|
||||
{list.tokens.length} tokens
|
||||
</StyledListUrlText>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0">
|
||||
<Settings stroke={isActive ? theme.bg1 : theme.text1} size={12} />
|
||||
</ButtonEmpty>
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>View list</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
Remove list
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>Update list</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</RowFixed>
|
||||
</Column>
|
||||
<ListToggle
|
||||
isActive={isActive}
|
||||
bgColor={listColor}
|
||||
toggle={() => {
|
||||
isActive ? handleDisableList() : handleEnableList()
|
||||
}}
|
||||
/>
|
||||
</RowWrapper>
|
||||
)
|
||||
})
|
||||
|
||||
const ListContainer = styled.div`
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding-bottom: 80px;
|
||||
`
|
||||
|
||||
export function ManageLists({
|
||||
setModalView,
|
||||
setImportList,
|
||||
setListUrl
|
||||
}: {
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportList: (list: TokenList) => void
|
||||
setListUrl: (url: string) => void
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const lists = useAllLists()
|
||||
|
||||
// sort by active but only if not visible
|
||||
const activeListUrls = useActiveListUrls()
|
||||
const [activeCopy, setActiveCopy] = useState<string[] | undefined>()
|
||||
useEffect(() => {
|
||||
if (!activeCopy && activeListUrls) {
|
||||
setActiveCopy(activeListUrls)
|
||||
}
|
||||
}, [activeCopy, activeListUrls])
|
||||
|
||||
const handleInput = useCallback(e => {
|
||||
setListUrlInput(e.target.value)
|
||||
}, [])
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter(listUrl => {
|
||||
// only show loaded lists, hide unsupported lists
|
||||
return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl))
|
||||
})
|
||||
.sort((u1, u2) => {
|
||||
const { current: l1 } = lists[u1]
|
||||
const { current: l2 } = lists[u2]
|
||||
|
||||
// first filter on active lists
|
||||
if (activeCopy?.includes(u1) && !activeCopy?.includes(u2)) {
|
||||
return -1
|
||||
}
|
||||
if (!activeCopy?.includes(u1) && activeCopy?.includes(u2)) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (l1 && l2) {
|
||||
return l1.name.toLowerCase() < l2.name.toLowerCase()
|
||||
? -1
|
||||
: l1.name.toLowerCase() === l2.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (l1) return -1
|
||||
if (l2) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists, activeCopy])
|
||||
|
||||
// temporary fetched list for import flow
|
||||
const [tempList, setTempList] = useState<TokenList>()
|
||||
const [addError, setAddError] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchTempList() {
|
||||
fetchList(listUrlInput, false)
|
||||
.then(list => setTempList(list))
|
||||
.catch(() => setAddError('Error importing list'))
|
||||
}
|
||||
// if valid url, fetch details for card
|
||||
if (validUrl) {
|
||||
fetchTempList()
|
||||
} else {
|
||||
setTempList(undefined)
|
||||
listUrlInput !== '' && setAddError('Enter valid list location')
|
||||
}
|
||||
|
||||
// reset error
|
||||
if (listUrlInput === '') {
|
||||
setAddError(undefined)
|
||||
}
|
||||
}, [fetchList, listUrlInput, validUrl])
|
||||
|
||||
// check if list is already imported
|
||||
const isImported = Object.keys(lists).includes(listUrlInput)
|
||||
|
||||
// set list values and have parent modal switch to import list view
|
||||
const handleImport = useCallback(() => {
|
||||
if (!tempList) return
|
||||
setImportList(tempList)
|
||||
setModalView(CurrencyModalView.importList)
|
||||
setListUrl(listUrlInput)
|
||||
}, [listUrlInput, setImportList, setListUrl, setModalView, tempList])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px">
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder="https:// or ipfs:// or ENS name"
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
{tempList && (
|
||||
<PaddedColumn style={{ paddingTop: 0 }}>
|
||||
<Card backgroundColor={theme.bg2} padding="12px 20px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
{tempList.logoURI && <ListLogo logoURI={tempList.logoURI} size="40px" />}
|
||||
<AutoColumn gap="4px" style={{ marginLeft: '20px' }}>
|
||||
<TYPE.body fontWeight={600}>{tempList.name}</TYPE.body>
|
||||
<TYPE.main fontSize={'12px'}>{tempList.tokens.length} tokens</TYPE.main>
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
{isImported ? (
|
||||
<RowFixed>
|
||||
<IconWrapper stroke={theme.text2} size="16px" marginRight={'10px'}>
|
||||
<CheckCircle />
|
||||
</IconWrapper>
|
||||
<TYPE.body color={theme.text2}>Loaded</TYPE.body>
|
||||
</RowFixed>
|
||||
) : (
|
||||
<ButtonPrimary
|
||||
style={{ fontSize: '14px' }}
|
||||
padding="6px 8px"
|
||||
width="fit-content"
|
||||
onClick={handleImport}
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</PaddedColumn>
|
||||
)}
|
||||
<Separator />
|
||||
<ListContainer>
|
||||
<AutoColumn gap="md">
|
||||
{sortedLists.map(listUrl => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} />
|
||||
))}
|
||||
</AutoColumn>
|
||||
</ListContainer>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
143
src/components/SearchModal/ManageTokens.tsx
Normal file
143
src/components/SearchModal/ManageTokens.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { useRef, RefObject, useCallback, useState, useMemo } from 'react'
|
||||
import Column from 'components/Column'
|
||||
import { PaddedColumn, Separator, SearchInput } from './styleds'
|
||||
import Row, { RowBetween, RowFixed } from 'components/Row'
|
||||
import { TYPE, ExternalLinkIcon, TrashIcon, ButtonText, ExternalLink } from 'theme'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import styled from 'styled-components'
|
||||
import { useUserAddedTokens, useRemoveUserAddedToken } from 'state/user/hooks'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getEtherscanLink, isAddress } from 'utils'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import Card from 'components/Card'
|
||||
import ImportRow from './ImportRow'
|
||||
import useTheme from '../../hooks/useTheme'
|
||||
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
position: relative;
|
||||
padding-bottom: 60px;
|
||||
`
|
||||
|
||||
const Footer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-radius: 20px;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top: 1px solid ${({ theme }) => theme.bg3};
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
export default function ManageTokens({
|
||||
setModalView,
|
||||
setImportToken
|
||||
}: {
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportToken: (token: Token) => void
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const theme = useTheme()
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}, [])
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(searchQuery)
|
||||
const searchToken = useToken(searchQuery)
|
||||
|
||||
// all tokens for local lisr
|
||||
const userAddedTokens: Token[] = useUserAddedTokens()
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
|
||||
const handleRemoveAll = useCallback(() => {
|
||||
if (chainId && userAddedTokens) {
|
||||
userAddedTokens.map(token => {
|
||||
return removeToken(chainId, token.address)
|
||||
})
|
||||
}
|
||||
}, [removeToken, userAddedTokens, chainId])
|
||||
|
||||
const tokenList = useMemo(() => {
|
||||
return (
|
||||
chainId &&
|
||||
userAddedTokens.map(token => (
|
||||
<RowBetween key={token.address} width="100%">
|
||||
<RowFixed>
|
||||
<CurrencyLogo currency={token} size={'20px'} />
|
||||
<ExternalLink href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<TYPE.main ml={'10px'} fontWeight={600}>
|
||||
{token.symbol}
|
||||
</TYPE.main>
|
||||
</ExternalLink>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<TrashIcon onClick={() => removeToken(chainId, token.address)} />
|
||||
<ExternalLinkIcon href={getEtherscanLink(chainId, token.address, 'address')} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
))
|
||||
)
|
||||
}, [userAddedTokens, chainId, removeToken])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={'0x0000'}
|
||||
value={searchQuery}
|
||||
autoComplete="off"
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</Row>
|
||||
{searchQuery !== '' && !isAddressSearch && <TYPE.error error={true}>Enter valid token address</TYPE.error>}
|
||||
{searchToken && (
|
||||
<Card backgroundColor={theme.bg2} padding="10px 0">
|
||||
<ImportRow
|
||||
token={searchToken}
|
||||
showImportView={() => setModalView(CurrencyModalView.importToken)}
|
||||
setImportToken={setImportToken}
|
||||
style={{ height: 'fit-content' }}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</PaddedColumn>
|
||||
<Separator />
|
||||
<PaddedColumn gap="lg">
|
||||
<RowBetween>
|
||||
<TYPE.main fontWeight={600}>
|
||||
{userAddedTokens?.length} Custom {userAddedTokens.length === 1 ? 'Token' : 'Tokens'}
|
||||
</TYPE.main>
|
||||
{userAddedTokens.length > 0 && (
|
||||
<ButtonText onClick={handleRemoveAll}>
|
||||
<TYPE.blue>Clear all</TYPE.blue>
|
||||
</ButtonText>
|
||||
)}
|
||||
</RowBetween>
|
||||
{tokenList}
|
||||
</PaddedColumn>
|
||||
</Column>
|
||||
<Footer>
|
||||
<TYPE.darkGray>Tip: Custom tokens are stored locally in your browser</TYPE.darkGray>
|
||||
</Footer>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { isAddress } from '../../utils'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
|
||||
@@ -30,7 +31,40 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
|
||||
return tokens.filter(token => {
|
||||
const { symbol, name } = token
|
||||
|
||||
return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name))
|
||||
})
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -11,15 +11,53 @@ export const ModalInfo = styled.div`
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
`
|
||||
export const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
export const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
top: 80px;
|
||||
`
|
||||
|
||||
export const TextDot = styled.div`
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
background-color: ${({ theme }) => theme.text2};
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
font-size: 14px;
|
||||
`
|
||||
export const Checkbox = styled.input`
|
||||
border: 1px solid ${({ theme }) => theme.red3};
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
|
||||
@@ -6,10 +6,10 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import { ApplicationModal } from '../../state/application/actions'
|
||||
import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import {
|
||||
useDarkModeManager,
|
||||
useExpertModeManager,
|
||||
useUserTransactionTTL,
|
||||
useUserSlippageTolerance
|
||||
useUserSlippageTolerance,
|
||||
useUserSingleHopOnly
|
||||
} from '../../state/user/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ButtonError } from '../Button'
|
||||
@@ -25,7 +25,11 @@ const StyledMenuIcon = styled(Settings)`
|
||||
width: 20px;
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
stroke: ${({ theme }) => theme.text2};
|
||||
}
|
||||
|
||||
:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -50,7 +54,6 @@ const StyledMenuButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
@@ -59,7 +62,6 @@ const StyledMenuButton = styled.button`
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -93,18 +95,12 @@ const MenuFlyout = styled.span`
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
top: 3rem;
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
min-width: 18.125rem;
|
||||
right: -46px;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
min-width: 18.125rem;
|
||||
top: -22rem;
|
||||
`};
|
||||
`
|
||||
|
||||
@@ -135,7 +131,7 @@ export default function SettingsTab() {
|
||||
|
||||
const [expertMode, toggleExpertMode] = useExpertModeManager()
|
||||
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly()
|
||||
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
@@ -233,10 +229,15 @@ export default function SettingsTab() {
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Dark Mode
|
||||
Disable Multihops
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Restricts swaps to direct pairs only." />
|
||||
</RowFixed>
|
||||
<Toggle isActive={darkMode} toggle={toggleDarkMode} />
|
||||
<Toggle
|
||||
id="toggle-disable-multihop-button"
|
||||
isActive={singleHopOnly}
|
||||
toggle={() => (singleHopOnly ? setSingleHopOnly(false) : setSingleHopOnly(true))}
|
||||
/>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</MenuFlyout>
|
||||
|
||||
56
src/components/Toggle/ListToggle.tsx
Normal file
56
src/components/Toggle/ListToggle.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0.4rem 0.4rem;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>`
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: ${({ isActive, bgColor, theme }) => (isActive ? bgColor : theme.bg4)};
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
|
||||
const StatusText = styled(TYPE.main)<{ isActive?: boolean }>`
|
||||
margin: 0 10px;
|
||||
width: 24px;
|
||||
color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)};
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
id?: string
|
||||
isActive: boolean
|
||||
bgColor: string
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function ListToggle({ id, isActive, bgColor, toggle }: ToggleProps) {
|
||||
return (
|
||||
<Wrapper id={id} isActive={isActive} onClick={toggle}>
|
||||
{isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={true}>
|
||||
ON
|
||||
</StatusText>
|
||||
)}
|
||||
<ToggleElement isActive={isActive} bgColor={bgColor} />
|
||||
{!isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={false}>
|
||||
OFF
|
||||
</StatusText>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -26,14 +26,12 @@ const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
/* border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)}; */
|
||||
background: ${({ theme }) => theme.bg3};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
/* background-color: transparent; */
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
|
||||
@@ -1,153 +1,22 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink, shortenAddress } from '../../utils'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import React from 'react'
|
||||
import Modal from '../Modal'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ButtonError } from '../Button'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme }) => transparentize(0.6, theme.bg3)};
|
||||
padding: 0.75rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: rgba(242, 150, 2, 0.05);
|
||||
border: 1px solid #f3841e;
|
||||
border-radius: 20px;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const StyledWarningIcon = styled(AlertTriangle)`
|
||||
stroke: ${({ theme }) => theme.red2};
|
||||
`
|
||||
|
||||
interface TokenWarningCardProps {
|
||||
token?: Token
|
||||
}
|
||||
|
||||
function TokenWarningCard({ token }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (!token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
if (userToken.equals(token)) {
|
||||
return false
|
||||
}
|
||||
return userToken.symbol?.toLowerCase() === tokenSymbol || userToken.name?.toLowerCase() === tokenName
|
||||
})
|
||||
}, [token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (!token) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol}>
|
||||
<AutoRow gap="6px">
|
||||
<AutoColumn gap="24px">
|
||||
<CurrencyLogo currency={token} size={'16px'} />
|
||||
<div> </div>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="10px" justify="flex-start">
|
||||
<TYPE.main>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}{' '}
|
||||
</TYPE.main>
|
||||
{chainId && (
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
<TYPE.blue title={token.address}>{shortenAddress(token.address)} (View on Etherscan)</TYPE.blue>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
import { ImportToken } from 'components/SearchModal/ImportToken'
|
||||
|
||||
export default function TokenWarningModal({
|
||||
isOpen,
|
||||
tokens,
|
||||
onConfirm
|
||||
onConfirm,
|
||||
onDismiss
|
||||
}: {
|
||||
isOpen: boolean
|
||||
tokens: Token[]
|
||||
onConfirm: () => void
|
||||
onDismiss: () => void
|
||||
}) {
|
||||
const [understandChecked, setUnderstandChecked] = useState(false)
|
||||
const toggleUnderstand = useCallback(() => setUnderstandChecked(uc => !uc), [])
|
||||
|
||||
const handleDismiss = useCallback(() => null, [])
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={handleDismiss} maxHeight={90}>
|
||||
<WarningContainer className="token-warning-container">
|
||||
<AutoColumn gap="lg">
|
||||
<AutoRow gap="6px">
|
||||
<StyledWarningIcon />
|
||||
<TYPE.main color={'red2'}>Token imported</TYPE.main>
|
||||
</AutoRow>
|
||||
<TYPE.body color={'red2'}>
|
||||
Anyone can create an ERC20 token on Ethereum with <em>any</em> name, including creating fake versions of
|
||||
existing tokens and tokens that claim to represent projects that do not have a token.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
This interface can load arbitrary tokens by token addresses. Please take extra caution and do your research
|
||||
when interacting with arbitrary ERC20 tokens.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
If you purchase an arbitrary token, <strong>you may be unable to sell it back.</strong>
|
||||
</TYPE.body>
|
||||
{tokens.map(token => {
|
||||
return <TokenWarningCard key={token.address} token={token} />
|
||||
})}
|
||||
<RowBetween>
|
||||
<div>
|
||||
<label style={{ cursor: 'pointer', userSelect: 'none' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="understand-checkbox"
|
||||
checked={understandChecked}
|
||||
onChange={toggleUnderstand}
|
||||
/>{' '}
|
||||
I understand
|
||||
</label>
|
||||
</div>
|
||||
<ButtonError
|
||||
disabled={!understandChecked}
|
||||
error={true}
|
||||
width={'140px'}
|
||||
padding="0.5rem 1rem"
|
||||
className="token-dismiss-button"
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onClick={() => {
|
||||
onConfirm()
|
||||
}}
|
||||
>
|
||||
<TYPE.body color="white">Continue</TYPE.body>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</WarningContainer>
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={100}>
|
||||
<ImportToken tokens={tokens} handleCurrencySelect={onConfirm} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { ChainId, Currency } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import Modal from '../Modal'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { CloseIcon, CustomLightSpinner } from '../../theme/components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { AlertTriangle, ArrowUpCircle } from 'react-feather'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather'
|
||||
import { ButtonPrimary, ButtonLight } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
|
||||
import MetaMaskLogo from '../../assets/images/metamask.png'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useAddTokenToMetamask from 'hooks/useAddTokenToMetamask'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
@@ -31,6 +32,12 @@ const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
const StyledLogo = styled.img`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: 6px;
|
||||
`
|
||||
|
||||
function ConfirmationPendingContent({ onDismiss, pendingText }: { onDismiss: () => void; pendingText: string }) {
|
||||
return (
|
||||
<Wrapper>
|
||||
@@ -63,14 +70,20 @@ function ConfirmationPendingContent({ onDismiss, pendingText }: { onDismiss: ()
|
||||
function TransactionSubmittedContent({
|
||||
onDismiss,
|
||||
chainId,
|
||||
hash
|
||||
hash,
|
||||
currencyToAdd
|
||||
}: {
|
||||
onDismiss: () => void
|
||||
hash: string | undefined
|
||||
chainId: ChainId
|
||||
currencyToAdd?: Currency | undefined
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const { addToken, success } = useAddTokenToMetamask(currencyToAdd)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
@@ -92,6 +105,20 @@ function TransactionSubmittedContent({
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
)}
|
||||
{currencyToAdd && library?.provider?.isMetaMask && (
|
||||
<ButtonLight mt="12px" padding="6px 12px" width="fit-content" onClick={addToken}>
|
||||
{!success ? (
|
||||
<RowFixed>
|
||||
Add {currencyToAdd.symbol} to Metamask <StyledLogo src={MetaMaskLogo} />
|
||||
</RowFixed>
|
||||
) : (
|
||||
<RowFixed>
|
||||
Added {currencyToAdd.symbol}{' '}
|
||||
<CheckCircle size={'16px'} stroke={theme.green1} style={{ marginLeft: '6px' }} />
|
||||
</RowFixed>
|
||||
)}
|
||||
</ButtonLight>
|
||||
)}
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
@@ -162,6 +189,7 @@ interface ConfirmationModalProps {
|
||||
content: () => React.ReactNode
|
||||
attemptingTxn: boolean
|
||||
pendingText: string
|
||||
currencyToAdd?: Currency | undefined
|
||||
}
|
||||
|
||||
export default function TransactionConfirmationModal({
|
||||
@@ -170,7 +198,8 @@ export default function TransactionConfirmationModal({
|
||||
attemptingTxn,
|
||||
hash,
|
||||
pendingText,
|
||||
content
|
||||
content,
|
||||
currencyToAdd
|
||||
}: ConfirmationModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
@@ -182,7 +211,12 @@ export default function TransactionConfirmationModal({
|
||||
{attemptingTxn ? (
|
||||
<ConfirmationPendingContent onDismiss={onDismiss} pendingText={pendingText} />
|
||||
) : hash ? (
|
||||
<TransactionSubmittedContent chainId={chainId} hash={hash} onDismiss={onDismiss} />
|
||||
<TransactionSubmittedContent
|
||||
chainId={chainId}
|
||||
hash={hash}
|
||||
onDismiss={onDismiss}
|
||||
currencyToAdd={currencyToAdd}
|
||||
/>
|
||||
) : (
|
||||
content()
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { unwrappedToken } from '../../utils/wrappedCurrency'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import useUSDCPrice from '../../utils/useUSDCPrice'
|
||||
import { BIG_INT_SECONDS_IN_WEEK } from '../../constants'
|
||||
|
||||
const StatContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -56,11 +57,6 @@ const TopSection = styled.div`
|
||||
`};
|
||||
`
|
||||
|
||||
// const APR = styled.div`
|
||||
// display: flex;
|
||||
// justify-content: flex-end;
|
||||
// `
|
||||
|
||||
const BottomSection = styled.div<{ showBackground: boolean }>`
|
||||
padding: 12px 16px;
|
||||
opacity: ${({ showBackground }) => (showBackground ? '1' : '0.4')};
|
||||
@@ -139,9 +135,15 @@ export default function PoolCard({ stakingInfo }: { stakingInfo: StakingInfo })
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.white> Pool rate </TYPE.white>
|
||||
<TYPE.white>{`${stakingInfo.totalRewardRate
|
||||
?.multiply(`${60 * 60 * 24 * 7}`)
|
||||
?.toFixed(0, { groupSeparator: ',' })} UNI / week`}</TYPE.white>
|
||||
<TYPE.white>
|
||||
{stakingInfo
|
||||
? stakingInfo.active
|
||||
? `${stakingInfo.totalRewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toFixed(0, { groupSeparator: ',' })} UNI / week`
|
||||
: '0 UNI / week'
|
||||
: '-'}
|
||||
</TYPE.white>
|
||||
</RowBetween>
|
||||
</StatContainer>
|
||||
|
||||
@@ -157,9 +159,13 @@ export default function PoolCard({ stakingInfo }: { stakingInfo: StakingInfo })
|
||||
<span role="img" aria-label="wizard-icon" style={{ marginRight: '0.5rem' }}>
|
||||
⚡
|
||||
</span>
|
||||
{`${stakingInfo.rewardRate
|
||||
?.multiply(`${60 * 60 * 24 * 7}`)
|
||||
?.toSignificant(4, { groupSeparator: ',' })} UNI / week`}
|
||||
{stakingInfo
|
||||
? stakingInfo.active
|
||||
? `${stakingInfo.rewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toSignificant(4, { groupSeparator: ',' })} UNI / week`
|
||||
: '0 UNI / week'
|
||||
: '-'}
|
||||
</TYPE.black>
|
||||
</BottomSection>
|
||||
</>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import { SectionBreak } from './styleds'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
const InfoLink = styled(ExternalLink)`
|
||||
@@ -30,7 +29,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<AutoColumn style={{ padding: '0 16px' }}>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
@@ -86,29 +85,33 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
const showRoute = Boolean(trade && trade.route.path.length > 2)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="0px">
|
||||
{trade && (
|
||||
<>
|
||||
<TradeSummary trade={trade} allowedSlippage={allowedSlippage} />
|
||||
{showRoute && (
|
||||
<>
|
||||
<SectionBreak />
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<RowBetween style={{ padding: '0 16px' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
</span>
|
||||
<SwapRoute trade={trade} />
|
||||
</AutoColumn>
|
||||
</RowBetween>
|
||||
</>
|
||||
)}
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<InfoLink href={'https://uniswap.info/pair/' + trade.route.pairs[0].liquidityToken.address} target="_blank">
|
||||
View pair analytics ↗
|
||||
</InfoLink>
|
||||
</AutoColumn>
|
||||
{!showRoute && (
|
||||
<AutoColumn style={{ padding: '12px 16px 0 16px' }}>
|
||||
<InfoLink
|
||||
href={'https://info.uniswap.org/pair/' + trade.route.pairs[0].liquidityToken.address}
|
||||
target="_blank"
|
||||
>
|
||||
View pair analytics ↗
|
||||
</InfoLink>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDet
|
||||
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
|
||||
@@ -104,6 +104,7 @@ export default function ConfirmSwapModal({
|
||||
hash={txHash}
|
||||
content={confirmationContent}
|
||||
pendingText={pendingText}
|
||||
currencyToAdd={trade?.outputAmount.currency}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
24
src/components/swap/SwapHeader.tsx
Normal file
24
src/components/swap/SwapHeader.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Settings from '../Settings'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
const StyledSwapHeader = styled.div`
|
||||
padding: 12px 1rem 0px 1.5rem;
|
||||
margin-bottom: -4px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
`
|
||||
|
||||
export default function SwapHeader() {
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<RowBetween>
|
||||
<TYPE.black fontWeight={500}>Swap</TYPE.black>
|
||||
<Settings />
|
||||
</RowBetween>
|
||||
</StyledSwapHeader>
|
||||
)
|
||||
}
|
||||
@@ -4,32 +4,23 @@ import { ChevronRight } from 'react-feather'
|
||||
import { Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { TYPE } from '../../theme'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { unwrappedToken } from 'utils/wrappedCurrency'
|
||||
|
||||
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
<Flex flexWrap="wrap" width="100%" justifyContent="flex-end" alignItems="center">
|
||||
{trade.route.path.map((token, i, path) => {
|
||||
const isLastItem: boolean = i === path.length - 1
|
||||
const currency = unwrappedToken(token)
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
|
||||
<CurrencyLogo currency={token} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
<Flex alignItems="end">
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.125rem" mr="0.125rem">
|
||||
{currency.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
{isLastItem ? null : <ChevronRight color={theme.text2} />}
|
||||
{isLastItem ? null : <ChevronRight size={12} color={theme.text2} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
104
src/components/swap/UnsupportedCurrencyFooter.tsx
Normal file
104
src/components/swap/UnsupportedCurrencyFooter.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { TYPE, CloseIcon, ExternalLink } from 'theme'
|
||||
import { ButtonEmpty } from 'components/Button'
|
||||
import Modal from 'components/Modal'
|
||||
import Card, { OutlineCard } from 'components/Card'
|
||||
import { RowBetween, AutoRow } from 'components/Row'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import { getEtherscanLink } from 'utils'
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import { wrappedCurrency } from 'utils/wrappedCurrency'
|
||||
import { useUnsupportedTokens } from '../../hooks/Tokens'
|
||||
|
||||
const DetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
|
||||
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||
transition: transform 300ms ease-in-out;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const AddressText = styled(TYPE.blue)`
|
||||
font-size: 12px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
font-size: 10px;
|
||||
`}
|
||||
`
|
||||
|
||||
export default function UnsupportedCurrencyFooter({
|
||||
show,
|
||||
currencies
|
||||
}: {
|
||||
show: boolean
|
||||
currencies: (Currency | undefined)[]
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const tokens =
|
||||
chainId && currencies
|
||||
? currencies.map(currency => {
|
||||
return wrappedCurrency(currency, chainId)
|
||||
})
|
||||
: []
|
||||
|
||||
const unsupportedTokens: { [address: string]: Token } = useUnsupportedTokens()
|
||||
|
||||
return (
|
||||
<DetailsFooter show={show}>
|
||||
<Modal isOpen={showDetails} onDismiss={() => setShowDetails(false)}>
|
||||
<Card padding="2rem">
|
||||
<AutoColumn gap="lg">
|
||||
<RowBetween>
|
||||
<TYPE.mediumHeader>Unsupported Assets</TYPE.mediumHeader>
|
||||
<CloseIcon onClick={() => setShowDetails(false)} />
|
||||
</RowBetween>
|
||||
{tokens.map(token => {
|
||||
return (
|
||||
token &&
|
||||
unsupportedTokens &&
|
||||
Object.keys(unsupportedTokens).includes(token.address) && (
|
||||
<OutlineCard key={token.address?.concat('not-supported')}>
|
||||
<AutoColumn gap="10px">
|
||||
<AutoRow gap="5px" align="center">
|
||||
<CurrencyLogo currency={token} size={'24px'} />
|
||||
<TYPE.body fontWeight={500}>{token.symbol}</TYPE.body>
|
||||
</AutoRow>
|
||||
{chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<AddressText>{token.address}</AddressText>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</OutlineCard>
|
||||
)
|
||||
)
|
||||
})}
|
||||
<AutoColumn gap="lg">
|
||||
<TYPE.body fontWeight={500}>
|
||||
Some assets are not available through this interface because they may not work well with our smart
|
||||
contract or we are unable to allow trading for legal reasons.
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
</Modal>
|
||||
<ButtonEmpty padding={'0'} onClick={() => setShowDetails(true)}>
|
||||
<TYPE.blue>Read more about unsupported assets</TYPE.blue>
|
||||
</ButtonEmpty>
|
||||
</DetailsFooter>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { AutoColumn } from '../Column'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||
@@ -145,3 +146,8 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
export const Separator = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
||||
@@ -7,6 +7,8 @@ export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
export { PRELOADED_PROPOSALS } from './proposals'
|
||||
|
||||
// a list of tokens by chain
|
||||
type ChainTokenList = {
|
||||
readonly [chainId in ChainId]: Token[]
|
||||
@@ -18,13 +20,17 @@ export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597
|
||||
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
|
||||
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
|
||||
export const AMPL = new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth')
|
||||
export const WBTC = new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 18, 'WBTC', 'Wrapped BTC')
|
||||
export const WBTC = new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC')
|
||||
|
||||
// TODO this is only approximate, it's actually based on blocks
|
||||
export const PROPOSAL_LENGTH_IN_DAYS = 7
|
||||
// Block time here is slightly higher (~1s) than average in order to avoid ongoing proposals past the displayed time
|
||||
export const AVERAGE_BLOCK_TIME_IN_SECS = 13
|
||||
export const PROPOSAL_LENGTH_IN_BLOCKS = 40_320
|
||||
export const PROPOSAL_LENGTH_IN_SECS = AVERAGE_BLOCK_TIME_IN_SECS * PROPOSAL_LENGTH_IN_BLOCKS
|
||||
|
||||
export const GOVERNANCE_ADDRESS = '0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F'
|
||||
|
||||
export const TIMELOCK_ADDRESS = '0x1a9C8182C09F50C8318d769245beA52c32BE35BC'
|
||||
|
||||
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
||||
export const UNI: { [chainId in ChainId]: Token } = {
|
||||
[ChainId.MAINNET]: new Token(ChainId.MAINNET, UNI_ADDRESS, 18, 'UNI', 'Uniswap'),
|
||||
@@ -35,9 +41,9 @@ export const UNI: { [chainId in ChainId]: Token } = {
|
||||
}
|
||||
|
||||
export const COMMON_CONTRACT_NAMES: { [address: string]: string } = {
|
||||
'0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984': 'UNI',
|
||||
'0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F': 'Governance Alpha',
|
||||
'0x1a9C8182C09F50C8318d769245beA52c32BE35BC': 'Timelock'
|
||||
[UNI_ADDRESS]: 'UNI',
|
||||
[GOVERNANCE_ADDRESS]: 'Governance',
|
||||
[TIMELOCK_ADDRESS]: 'Timelock'
|
||||
}
|
||||
|
||||
// TODO: specify merkle distributor for mainnet
|
||||
@@ -56,7 +62,7 @@ const WETH_ONLY: ChainTokenList = {
|
||||
// used to construct intermediary pairs for trading
|
||||
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR, WBTC]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,13 +78,13 @@ export const CUSTOM_BASES: { [chainId in ChainId]?: { [tokenAddress: string]: To
|
||||
// used for display in the default list when adding liquidity
|
||||
export const SUGGESTED_BASES: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, WBTC]
|
||||
}
|
||||
|
||||
// used to construct the list of all pairs we consider by default in the frontend
|
||||
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, WBTC]
|
||||
}
|
||||
|
||||
export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] } = {
|
||||
@@ -175,6 +181,9 @@ export const INITIAL_ALLOWED_SLIPPAGE = 50
|
||||
// 20 minutes, denominated in seconds
|
||||
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 20
|
||||
|
||||
// used for rewards deadlines
|
||||
export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7)
|
||||
|
||||
export const BIG_INT_ZERO = JSBI.BigInt(0)
|
||||
|
||||
// one basis point
|
||||
@@ -191,4 +200,16 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
|
||||
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), JSBI.BigInt(10000))
|
||||
|
||||
export const ZERO_PERCENT = new Percent('0')
|
||||
export const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
|
||||
// SDN OFAC addresses
|
||||
export const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x7F367cC41522cE07553e823bf3be79A889DEbe1B',
|
||||
'0xd882cFc20F52f2599D84b8e8D58C7FB62cfE344b',
|
||||
'0x901bb9583b24D97e995513C6778dc6888AB6870e',
|
||||
'0xA7e5d5A720f06526557c513402f2e6B5fA20b008',
|
||||
'0x8576aCC5C05D6Ce88f4e49bf65BdF0C62F91353C'
|
||||
]
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
// the Uniswap Default token list lives here
|
||||
export const DEFAULT_TOKEN_LIST_URL = 'tokens.uniswap.eth'
|
||||
// used to mark unsupported tokens, these are hosted lists of unsupported tokens
|
||||
|
||||
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
|
||||
const UMA_LIST = 'https://umaproject.org/uma.tokenlist.json'
|
||||
const AAVE_LIST = 'tokenlist.aave.eth'
|
||||
const SYNTHETIX_LIST = 'synths.snx.eth'
|
||||
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
|
||||
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
|
||||
const OPYN_LIST = 'https://raw.githubusercontent.com/opynfinance/opyn-tokenlist/master/opyn-v1.tokenlist.json'
|
||||
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
|
||||
const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
|
||||
const CMC_ALL_LIST = 'defi.cmc.eth'
|
||||
const CMC_STABLECOIN = 'stablecoin.cmc.eth'
|
||||
const KLEROS_LIST = 't2crtokens.eth'
|
||||
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
|
||||
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
|
||||
|
||||
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST]
|
||||
|
||||
// lower index == higher priority for token import
|
||||
export const DEFAULT_LIST_OF_LISTS: string[] = [
|
||||
DEFAULT_TOKEN_LIST_URL,
|
||||
't2crtokens.eth', // kleros
|
||||
'tokens.1inch.eth', // 1inch
|
||||
'synths.snx.eth',
|
||||
'tokenlist.dharma.eth',
|
||||
'defi.cmc.eth',
|
||||
'erc20.cmc.eth',
|
||||
'stablecoin.cmc.eth',
|
||||
'tokenlist.zerion.eth',
|
||||
'tokenlist.aave.eth',
|
||||
'https://tokens.coingecko.com/uniswap/all.json',
|
||||
'https://app.tryroll.com/tokens.json',
|
||||
'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json',
|
||||
'https://defiprime.com/defiprime.tokenlist.json',
|
||||
'https://umaproject.org/uma.tokenlist.json'
|
||||
COMPOUND_LIST,
|
||||
AAVE_LIST,
|
||||
SYNTHETIX_LIST,
|
||||
UMA_LIST,
|
||||
WRAPPED_LIST,
|
||||
SET_LIST,
|
||||
OPYN_LIST,
|
||||
ROLL_LIST,
|
||||
COINGECKO_LIST,
|
||||
CMC_ALL_LIST,
|
||||
CMC_STABLECOIN,
|
||||
KLEROS_LIST,
|
||||
GEMINI_LIST,
|
||||
...UNSUPPORTED_LIST_URLS // need to load unsupported tokens as well
|
||||
]
|
||||
|
||||
// default lists to be 'active' aka searched across
|
||||
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [GEMINI_LIST]
|
||||
|
||||
4
src/constants/proposals/index.ts
Normal file
4
src/constants/proposals/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { UNISWAP_GRANTS } from './uniswap_grants'
|
||||
|
||||
// Proposals are 0-indexed
|
||||
export const PRELOADED_PROPOSALS = new Map([[2, UNISWAP_GRANTS]])
|
||||
106
src/constants/proposals/uniswap_grants.ts
Normal file
106
src/constants/proposals/uniswap_grants.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
export const UNISWAP_GRANTS = `# Uniswap Grants Program v0.1
|
||||
|
||||
*co-authored with [Ken Ng](https://twitter.com/nkennethk?lang=en)*
|
||||
|
||||
## Summary:
|
||||
|
||||
**This post outlines a framework for funding Uniswap ecosystem development with grants from the[ UNI Community Treasury](https://uniswap.org/blog/uni/).** The program starts small—sponsoring hackathons, [for example](https://gov.uniswap.org/c/proposal-discussion/5)—but could grow in significance over time (with renewals approved by governance) to fund core protocol development. Grants administration is a subjective process that cannot be easily automated, and thus we propose a nimble committee of 6 members —1 lead and 5 reviewers—to deliver an efficient, predictable process to applicants, such that funding can be administered without having to put each application to a vote. We propose the program start with an initial cap of $750K per quarter and a limit of 2 quarters before renewal—a sum that we feel is appropriate for an MVP relative to the size of the treasury that UNI token holders are entrusted with allocating.
|
||||
|
||||
**Purpose:**
|
||||
|
||||
**The mission of the UGP is to provide valuable resources to help grow the Uniswap ecosystem.** Through public discourse and inbound applications, the community will get first-hand exposure to identify and respond to the most pressing needs of the ecosystem, as well as the ability to support innovative projects expanding the capabilities of Uniswap. By rewarding talent early with developer incentives, bounties, and infrastructure support, UGP acts as a catalyst for growth and helps to maintain Uniswap as a nexus for DeFi on Ethereum.
|
||||
|
||||
**Quarterly Budget:**
|
||||
|
||||
* Max quarterly budget of up to $750k
|
||||
* Budget and caps to be assessed every six months
|
||||
|
||||
**Grant Allocation Committee:**
|
||||
|
||||
* Of 6 committee members: 1 lead and 5 reviewers
|
||||
* Each committee has a term of 2 quarters (6 months) after which the program needs to be renewed by UNI governance
|
||||
* Committee functions as a 4 of 5 multi-sig
|
||||
|
||||
**Committee Members**
|
||||
|
||||
While the goals and priorities of the grant program will be thoroughly discussed and reviewed by the community through public discourse, **the decision to start UGP by operating as a small committee is to ensure that the application and decision process will be efficient and predictable, so applicants have clear objectives and timely decisions.**
|
||||
|
||||
Starting with just six members enables the committee to efficiently fund projects with tight feedback loops and rapid iterations. The purpose of this committee would be to test the hypothesis that the Grants Program can successfully provide value for the UNI ecosystem before scaling the program.
|
||||
|
||||
**We suggest the grants program is administered by a single lead. Here we propose Kenneth Ng, a co-author of this post**. Ken has helped grow the Ethereum Foundation Grants Program over the last two years in establishing high level priorities and adapting for the ecosystems needs.
|
||||
|
||||
**The other 5 committee members should be thought of as “reviewers” — UNI community members who will keep the grants program functional by ensuring Ken is leading effectively and, of course, not absconding with funds.** Part of the reviewers job is to hold the program accountable for success (defined below) and/or return any excess funds to the UNI treasury. Reviewers are not compensated as part of this proposal as we expect their time commitment to be minimal. Compensation for the lead role is discussed below, as we expect this to be a labor intensive role.
|
||||
|
||||
**Program Lead:** [Ken Ng](https://twitter.com/nkennethk?lang=en) (HL Creative Corp)
|
||||
*Ecosystem Support Program at the Ethereum Foundation*
|
||||
|
||||
1. Reviewer: [Jesse Walden](https://twitter.com/jessewldn) (o/b/o Unofficial LLC dba [Variant Fund](http://twitter.com/variantfund))
|
||||
*Founder and Investor at Variant Fund (holds UNI)*
|
||||
|
||||
2. Reviewer: [Monet Supply](https://twitter.com/MonetSupply)
|
||||
*Risk Analyst at MakerDAO*
|
||||
|
||||
3. Reviewer: [Robert Leshner](https://twitter.com/rleshner)
|
||||
*Founder and CEO of Compound Finance*
|
||||
|
||||
4. Reviewer: [Kain Warwick](https://twitter.com/kaiynne)
|
||||
*Founder of Synthetix*
|
||||
|
||||
5. Reviewer: [Ashleigh Schap](https://twitter.com/ashleighschap)
|
||||
*Growth Lead, Uniswap (Company)*
|
||||
|
||||
## Methodology
|
||||
|
||||
**1.1 Budget**
|
||||
|
||||
This proposal recommends a max cap of $750K per quarter, with the ability to reevaluate biannually at each epoch (two fiscal quarters). While the UGP Grants Committee will be the decision makers around individual grants, respective budgets, roadmaps, and milestones, any top-level changes to UGP including epochs and max cap, will require full community quorum (4% approval).
|
||||
|
||||
The UGP will be funded by the UNI treasury according to the[ release schedule](https://uniswap.org/blog/uni/) set out by the Uniswap team, whereby 43% of the UNI treasury is released over a four-year timeline. In Year 1 this will total to 172,000,000 UNI (~$344M as of writing).
|
||||
|
||||
Taking into consideration the current landscape of ecosystem funding across Ethereum, the community would be hard-pressed to allocate even 5% of Year 1’s treasury. For context Gitcoin CLR Round 7 distributed $725k ($450k in matched) across 857 projects and YTD, Moloch has granted just under $200k but in contrast, the EF has committed to somewhere in the 8 figure range.
|
||||
|
||||
**1.2 Committee Compensation**
|
||||
|
||||
Operating a successful grants program takes considerable time and effort. Take, for instance, the EF Ecosystem Support Program, which has awarded grants since 2018, consists of an internal team at the Foundation as well as an ever increasing roster of community advisors in order to keep expanding and adapting to best serve the needs of the Ethereum ecosystem. While the structure of the allocation committee has six members, we propose that only the lead will be remunerated for their work in establishing the initial processes, vetting applications, and managing the program overall as this role is expected to be time consuming if the program is to be succesful. We propose that the other committee members be unpaid UNI ecosystem stakeholders who have an interest in seeing the protocol ecosystem continue to operate and grow.
|
||||
|
||||
**We propose the lead be compensated 25 UNI/hr (approximately $100 USD at time of this writing) capped at 30 hours/week. This compensation, along with the quarterly budget, will be allocated to the UGP multisig from the UNI treasury**. In keeping with the committee’s commitment to the community, hours and duties will be logged publicly and transparently .
|
||||
|
||||
**2.1 Priorities**
|
||||
|
||||
Initially, the program aims to start narrow in scope, funding peripheral ecosystem initiatives, such as targeted bounties, hackathon sponsorships, and other low-stakes means of building out the Uniswap developer ecosystem. Over time, if the program proves effective, the grant allocations can grow in scope to include, for example, improved frontends, trading interfaces, and eventually, core protocol development.
|
||||
|
||||

|
||||
|
||||
With the initial priorities in mind, some effective measures for quick successes might look like:
|
||||
|
||||
* Total number of projects funded
|
||||
* Quarterly increase in applications
|
||||
* Project engagement post-event/funding
|
||||
* Overall community engagement/sentiment
|
||||
|
||||
**2.2 Timeline**
|
||||
|
||||
In keeping with the fast pace of the UNI ecosystem, we organize time in epochs, or two calendar quarters. Each epoch will see two funding rounds, one per quarter, after which the community can review and create proposals to improve or revamp the program as they deem fit.
|
||||
|
||||
****
|
||||
|
||||
**Rolling Wave 1 & 2 Applications**
|
||||
|
||||
* Starting in Q1 2021, UGP will start accepting applications for events looking for support in the form of bounties or prizes that in parallel can be proactively sourced. During these first two waves of funding projects, the allocation committee lead can begin laying out guardrails for continued funding
|
||||
|
||||
* Considering the immediate feedback loops for the first epoch, we expect these allocation decisions to be discussed and reviewed by the larger community. Should this not be of value, the community can submit a formal governance proposal to change any piece of UGP that was not effective
|
||||
|
||||
**Wave 3 & Beyond**
|
||||
|
||||
* Beginning with Wave 3, there should have been enough time with impactful projects funded to be considered for follow-on funding, should it make sense. In the same vein, projects within scope will be expanded to also include those working on integrations and and other key components.
|
||||
|
||||
* Beyond the second epoch, as the community helps to review and help adapt UGP to be most effective, the scope will continue to grow in order to accommodate the state of the ecosystem including that of core protocol improvements
|
||||
|
||||
## Conclusion:
|
||||
|
||||
**If this proposal is successfully approved, UGP will start accepting applications on a rolling basis beginning at the start of 2021.** With the phases and priorities laid out above, UGP will aim to make a significant impact by catalyzing growth and innovation in the UNI ecosystem.
|
||||
|
||||
**This program is meant to be the simplest possible MVP of a Uniswap Ecosystem Grants initiative.** While the multi-sig committee comes with trust assumptions about the members, our hope is the community will approve this limited engagement to get the ball rolling in an efficient structure. **After the first epoch (2 fiscal quarters) the burden of proof will be on UGP to show empirical evidence that the program is worth continuing in its existing form and will submit to governance to renew treasury funding.**
|
||||
|
||||
If this program proves successful, we hope it will inspire others to follow suit and create their own funding committees for allocating treasury capital—ideally with different specializations.
|
||||
`
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Uniswap V2 Unsupported List",
|
||||
"timestamp": "2021-01-05T20:47:02.923Z",
|
||||
"version": {
|
||||
"major": 1,
|
||||
"minor": 0,
|
||||
"patch": 0
|
||||
},
|
||||
"tags": {},
|
||||
"logoURI": "ipfs://QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir",
|
||||
"keywords": ["uniswap", "unsupported"],
|
||||
"tokens": [
|
||||
{
|
||||
"name": "Gold Tether",
|
||||
"address": "0x4922a015c4407F87432B179bb209e125432E4a2A",
|
||||
"symbol": "XAUt",
|
||||
"decimals": 6,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4922a015c4407F87432B179bb209e125432E4a2A/logo.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,11 +3,9 @@ import {
|
||||
BigintIsh,
|
||||
Currency,
|
||||
CurrencyAmount,
|
||||
currencyEquals,
|
||||
ETHER,
|
||||
JSBI,
|
||||
Pair,
|
||||
Percent,
|
||||
Route,
|
||||
Token,
|
||||
TokenAmount,
|
||||
@@ -157,31 +155,3 @@ export function useV1TradeExchangeAddress(trade: Trade | undefined): string | un
|
||||
}, [trade])
|
||||
return useV1ExchangeAddress(tokenAddress)
|
||||
}
|
||||
|
||||
const ZERO_PERCENT = new Percent('0')
|
||||
const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
|
||||
// returns whether tradeB is better than tradeA by at least a threshold percentage amount
|
||||
export function isTradeBetter(
|
||||
tradeA: Trade | undefined,
|
||||
tradeB: Trade | undefined,
|
||||
minimumDelta: Percent = ZERO_PERCENT
|
||||
): boolean | undefined {
|
||||
if (tradeA && !tradeB) return false
|
||||
if (tradeB && !tradeA) return true
|
||||
if (!tradeA || !tradeB) return undefined
|
||||
|
||||
if (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
|
||||
!currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency)
|
||||
) {
|
||||
throw new Error('Trades are not comparable')
|
||||
}
|
||||
|
||||
if (minimumDelta.equalTo(ZERO_PERCENT)) {
|
||||
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
|
||||
} else {
|
||||
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,129 @@
|
||||
import { TokenAddressMap, useDefaultTokenList, useUnsupportedTokenList } from './../state/lists/hooks'
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { Currency, ETHER, Token, currencyEquals } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useSelectedTokenList } from '../state/lists/hooks'
|
||||
import { useCombinedActiveList, useCombinedInactiveList } from '../state/lists/hooks'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
import { filterTokens } from '../components/SearchModal/filtering'
|
||||
import { arrayify } from 'ethers/lib/utils'
|
||||
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
// reduce token map into standard address <-> Token mapping, optionally include user added tokens
|
||||
function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean): { [address: string]: Token } {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
const allTokens = useSelectedTokenList()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId) return {}
|
||||
return (
|
||||
userAddedTokens
|
||||
// reduce into all ALL_TOKENS filtered by the current chain
|
||||
.reduce<{ [address: string]: Token }>(
|
||||
(tokenMap, token) => {
|
||||
tokenMap[token.address] = token
|
||||
return tokenMap
|
||||
},
|
||||
// must make a copy because reduce modifies the map, and we do not
|
||||
// want to make a copy in every iteration
|
||||
{ ...allTokens[chainId] }
|
||||
)
|
||||
)
|
||||
}, [chainId, userAddedTokens, allTokens])
|
||||
|
||||
// reduce to just tokens
|
||||
const mapWithoutUrls = Object.keys(tokenMap[chainId]).reduce<{ [address: string]: Token }>((newMap, address) => {
|
||||
newMap[address] = tokenMap[chainId][address].token
|
||||
return newMap
|
||||
}, {})
|
||||
|
||||
if (includeUserAdded) {
|
||||
return (
|
||||
userAddedTokens
|
||||
// reduce into all ALL_TOKENS filtered by the current chain
|
||||
.reduce<{ [address: string]: Token }>(
|
||||
(tokenMap, token) => {
|
||||
tokenMap[token.address] = token
|
||||
return tokenMap
|
||||
},
|
||||
// must make a copy because reduce modifies the map, and we do not
|
||||
// want to make a copy in every iteration
|
||||
{ ...mapWithoutUrls }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return mapWithoutUrls
|
||||
}, [chainId, userAddedTokens, tokenMap, includeUserAdded])
|
||||
}
|
||||
|
||||
export function useDefaultTokens(): { [address: string]: Token } {
|
||||
const defaultList = useDefaultTokenList()
|
||||
return useTokensFromMap(defaultList, false)
|
||||
}
|
||||
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const allTokens = useCombinedActiveList()
|
||||
return useTokensFromMap(allTokens, true)
|
||||
}
|
||||
|
||||
export function useAllInactiveTokens(): { [address: string]: Token } {
|
||||
// get inactive tokens
|
||||
const inactiveTokensMap = useCombinedInactiveList()
|
||||
const inactiveTokens = useTokensFromMap(inactiveTokensMap, false)
|
||||
|
||||
// filter out any token that are on active list
|
||||
const activeTokensAddresses = Object.keys(useAllTokens())
|
||||
const filteredInactive = activeTokensAddresses
|
||||
? Object.keys(inactiveTokens).reduce<{ [address: string]: Token }>((newMap, address) => {
|
||||
if (!activeTokensAddresses.includes(address)) {
|
||||
newMap[address] = inactiveTokens[address]
|
||||
}
|
||||
return newMap
|
||||
}, {})
|
||||
: inactiveTokens
|
||||
|
||||
return filteredInactive
|
||||
}
|
||||
|
||||
export function useUnsupportedTokens(): { [address: string]: Token } {
|
||||
const unsupportedTokensMap = useUnsupportedTokenList()
|
||||
return useTokensFromMap(unsupportedTokensMap, false)
|
||||
}
|
||||
|
||||
export function useIsTokenActive(token: Token | undefined | null): boolean {
|
||||
const activeTokens = useAllTokens()
|
||||
|
||||
if (!activeTokens || !token) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!activeTokens[token.address]
|
||||
}
|
||||
|
||||
// used to detect extra search results
|
||||
export function useFoundOnInactiveList(searchQuery: string): Token[] | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const inactiveTokens = useAllInactiveTokens()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId || searchQuery === '') {
|
||||
return undefined
|
||||
} else {
|
||||
const tokens = filterTokens(Object.values(inactiveTokens), searchQuery)
|
||||
return tokens
|
||||
}
|
||||
}, [chainId, inactiveTokens, searchQuery])
|
||||
}
|
||||
|
||||
// Check if currency is included in custom list from user storage
|
||||
export function useIsUserAddedToken(currency: Currency): boolean {
|
||||
export function useIsUserAddedToken(currency: Currency | undefined | null): boolean {
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
|
||||
if (!currency) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!userAddedTokens.find(token => currencyEquals(currency, token))
|
||||
}
|
||||
|
||||
// parse a name or symbol from a token response
|
||||
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
|
||||
|
||||
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
|
||||
return str && str.length > 0
|
||||
? str
|
||||
: bytes32 && BYTES32_REGEX.test(bytes32)
|
||||
: // need to check for proper bytes string and valid terminator
|
||||
bytes32 && BYTES32_REGEX.test(bytes32) && arrayify(bytes32)[31] === 0
|
||||
? parseBytes32String(bytes32)
|
||||
: defaultValue
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { isTradeBetter } from 'utils/trades'
|
||||
import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES } from '../constants'
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES, BETTER_TRADE_LESS_HOPS_THRESHOLD } from '../constants'
|
||||
import { PairState, usePairs } from '../data/Reserves'
|
||||
import { wrappedCurrency } from '../utils/wrappedCurrency'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useUnsupportedTokens } from './Tokens'
|
||||
import { useUserSingleHopOnly } from 'state/user/hooks'
|
||||
|
||||
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
@@ -78,19 +81,40 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
)
|
||||
}
|
||||
|
||||
const MAX_HOPS = 3
|
||||
|
||||
/**
|
||||
* Returns the best trade for the exact amount of tokens in to the given token out
|
||||
*/
|
||||
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
|
||||
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
|
||||
|
||||
const [singleHopOnly] = useUserSingleHopOnly()
|
||||
|
||||
return useMemo(() => {
|
||||
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
|
||||
return (
|
||||
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||
)
|
||||
if (singleHopOnly) {
|
||||
return (
|
||||
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 1, maxNumResults: 1 })[0] ??
|
||||
null
|
||||
)
|
||||
}
|
||||
// search through trades with varying hops, find best trade out of them
|
||||
let bestTradeSoFar: Trade | null = null
|
||||
for (let i = 1; i <= MAX_HOPS; i++) {
|
||||
const currentTrade: Trade | null =
|
||||
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: i, maxNumResults: 1 })[0] ??
|
||||
null
|
||||
// if current trade is best yet, save it
|
||||
if (isTradeBetter(bestTradeSoFar, currentTrade, BETTER_TRADE_LESS_HOPS_THRESHOLD)) {
|
||||
bestTradeSoFar = currentTrade
|
||||
}
|
||||
}
|
||||
return bestTradeSoFar
|
||||
}
|
||||
|
||||
return null
|
||||
}, [allowedPairs, currencyAmountIn, currencyOut])
|
||||
}, [allowedPairs, currencyAmountIn, currencyOut, singleHopOnly])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,13 +123,48 @@ export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?:
|
||||
export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null {
|
||||
const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency)
|
||||
|
||||
const [singleHopOnly] = useUserSingleHopOnly()
|
||||
|
||||
return useMemo(() => {
|
||||
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
|
||||
return (
|
||||
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
|
||||
null
|
||||
)
|
||||
if (singleHopOnly) {
|
||||
return (
|
||||
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 1, maxNumResults: 1 })[0] ??
|
||||
null
|
||||
)
|
||||
}
|
||||
// search through trades with varying hops, find best trade out of them
|
||||
let bestTradeSoFar: Trade | null = null
|
||||
for (let i = 1; i <= MAX_HOPS; i++) {
|
||||
const currentTrade =
|
||||
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: i, maxNumResults: 1 })[0] ??
|
||||
null
|
||||
if (isTradeBetter(bestTradeSoFar, currentTrade, BETTER_TRADE_LESS_HOPS_THRESHOLD)) {
|
||||
bestTradeSoFar = currentTrade
|
||||
}
|
||||
}
|
||||
return bestTradeSoFar
|
||||
}
|
||||
return null
|
||||
}, [allowedPairs, currencyIn, currencyAmountOut])
|
||||
}, [currencyIn, currencyAmountOut, allowedPairs, singleHopOnly])
|
||||
}
|
||||
|
||||
export function useIsTransactionUnsupported(currencyIn?: Currency, currencyOut?: Currency): boolean {
|
||||
const unsupportedTokens: { [address: string]: Token } = useUnsupportedTokens()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const tokenIn = wrappedCurrency(currencyIn, chainId)
|
||||
const tokenOut = wrappedCurrency(currencyOut, chainId)
|
||||
|
||||
// if unsupported list loaded & either token on list, mark as unsupported
|
||||
if (unsupportedTokens) {
|
||||
if (tokenIn && Object.keys(unsupportedTokens).includes(tokenIn.address)) {
|
||||
return true
|
||||
}
|
||||
if (tokenOut && Object.keys(unsupportedTokens).includes(tokenOut.address)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
43
src/hooks/useAddTokenToMetamask.ts
Normal file
43
src/hooks/useAddTokenToMetamask.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { getTokenLogoURL } from './../components/CurrencyLogo/index'
|
||||
import { wrappedCurrency } from 'utils/wrappedCurrency'
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
|
||||
export default function useAddTokenToMetamask(
|
||||
currencyToAdd: Currency | undefined
|
||||
): { addToken: () => void; success: boolean | undefined } {
|
||||
const { library, chainId } = useActiveWeb3React()
|
||||
|
||||
const token: Token | undefined = wrappedCurrency(currencyToAdd, chainId)
|
||||
|
||||
const [success, setSuccess] = useState<boolean | undefined>()
|
||||
|
||||
const addToken = useCallback(() => {
|
||||
if (library && library.provider.isMetaMask && library.provider.request && token) {
|
||||
library.provider
|
||||
.request({
|
||||
method: 'wallet_watchAsset',
|
||||
params: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
//@ts-ignore // need this for incorrect ethers provider type
|
||||
type: 'ERC20',
|
||||
options: {
|
||||
address: token.address,
|
||||
symbol: token.symbol,
|
||||
decimals: token.decimals,
|
||||
image: getTokenLogoURL(token.address)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(success => {
|
||||
setSuccess(success)
|
||||
})
|
||||
.catch(() => setSuccess(false))
|
||||
} else {
|
||||
setSuccess(false)
|
||||
}
|
||||
}, [library, token])
|
||||
|
||||
return { addToken, success }
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { shade } from 'polished'
|
||||
import Vibrant from 'node-vibrant'
|
||||
import { hex } from 'wcag-contrast'
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
import uriToHttp from 'utils/uriToHttp'
|
||||
|
||||
async function getColorFromToken(token: Token): Promise<string | null> {
|
||||
if (token.chainId === ChainId.RINKEBY && token.address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
|
||||
@@ -28,6 +29,20 @@ async function getColorFromToken(token: Token): Promise<string | null> {
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
async function getColorFromUriPath(uri: string): Promise<string | null> {
|
||||
const formattedPath = uriToHttp(uri)[0]
|
||||
|
||||
return Vibrant.from(formattedPath)
|
||||
.getPalette()
|
||||
.then(palette => {
|
||||
if (palette?.Vibrant) {
|
||||
return palette.Vibrant.hex
|
||||
}
|
||||
return null
|
||||
})
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
export function useColor(token?: Token) {
|
||||
const [color, setColor] = useState('#2172E5')
|
||||
|
||||
@@ -50,3 +65,26 @@ export function useColor(token?: Token) {
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
export function useListColor(listImageUri?: string) {
|
||||
const [color, setColor] = useState('#2172E5')
|
||||
|
||||
useLayoutEffect(() => {
|
||||
let stale = false
|
||||
|
||||
if (listImageUri) {
|
||||
getColorFromUriPath(listImageUri).then(color => {
|
||||
if (!stale && color !== null) {
|
||||
setColor(color)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setColor('#2172E5')
|
||||
}
|
||||
}, [listImageUri])
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import getTokenList from '../utils/getTokenList'
|
||||
import resolveENSContentHash from '../utils/resolveENSContentHash'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
export function useFetchListCallback(): (listUrl: string) => Promise<TokenList> {
|
||||
export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean) => Promise<TokenList> {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
@@ -30,18 +30,19 @@ export function useFetchListCallback(): (listUrl: string) => Promise<TokenList>
|
||||
[chainId, library]
|
||||
)
|
||||
|
||||
// note: prevent dispatch if using for list search or unsupported list
|
||||
return useCallback(
|
||||
async (listUrl: string) => {
|
||||
async (listUrl: string, sendDispatch = true) => {
|
||||
const requestId = nanoid()
|
||||
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
return getTokenList(listUrl, ensResolver)
|
||||
.then(tokenList => {
|
||||
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
return tokenList
|
||||
})
|
||||
.catch(error => {
|
||||
console.debug(`Failed to get list at url ${listUrl}`, error)
|
||||
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
||||
6
src/hooks/useTheme.ts
Normal file
6
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export default function useTheme() {
|
||||
return useContext(ThemeContext)
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import ReactDOM from 'react-dom'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
import Blocklist from './components/Blocklist'
|
||||
import { NetworkContextName } from './constants'
|
||||
import './i18n'
|
||||
import App from './pages/App'
|
||||
import store from './state'
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration'
|
||||
import ApplicationUpdater from './state/application/updater'
|
||||
import ListsUpdater from './state/lists/updater'
|
||||
import MulticallUpdater from './state/multicall/updater'
|
||||
@@ -20,8 +22,8 @@ import getLibrary from './utils/getLibrary'
|
||||
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
|
||||
if ('ethereum' in window) {
|
||||
;(window.ethereum as any).autoRefreshOnNetworkChange = false
|
||||
if (!!window.ethereum) {
|
||||
window.ethereum.autoRefreshOnNetworkChange = false
|
||||
}
|
||||
|
||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||
@@ -58,17 +60,21 @@ ReactDOM.render(
|
||||
<FixedGlobalStyle />
|
||||
<Web3ReactProvider getLibrary={getLibrary}>
|
||||
<Web3ProviderNetwork getLibrary={getLibrary}>
|
||||
<Provider store={store}>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
<ThemedGlobalStyle />
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
<Blocklist>
|
||||
<Provider store={store}>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
<ThemedGlobalStyle />
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</Blocklist>
|
||||
</Web3ProviderNetwork>
|
||||
</Web3ReactProvider>
|
||||
</StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
serviceWorkerRegistration.unregister()
|
||||
|
||||
@@ -38,6 +38,8 @@ import { Dots, Wrapper } from '../Pool/styleds'
|
||||
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import { PoolPriceBar } from './PoolPriceBar'
|
||||
import { useIsTransactionUnsupported } from 'hooks/Trades'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
|
||||
export default function AddLiquidity({
|
||||
match: {
|
||||
@@ -76,6 +78,7 @@ export default function AddLiquidity({
|
||||
poolTokenPercentage,
|
||||
error
|
||||
} = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
|
||||
|
||||
const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)
|
||||
|
||||
const isValid = !error
|
||||
@@ -304,6 +307,8 @@ export default function AddLiquidity({
|
||||
|
||||
const isCreate = history.location.pathname.includes('/create')
|
||||
|
||||
const addIsUnsupported = useIsTransactionUnsupported(currencies?.CURRENCY_A, currencies?.CURRENCY_B)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBody>
|
||||
@@ -323,10 +328,11 @@ export default function AddLiquidity({
|
||||
/>
|
||||
)}
|
||||
pendingText={pendingText}
|
||||
currencyToAdd={pair?.liquidityToken}
|
||||
/>
|
||||
<AutoColumn gap="20px">
|
||||
{noLiquidity ||
|
||||
(isCreate && (
|
||||
(isCreate ? (
|
||||
<ColumnCenter>
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
@@ -342,6 +348,18 @@ export default function AddLiquidity({
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
</ColumnCenter>
|
||||
) : (
|
||||
<ColumnCenter>
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||
<b>Tip:</b> When you add liquidity, you will receive pool tokens representing your position.
|
||||
These tokens automatically earn fees proportional to your share of the pool, and can be redeemed
|
||||
at any time.
|
||||
</TYPE.link>
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
</ColumnCenter>
|
||||
))}
|
||||
<CurrencyInputPanel
|
||||
value={formattedAmounts[Field.CURRENCY_A]}
|
||||
@@ -390,7 +408,11 @@ export default function AddLiquidity({
|
||||
</>
|
||||
)}
|
||||
|
||||
{!account ? (
|
||||
{addIsUnsupported ? (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<TYPE.main mb="4px">Unsupported Asset</TYPE.main>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : (
|
||||
<AutoColumn gap={'md'}>
|
||||
@@ -444,12 +466,18 @@ export default function AddLiquidity({
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{pair && !noLiquidity && pairState !== PairState.INVALID ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}>
|
||||
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
{!addIsUnsupported ? (
|
||||
pair && !noLiquidity && pairState !== PairState.INVALID ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}>
|
||||
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
|
||||
</AutoColumn>
|
||||
) : null
|
||||
) : (
|
||||
<UnsupportedCurrencyFooter
|
||||
show={addIsUnsupported}
|
||||
currencies={[currencies.CURRENCY_A, currencies.CURRENCY_B]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import RemoveLiquidity from './RemoveLiquidity'
|
||||
import { RedirectOldRemoveLiquidityPathStructure } from './RemoveLiquidity/redirects'
|
||||
import Swap from './Swap'
|
||||
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||
|
||||
import Vote from './Vote'
|
||||
import VotePage from './Vote/VotePage'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const BodyWrapper = styled.div`
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 30px;
|
||||
padding: 1rem;
|
||||
/* padding: 1rem; */
|
||||
`
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import useUSDCPrice from '../../utils/useUSDCPrice'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
import { BIG_INT_ZERO, BIG_INT_SECONDS_IN_WEEK } from '../../constants'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
max-width: 640px;
|
||||
@@ -177,9 +177,11 @@ export default function Manage({
|
||||
<AutoColumn gap="sm">
|
||||
<TYPE.body style={{ margin: 0 }}>Pool Rate</TYPE.body>
|
||||
<TYPE.body fontSize={24} fontWeight={500}>
|
||||
{stakingInfo?.totalRewardRate
|
||||
?.multiply((60 * 60 * 24 * 7).toString())
|
||||
?.toFixed(0, { groupSeparator: ',' }) ?? '-'}
|
||||
{stakingInfo?.active
|
||||
? stakingInfo?.totalRewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toFixed(0, { groupSeparator: ',' }) ?? '-'
|
||||
: '0'}
|
||||
{' UNI / week'}
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
@@ -293,9 +295,11 @@ export default function Manage({
|
||||
<span role="img" aria-label="wizard-icon" style={{ marginRight: '8px ' }}>
|
||||
⚡
|
||||
</span>
|
||||
{stakingInfo?.rewardRate
|
||||
?.multiply((60 * 60 * 24 * 7).toString())
|
||||
?.toSignificant(4, { groupSeparator: ',' }) ?? '-'}
|
||||
{stakingInfo?.active
|
||||
? stakingInfo?.rewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toSignificant(4, { groupSeparator: ',' }) ?? '-'
|
||||
: '0'}
|
||||
{' UNI / week'}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
@@ -311,9 +315,11 @@ export default function Manage({
|
||||
|
||||
{!showAddLiquidityButton && (
|
||||
<DataRow style={{ marginBottom: '1rem' }}>
|
||||
<ButtonPrimary padding="8px" borderRadius="8px" width="160px" onClick={handleDepositClick}>
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) ? 'Deposit' : 'Deposit UNI-V2 LP Tokens'}
|
||||
</ButtonPrimary>
|
||||
{stakingInfo && stakingInfo.active && (
|
||||
<ButtonPrimary padding="8px" borderRadius="8px" width="160px" onClick={handleDepositClick}>
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) ? 'Deposit' : 'Deposit UNI-V2 LP Tokens'}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) && (
|
||||
<>
|
||||
@@ -329,7 +335,7 @@ export default function Manage({
|
||||
)}
|
||||
</DataRow>
|
||||
)}
|
||||
{!userLiquidityUnstaked ? null : userLiquidityUnstaked.equalTo('0') ? null : (
|
||||
{!userLiquidityUnstaked ? null : userLiquidityUnstaked.equalTo('0') ? null : !stakingInfo?.active ? null : (
|
||||
<TYPE.main>{userLiquidityUnstaked.toSignificant(6)} UNI-V2 LP tokens available</TYPE.main>
|
||||
)}
|
||||
</PositionInfo>
|
||||
|
||||
@@ -9,6 +9,9 @@ import { CardSection, DataCard, CardNoise, CardBGImage } from '../../components/
|
||||
import { Countdown } from './Countdown'
|
||||
import Loader from '../../components/Loader'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { JSBI } from '@uniswap/sdk'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
import { OutlineCard } from '../../components/Card'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
max-width: 640px;
|
||||
@@ -29,16 +32,25 @@ const PoolSection = styled.div`
|
||||
justify-self: center;
|
||||
`
|
||||
|
||||
const DataRow = styled(RowBetween)`
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
flex-direction: column;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Earn() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// staking info for connected account
|
||||
const stakingInfos = useStakingInfo()
|
||||
|
||||
const DataRow = styled(RowBetween)`
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
flex-direction: column;
|
||||
`};
|
||||
`
|
||||
/**
|
||||
* only show staking cards with balance
|
||||
* @todo only account for this if rewards are inactive
|
||||
*/
|
||||
const stakingInfosWithBalance = stakingInfos?.filter(s => JSBI.greaterThan(s.stakedAmount.raw, BIG_INT_ZERO))
|
||||
|
||||
// toggle copy if rewards are inactive
|
||||
const stakingRewardsExist = Boolean(typeof chainId === 'number' && (STAKING_REWARDS_INFO[chainId]?.length ?? 0) > 0)
|
||||
|
||||
return (
|
||||
@@ -81,9 +93,11 @@ export default function Earn() {
|
||||
{stakingRewardsExist && stakingInfos?.length === 0 ? (
|
||||
<Loader style={{ margin: 'auto' }} />
|
||||
) : !stakingRewardsExist ? (
|
||||
'No active rewards'
|
||||
<OutlineCard>No active pools</OutlineCard>
|
||||
) : stakingInfos?.length !== 0 && stakingInfosWithBalance.length === 0 ? (
|
||||
<OutlineCard>No active pools</OutlineCard>
|
||||
) : (
|
||||
stakingInfos?.map(stakingInfo => {
|
||||
stakingInfosWithBalance?.map(stakingInfo => {
|
||||
// need to sort by added liquidity here
|
||||
return <PoolCard key={stakingInfo.stakingRewardAddress} stakingInfo={stakingInfo} />
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SearchInput } from '../../components/SearchModal/styleds'
|
||||
import { useAllTokenV1Exchanges } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useSelectedTokenList } from '../../state/lists/hooks'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { BackArrow, TYPE } from '../../theme'
|
||||
import { LightCard } from '../../components/Card'
|
||||
@@ -18,6 +17,7 @@ import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useAddUserToken } from '../../state/user/hooks'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
import { useCombinedActiveList } from '../../state/lists/hooks'
|
||||
|
||||
export default function MigrateV1() {
|
||||
const theme = useContext(ThemeContext)
|
||||
@@ -28,7 +28,7 @@ export default function MigrateV1() {
|
||||
|
||||
// automatically add the search token
|
||||
const token = useToken(tokenSearch)
|
||||
const selectedTokenListTokens = useSelectedTokenList()
|
||||
const selectedTokenListTokens = useCombinedActiveList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenListTokens, token ?? undefined)
|
||||
const allTokens = useAllTokens()
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { Pair } from '@uniswap/sdk'
|
||||
import { Pair, JSBI } from '@uniswap/sdk'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { SwapPoolTabs } from '../../components/NavigationTabs'
|
||||
|
||||
@@ -19,6 +19,8 @@ import { usePairs } from '../../data/Reserves'
|
||||
import { toV2LiquidityToken, useTrackedTokenPairs } from '../../state/user/hooks'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { CardSection, DataCard, CardNoise, CardBGImage } from '../../components/earn/styled'
|
||||
import { useStakingInfo } from '../../state/stake/hooks'
|
||||
import { BIG_INT_ZERO } from '../../constants'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
max-width: 640px;
|
||||
@@ -107,11 +109,24 @@ export default function Pool() {
|
||||
|
||||
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
|
||||
|
||||
// show liquidity even if its deposited in rewards contract
|
||||
const stakingInfo = useStakingInfo()
|
||||
const stakingInfosWithBalance = stakingInfo?.filter(pool => JSBI.greaterThan(pool.stakedAmount.raw, BIG_INT_ZERO))
|
||||
const stakingPairs = usePairs(stakingInfosWithBalance?.map(stakingInfo => stakingInfo.tokens))
|
||||
|
||||
// remove any pairs that also are included in pairs with stake in mining pool
|
||||
const v2PairsWithoutStakedAmount = allV2PairsWithLiquidity.filter(v2Pair => {
|
||||
return (
|
||||
stakingPairs
|
||||
?.map(stakingPair => stakingPair[1])
|
||||
.filter(stakingPair => stakingPair?.liquidityToken.address === v2Pair.liquidityToken.address).length === 0
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageWrapper>
|
||||
<SwapPoolTabs active={'pool'} />
|
||||
|
||||
<VoteCard>
|
||||
<CardBGImage />
|
||||
<CardNoise />
|
||||
@@ -150,7 +165,13 @@ export default function Pool() {
|
||||
<ResponsiveButtonSecondary as={Link} padding="6px 8px" to="/create/ETH">
|
||||
Create a pair
|
||||
</ResponsiveButtonSecondary>
|
||||
<ResponsiveButtonPrimary id="join-pool-button" as={Link} padding="6px 8px" to="/add/ETH">
|
||||
<ResponsiveButtonPrimary
|
||||
id="join-pool-button"
|
||||
as={Link}
|
||||
padding="6px 8px"
|
||||
borderRadius="12px"
|
||||
to="/add/ETH"
|
||||
>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Add Liquidity
|
||||
</Text>
|
||||
@@ -170,7 +191,7 @@ export default function Pool() {
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</EmptyProposals>
|
||||
) : allV2PairsWithLiquidity?.length > 0 ? (
|
||||
) : allV2PairsWithLiquidity?.length > 0 || stakingPairs?.length > 0 ? (
|
||||
<>
|
||||
<ButtonSecondary>
|
||||
<RowBetween>
|
||||
@@ -180,10 +201,19 @@ export default function Pool() {
|
||||
<span> ↗</span>
|
||||
</RowBetween>
|
||||
</ButtonSecondary>
|
||||
|
||||
{allV2PairsWithLiquidity.map(v2Pair => (
|
||||
{v2PairsWithoutStakedAmount.map(v2Pair => (
|
||||
<FullPositionCard key={v2Pair.liquidityToken.address} pair={v2Pair} />
|
||||
))}
|
||||
{stakingPairs.map(
|
||||
(stakingPair, i) =>
|
||||
stakingPair[1] && ( // skip pairs that arent loaded
|
||||
<FullPositionCard
|
||||
key={stakingInfosWithBalance[i].stakingRewardAddress}
|
||||
pair={stakingPair[1]}
|
||||
stakedBalance={stakingInfosWithBalance[i].stakedAmount}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyProposals>
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled from 'styled-components'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
export const ClickableText = styled(Text)`
|
||||
|
||||
@@ -18,6 +18,8 @@ import { StyledInternalLink } from '../../theme'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import AppBody from '../AppBody'
|
||||
import { Dots } from '../Pool/styleds'
|
||||
import { BlueCard } from '../../components/Card'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
enum Fields {
|
||||
TOKEN0 = 0,
|
||||
@@ -79,7 +81,14 @@ export default function PoolFinder() {
|
||||
return (
|
||||
<AppBody>
|
||||
<FindPoolTabs />
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn style={{ padding: '1rem' }} gap="md">
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||
<b>Tip:</b> Use this tool to find pairs that don't automatically appear in the interface.
|
||||
</TYPE.link>
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { RouteComponentProps } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { BlueCard, LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
@@ -492,6 +492,14 @@ export default function RemoveLiquidity({
|
||||
pendingText={pendingText}
|
||||
/>
|
||||
<AutoColumn gap="md">
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||
<b>Tip:</b> Removing pool tokens converts your position back into underlying tokens at the current
|
||||
rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.
|
||||
</TYPE.link>
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
<LightCard>
|
||||
<AutoColumn gap="20px">
|
||||
<RowBetween>
|
||||
|
||||
@@ -19,11 +19,12 @@ import { ArrowWrapper, BottomGrouping, SwapCallbackError, Wrapper } from '../../
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import TokenWarningModal from '../../components/TokenWarningModal'
|
||||
import ProgressSteps from '../../components/ProgressSteps'
|
||||
import SwapHeader from '../../components/swap/SwapHeader'
|
||||
|
||||
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
||||
import { INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { getTradeVersion } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useCurrency } from '../../hooks/Tokens'
|
||||
import { useCurrency, useAllTokens } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
|
||||
import useENSAddress from '../../hooks/useENSAddress'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
@@ -37,15 +38,19 @@ import {
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { useExpertModeManager, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { useExpertModeManager, useUserSlippageTolerance, useUserSingleHopOnly } from '../../state/user/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||
import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
import Loader from '../../components/Loader'
|
||||
import { useIsTransactionUnsupported } from 'hooks/Trades'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
import { isTradeBetter } from 'utils/trades'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
export default function Swap() {
|
||||
export default function Swap({ history }: RouteComponentProps) {
|
||||
const loadedUrlParams = useDefaultsFromURLSearch()
|
||||
|
||||
// token warning stuff
|
||||
@@ -62,6 +67,14 @@ export default function Swap() {
|
||||
setDismissTokenWarning(true)
|
||||
}, [])
|
||||
|
||||
// dismiss warning if all imported tokens are in active lists
|
||||
const defaultTokens = useAllTokens()
|
||||
const importTokensNotInDefault =
|
||||
urlLoadedTokens &&
|
||||
urlLoadedTokens.filter((token: Token) => {
|
||||
return !Boolean(token.address in defaultTokens)
|
||||
})
|
||||
|
||||
const { account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
@@ -85,6 +98,7 @@ export default function Swap() {
|
||||
currencies,
|
||||
inputError: swapInputError
|
||||
} = useDerivedSwapInfo()
|
||||
|
||||
const { wrapType, execute: onWrap, inputError: wrapInputError } = useWrapCallback(
|
||||
currencies[Field.INPUT],
|
||||
currencies[Field.OUTPUT],
|
||||
@@ -100,12 +114,8 @@ export default function Swap() {
|
||||
const trade = showWrap ? undefined : tradesByVersion[toggledVersion]
|
||||
const defaultTrade = showWrap ? undefined : tradesByVersion[DEFAULT_VERSION]
|
||||
|
||||
const betterTradeLinkVersion: Version | undefined =
|
||||
toggledVersion === Version.v2 && isTradeBetter(v2Trade, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
||||
? Version.v1
|
||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade)
|
||||
? Version.v2
|
||||
: undefined
|
||||
const betterTradeLinkV2: Version | undefined =
|
||||
toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade) ? Version.v2 : undefined
|
||||
|
||||
const parsedAmounts = showWrap
|
||||
? {
|
||||
@@ -134,6 +144,12 @@ export default function Swap() {
|
||||
[onUserInput]
|
||||
)
|
||||
|
||||
// reset if they close warning without tokens in params
|
||||
const handleDismissTokenWarning = useCallback(() => {
|
||||
setDismissTokenWarning(true)
|
||||
history.push('/swap/')
|
||||
}, [history])
|
||||
|
||||
// modal and loading
|
||||
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
|
||||
showConfirm: boolean
|
||||
@@ -183,6 +199,8 @@ export default function Swap() {
|
||||
|
||||
const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade)
|
||||
|
||||
const [singleHopOnly] = useUserSingleHopOnly()
|
||||
|
||||
const handleSwap = useCallback(() => {
|
||||
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
|
||||
return
|
||||
@@ -209,6 +227,11 @@ export default function Swap() {
|
||||
getTradeVersion(trade)
|
||||
].join('/')
|
||||
})
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Routing',
|
||||
action: singleHopOnly ? 'Swap with multihop disabled' : 'Swap with multihop enabled'
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
setSwapState({
|
||||
@@ -219,7 +242,17 @@ export default function Swap() {
|
||||
txHash: undefined
|
||||
})
|
||||
})
|
||||
}, [tradeToConfirm, account, priceImpactWithoutFee, recipient, recipientAddress, showConfirm, swapCallback, trade])
|
||||
}, [
|
||||
priceImpactWithoutFee,
|
||||
swapCallback,
|
||||
tradeToConfirm,
|
||||
showConfirm,
|
||||
recipient,
|
||||
recipientAddress,
|
||||
account,
|
||||
trade,
|
||||
singleHopOnly
|
||||
])
|
||||
|
||||
// errors
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
@@ -264,15 +297,19 @@ export default function Swap() {
|
||||
onCurrencySelection
|
||||
])
|
||||
|
||||
const swapIsUnsupported = useIsTransactionUnsupported(currencies?.INPUT, currencies?.OUTPUT)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TokenWarningModal
|
||||
isOpen={urlLoadedTokens.length > 0 && !dismissTokenWarning}
|
||||
tokens={urlLoadedTokens}
|
||||
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
|
||||
tokens={importTokensNotInDefault}
|
||||
onConfirm={handleConfirmTokenWarning}
|
||||
onDismiss={handleDismissTokenWarning}
|
||||
/>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<AppBody>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<SwapHeader />
|
||||
<Wrapper id="swap-page">
|
||||
<ConfirmSwapModal
|
||||
isOpen={showConfirm}
|
||||
@@ -345,8 +382,8 @@ export default function Swap() {
|
||||
) : null}
|
||||
|
||||
{showWrap ? null : (
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<Card padding={showWrap ? '.25rem 1rem 0 1rem' : '0px'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="8px" style={{ padding: '0 16px' }}>
|
||||
{Boolean(trade) && (
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
@@ -374,7 +411,11 @@ export default function Swap() {
|
||||
)}
|
||||
</AutoColumn>
|
||||
<BottomGrouping>
|
||||
{!account ? (
|
||||
{swapIsUnsupported ? (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<TYPE.main mb="4px">Unsupported Asset</TYPE.main>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : showWrap ? (
|
||||
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}>
|
||||
@@ -384,6 +425,7 @@ export default function Swap() {
|
||||
) : noRoute && userHasSpecifiedInputOutput ? (
|
||||
<GreyCard style={{ textAlign: 'center' }}>
|
||||
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
|
||||
{singleHopOnly && <TYPE.main mb="4px">Try enabling multi-hop trades.</TYPE.main>}
|
||||
</GreyCard>
|
||||
) : showApproveFlow ? (
|
||||
<RowBetween>
|
||||
@@ -466,15 +508,19 @@ export default function Swap() {
|
||||
</Column>
|
||||
)}
|
||||
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
{betterTradeLinkVersion ? (
|
||||
<BetterTradeLink version={betterTradeLinkVersion} />
|
||||
{betterTradeLinkV2 && !swapIsUnsupported && toggledVersion === Version.v1 ? (
|
||||
<BetterTradeLink version={betterTradeLinkV2} />
|
||||
) : toggledVersion !== DEFAULT_VERSION && defaultTrade ? (
|
||||
<DefaultVersionLink />
|
||||
) : null}
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||
{!swapIsUnsupported ? (
|
||||
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||
) : (
|
||||
<UnsupportedCurrencyFooter show={swapIsUnsupported} currencies={[currencies.INPUT, currencies.OUTPUT]} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,19 +9,21 @@ import { CardSection, DataCard } from '../../components/earn/styled'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { ButtonPrimary } from '../../components/Button'
|
||||
import { ProposalStatus } from './styled'
|
||||
import { useProposalData, useUserVotes, useUserDelegatee, ProposalData } from '../../state/governance/hooks'
|
||||
import { useTimestampFromBlock } from '../../hooks/useTimestampFromBlock'
|
||||
import { useProposalData, useUserVotesAsOfBlock, ProposalData, useUserDelegatee } from '../../state/governance/hooks'
|
||||
import { DateTime } from 'luxon'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import VoteModal from '../../components/vote/VoteModal'
|
||||
import { TokenAmount, JSBI } from '@uniswap/sdk'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { UNI, ZERO_ADDRESS, PROPOSAL_LENGTH_IN_DAYS, COMMON_CONTRACT_NAMES } from '../../constants'
|
||||
import { AVERAGE_BLOCK_TIME_IN_SECS, COMMON_CONTRACT_NAMES, UNI, ZERO_ADDRESS } from '../../constants'
|
||||
import { isAddress, getEtherscanLink } from '../../utils'
|
||||
import { ApplicationModal } from '../../state/application/actions'
|
||||
import { useModalOpen, useToggleDelegateModal, useToggleVoteModal } from '../../state/application/hooks'
|
||||
import { useModalOpen, useToggleDelegateModal, useToggleVoteModal, useBlockNumber } from '../../state/application/hooks'
|
||||
import DelegateModal from '../../components/vote/DelegateModal'
|
||||
import { GreyCard } from '../../components/Card'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import { BigNumber } from 'ethers'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
@@ -97,12 +99,16 @@ const DetailText = styled.div`
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
const ProposerAddressLink = styled(ExternalLink)`
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
export default function VotePage({
|
||||
match: {
|
||||
params: { id }
|
||||
}
|
||||
}: RouteComponentProps<{ id: string }>) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
// get data for this specific proposal
|
||||
const proposalData: ProposalData | undefined = useProposalData(id)
|
||||
@@ -116,13 +122,19 @@ export default function VotePage({
|
||||
|
||||
// toggle for showing delegation modal
|
||||
const showDelegateModal = useModalOpen(ApplicationModal.DELEGATE)
|
||||
const toggelDelegateModal = useToggleDelegateModal()
|
||||
const toggleDelegateModal = useToggleDelegateModal()
|
||||
|
||||
// get and format date from data
|
||||
const startTimestamp: number | undefined = useTimestampFromBlock(proposalData?.startBlock)
|
||||
const endDate: DateTime | undefined = startTimestamp
|
||||
? DateTime.fromSeconds(startTimestamp).plus({ days: PROPOSAL_LENGTH_IN_DAYS })
|
||||
: undefined
|
||||
const currentTimestamp = useCurrentBlockTimestamp()
|
||||
const currentBlock = useBlockNumber()
|
||||
const endDate: DateTime | undefined =
|
||||
proposalData && currentTimestamp && currentBlock
|
||||
? DateTime.fromSeconds(
|
||||
currentTimestamp
|
||||
.add(BigNumber.from(AVERAGE_BLOCK_TIME_IN_SECS).mul(BigNumber.from(proposalData.endBlock - currentBlock)))
|
||||
.toNumber()
|
||||
)
|
||||
: undefined
|
||||
const now: DateTime = DateTime.local()
|
||||
|
||||
// get total votes and format percentages for UI
|
||||
@@ -132,11 +144,21 @@ export default function VotePage({
|
||||
const againstPercentage: string =
|
||||
proposalData && totalVotes ? ((proposalData.againstCount * 100) / totalVotes).toFixed(0) + '%' : '0%'
|
||||
|
||||
// show delegation option if they have have a balance, have not delegated
|
||||
const availableVotes: TokenAmount | undefined = useUserVotes()
|
||||
// only count available votes as of the proposal start block
|
||||
const availableVotes: TokenAmount | undefined = useUserVotesAsOfBlock(proposalData?.startBlock ?? undefined)
|
||||
|
||||
// only show voting if user has > 0 votes at proposal start block and proposal is active,
|
||||
const showVotingButtons =
|
||||
availableVotes &&
|
||||
JSBI.greaterThan(availableVotes.raw, JSBI.BigInt(0)) &&
|
||||
proposalData &&
|
||||
proposalData.status === 'active'
|
||||
|
||||
const uniBalance: TokenAmount | undefined = useTokenBalance(account ?? undefined, chainId ? UNI[chainId] : undefined)
|
||||
const userDelegatee: string | undefined = useUserDelegatee()
|
||||
const showUnlockVoting = Boolean(
|
||||
|
||||
// in blurb link to home page if they are able to unlock
|
||||
const showLinkForUnlock = Boolean(
|
||||
uniBalance && JSBI.notEqual(uniBalance.raw, JSBI.BigInt(0)) && userDelegatee === ZERO_ADDRESS
|
||||
)
|
||||
|
||||
@@ -153,7 +175,7 @@ export default function VotePage({
|
||||
return (
|
||||
<PageWrapper gap="lg" justify="center">
|
||||
<VoteModal isOpen={showVoteModal} onDismiss={toggleVoteModal} proposalId={proposalData?.id} support={support} />
|
||||
<DelegateModal isOpen={showDelegateModal} onDismiss={toggelDelegateModal} title="Unlock Votes" />
|
||||
<DelegateModal isOpen={showDelegateModal} onDismiss={toggleDelegateModal} title="Unlock Votes" />
|
||||
<ProposalInfo gap="lg" justify="start">
|
||||
<RowBetween style={{ width: '100%' }}>
|
||||
<ArrowWrapper to="/vote">
|
||||
@@ -171,23 +193,22 @@ export default function VotePage({
|
||||
? 'Voting ends approximately ' + (endDate && endDate.toLocaleString(DateTime.DATETIME_FULL))
|
||||
: ''}
|
||||
</TYPE.main>
|
||||
{showUnlockVoting && endDate && endDate > now && (
|
||||
<ButtonPrimary
|
||||
style={{ width: 'fit-content' }}
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
onClick={toggelDelegateModal}
|
||||
>
|
||||
Unlock Voting
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
{proposalData && proposalData.status === 'active' && !showVotingButtons && (
|
||||
<GreyCard>
|
||||
<TYPE.black>
|
||||
Only UNI votes that were self delegated or delegated to another address before block{' '}
|
||||
{proposalData.startBlock} are eligible for voting.{' '}
|
||||
{showLinkForUnlock && (
|
||||
<span>
|
||||
<StyledInternalLink to="/vote">Unlock voting</StyledInternalLink> to prepare for the next proposal.
|
||||
</span>
|
||||
)}
|
||||
</TYPE.black>
|
||||
</GreyCard>
|
||||
)}
|
||||
</AutoColumn>
|
||||
{!showUnlockVoting &&
|
||||
availableVotes &&
|
||||
JSBI.greaterThan(availableVotes?.raw, JSBI.BigInt(0)) &&
|
||||
endDate &&
|
||||
endDate > now ? (
|
||||
{showVotingButtons ? (
|
||||
<RowFixed style={{ width: '100%', gap: '12px' }}>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
@@ -273,11 +294,11 @@ export default function VotePage({
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="md">
|
||||
<TYPE.mediumHeader fontWeight={600}>Proposer</TYPE.mediumHeader>
|
||||
<ExternalLink
|
||||
<ProposerAddressLink
|
||||
href={proposalData?.proposer && chainId ? getEtherscanLink(chainId, proposalData?.proposer, 'address') : ''}
|
||||
>
|
||||
<ReactMarkdown source={proposalData?.proposer} />
|
||||
</ExternalLink>
|
||||
</ProposerAddressLink>
|
||||
</AutoColumn>
|
||||
</ProposalInfo>
|
||||
</PageWrapper>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function Vote() {
|
||||
|
||||
// toggle for showing delegation modal
|
||||
const showDelegateModal = useModalOpen(ApplicationModal.DELEGATE)
|
||||
const toggelDelegateModal = useToggleDelegateModal()
|
||||
const toggleDelegateModal = useToggleDelegateModal()
|
||||
|
||||
// get data to list all proposals
|
||||
const allProposals: ProposalData[] = useAllProposalData()
|
||||
@@ -126,7 +126,7 @@ export default function Vote() {
|
||||
<PageWrapper gap="lg" justify="center">
|
||||
<DelegateModal
|
||||
isOpen={showDelegateModal}
|
||||
onDismiss={toggelDelegateModal}
|
||||
onDismiss={toggleDelegateModal}
|
||||
title={showUnlockVoting ? 'Unlock Votes' : 'Update Delegation'}
|
||||
/>
|
||||
<TopSection gap="md">
|
||||
@@ -166,7 +166,7 @@ export default function Vote() {
|
||||
style={{ width: 'fit-content' }}
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
onClick={toggelDelegateModal}
|
||||
onClick={toggleDelegateModal}
|
||||
>
|
||||
Unlock Voting
|
||||
</ButtonPrimary>
|
||||
@@ -200,7 +200,7 @@ export default function Vote() {
|
||||
>
|
||||
{userDelegatee === account ? 'Self' : shortenAddress(userDelegatee)}
|
||||
</StyledExternalLink>
|
||||
<TextButton onClick={toggelDelegateModal} style={{ marginLeft: '4px' }}>
|
||||
<TextButton onClick={toggleDelegateModal} style={{ marginLeft: '4px' }}>
|
||||
(edit)
|
||||
</TextButton>
|
||||
</AddressButton>
|
||||
|
||||
1
src/react-app-env.d.ts
vendored
1
src/react-app-env.d.ts
vendored
@@ -11,6 +11,7 @@ interface Window {
|
||||
isMetaMask?: true
|
||||
on?: (...args: any[]) => void
|
||||
removeListener?: (...args: any[]) => void
|
||||
autoRefreshOnNetworkChange?: boolean
|
||||
}
|
||||
web3?: {}
|
||||
}
|
||||
|
||||
80
src/service-worker.ts
Normal file
80
src/service-worker.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/// <reference lib="webworker" />
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
// This service worker can be customized!
|
||||
// See https://developers.google.com/web/tools/workbox/modules
|
||||
// for the list of available Workbox modules, or add any other
|
||||
// code you'd like.
|
||||
// You can also remove this file if you'd prefer not to use a
|
||||
// service worker, and the Workbox build step will be skipped.
|
||||
|
||||
import { clientsClaim } from 'workbox-core'
|
||||
import { ExpirationPlugin } from 'workbox-expiration'
|
||||
import { createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
|
||||
import { registerRoute } from 'workbox-routing'
|
||||
import { StaleWhileRevalidate } from 'workbox-strategies'
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope
|
||||
|
||||
clientsClaim()
|
||||
|
||||
// Precache all of the assets generated by your build process.
|
||||
// Their URLs are injected into the manifest variable below.
|
||||
// This variable must be present somewhere in your service worker file,
|
||||
// even if you decide not to use precaching. See https://cra.link/PWA
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
// Set up App Shell-style routing, so that all navigation requests
|
||||
// are fulfilled with your index.html shell. Learn more at
|
||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
|
||||
registerRoute(
|
||||
// Return false to exempt requests from being fulfilled by index.html.
|
||||
({ request, url }: { request: Request; url: URL }) => {
|
||||
// If this isn't a navigation, skip.
|
||||
if (request.mode !== 'navigate') {
|
||||
return false
|
||||
}
|
||||
|
||||
// If this is a URL that starts with /_, skip.
|
||||
if (url.pathname.startsWith('/_')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If this looks like a URL for a resource, because it contains
|
||||
// a file extension, skip.
|
||||
if (url.pathname.match(fileExtensionRegexp)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Return true to signal that we want to use the handler.
|
||||
return true
|
||||
},
|
||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||
)
|
||||
|
||||
// An example runtime caching route for requests that aren't handled by the
|
||||
// precache, in this case same-origin .png requests like those from in public/
|
||||
registerRoute(
|
||||
// Add in any other file extensions or routing criteria as needed.
|
||||
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
|
||||
// Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'images',
|
||||
plugins: [
|
||||
// Ensure that once this runtime cache reaches a maximum size the
|
||||
// least-recently used images are removed.
|
||||
new ExpirationPlugin({ maxEntries: 50 })
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
// This allows the web app to trigger skipWaiting via
|
||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||
self.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
}
|
||||
})
|
||||
|
||||
// Any other custom service worker logic can go here.
|
||||
139
src/serviceWorkerRegistration.ts
Normal file
139
src/serviceWorkerRegistration.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://cra.link/PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||
)
|
||||
|
||||
type Config = {
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing
|
||||
if (installingWorker == null) {
|
||||
return
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||
)
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration)
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.')
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' }
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('No internet connection found. App is running in offline mode.')
|
||||
})
|
||||
}
|
||||
|
||||
export function register(config?: Config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config)
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://cra.link/PWA'
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,18 +29,16 @@ function fetchClaim(account: string, chainId: ChainId): Promise<UserClaimData |
|
||||
|
||||
return (CLAIM_PROMISES[key] =
|
||||
CLAIM_PROMISES[key] ??
|
||||
fetch(`https://gentle-frost-9e74.uniswap.workers.dev/${chainId}/${formatted}`)
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
return res.json()
|
||||
} else {
|
||||
console.debug(`No claim for account ${formatted} on chain ID ${chainId}`)
|
||||
return null
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to get claim data', error)
|
||||
}))
|
||||
fetch('https://merkle-drop-1.uniswap.workers.dev/', {
|
||||
body: JSON.stringify({ chainId, address: formatted }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Referrer-Policy': 'no-referrer'
|
||||
},
|
||||
method: 'POST'
|
||||
})
|
||||
.then(res => (res.ok ? res.json() : console.log(`No claim for account ${formatted} on chain ID ${chainId}`)))
|
||||
.catch(error => console.error('Failed to get claim data', error)))
|
||||
}
|
||||
|
||||
// parse distributorContract blob and detect if user has claim data
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UNI } from './../../constants/index'
|
||||
import { UNI, PRELOADED_PROPOSALS } from './../../constants/index'
|
||||
import { TokenAmount } from '@uniswap/sdk'
|
||||
import { isAddress } from 'ethers/lib/utils'
|
||||
import { useGovernanceContract, useUniContract } from '../../hooks/useContract'
|
||||
@@ -121,10 +121,11 @@ export function useAllProposalData() {
|
||||
return Boolean(p.result) && Boolean(allProposalStates[i]?.result) && Boolean(formattedEvents[i])
|
||||
})
|
||||
.map((p, i) => {
|
||||
const description = PRELOADED_PROPOSALS.get(allProposals.length - i - 1) || formattedEvents[i].description
|
||||
const formattedProposal: ProposalData = {
|
||||
id: allProposals[i]?.result?.id.toString(),
|
||||
title: formattedEvents[i].description?.split(/# |\n/g)[1] || 'Untitled',
|
||||
description: formattedEvents[i].description?.split(/# /)[1] || 'No description.',
|
||||
title: description?.split(/# |\n/g)[1] || 'Untitled',
|
||||
description: description || 'No description.',
|
||||
proposer: allProposals[i]?.result?.proposer,
|
||||
status: enumerateProposalState(allProposalStates[i]?.result?.[0]) ?? 'Undetermined',
|
||||
forCount: parseFloat(ethers.utils.formatUnits(allProposals[i]?.result?.forVotes.toString(), 18)),
|
||||
@@ -153,6 +154,7 @@ export function useUserDelegatee(): string {
|
||||
return result?.[0] ?? undefined
|
||||
}
|
||||
|
||||
// gets the users current votes
|
||||
export function useUserVotes(): TokenAmount | undefined {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const uniContract = useUniContract()
|
||||
@@ -163,6 +165,18 @@ export function useUserVotes(): TokenAmount | undefined {
|
||||
return votes && uni ? new TokenAmount(uni, votes) : undefined
|
||||
}
|
||||
|
||||
// fetch available votes as of block (usually proposal start block)
|
||||
export function useUserVotesAsOfBlock(block: number | undefined): TokenAmount | undefined {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const uniContract = useUniContract()
|
||||
|
||||
// check for available votes
|
||||
const uni = chainId ? UNI[chainId] : undefined
|
||||
const votes = useSingleCallResult(uniContract, 'getPriorVotes', [account ?? undefined, block ?? undefined])
|
||||
?.result?.[0]
|
||||
return votes && uni ? new TokenAmount(uni, votes) : undefined
|
||||
}
|
||||
|
||||
export function useDelegateCallback(): (delegatee: string | undefined) => undefined | Promise<string> {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
@@ -10,9 +10,14 @@ export const fetchTokenList: Readonly<{
|
||||
fulfilled: createAction('lists/fetchTokenList/fulfilled'),
|
||||
rejected: createAction('lists/fetchTokenList/rejected')
|
||||
}
|
||||
|
||||
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
|
||||
// add and remove from list options
|
||||
export const addList = createAction<string>('lists/addList')
|
||||
export const removeList = createAction<string>('lists/removeList')
|
||||
export const selectList = createAction<string>('lists/selectList')
|
||||
|
||||
// select which lists to search across from loaded lists
|
||||
export const enableList = createAction<string>('lists/enableList')
|
||||
export const disableList = createAction<string>('lists/disableList')
|
||||
|
||||
// versioning
|
||||
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
|
||||
export const rejectVersionUpdate = createAction<Version>('lists/rejectVersionUpdate')
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { UNSUPPORTED_LIST_URLS } from './../../constants/lists'
|
||||
import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'
|
||||
import { ChainId, Token } from '@uniswap/sdk'
|
||||
import { Tags, TokenInfo, TokenList } from '@uniswap/token-lists'
|
||||
import { useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppState } from '../index'
|
||||
import sortByListPriority from 'utils/listSort'
|
||||
import UNSUPPORTED_TOKEN_LIST from '../../constants/tokenLists/uniswap-v2-unsupported.tokenlist.json'
|
||||
|
||||
type TagDetails = Tags[keyof Tags]
|
||||
export interface TagInfo extends TagDetails {
|
||||
@@ -25,7 +29,9 @@ export class WrappedTokenInfo extends Token {
|
||||
}
|
||||
}
|
||||
|
||||
export type TokenAddressMap = Readonly<{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: WrappedTokenInfo }> }>
|
||||
export type TokenAddressMap = Readonly<
|
||||
{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: { token: WrappedTokenInfo; list: TokenList } }> }
|
||||
>
|
||||
|
||||
/**
|
||||
* An empty result, useful as a default.
|
||||
@@ -60,7 +66,10 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
...tokenMap,
|
||||
[token.chainId]: {
|
||||
...tokenMap[token.chainId],
|
||||
[token.address]: token
|
||||
[token.address]: {
|
||||
token,
|
||||
list: list
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -70,49 +79,99 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
return map
|
||||
}
|
||||
|
||||
export function useTokenList(url: string | undefined): TokenAddressMap {
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
return useMemo(() => {
|
||||
if (!url) return EMPTY_LIST
|
||||
const current = lists[url]?.current
|
||||
if (!current) return EMPTY_LIST
|
||||
try {
|
||||
return listToTokenMap(current)
|
||||
} catch (error) {
|
||||
console.error('Could not show token list due to error', error)
|
||||
return EMPTY_LIST
|
||||
}
|
||||
}, [lists, url])
|
||||
export function useAllLists(): {
|
||||
readonly [url: string]: {
|
||||
readonly current: TokenList | null
|
||||
readonly pendingUpdate: TokenList | null
|
||||
readonly loadingRequestId: string | null
|
||||
readonly error: string | null
|
||||
}
|
||||
} {
|
||||
return useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
}
|
||||
|
||||
export function useSelectedListUrl(): string | undefined {
|
||||
return useSelector<AppState, AppState['lists']['selectedListUrl']>(state => state.lists.selectedListUrl)
|
||||
}
|
||||
|
||||
export function useSelectedTokenList(): TokenAddressMap {
|
||||
return useTokenList(useSelectedListUrl())
|
||||
}
|
||||
|
||||
export function useSelectedListInfo(): { current: TokenList | null; pending: TokenList | null; loading: boolean } {
|
||||
const selectedUrl = useSelectedListUrl()
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const list = selectedUrl ? listsByUrl[selectedUrl] : undefined
|
||||
function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddressMap {
|
||||
return {
|
||||
current: list?.current ?? null,
|
||||
pending: list?.pendingUpdate ?? null,
|
||||
loading: list?.loadingRequestId !== null
|
||||
1: { ...map1[1], ...map2[1] },
|
||||
3: { ...map1[3], ...map2[3] },
|
||||
4: { ...map1[4], ...map2[4] },
|
||||
5: { ...map1[5], ...map2[5] },
|
||||
42: { ...map1[42], ...map2[42] }
|
||||
}
|
||||
}
|
||||
|
||||
// returns all downloaded current lists
|
||||
export function useAllLists(): TokenList[] {
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
// merge tokens contained within lists from urls
|
||||
function useCombinedTokenMapFromUrls(urls: string[] | undefined): TokenAddressMap {
|
||||
const lists = useAllLists()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.keys(lists)
|
||||
.map(url => lists[url].current)
|
||||
.filter((l): l is TokenList => Boolean(l)),
|
||||
[lists]
|
||||
return useMemo(() => {
|
||||
if (!urls) return EMPTY_LIST
|
||||
|
||||
return (
|
||||
urls
|
||||
.slice()
|
||||
// sort by priority so top priority goes last
|
||||
.sort(sortByListPriority)
|
||||
.reduce((allTokens, currentUrl) => {
|
||||
const current = lists[currentUrl]?.current
|
||||
if (!current) return allTokens
|
||||
try {
|
||||
const newTokens = Object.assign(listToTokenMap(current))
|
||||
return combineMaps(allTokens, newTokens)
|
||||
} catch (error) {
|
||||
console.error('Could not show token list due to error', error)
|
||||
return allTokens
|
||||
}
|
||||
}, EMPTY_LIST)
|
||||
)
|
||||
}, [lists, urls])
|
||||
}
|
||||
|
||||
// filter out unsupported lists
|
||||
export function useActiveListUrls(): string[] | undefined {
|
||||
return useSelector<AppState, AppState['lists']['activeListUrls']>(state => state.lists.activeListUrls)?.filter(
|
||||
url => !UNSUPPORTED_LIST_URLS.includes(url)
|
||||
)
|
||||
}
|
||||
|
||||
export function useInactiveListUrls(): string[] {
|
||||
const lists = useAllLists()
|
||||
const allActiveListUrls = useActiveListUrls()
|
||||
return Object.keys(lists).filter(url => !allActiveListUrls?.includes(url) && !UNSUPPORTED_LIST_URLS.includes(url))
|
||||
}
|
||||
|
||||
// get all the tokens from active lists, combine with local default tokens
|
||||
export function useCombinedActiveList(): TokenAddressMap {
|
||||
const activeListUrls = useActiveListUrls()
|
||||
const activeTokens = useCombinedTokenMapFromUrls(activeListUrls)
|
||||
const defaultTokenMap = listToTokenMap(DEFAULT_TOKEN_LIST)
|
||||
return combineMaps(activeTokens, defaultTokenMap)
|
||||
}
|
||||
|
||||
// all tokens from inactive lists
|
||||
export function useCombinedInactiveList(): TokenAddressMap {
|
||||
const allInactiveListUrls: string[] = useInactiveListUrls()
|
||||
return useCombinedTokenMapFromUrls(allInactiveListUrls)
|
||||
}
|
||||
|
||||
// used to hide warnings on import for default tokens
|
||||
export function useDefaultTokenList(): TokenAddressMap {
|
||||
return listToTokenMap(DEFAULT_TOKEN_LIST)
|
||||
}
|
||||
|
||||
// list of tokens not supported on interface, used to show warnings and prevent swaps and adds
|
||||
export function useUnsupportedTokenList(): TokenAddressMap {
|
||||
// get hard coded unsupported tokens
|
||||
const localUnsupportedListMap = listToTokenMap(UNSUPPORTED_TOKEN_LIST)
|
||||
|
||||
// get any loaded unsupported tokens
|
||||
const loadedUnsupportedListMap = useCombinedTokenMapFromUrls(UNSUPPORTED_LIST_URLS)
|
||||
|
||||
// format into one token address map
|
||||
return combineMaps(localUnsupportedListMap, loadedUnsupportedListMap)
|
||||
}
|
||||
|
||||
export function useIsListActive(url: string): boolean {
|
||||
const activeListUrls = useActiveListUrls()
|
||||
return Boolean(activeListUrls?.includes(url))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { DEFAULT_ACTIVE_LIST_URLS } from './../../constants/lists'
|
||||
import { createStore, Store } from 'redux'
|
||||
import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists'
|
||||
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
|
||||
import { updateVersion } from '../global/actions'
|
||||
import { fetchTokenList, acceptListUpdate, addList, removeList, selectList } from './actions'
|
||||
import { fetchTokenList, acceptListUpdate, addList, removeList, enableList } from './actions'
|
||||
import reducer, { ListsState } from './reducer'
|
||||
|
||||
const STUB_TOKEN_LIST = {
|
||||
@@ -30,7 +31,7 @@ describe('list reducer', () => {
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@@ -61,7 +62,7 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
|
||||
store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' }))
|
||||
@@ -74,7 +75,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -93,7 +94,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@@ -113,7 +114,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@@ -134,7 +135,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('does not save to current if list is newer minor version', () => {
|
||||
@@ -154,7 +155,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: MINOR_UPDATED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('does not save to pending if list is newer major version', () => {
|
||||
@@ -174,7 +175,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: MAJOR_UPDATED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -184,7 +185,7 @@ describe('list reducer', () => {
|
||||
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@@ -198,7 +199,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
@@ -210,7 +211,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -228,7 +229,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('no op for existing list', () => {
|
||||
@@ -241,7 +242,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(addList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
@@ -253,7 +254,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -269,7 +270,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(acceptListUpdate('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
@@ -281,7 +282,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -297,15 +298,15 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(removeList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('selects the default list if removed list was selected', () => {
|
||||
it('Removes from active lists if active list is removed', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@@ -315,18 +316,18 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
activeListUrls: ['fake-url']
|
||||
})
|
||||
store.dispatch(removeList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: 'tokens.uniswap.eth'
|
||||
activeListUrls: []
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectList', () => {
|
||||
it('sets the selected list url', () => {
|
||||
describe('enableList', () => {
|
||||
it('enables a list url', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@@ -336,9 +337,9 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url'))
|
||||
store.dispatch(enableList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@@ -348,10 +349,10 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
activeListUrls: ['fake-url']
|
||||
})
|
||||
})
|
||||
it('selects if not present already', () => {
|
||||
it('adds to url keys if not present already on enable', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@@ -361,9 +362,9 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url-invalid'))
|
||||
store.dispatch(enableList('fake-url-invalid'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@@ -379,10 +380,10 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url-invalid'
|
||||
activeListUrls: ['fake-url-invalid']
|
||||
})
|
||||
})
|
||||
it('works if list already added', () => {
|
||||
it('enable works if list already added', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@@ -392,9 +393,9 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url'))
|
||||
store.dispatch(enableList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@@ -404,7 +405,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
activeListUrls: ['fake-url']
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -427,7 +428,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(updateVersion())
|
||||
})
|
||||
@@ -445,36 +446,19 @@ describe('list reducer', () => {
|
||||
it('all lists are empty', () => {
|
||||
const s = store.getState()
|
||||
Object.keys(s.byUrl).forEach(url => {
|
||||
if (url === DEFAULT_TOKEN_LIST_URL) {
|
||||
expect(s.byUrl[url]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
} else {
|
||||
expect(s.byUrl[url]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
}
|
||||
expect(s.byUrl[url]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
})
|
||||
})
|
||||
it('sets initialized lists', () => {
|
||||
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
|
||||
})
|
||||
it('sets selected list', () => {
|
||||
expect(store.getState().selectedListUrl).toEqual(DEFAULT_TOKEN_LIST_URL)
|
||||
})
|
||||
it('default list is initialized', () => {
|
||||
expect(store.getState().byUrl[DEFAULT_TOKEN_LIST_URL]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
expect(store.getState().activeListUrls).toEqual(DEFAULT_ACTIVE_LIST_URLS)
|
||||
})
|
||||
})
|
||||
describe('initialized with a different set of lists', () => {
|
||||
@@ -494,7 +478,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined,
|
||||
activeListUrls: undefined,
|
||||
lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest']
|
||||
})
|
||||
store.dispatch(updateVersion())
|
||||
@@ -514,10 +498,6 @@ describe('list reducer', () => {
|
||||
expect(store.getState().byUrl['https://unpkg.com/@uniswap/default-token-list@latest']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('adds all the lists in the default list of lists', () => {
|
||||
expect(Object.keys(store.getState().byUrl)).toContain(DEFAULT_TOKEN_LIST_URL)
|
||||
})
|
||||
|
||||
it('each of those initialized lists is empty', () => {
|
||||
const byUrl = store.getState().byUrl
|
||||
// note we don't expect the uniswap default list to be prepopulated
|
||||
@@ -538,15 +518,7 @@ describe('list reducer', () => {
|
||||
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
|
||||
})
|
||||
it('sets default list to selected list', () => {
|
||||
expect(store.getState().selectedListUrl).toEqual(DEFAULT_TOKEN_LIST_URL)
|
||||
})
|
||||
it('default list is initialized', () => {
|
||||
expect(store.getState().byUrl[DEFAULT_TOKEN_LIST_URL]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
expect(store.getState().activeListUrls).toEqual(DEFAULT_ACTIVE_LIST_URLS)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { DEFAULT_ACTIVE_LIST_URLS, UNSUPPORTED_LIST_URLS } from './../../constants/lists'
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { getVersionUpgrade, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { TokenList } from '@uniswap/token-lists/dist/types'
|
||||
import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists'
|
||||
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
|
||||
import { updateVersion } from '../global/actions'
|
||||
import { acceptListUpdate, addList, fetchTokenList, removeList, selectList } from './actions'
|
||||
import { acceptListUpdate, addList, fetchTokenList, removeList, enableList, disableList } from './actions'
|
||||
|
||||
export interface ListsState {
|
||||
readonly byUrl: {
|
||||
@@ -16,7 +17,9 @@ export interface ListsState {
|
||||
}
|
||||
// this contains the default list of lists from the last time the updateVersion was called, i.e. the app was reloaded
|
||||
readonly lastInitializedDefaultListOfLists?: string[]
|
||||
readonly selectedListUrl: string | undefined
|
||||
|
||||
// currently active lists
|
||||
readonly activeListUrls: string[] | undefined
|
||||
}
|
||||
|
||||
type ListState = ListsState['byUrl'][string]
|
||||
@@ -33,12 +36,12 @@ type Mutable<T> = { -readonly [P in keyof T]: T[P] extends ReadonlyArray<infer U
|
||||
const initialState: ListsState = {
|
||||
lastInitializedDefaultListOfLists: DEFAULT_LIST_OF_LISTS,
|
||||
byUrl: {
|
||||
...DEFAULT_LIST_OF_LISTS.reduce<Mutable<ListsState['byUrl']>>((memo, listUrl) => {
|
||||
...DEFAULT_LIST_OF_LISTS.concat(...UNSUPPORTED_LIST_URLS).reduce<Mutable<ListsState['byUrl']>>((memo, listUrl) => {
|
||||
memo[listUrl] = NEW_LIST_STATE
|
||||
return memo
|
||||
}, {})
|
||||
},
|
||||
selectedListUrl: DEFAULT_TOKEN_LIST_URL
|
||||
activeListUrls: DEFAULT_ACTIVE_LIST_URLS
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
@@ -59,6 +62,7 @@ export default createReducer(initialState, builder =>
|
||||
// no-op if update does nothing
|
||||
if (current) {
|
||||
const upgradeType = getVersionUpgrade(current.version, tokenList.version)
|
||||
|
||||
if (upgradeType === VersionUpgrade.NONE) return
|
||||
if (loadingRequestId === null || loadingRequestId === requestId) {
|
||||
state.byUrl[url] = {
|
||||
@@ -70,6 +74,11 @@ export default createReducer(initialState, builder =>
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// activate if on default active
|
||||
if (DEFAULT_ACTIVE_LIST_URLS.includes(url)) {
|
||||
state.activeListUrls?.push(url)
|
||||
}
|
||||
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
loadingRequestId: null,
|
||||
@@ -93,13 +102,6 @@ export default createReducer(initialState, builder =>
|
||||
pendingUpdate: null
|
||||
}
|
||||
})
|
||||
.addCase(selectList, (state, { payload: url }) => {
|
||||
state.selectedListUrl = url
|
||||
// automatically adds list
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
}
|
||||
})
|
||||
.addCase(addList, (state, { payload: url }) => {
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
@@ -109,8 +111,27 @@ export default createReducer(initialState, builder =>
|
||||
if (state.byUrl[url]) {
|
||||
delete state.byUrl[url]
|
||||
}
|
||||
if (state.selectedListUrl === url) {
|
||||
state.selectedListUrl = url === DEFAULT_TOKEN_LIST_URL ? Object.keys(state.byUrl)[0] : DEFAULT_TOKEN_LIST_URL
|
||||
// remove list from active urls if needed
|
||||
if (state.activeListUrls && state.activeListUrls.includes(url)) {
|
||||
state.activeListUrls = state.activeListUrls.filter(u => u !== url)
|
||||
}
|
||||
})
|
||||
.addCase(enableList, (state, { payload: url }) => {
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
}
|
||||
|
||||
if (state.activeListUrls && !state.activeListUrls.includes(url)) {
|
||||
state.activeListUrls.push(url)
|
||||
}
|
||||
|
||||
if (!state.activeListUrls) {
|
||||
state.activeListUrls = [url]
|
||||
}
|
||||
})
|
||||
.addCase(disableList, (state, { payload: url }) => {
|
||||
if (state.activeListUrls && state.activeListUrls.includes(url)) {
|
||||
state.activeListUrls = state.activeListUrls.filter(u => u !== url)
|
||||
}
|
||||
})
|
||||
.addCase(acceptListUpdate, (state, { payload: url }) => {
|
||||
@@ -127,7 +148,7 @@ export default createReducer(initialState, builder =>
|
||||
// state loaded from localStorage, but new lists have never been initialized
|
||||
if (!state.lastInitializedDefaultListOfLists) {
|
||||
state.byUrl = initialState.byUrl
|
||||
state.selectedListUrl = DEFAULT_TOKEN_LIST_URL
|
||||
state.activeListUrls = initialState.activeListUrls
|
||||
} else if (state.lastInitializedDefaultListOfLists) {
|
||||
const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>(
|
||||
(s, l) => s.add(l),
|
||||
@@ -150,11 +171,17 @@ export default createReducer(initialState, builder =>
|
||||
|
||||
state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS
|
||||
|
||||
if (!state.selectedListUrl) {
|
||||
state.selectedListUrl = DEFAULT_TOKEN_LIST_URL
|
||||
if (!state.byUrl[DEFAULT_TOKEN_LIST_URL]) {
|
||||
state.byUrl[DEFAULT_TOKEN_LIST_URL] = NEW_LIST_STATE
|
||||
}
|
||||
// if no active lists, activate defaults
|
||||
if (!state.activeListUrls) {
|
||||
state.activeListUrls = DEFAULT_ACTIVE_LIST_URLS
|
||||
|
||||
// for each list on default list, initialize if needed
|
||||
DEFAULT_ACTIVE_LIST_URLS.map((listUrl: string) => {
|
||||
if (!state.byUrl[listUrl]) {
|
||||
state.byUrl[listUrl] = NEW_LIST_STATE
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import { useAllLists } from 'state/lists/hooks'
|
||||
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
|
||||
import { addPopup } from '../application/actions'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { AppDispatch } from '../index'
|
||||
import { acceptListUpdate } from './actions'
|
||||
import { useActiveListUrls } from './hooks'
|
||||
import { useAllInactiveTokens } from 'hooks/Tokens'
|
||||
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
|
||||
|
||||
export default function Updater(): null {
|
||||
const { library } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
|
||||
const isWindowVisible = useIsWindowVisible()
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
// get all loaded lists, and the active urls
|
||||
const lists = useAllLists()
|
||||
const activeListUrls = useActiveListUrls()
|
||||
|
||||
// initiate loading
|
||||
useAllInactiveTokens()
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
const fetchAllListsCallback = useCallback(() => {
|
||||
if (!isWindowVisible) return
|
||||
Object.keys(lists).forEach(url =>
|
||||
@@ -32,13 +39,22 @@ export default function Updater(): null {
|
||||
useEffect(() => {
|
||||
Object.keys(lists).forEach(listUrl => {
|
||||
const list = lists[listUrl]
|
||||
|
||||
if (!list.current && !list.loadingRequestId && !list.error) {
|
||||
fetchList(listUrl).catch(error => console.debug('list added fetching error', error))
|
||||
}
|
||||
})
|
||||
}, [dispatch, fetchList, library, lists])
|
||||
|
||||
// if any lists from unsupported lists are loaded, check them too (in case new updates since last visit)
|
||||
useEffect(() => {
|
||||
Object.keys(UNSUPPORTED_LIST_URLS).forEach(listUrl => {
|
||||
const list = lists[listUrl]
|
||||
if (!list || (!list.current && !list.loadingRequestId && !list.error)) {
|
||||
fetchList(listUrl).catch(error => console.debug('list added fetching error', error))
|
||||
}
|
||||
})
|
||||
}, [dispatch, fetchList, library, lists])
|
||||
|
||||
// automatically update lists if versions are minor/patch
|
||||
useEffect(() => {
|
||||
Object.keys(lists).forEach(listUrl => {
|
||||
@@ -54,19 +70,6 @@ export default function Updater(): null {
|
||||
// automatically update minor/patch as long as bump matches the min update
|
||||
if (bump >= min) {
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate,
|
||||
auto: true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
console.error(
|
||||
`List at url ${listUrl} could not automatically update because the version bump was only PATCH/MINOR while the update had breaking changes and should have been MAJOR`
|
||||
@@ -74,25 +77,13 @@ export default function Updater(): null {
|
||||
}
|
||||
break
|
||||
|
||||
// update any active or inactive lists
|
||||
case VersionUpgrade.MAJOR:
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
auto: false,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate
|
||||
}
|
||||
},
|
||||
removeAfterMs: null
|
||||
})
|
||||
)
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [dispatch, lists])
|
||||
}, [dispatch, lists, activeListUrls])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -17,6 +17,33 @@ export function useMintState(): AppState['mint'] {
|
||||
return useSelector<AppState, AppState['mint']>(state => state.mint)
|
||||
}
|
||||
|
||||
export function useMintActionHandlers(
|
||||
noLiquidity: boolean | undefined
|
||||
): {
|
||||
onFieldAInput: (typedValue: string) => void
|
||||
onFieldBInput: (typedValue: string) => void
|
||||
} {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const onFieldAInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_A, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
const onFieldBInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_B, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
|
||||
return {
|
||||
onFieldAInput,
|
||||
onFieldBInput
|
||||
}
|
||||
}
|
||||
|
||||
export function useDerivedMintInfo(
|
||||
currencyA: Currency | undefined,
|
||||
currencyB: Currency | undefined
|
||||
@@ -167,30 +194,3 @@ export function useDerivedMintInfo(
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
export function useMintActionHandlers(
|
||||
noLiquidity: boolean | undefined
|
||||
): {
|
||||
onFieldAInput: (typedValue: string) => void
|
||||
onFieldBInput: (typedValue: string) => void
|
||||
} {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const onFieldAInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_A, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
const onFieldBInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_B, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
|
||||
return {
|
||||
onFieldAInput,
|
||||
onFieldBInput
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { STAKING_REWARDS_INTERFACE } from '../../constants/abis/staking-rewards'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { NEVER_RELOAD, useMultipleContractSingleData } from '../multicall/hooks'
|
||||
import { tryParseAmount } from '../swap/hooks'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
|
||||
export const STAKING_GENESIS = 1600387200
|
||||
|
||||
@@ -55,6 +56,8 @@ export interface StakingInfo {
|
||||
rewardRate: TokenAmount
|
||||
// when the period ends
|
||||
periodFinish: Date | undefined
|
||||
// if pool is active
|
||||
active: boolean
|
||||
// calculates a hypothetical amount of token distributed to the active account per second.
|
||||
getHypotheticalRewardRate: (
|
||||
stakedAmount: TokenAmount,
|
||||
@@ -67,6 +70,9 @@ export interface StakingInfo {
|
||||
export function useStakingInfo(pairToFilterBy?: Pair | null): StakingInfo[] {
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
// detect if staking is ended
|
||||
const currentBlockTimestamp = useCurrentBlockTimestamp()
|
||||
|
||||
const info = useMemo(
|
||||
() =>
|
||||
chainId
|
||||
@@ -170,7 +176,12 @@ export function useStakingInfo(pairToFilterBy?: Pair | null): StakingInfo[] {
|
||||
|
||||
const individualRewardRate = getHypotheticalRewardRate(stakedAmount, totalStakedAmount, totalRewardRate)
|
||||
|
||||
const periodFinishMs = periodFinishState.result?.[0]?.mul(1000)?.toNumber()
|
||||
const periodFinishSeconds = periodFinishState.result?.[0]?.toNumber()
|
||||
const periodFinishMs = periodFinishSeconds * 1000
|
||||
|
||||
// compare period end timestamp vs current block timestamp (in seconds)
|
||||
const active =
|
||||
periodFinishSeconds && currentBlockTimestamp ? periodFinishSeconds > currentBlockTimestamp.toNumber() : true
|
||||
|
||||
memo.push({
|
||||
stakingRewardAddress: rewardsAddress,
|
||||
@@ -181,12 +192,24 @@ export function useStakingInfo(pairToFilterBy?: Pair | null): StakingInfo[] {
|
||||
totalRewardRate: totalRewardRate,
|
||||
stakedAmount: stakedAmount,
|
||||
totalStakedAmount: totalStakedAmount,
|
||||
getHypotheticalRewardRate
|
||||
getHypotheticalRewardRate,
|
||||
active
|
||||
})
|
||||
}
|
||||
return memo
|
||||
}, [])
|
||||
}, [balances, chainId, earnedAmounts, info, periodFinishes, rewardRates, rewardsAddresses, totalSupplies, uni])
|
||||
}, [
|
||||
balances,
|
||||
chainId,
|
||||
currentBlockTimestamp,
|
||||
earnedAmounts,
|
||||
info,
|
||||
periodFinishes,
|
||||
rewardRates,
|
||||
rewardsAddresses,
|
||||
totalSupplies,
|
||||
uni
|
||||
])
|
||||
}
|
||||
|
||||
export function useTotalUniEarned(): TokenAmount | undefined {
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface SerializedPair {
|
||||
export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode')
|
||||
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
|
||||
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
|
||||
export const updateUserSingleHopOnly = createAction<{ userSingleHopOnly: boolean }>('user/updateUserSingleHopOnly')
|
||||
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>(
|
||||
'user/updateUserSlippageTolerance'
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChainId, Pair, Token } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
|
||||
import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants'
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
updateUserDeadline,
|
||||
updateUserExpertMode,
|
||||
updateUserSlippageTolerance,
|
||||
toggleURLWarning
|
||||
toggleURLWarning,
|
||||
updateUserSingleHopOnly
|
||||
} from './actions'
|
||||
|
||||
function serializeToken(token: Token): SerializedToken {
|
||||
@@ -81,6 +83,27 @@ export function useExpertModeManager(): [boolean, () => void] {
|
||||
return [expertMode, toggleSetExpertMode]
|
||||
}
|
||||
|
||||
export function useUserSingleHopOnly(): [boolean, (newSingleHopOnly: boolean) => void] {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const singleHopOnly = useSelector<AppState, AppState['user']['userSingleHopOnly']>(
|
||||
state => state.user.userSingleHopOnly
|
||||
)
|
||||
|
||||
const setSingleHopOnly = useCallback(
|
||||
(newSingleHopOnly: boolean) => {
|
||||
ReactGA.event({
|
||||
category: 'Routing',
|
||||
action: newSingleHopOnly ? 'enable single hop' : 'disable single hop'
|
||||
})
|
||||
dispatch(updateUserSingleHopOnly({ userSingleHopOnly: newSingleHopOnly }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return [singleHopOnly, setSingleHopOnly]
|
||||
}
|
||||
|
||||
export function useUserSlippageTolerance(): [number, (slippage: number) => void] {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>(state => {
|
||||
@@ -139,7 +162,7 @@ export function useUserAddedTokens(): Token[] {
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId) return []
|
||||
return Object.values(serializedTokensMap[chainId as ChainId] ?? {}).map(deserializeToken)
|
||||
return Object.values(serializedTokensMap?.[chainId as ChainId] ?? {}).map(deserializeToken)
|
||||
}, [serializedTokensMap, chainId])
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
updateUserExpertMode,
|
||||
updateUserSlippageTolerance,
|
||||
updateUserDeadline,
|
||||
toggleURLWarning
|
||||
toggleURLWarning,
|
||||
updateUserSingleHopOnly
|
||||
} from './actions'
|
||||
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
@@ -27,6 +28,8 @@ export interface UserState {
|
||||
|
||||
userExpertMode: boolean
|
||||
|
||||
userSingleHopOnly: boolean // only allow swaps on direct pairs
|
||||
|
||||
// user defined slippage tolerance in bips, used in all txns
|
||||
userSlippageTolerance: number
|
||||
|
||||
@@ -58,6 +61,7 @@ export const initialState: UserState = {
|
||||
userDarkMode: null,
|
||||
matchesDarkMode: false,
|
||||
userExpertMode: false,
|
||||
userSingleHopOnly: false,
|
||||
userSlippageTolerance: INITIAL_ALLOWED_SLIPPAGE,
|
||||
userDeadline: DEFAULT_DEADLINE_FROM_NOW,
|
||||
tokens: {},
|
||||
@@ -103,12 +107,21 @@ export default createReducer(initialState, builder =>
|
||||
state.userDeadline = action.payload.userDeadline
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(updateUserSingleHopOnly, (state, action) => {
|
||||
state.userSingleHopOnly = action.payload.userSingleHopOnly
|
||||
})
|
||||
.addCase(addSerializedToken, (state, { payload: { serializedToken } }) => {
|
||||
if (!state.tokens) {
|
||||
state.tokens = {}
|
||||
}
|
||||
state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {}
|
||||
state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(removeSerializedToken, (state, { payload: { address, chainId } }) => {
|
||||
if (!state.tokens) {
|
||||
state.tokens = {}
|
||||
}
|
||||
state.tokens[chainId] = state.tokens[chainId] || {}
|
||||
delete state.tokens[chainId][address]
|
||||
state.timestamp = currentTimestamp()
|
||||
|
||||
@@ -3,7 +3,25 @@ import ReactGA from 'react-ga'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { ArrowLeft, X } from 'react-feather'
|
||||
import { ArrowLeft, X, ExternalLink as LinkIconFeather, Trash } from 'react-feather'
|
||||
|
||||
export const ButtonText = styled.button`
|
||||
outline: none;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
export const Button = styled.button.attrs<{ warning: boolean }, { backgroundColor: string }>(({ warning, theme }) => ({
|
||||
backgroundColor: warning ? theme.red1 : theme.primary1
|
||||
@@ -39,6 +57,20 @@ export const CloseIcon = styled(X)<{ onClick: () => void }>`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
// for wrapper react feather icons
|
||||
export const IconWrapper = styled.div<{ stroke?: string; size?: string; marginRight?: string; marginLeft?: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: ${({ size }) => size ?? '20px'};
|
||||
height: ${({ size }) => size ?? '20px'};
|
||||
margin-right: ${({ marginRight }) => marginRight ?? 0};
|
||||
margin-left: ${({ marginLeft }) => marginLeft ?? 0};
|
||||
& > * {
|
||||
stroke: ${({ theme, stroke }) => stroke ?? theme.blue1};
|
||||
}
|
||||
`
|
||||
|
||||
// A button that triggers some onClick result, but looks like a link.
|
||||
export const LinkStyledButton = styled.button<{ disabled?: boolean }>`
|
||||
border: none;
|
||||
@@ -104,6 +136,51 @@ const StyledLink = styled.a`
|
||||
}
|
||||
`
|
||||
|
||||
const LinkIconWrapper = styled.a`
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const LinkIcon = styled(LinkIconFeather)`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 10px;
|
||||
stroke: ${({ theme }) => theme.blue1};
|
||||
`
|
||||
|
||||
export const TrashIcon = styled(Trash)`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 10px;
|
||||
stroke: ${({ theme }) => theme.text3};
|
||||
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
|
||||
:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
const rotateImg = keyframes`
|
||||
0% {
|
||||
transform: perspective(1000px) rotateY(0deg);
|
||||
@@ -149,6 +226,36 @@ export function ExternalLink({
|
||||
return <StyledLink target={target} rel={rel} href={href} onClick={handleClick} {...rest} />
|
||||
}
|
||||
|
||||
export function ExternalLinkIcon({
|
||||
target = '_blank',
|
||||
href,
|
||||
rel = 'noopener noreferrer',
|
||||
...rest
|
||||
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref' | 'onClick'> & { href: string }) {
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// don't prevent default, don't redirect if it's a new tab
|
||||
if (target === '_blank' || event.ctrlKey || event.metaKey) {
|
||||
ReactGA.outboundLink({ label: href }, () => {
|
||||
console.debug('Fired outbound link event', href)
|
||||
})
|
||||
} else {
|
||||
event.preventDefault()
|
||||
// send a ReactGA event and then trigger a location change
|
||||
ReactGA.outboundLink({ label: href }, () => {
|
||||
window.location.href = href
|
||||
})
|
||||
}
|
||||
},
|
||||
[href, target]
|
||||
)
|
||||
return (
|
||||
<LinkIconWrapper target={target} rel={rel} href={href} onClick={handleClick} {...rest}>
|
||||
<LinkIcon />
|
||||
</LinkIconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@@ -185,3 +292,16 @@ export const HideSmall = styled.span`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export const HideExtraSmall = styled.span`
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export const ExtraSmallOnly = styled.span`
|
||||
display: none;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: block;
|
||||
`};
|
||||
`
|
||||
|
||||
@@ -74,8 +74,9 @@ export function colors(darkMode: boolean): Colors {
|
||||
secondary3: darkMode ? '#17000b26' : '#FDEAF1',
|
||||
|
||||
// other
|
||||
red1: '#FF6871',
|
||||
red1: '#FD4040',
|
||||
red2: '#F82D3A',
|
||||
red3: '#D60000',
|
||||
green1: '#27AE60',
|
||||
yellow1: '#FFE270',
|
||||
yellow2: '#F3841E',
|
||||
@@ -156,7 +157,7 @@ export const TYPE = {
|
||||
return <TextWrapper fontWeight={500} fontSize={11} {...props} />
|
||||
},
|
||||
blue(props: TextProps) {
|
||||
return <TextWrapper fontWeight={500} color={'primary1'} {...props} />
|
||||
return <TextWrapper fontWeight={500} color={'blue1'} {...props} />
|
||||
},
|
||||
yellow(props: TextProps) {
|
||||
return <TextWrapper fontWeight={500} color={'yellow1'} {...props} />
|
||||
|
||||
1
src/theme/styled.d.ts
vendored
1
src/theme/styled.d.ts
vendored
@@ -40,6 +40,7 @@ export interface Colors {
|
||||
// other
|
||||
red1: Color
|
||||
red2: Color
|
||||
red3: Color
|
||||
green1: Color
|
||||
yellow1: Color
|
||||
yellow2: Color
|
||||
|
||||
12
src/utils/listSort.ts
Normal file
12
src/utils/listSort.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { DEFAULT_LIST_OF_LISTS } from './../constants/lists'
|
||||
|
||||
// use ordering of default list of lists to assign priority
|
||||
export default function sortByListPriority(urlA: string, urlB: string) {
|
||||
const first = DEFAULT_LIST_OF_LISTS.includes(urlA) ? DEFAULT_LIST_OF_LISTS.indexOf(urlA) : Number.MAX_SAFE_INTEGER
|
||||
const second = DEFAULT_LIST_OF_LISTS.includes(urlB) ? DEFAULT_LIST_OF_LISTS.indexOf(urlB) : Number.MAX_SAFE_INTEGER
|
||||
|
||||
// need reverse order to make sure mapping includes top priority last
|
||||
if (first < second) return 1
|
||||
else if (first > second) return -1
|
||||
return 0
|
||||
}
|
||||
@@ -10,5 +10,10 @@ describe('parseENSAddress', () => {
|
||||
expect(parseENSAddress('abso.lutely.eth')).toEqual({ ensName: 'abso.lutely.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('eth')).toEqual(undefined)
|
||||
expect(parseENSAddress('eth/hello-world')).toEqual(undefined)
|
||||
expect(parseENSAddress('hello-world.eth')).toEqual({ ensName: 'hello-world.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('-prefix-dash.eth')).toEqual(undefined)
|
||||
expect(parseENSAddress('suffix-dash-.eth')).toEqual(undefined)
|
||||
expect(parseENSAddress('it.eth')).toEqual({ ensName: 'it.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('only-single--dash.eth')).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+\.)+)eth(\/.*)?$/
|
||||
const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+)eth(\/.*)?$/
|
||||
|
||||
export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined {
|
||||
const match = ensAddress.match(ENS_NAME_REGEX)
|
||||
if (!match) return undefined
|
||||
return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[3] }
|
||||
return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[4] }
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(BASE_FEE)
|
||||
|
||||
// computes price breakdown for the trade
|
||||
export function computeTradePriceBreakdown(
|
||||
trade?: Trade
|
||||
): { priceImpactWithoutFee?: Percent; realizedLPFee?: CurrencyAmount } {
|
||||
trade?: Trade | null
|
||||
): { priceImpactWithoutFee: Percent | undefined; realizedLPFee: CurrencyAmount | undefined | null } {
|
||||
// for each hop in our trade, take away the x*y=k price impact from 0.3% fees
|
||||
// e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
|
||||
const realizedLPFee = !trade
|
||||
|
||||
27
src/utils/trades.ts
Normal file
27
src/utils/trades.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ZERO_PERCENT, ONE_HUNDRED_PERCENT } from './../constants/index'
|
||||
import { Trade, Percent, currencyEquals } from '@uniswap/sdk'
|
||||
|
||||
// returns whether tradeB is better than tradeA by at least a threshold percentage amount
|
||||
export function isTradeBetter(
|
||||
tradeA: Trade | undefined | null,
|
||||
tradeB: Trade | undefined | null,
|
||||
minimumDelta: Percent = ZERO_PERCENT
|
||||
): boolean | undefined {
|
||||
if (tradeA && !tradeB) return false
|
||||
if (tradeB && !tradeA) return true
|
||||
if (!tradeA || !tradeB) return undefined
|
||||
|
||||
if (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
|
||||
!currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency)
|
||||
) {
|
||||
throw new Error('Trades are not comparable')
|
||||
}
|
||||
|
||||
if (minimumDelta.equalTo(ZERO_PERCENT)) {
|
||||
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
|
||||
} else {
|
||||
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,8 @@
|
||||
"jsx": "preserve",
|
||||
"downlevelIteration": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["react-spring", "jest"]
|
||||
"types": ["react-spring", "jest"],
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"exclude": ["node_modules", "cypress"],
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "src/components/Confetti/index.js"]
|
||||
|
||||
56
yarn.lock
56
yarn.lock
@@ -2691,6 +2691,11 @@
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@uniswap/default-token-list@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-2.0.0.tgz#095b4c22635e532c817c3ba70e8838d8bd699716"
|
||||
integrity sha512-P37PqBtUjEB9DIRFfmEsgougkV0555JQKiGPISeN9UFk1UgCQM5sg6+bBaShiyaqEpdtttAaBFI1QESCzPGvXw==
|
||||
|
||||
"@uniswap/governance@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/governance/-/governance-1.0.2.tgz#7371ab54dea9a5c045275001e2d5325ff2c11a93"
|
||||
@@ -2726,10 +2731,10 @@
|
||||
tiny-warning "^1.0.3"
|
||||
toformat "^2.0.0"
|
||||
|
||||
"@uniswap/token-lists@^1.0.0-beta.17":
|
||||
version "1.0.0-beta.17"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.17.tgz#a861fe96a0f3de91b01eae05dec05a6a2018b38e"
|
||||
integrity sha512-UVRmSsP/ghJ7Dg8BHYjAZmZL96jHlZKrFoIEuKD3g1E4FbahfwS2j2KAaf6iQuLx6RWo8PQmDZ99rfK6T2xi8w==
|
||||
"@uniswap/token-lists@^1.0.0-beta.19":
|
||||
version "1.0.0-beta.19"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.19.tgz#5256db144fba721a6233f43b92ffb388cbd58327"
|
||||
integrity sha512-19V3KM7DAe40blWW1ApiaSYwqbq0JTKMO3yChGBrXzQBl+BoQZRTNZ4waCyoZ5QM45Q0Mxd6bCn6jXcH9G1kjg==
|
||||
|
||||
"@uniswap/v2-core@1.0.0":
|
||||
version "1.0.0"
|
||||
@@ -4795,9 +4800,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097:
|
||||
version "1.0.30001105"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001105.tgz#d2cb0b31e5cf2f3ce845033b61c5c01566549abf"
|
||||
integrity sha512-JupOe6+dGMr7E20siZHIZQwYqrllxotAhiaej96y6x00b/48rPt42o+SzOSCPbrpsDWvRja40Hwrj0g0q6LZJg==
|
||||
version "1.0.30001183"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001183.tgz"
|
||||
integrity sha512-7JkwTEE1hlRKETbCFd8HDZeLiQIUcl8rC6JgNjvHCNaxOeNmQ9V4LvQXRUsKIV2CC73qKxljwVhToaA3kLRqTw==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -13757,7 +13762,7 @@ serve-static@1.14.1:
|
||||
parseurl "~1.3.3"
|
||||
send "0.17.1"
|
||||
|
||||
serve@^11.3.0:
|
||||
serve@^11.3.2:
|
||||
version "11.3.2"
|
||||
resolved "https://registry.yarnpkg.com/serve/-/serve-11.3.2.tgz#b905e980616feecd170e51c8f979a7b2374098f5"
|
||||
integrity sha512-yKWQfI3xbj/f7X1lTBg91fXBP0FqjJ4TEi+ilES5yzH0iKJpN5LjNb1YzIfQg9Rqn4ECUS2SOf2+Kmepogoa5w==
|
||||
@@ -15650,6 +15655,11 @@ workbox-core@^4.3.1:
|
||||
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-4.3.1.tgz#005d2c6a06a171437afd6ca2904a5727ecd73be6"
|
||||
integrity sha512-I3C9jlLmMKPxAC1t0ExCq+QoAMd0vAAHULEgRZ7kieCdUd919n53WC0AfvokHNwqRhGn+tIIj7vcb5duCjs2Kg==
|
||||
|
||||
workbox-core@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.1.0.tgz#2671b64f76550e83a4c2202676b67ce372e10881"
|
||||
integrity sha512-s3KqTJfBreO4xCZpR2LB5p/EknAx8eg0QumKiIgxM4hRO0RtwS2pJvTieNEM23X3RqxRhqweriLD8To19KUvjg==
|
||||
|
||||
workbox-expiration@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-4.3.1.tgz#d790433562029e56837f341d7f553c4a78ebe921"
|
||||
@@ -15657,6 +15667,13 @@ workbox-expiration@^4.3.1:
|
||||
dependencies:
|
||||
workbox-core "^4.3.1"
|
||||
|
||||
workbox-expiration@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.1.0.tgz#cf6bb384e49d0c92b79233c46671d9c6d82478a2"
|
||||
integrity sha512-jp2xGk+LC4AhCoOxO/bC06GQkq/oVp0ZIf1zXLQh6OD2fWZPkXNjLLSuDnjXoGGPibYrq7gEE/xjAdYGjNWl1A==
|
||||
dependencies:
|
||||
workbox-core "^6.1.0"
|
||||
|
||||
workbox-google-analytics@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-4.3.1.tgz#9eda0183b103890b5c256e6f4ea15a1f1548519a"
|
||||
@@ -15681,6 +15698,15 @@ workbox-precaching@^4.3.1:
|
||||
dependencies:
|
||||
workbox-core "^4.3.1"
|
||||
|
||||
workbox-precaching@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.1.0.tgz#9ee3d28f27cd78daa62f5bd6a0d33f5682ac97a7"
|
||||
integrity sha512-zjye8MVzieBVJ3sS0hFcbKLp7pTHMfJM17YqxCxB0KykXWnxLOpYnStQ9M+bjWJsKJOQvbkPqvq5u9+mtA923g==
|
||||
dependencies:
|
||||
workbox-core "^6.1.0"
|
||||
workbox-routing "^6.1.0"
|
||||
workbox-strategies "^6.1.0"
|
||||
|
||||
workbox-range-requests@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-4.3.1.tgz#f8a470188922145cbf0c09a9a2d5e35645244e74"
|
||||
@@ -15695,6 +15721,13 @@ workbox-routing@^4.3.1:
|
||||
dependencies:
|
||||
workbox-core "^4.3.1"
|
||||
|
||||
workbox-routing@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.1.0.tgz#f885cb7801e2c9c5678f197656cf27a2b649c1d5"
|
||||
integrity sha512-FXQ5cwb6Mk90fC0rfQLX0pN+r/N4eBafwkh/QanJUq0e6jMPdDFLrlsikZL/0LcXEx+yAkWLytoiS+d2HOEBOw==
|
||||
dependencies:
|
||||
workbox-core "^6.1.0"
|
||||
|
||||
workbox-strategies@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-4.3.1.tgz#d2be03c4ef214c115e1ab29c9c759c9fe3e9e646"
|
||||
@@ -15702,6 +15735,13 @@ workbox-strategies@^4.3.1:
|
||||
dependencies:
|
||||
workbox-core "^4.3.1"
|
||||
|
||||
workbox-strategies@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.1.0.tgz#9ddcee44408d2fb403f22a7989803b5c58560590"
|
||||
integrity sha512-HvUknzJdZWeV3x7Eq33a7TGAv9/r1TEiQK6kQ1QNzN+IKiqhIjnhKFHmMxb5hK1Gw9/aDSJTLNPDaLPfIJRQFQ==
|
||||
dependencies:
|
||||
workbox-core "^6.1.0"
|
||||
|
||||
workbox-streams@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-4.3.1.tgz#0b57da70e982572de09c8742dd0cb40a6b7c2cc3"
|
||||
|
||||
Reference in New Issue
Block a user